fix: Test Connection names the volume a /health 503 says refused #143

Merged
bjoern merged 2 commits from fix/connection-test-names-the-volume-that-refused into main 2026-09-18 23:14:18 +02:00
Member

What's in

  • Test Connection now tells a server that was reached but is not ready apart from one that did not answer: a /health 503 with the documented body reads Reachable, but the server cannot write to its images volume (/app/data/images): <detail>, one line per refusing directory (It also cannot … for the second and later). A db entry whose detail starts with SQLite Error or journal_mode stayed (the two shapes DEPLOYMENT.md §7 documents) reads cannot host its database on the db volume (<path>): <detail>; a db entry the filesystem refused reads like any other volume. A #112-era entry without detail still names the volume, with no dangling colon.
  • A 503 without that body (a proxy/gateway page, or JSON of another shape) still says the server was reached (Reachable, but the server answered HTTP 503 without saying which volume refused …). Every other status and every transport failure keeps today's message.
  • The connection section shows the not-ready verdict as a new amber BannerTone.warning (Icons.warning_amber, colorScheme.secondary) on the shared InlineBanner, so it is visibly different from the red "could not connect" banner.
  • Shape: HealthRepository.check keeps returning Future<bool>; DioHealthRepository throws a typed domain ServerNotReadyException(message), _testConnectionEpic maps it to ConnectionTestResultAction(ok: false, error, serverNotReady: true), and SettingsState gained serverNotReady (reset by TestConnectionAction and SaveSettingsAction). The regenerated app_state.freezed.dart is committed.
  • No ADR amended (ADR 0010's /health contract is read, not changed) and no new DOUJIN_MANAGER_* variable. README's Settings sentence gained the Test Connection clause; the #112 backlog item that asked for this is removed.

Verification

  • scripts/verify.sh app, run from the worktree root at the final commit (b6ea1e6) and waited to completion: submodule check, flutter pub get, build_runner clean + build with the generated-file checksum guard (no drift reported), flutter analyze → No issues found! (ran in 2.8s), flutter test → 00:47 +997: All tests passed!, == verify OK (app) ==, exit 0. The backend is untouched, so verify.sh backend was not run.
  • Layers exercised (all app-side): repository — DioHealthRepository against a dio fake adapter with real JSON decoding, in the a 503 from /health group of app/test/data_layer_test.dart (volume + path + detail; db SQLite vs. filesystem wording; only db gets the host wording; several refusals; a body dio left as text; the detail-less #112 body and detail: ''; an entry missing name and path; other-shape JSON object and array; text/html body; nothing refusing; a 500 with the same body stays the plain HTTP message). Reducer — app/test/app_test.dart (result action carries serverNotReady; TestConnectionAction and SaveSettingsAction clear it). Epic through createStore and the connection-section widget — app/test/settings_connection_test.dart (not-ready reaches state as such; a plain failure stays a failure; a later 200 clears the verdict; the widget renders the message with the warning tone; an error stays an error; warning and error differ in icon and colour). Widget tests use bounded pumps, no pumpAndSettle. No REST, use-case or persistence layer changed.
  • Mutation probes (mutate → run the suite → git checkout restore; each went red on exactly the named tests and nothing else, tree clean afterwards): if (code == 503) disabled → 7 red; the SQLite-wording heuristic forced (name == db alone) and negated (false) → the two db wording tests; clauses.skip(clauses.length) → "several refusals are one line each"; the String-body path returning null → "a body dio left as text"; the clauses.isEmpty guard removed → "nothing refusing is generic"; detail == null || detail.isEmpty ? '' : ': $detail' collapsed → "a refusal without a reason still names the volume"; the name == _kDbDirectory && conjunct removed → "only the db volume can fail to host the database"; body is! Map and directories is! List guards turned into throws → "JSON of another shape is generic"; the null-path and null-name fallbacks removed → "a refusing entry missing name and path still reads"; the reducer's serverNotReady: lines removed from the result, TestConnectionAction and SaveSettingsAction arms → the three reducer tests; the epic's on ServerNotReadyException arm removed → 3 red in settings_connection_test; the page tone collapsed to success/error → 2 red; the banner's warning arm styled as error → 1 red. A second, independent probe pass at b6ea1e6 re-ran the second-round probes and the epic/page-arm probes and confirmed each red on the same test.
  • Unverified / by reading only: no running backend answered a real 503 in this work — the body shape comes from DataDirectoryHealthDto in the RestAdapter and DEPLOYMENT.md §7, and the #112-era detail-less shape from git show fc50854. Two conditionals in the malformed/partial-body class remain unpinned (probes stayed green): the detail != null && conjunct of the SQLite heuristic (a #112-era server whose db volume refuses reads cannot write to its db volume, which is the intended fallback) and the entry is Map guard on a directories element.

Follow-ups

  • docs/BACKLOG.md (App, where the removed #112 item stood): the connection banner shows a plain transport failure as Exception: Could not connect to … because _testConnectionEpic in app/lib/presentation/middleware/epics.dart stores e.toString(); the epic should store the message alone (pre-existing, noticed while Test Connection learned to read the 503 body).

🤖 Generated with Claude Code

## What's in - Test Connection now tells a server that was **reached but is not ready** apart from one that did not answer: a `/health` 503 with the documented body reads `Reachable, but the server cannot write to its images volume (/app/data/images): <detail>`, one line per refusing directory (`It also cannot …` for the second and later). A `db` entry whose detail starts with `SQLite Error` or `journal_mode stayed` (the two shapes DEPLOYMENT.md §7 documents) reads `cannot host its database on the db volume (<path>): <detail>`; a `db` entry the filesystem refused reads like any other volume. A #112-era entry without `detail` still names the volume, with no dangling colon. - A 503 **without** that body (a proxy/gateway page, or JSON of another shape) still says the server was reached (`Reachable, but the server answered HTTP 503 without saying which volume refused …`). Every other status and every transport failure keeps today's message. - The connection section shows the not-ready verdict as a new amber `BannerTone.warning` (`Icons.warning_amber`, `colorScheme.secondary`) on the shared `InlineBanner`, so it is visibly different from the red "could not connect" banner. - Shape: `HealthRepository.check` keeps returning `Future<bool>`; `DioHealthRepository` throws a typed domain `ServerNotReadyException(message)`, `_testConnectionEpic` maps it to `ConnectionTestResultAction(ok: false, error, serverNotReady: true)`, and `SettingsState` gained `serverNotReady` (reset by `TestConnectionAction` and `SaveSettingsAction`). The regenerated `app_state.freezed.dart` is committed. - No ADR amended (ADR 0010's `/health` contract is read, not changed) and no new `DOUJIN_MANAGER_*` variable. README's Settings sentence gained the Test Connection clause; the #112 backlog item that asked for this is removed. ## Verification - `scripts/verify.sh app`, run from the worktree root at the final commit (b6ea1e6) and waited to completion: submodule check, `flutter pub get`, `build_runner clean` + `build` with the generated-file checksum guard (no drift reported), `flutter analyze` → `No issues found! (ran in 2.8s)`, `flutter test` → `00:47 +997: All tests passed!`, `== verify OK (app) ==`, exit 0. The backend is untouched, so `verify.sh backend` was not run. - Layers exercised (all app-side): **repository** — `DioHealthRepository` against a dio fake adapter with real JSON decoding, in the `a 503 from /health` group of `app/test/data_layer_test.dart` (volume + path + detail; db SQLite vs. filesystem wording; only `db` gets the host wording; several refusals; a body dio left as text; the detail-less #112 body and `detail: ''`; an entry missing name and path; other-shape JSON object and array; text/html body; nothing refusing; a 500 with the same body stays the plain HTTP message). **Reducer** — `app/test/app_test.dart` (result action carries `serverNotReady`; `TestConnectionAction` and `SaveSettingsAction` clear it). **Epic through `createStore` and the connection-section widget** — `app/test/settings_connection_test.dart` (not-ready reaches state as such; a plain failure stays a failure; a later 200 clears the verdict; the widget renders the message with the warning tone; an error stays an error; warning and error differ in icon and colour). Widget tests use bounded pumps, no `pumpAndSettle`. No REST, use-case or persistence layer changed. - Mutation probes (mutate → run the suite → `git checkout` restore; each went red on exactly the named tests and nothing else, tree clean afterwards): `if (code == 503)` disabled → 7 red; the SQLite-wording heuristic forced (`name == db` alone) and negated (`false`) → the two db wording tests; `clauses.skip(clauses.length)` → "several refusals are one line each"; the String-body path returning null → "a body dio left as text"; the `clauses.isEmpty` guard removed → "nothing refusing is generic"; `detail == null || detail.isEmpty ? '' : ': $detail'` collapsed → "a refusal without a reason still names the volume"; the `name == _kDbDirectory &&` conjunct removed → "only the db volume can fail to host the database"; `body is! Map` and `directories is! List` guards turned into throws → "JSON of another shape is generic"; the null-path and null-name fallbacks removed → "a refusing entry missing name and path still reads"; the reducer's `serverNotReady:` lines removed from the result, `TestConnectionAction` and `SaveSettingsAction` arms → the three reducer tests; the epic's `on ServerNotReadyException` arm removed → 3 red in `settings_connection_test`; the page tone collapsed to success/error → 2 red; the banner's warning arm styled as error → 1 red. A second, independent probe pass at b6ea1e6 re-ran the second-round probes and the epic/page-arm probes and confirmed each red on the same test. - Unverified / by reading only: no running backend answered a real 503 in this work — the body shape comes from `DataDirectoryHealthDto` in the RestAdapter and DEPLOYMENT.md §7, and the #112-era detail-less shape from `git show fc50854`. Two conditionals in the malformed/partial-body class remain unpinned (probes stayed green): the `detail != null &&` conjunct of the SQLite heuristic (a #112-era server whose `db` volume refuses reads `cannot write to its db volume`, which is the intended fallback) and the `entry is Map` guard on a directories element. ## Follow-ups - docs/BACKLOG.md (App, where the removed #112 item stood): the connection banner shows a plain transport failure as `Exception: Could not connect to …` because `_testConnectionEpic` in `app/lib/presentation/middleware/epics.dart` stores `e.toString()`; the epic should store the message alone (pre-existing, noticed while Test Connection learned to read the 503 body). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A 503 from /health is reachable-but-not-ready: DioHealthRepository reads the documented body and throws ServerNotReadyException naming each directory that refused (volume, path, detail; db with a SQLite detail as 'cannot host its database'), the epic carries it as serverNotReady, and the connection section shows it as a warning rather than the red of 'nothing answered'. A 503 without the body keeps a generic message that still says the server was reached.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
test: pin the detail-less 503 body, the gateway shapes and the reducer reset
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 6m9s
b6ea1e679a
A server from PR #112 answers a refusing volume with no detail; Test Connection now provably names the volume without a dangling colon. The other-shape JSON guards, the db-only host wording and the missing name/path fallbacks are pinned too. SaveSettingsAction resets serverNotReady with the rest of the connection verdict, and the pre-existing Exception: prefix on transport failures is recorded in BACKLOG.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Flutter Coverage

File Line coverage
lib/app/store.dart 95.0% (19 of 20)
lib/data/models/doujin_models.dart 86.4% (38 of 44)
lib/data/models/doujin_models.g.dart 61.7% (195 of 316)
lib/domain/entities/stored_settings.dart 100.0% (9 of 9)
lib/domain/repositories/repositories.dart 33.3% (1 of 3)
lib/presentation/state/app_state.dart 84.6% (33 of 39)
lib/agent/agent_service.dart 88.7% (344 of 388)
lib/agent/approval_gate.dart 100.0% (16 of 16)
lib/agent/assistant_context.dart 52.8% (28 of 53)
lib/agent/browser_budget.dart 80.0% (4 of 5)
lib/agent/caching_describer.dart 100.0% (13 of 13)
lib/agent/memory_store.dart 92.9% (13 of 14)
lib/agent/skills/skill_registry.dart 98.2% (54 of 55)
lib/agent/system_prompt.dart 100.0% (69 of 69)
lib/agent/tools/budgeted_browser_tool.dart 84.2% (16 of 19)
lib/agent/tools/doujin_write_tool.dart 86.4% (319 of 369)
lib/agent/tools/entity_write_tool.dart 73.5% (164 of 223)
lib/agent/tools/fetch_page_tool.dart 89.3% (67 of 75)
lib/agent/tools/get_doujin_tool.dart 84.4% (27 of 32)
lib/agent/tools/list_entities_tool.dart 76.1% (54 of 71)
lib/agent/tools/navigate_tool.dart 95.1% (39 of 41)
lib/agent/tools/read_skill_tool.dart 82.4% (14 of 17)
lib/agent/tools/reflection_tools.dart 75.7% (28 of 37)
lib/agent/tools/search_doujins_tool.dart 100.0% (99 of 99)
lib/agent/tools/view_images_tool.dart 95.0% (38 of 40)
lib/domain/entities/assistant_entry.dart 20.0% (1 of 5)
lib/presentation/state/actions/assistant_actions.dart 54.5% (6 of 11)
lib/core/app_config.dart 70.0% (7 of 10)
lib/domain/entities/entity_model.dart 100.0% (1 of 1)
lib/data/repositories/entity_in_use_exception.dart 33.3% (1 of 3)
lib/data/models/search_query.dart 50.0% (2 of 4)
lib/data/models/search_query.g.dart 33.3% (25 of 75)
lib/core/constants.dart 40.0% (4 of 10)
lib/data/offline/offline_download_service.dart 93.0% (66 of 71)
lib/data/reading_progress_store.dart 92.3% (36 of 39)
lib/domain/entities/reading_progress.dart 100.0% (26 of 26)
lib/presentation/middleware/assistant_epics.dart 83.1% (74 of 89)
lib/presentation/middleware/backup_save_dialog.dart 25.0% (1 of 4)
lib/presentation/middleware/epics.dart 91.5% (388 of 424)
lib/presentation/middleware/offline_epics.dart 98.4% (60 of 61)
lib/presentation/state/reducers.dart 100.0% (14 of 14)
lib/core/chunking.dart 100.0% (12 of 12)
lib/core/code_point_compare.dart 100.0% (8 of 8)
lib/core/languages.dart 100.0% (8 of 8)
lib/core/reading_order.dart 100.0% (22 of 22)
lib/core/title_display.dart 90.0% (9 of 10)
lib/core/variant_display.dart 100.0% (22 of 22)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 53.7% (36 of 67)
lib/data/models/server_models.dart 97.9% (47 of 48)
lib/data/models/server_models.g.dart 55.2% (90 of 163)
lib/domain/entities/offline_manifest.dart 100.0% (78 of 78)
lib/data/repositories/backup_exception.dart 100.0% (3 of 3)
lib/data/repositories/upload_exception.dart 33.3% (1 of 3)
lib/domain/entities/filter_token.dart 92.2% (107 of 116)
lib/presentation/middleware/duplicates_epics.dart 100.0% (39 of 39)
lib/presentation/middleware/editor_epics.dart 57.3% (168 of 293)
lib/presentation/middleware/entity_ops.dart 58.0% (40 of 69)
lib/presentation/middleware/server_epics.dart 100.0% (97 of 97)
lib/presentation/middleware/upload_epics.dart 98.1% (104 of 106)
lib/presentation/state/actions/offline_actions.dart 91.7% (11 of 12)
lib/presentation/state/actions/detail_actions.dart 85.7% (6 of 7)
lib/presentation/state/actions/duplicates_actions.dart 83.3% (5 of 6)
lib/presentation/state/actions/editor_actions.dart 56.8% (21 of 37)
lib/presentation/state/actions/entity_actions.dart 62.5% (10 of 16)
lib/presentation/state/actions/library_actions.dart 50.0% (11 of 22)
lib/presentation/state/actions/metadata_actions.dart 100.0% (5 of 5)
lib/presentation/state/actions/reader_actions.dart 75.0% (3 of 4)
lib/presentation/state/actions/reading_progress_actions.dart 100.0% (2 of 2)
lib/presentation/state/actions/server_actions.dart 75.0% (21 of 28)
lib/presentation/state/actions/settings_actions.dart 87.5% (7 of 8)
lib/presentation/state/actions/upload_actions.dart 90.9% (10 of 11)
lib/presentation/state/reducers/assistant_reducer.dart 95.0% (57 of 60)
lib/presentation/state/reducers/detail_reducer.dart 100.0% (24 of 24)
lib/presentation/state/reducers/duplicates_reducer.dart 100.0% (38 of 38)
lib/presentation/state/reducers/editor_reducer.dart 97.1% (99 of 102)
lib/presentation/state/reducers/entity_reducer.dart 98.8% (82 of 83)
lib/presentation/state/reducers/library_reducer.dart 100.0% (135 of 135)
lib/presentation/state/reducers/metadata_reducer.dart 90.5% (38 of 42)
lib/presentation/state/reducers/offline_reducer.dart 100.0% (61 of 61)
lib/presentation/state/reducers/reader_reducer.dart 100.0% (18 of 18)
lib/presentation/state/reducers/reading_progress_reducer.dart 100.0% (20 of 20)
lib/presentation/state/reducers/server_reducer.dart 100.0% (129 of 129)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (61 of 61)
lib/presentation/state/reducers/upload_reducer.dart 100.0% (45 of 45)
lib/presentation/pages/editor/page_grid.dart 92.8% (309 of 333)
lib/presentation/pages/editor/variants_tab.dart 55.3% (52 of 94)
lib/core/natural_sort.dart 100.0% (27 of 27)
lib/core/url_utils.dart 100.0% (4 of 4)
lib/presentation/pages/editor/chapter_panel.dart 100.0% (55 of 55)
lib/presentation/pages/editor/chapter_title_dialog.dart 100.0% (27 of 27)
lib/presentation/widgets/cover_thumbnail.dart 83.3% (30 of 36)
lib/presentation/pages/editor/upload_panel.dart 38.6% (61 of 158)
lib/presentation/pages/editor/variant_dialog.dart 0.0% (0 of 61)
lib/presentation/widgets/language_dropdown.dart 84.6% (11 of 13)
lib/presentation/pages/reader/reader_page.dart 89.4% (236 of 264)
lib/presentation/pages/reader/reader_overlay.dart 94.4% (51 of 54)
lib/presentation/pages/reader/reader_sequence.dart 100.0% (16 of 16)
lib/presentation/widgets/star_rating.dart 100.0% (72 of 72)
lib/app/di.dart 48.3% (14 of 29)
lib/presentation/assistant/assistant_panel.dart 91.3% (84 of 92)
lib/presentation/layout/main_layout.dart 86.3% (44 of 51)
lib/data/api_client.dart 92.8% (64 of 69)
lib/data/repositories/doujin_api_repository.dart 37.5% (163 of 435)
lib/data/repositories/health_repository.dart 87.0% (47 of 54)
lib/data/secure_storage.dart 0.0% (0 of 26)
lib/core/theme.dart 96.9% (31 of 32)
lib/presentation/assistant/approval_card.dart 95.0% (38 of 40)
lib/presentation/assistant/assistant_markdown.dart 100.0% (4 of 4)
lib/presentation/assistant/chat_entries.dart 87.5% (35 of 40)
lib/presentation/pages/people/people_page.dart 55.6% (10 of 18)
lib/presentation/widgets/entity_editor.dart 91.6% (131 of 143)
lib/presentation/widgets/entity_management_page.dart 86.8% (217 of 250)
lib/presentation/pages/editor/editor_page.dart 77.6% (59 of 76)
lib/presentation/pages/editor/editor_pane.dart 69.8% (37 of 53)
lib/presentation/pages/editor/association_picker.dart 95.5% (106 of 111)
lib/presentation/pages/editor/associations_tab.dart 73.8% (90 of 122)
lib/presentation/pages/editor/doujin_list_pane.dart 67.2% (43 of 64)
lib/presentation/pages/editor/edit_title_dialog.dart 91.7% (55 of 60)
lib/presentation/pages/editor/new_doujin_dialog.dart 66.7% (30 of 45)
lib/presentation/pages/editor/metadata_tab.dart 70.5% (93 of 132)
lib/presentation/pages/characters/characters_page.dart 57.9% (11 of 19)
lib/presentation/pages/tags/tags_page.dart 100.0% (17 of 17)
lib/presentation/pages/detail/detail_page.dart 86.5% (186 of 215)
lib/presentation/pages/detail/variant_tabs_panel.dart 97.2% (239 of 246)
lib/data/offline/filesystem_offline_store.dart 94.1% (144 of 153)
lib/core/offline_paths.dart 100.0% (3 of 3)
lib/presentation/pages/offline/offline_page.dart 98.2% (164 of 167)
lib/app/app.dart 67.1% (47 of 70)
lib/presentation/pages/settings/settings_page.dart 99.3% (287 of 289)
lib/presentation/pages/circles/circles_page.dart 52.6% (10 of 19)
lib/presentation/pages/duplicates/duplicates_page.dart 100.0% (157 of 157)
lib/presentation/pages/library/library_page.dart 93.1% (215 of 231)
lib/presentation/pages/series/series_page.dart 50.0% (9 of 18)
lib/presentation/widgets/smart_filter_bar.dart 84.1% (207 of 246)
lib/presentation/pages/settings/server_sections.dart 99.3% (557 of 561)
lib/presentation/pages/settings/settings_layout.dart 100.0% (78 of 78)
lib/presentation/widgets/model_combo_field.dart 68.5% (98 of 143)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 83.0% (9024 of 10876)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/app/store.dart | 95.0% (19 of 20) | | lib/data/models/doujin_models.dart | 86.4% (38 of 44) | | lib/data/models/doujin_models.g.dart | 61.7% (195 of 316) | | lib/domain/entities/stored_settings.dart | 100.0% (9 of 9) | | lib/domain/repositories/repositories.dart | 33.3% (1 of 3) | | lib/presentation/state/app_state.dart | 84.6% (33 of 39) | | lib/agent/agent_service.dart | 88.7% (344 of 388) | | lib/agent/approval_gate.dart | 100.0% (16 of 16) | | lib/agent/assistant_context.dart | 52.8% (28 of 53) | | lib/agent/browser_budget.dart | 80.0% (4 of 5) | | lib/agent/caching_describer.dart | 100.0% (13 of 13) | | lib/agent/memory_store.dart | 92.9% (13 of 14) | | lib/agent/skills/skill_registry.dart | 98.2% (54 of 55) | | lib/agent/system_prompt.dart | 100.0% (69 of 69) | | lib/agent/tools/budgeted_browser_tool.dart | 84.2% (16 of 19) | | lib/agent/tools/doujin_write_tool.dart | 86.4% (319 of 369) | | lib/agent/tools/entity_write_tool.dart | 73.5% (164 of 223) | | lib/agent/tools/fetch_page_tool.dart | 89.3% (67 of 75) | | lib/agent/tools/get_doujin_tool.dart | 84.4% (27 of 32) | | lib/agent/tools/list_entities_tool.dart | 76.1% (54 of 71) | | lib/agent/tools/navigate_tool.dart | 95.1% (39 of 41) | | lib/agent/tools/read_skill_tool.dart | 82.4% (14 of 17) | | lib/agent/tools/reflection_tools.dart | 75.7% (28 of 37) | | lib/agent/tools/search_doujins_tool.dart | 100.0% (99 of 99) | | lib/agent/tools/view_images_tool.dart | 95.0% (38 of 40) | | lib/domain/entities/assistant_entry.dart | 20.0% (1 of 5) | | lib/presentation/state/actions/assistant_actions.dart | 54.5% (6 of 11) | | lib/core/app_config.dart | 70.0% (7 of 10) | | lib/domain/entities/entity_model.dart | 100.0% (1 of 1) | | lib/data/repositories/entity_in_use_exception.dart | 33.3% (1 of 3) | | lib/data/models/search_query.dart | 50.0% (2 of 4) | | lib/data/models/search_query.g.dart | 33.3% (25 of 75) | | lib/core/constants.dart | 40.0% (4 of 10) | | lib/data/offline/offline_download_service.dart | 93.0% (66 of 71) | | lib/data/reading_progress_store.dart | 92.3% (36 of 39) | | lib/domain/entities/reading_progress.dart | 100.0% (26 of 26) | | lib/presentation/middleware/assistant_epics.dart | 83.1% (74 of 89) | | lib/presentation/middleware/backup_save_dialog.dart | 25.0% (1 of 4) | | lib/presentation/middleware/epics.dart | 91.5% (388 of 424) | | lib/presentation/middleware/offline_epics.dart | 98.4% (60 of 61) | | lib/presentation/state/reducers.dart | 100.0% (14 of 14) | | lib/core/chunking.dart | 100.0% (12 of 12) | | lib/core/code_point_compare.dart | 100.0% (8 of 8) | | lib/core/languages.dart | 100.0% (8 of 8) | | lib/core/reading_order.dart | 100.0% (22 of 22) | | lib/core/title_display.dart | 90.0% (9 of 10) | | lib/core/variant_display.dart | 100.0% (22 of 22) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 53.7% (36 of 67) | | lib/data/models/server_models.dart | 97.9% (47 of 48) | | lib/data/models/server_models.g.dart | 55.2% (90 of 163) | | lib/domain/entities/offline_manifest.dart | 100.0% (78 of 78) | | lib/data/repositories/backup_exception.dart | 100.0% (3 of 3) | | lib/data/repositories/upload_exception.dart | 33.3% (1 of 3) | | lib/domain/entities/filter_token.dart | 92.2% (107 of 116) | | lib/presentation/middleware/duplicates_epics.dart | 100.0% (39 of 39) | | lib/presentation/middleware/editor_epics.dart | 57.3% (168 of 293) | | lib/presentation/middleware/entity_ops.dart | 58.0% (40 of 69) | | lib/presentation/middleware/server_epics.dart | 100.0% (97 of 97) | | lib/presentation/middleware/upload_epics.dart | 98.1% (104 of 106) | | lib/presentation/state/actions/offline_actions.dart | 91.7% (11 of 12) | | lib/presentation/state/actions/detail_actions.dart | 85.7% (6 of 7) | | lib/presentation/state/actions/duplicates_actions.dart | 83.3% (5 of 6) | | lib/presentation/state/actions/editor_actions.dart | 56.8% (21 of 37) | | lib/presentation/state/actions/entity_actions.dart | 62.5% (10 of 16) | | lib/presentation/state/actions/library_actions.dart | 50.0% (11 of 22) | | lib/presentation/state/actions/metadata_actions.dart | 100.0% (5 of 5) | | lib/presentation/state/actions/reader_actions.dart | 75.0% (3 of 4) | | lib/presentation/state/actions/reading_progress_actions.dart | 100.0% (2 of 2) | | lib/presentation/state/actions/server_actions.dart | 75.0% (21 of 28) | | lib/presentation/state/actions/settings_actions.dart | 87.5% (7 of 8) | | lib/presentation/state/actions/upload_actions.dart | 90.9% (10 of 11) | | lib/presentation/state/reducers/assistant_reducer.dart | 95.0% (57 of 60) | | lib/presentation/state/reducers/detail_reducer.dart | 100.0% (24 of 24) | | lib/presentation/state/reducers/duplicates_reducer.dart | 100.0% (38 of 38) | | lib/presentation/state/reducers/editor_reducer.dart | 97.1% (99 of 102) | | lib/presentation/state/reducers/entity_reducer.dart | 98.8% (82 of 83) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (135 of 135) | | lib/presentation/state/reducers/metadata_reducer.dart | 90.5% (38 of 42) | | lib/presentation/state/reducers/offline_reducer.dart | 100.0% (61 of 61) | | lib/presentation/state/reducers/reader_reducer.dart | 100.0% (18 of 18) | | lib/presentation/state/reducers/reading_progress_reducer.dart | 100.0% (20 of 20) | | lib/presentation/state/reducers/server_reducer.dart | 100.0% (129 of 129) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (61 of 61) | | lib/presentation/state/reducers/upload_reducer.dart | 100.0% (45 of 45) | | lib/presentation/pages/editor/page_grid.dart | 92.8% (309 of 333) | | lib/presentation/pages/editor/variants_tab.dart | 55.3% (52 of 94) | | lib/core/natural_sort.dart | 100.0% (27 of 27) | | lib/core/url_utils.dart | 100.0% (4 of 4) | | lib/presentation/pages/editor/chapter_panel.dart | 100.0% (55 of 55) | | lib/presentation/pages/editor/chapter_title_dialog.dart | 100.0% (27 of 27) | | lib/presentation/widgets/cover_thumbnail.dart | 83.3% (30 of 36) | | lib/presentation/pages/editor/upload_panel.dart | 38.6% (61 of 158) | | lib/presentation/pages/editor/variant_dialog.dart | 0.0% (0 of 61) | | lib/presentation/widgets/language_dropdown.dart | 84.6% (11 of 13) | | lib/presentation/pages/reader/reader_page.dart | 89.4% (236 of 264) | | lib/presentation/pages/reader/reader_overlay.dart | 94.4% (51 of 54) | | lib/presentation/pages/reader/reader_sequence.dart | 100.0% (16 of 16) | | lib/presentation/widgets/star_rating.dart | 100.0% (72 of 72) | | lib/app/di.dart | 48.3% (14 of 29) | | lib/presentation/assistant/assistant_panel.dart | 91.3% (84 of 92) | | lib/presentation/layout/main_layout.dart | 86.3% (44 of 51) | | lib/data/api_client.dart | 92.8% (64 of 69) | | lib/data/repositories/doujin_api_repository.dart | 37.5% (163 of 435) | | lib/data/repositories/health_repository.dart | 87.0% (47 of 54) | | lib/data/secure_storage.dart | 0.0% (0 of 26) | | lib/core/theme.dart | 96.9% (31 of 32) | | lib/presentation/assistant/approval_card.dart | 95.0% (38 of 40) | | lib/presentation/assistant/assistant_markdown.dart | 100.0% (4 of 4) | | lib/presentation/assistant/chat_entries.dart | 87.5% (35 of 40) | | lib/presentation/pages/people/people_page.dart | 55.6% (10 of 18) | | lib/presentation/widgets/entity_editor.dart | 91.6% (131 of 143) | | lib/presentation/widgets/entity_management_page.dart | 86.8% (217 of 250) | | lib/presentation/pages/editor/editor_page.dart | 77.6% (59 of 76) | | lib/presentation/pages/editor/editor_pane.dart | 69.8% (37 of 53) | | lib/presentation/pages/editor/association_picker.dart | 95.5% (106 of 111) | | lib/presentation/pages/editor/associations_tab.dart | 73.8% (90 of 122) | | lib/presentation/pages/editor/doujin_list_pane.dart | 67.2% (43 of 64) | | lib/presentation/pages/editor/edit_title_dialog.dart | 91.7% (55 of 60) | | lib/presentation/pages/editor/new_doujin_dialog.dart | 66.7% (30 of 45) | | lib/presentation/pages/editor/metadata_tab.dart | 70.5% (93 of 132) | | lib/presentation/pages/characters/characters_page.dart | 57.9% (11 of 19) | | lib/presentation/pages/tags/tags_page.dart | 100.0% (17 of 17) | | lib/presentation/pages/detail/detail_page.dart | 86.5% (186 of 215) | | lib/presentation/pages/detail/variant_tabs_panel.dart | 97.2% (239 of 246) | | lib/data/offline/filesystem_offline_store.dart | 94.1% (144 of 153) | | lib/core/offline_paths.dart | 100.0% (3 of 3) | | lib/presentation/pages/offline/offline_page.dart | 98.2% (164 of 167) | | lib/app/app.dart | 67.1% (47 of 70) | | lib/presentation/pages/settings/settings_page.dart | 99.3% (287 of 289) | | lib/presentation/pages/circles/circles_page.dart | 52.6% (10 of 19) | | lib/presentation/pages/duplicates/duplicates_page.dart | 100.0% (157 of 157) | | lib/presentation/pages/library/library_page.dart | 93.1% (215 of 231) | | lib/presentation/pages/series/series_page.dart | 50.0% (9 of 18) | | lib/presentation/widgets/smart_filter_bar.dart | 84.1% (207 of 246) | | lib/presentation/pages/settings/server_sections.dart | 99.3% (557 of 561) | | lib/presentation/pages/settings/settings_layout.dart | 100.0% (78 of 78) | | lib/presentation/widgets/model_combo_field.dart | 68.5% (98 of 143) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 83.0% (9024 of 10876)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! The client finally listens to what the server is telling it~ ♡ A 503 that used to collapse into Server returned HTTP 503 now names the exact volume, its path, and the refusing party's own words — one line per refusal. That's the difference between "something is broken" and "go fix your CIFS mount on the NAS." This is what diagnostics are for~ ♪

Verdict: ✅ Looks good to me~

Contract verification. I read both ends of the wire myself: HealthDtos.cs (name/path/writable/detail, camelCase via JsonPropertyName) and DEPLOYMENT.md §7's table on the db volume. _kSqliteDetailPrefixes = ['SQLite Error', 'journal_mode stayed'] matches §7's documented shapes exactly, and the db-alone hosting distinction mirrors the DTO doc's "just hosted a SQLite database there" wording. The #112-era detail-less body (git show fc50854 — three fields, no detail) is handled without a dangling colon, and the PR-body claim that pre-#112 servers never answered 503 holds: the 503 came with #112 itself. ♡

Layering. Textbook: typed domain exception (ServerNotReadyException) in the domain layer, thrown by the data layer, mapped in the epic — the interface stays Future<bool> and no caller parses text. Redux threading is complete: serverNotReady is reset in all three arms (TestConnectionAction, SaveSettingsAction, result action), so a stale amber banner can't survive a new test or a save. The regenerated freezed file is committed with the field threaded through equality/hashCode/toString/copyWith — no drift.

Fail-safe parsing. Every malformed-shape arm returns the generic "reached, answered 503, no volume named" message rather than crashing: non-Map bodies, non-List directories, text/html proxy pages, nothing-refusing bodies, entries missing name/path. I probed this personally — see below~

Mutation probes (mine, independent, at head b6ea1e6):

  1. SQLite-hosting ternary collapsed to plain wording → a db volume SQLite refused cannot host the database went red. ✓
  2. name == _kDbDirectory conjunct removed (any volume's SQLite words get host wording) → only the db volume can fail to host the database went red. ✓
  3. Epic's on ServerNotReadyException arm removed → exactly 3 red in settings_connection_test.dart. ✓ (My first run of this probe stayed green — that was CPU contention from a concurrent full-suite run in my sandbox, not the code; clean rerun went red exactly as the PR body documents. I re-verified after restoring.)
  4. The disclosed-unpinned detail != null && conjunct: I tried to remove it — it doesn't compile (String? null-safety at detail.startsWith). The residual is type-system-pinned. Fufu, that's a lovely property~
  5. The disclosed-unpinned entry is Map guard: removed it and fed a "not-a-map" string element → type 'String' is not a subtype of type 'int' of 'index' — confirming the guard is what keeps garbage elements from crashing the parse. It's genuinely defensive, not dead code.

Tree restored pristine after every probe (git status 0 changes).

💡 Little ideas (non-blocking)~

  1. health_repository.dart:113 (clauses.skip(1)) — the "It also …" continuation lines are worded for a body-text banner but joined with \n; if InlineBanner ever hard-wraps oddly on narrow windows the multi-line shape is the place to look. Purely speculative polish, no action needed.
  2. BACKLOG follow-up already parked — the Exception: Could not connect to … prefix leak in _testConnectionEpic's generic catch sits right next to this code and is now more visible next to the worded banner; the parked backlog item is the right home for it. Next to the new amber banner the prefix does stand out a little, but it's pre-existing and documented.

✅ What I liked~

  • Verification discipline that borders on the obsessive — a 17-mutation probe table in the PR body, with two honestly-disclosed unpinned residuals. Both residuals turned out to be benign: one compile-pinned, one crash-guard. I checked because I don't take "unverified" on faith~ ♡
  • The widget test that asserts a warning and an error actually differ in icon and color — not just that each renders. That's the test most authors never write.
  • The fake adapter got a contentType knob precisely so the undecoded-String path (no content type) is exercised against the real dio decode pipeline. Testing the transport reality, not a fantasy.
  • DEPLOYMENT.md §7 is read, not changed; ADR 0010's contract is honored; no new env vars; README's one-sentence amendment is exactly the right size. Scope control is immaculate.
  • Coverage confirms the tests bite: settings_reducer.dart 100%, settings_layout.dart 100%, settings_page.dart 99.3%, health_repository.dart 87.0% (the 7 uncovered lines are the pre-existing transport-error arms, untouched by this PR).

Automated review by Jibril · 2026-09-18 20:37 CEST
CI/CD: passed for head b6ea1e6 (analyze + 997 tests + coverage comment 8094) · Local checks: flutter analyze clean; mutation probes 5/5 at head; full local suite deferred to CI (sandbox CPU contention made local timing unrepresentative)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! The client finally *listens* to what the server is telling it~ ♡ A 503 that used to collapse into `Server returned HTTP 503` now names the exact volume, its path, and the refusing party's own words — one line per refusal. That's the difference between "something is broken" and "go fix your CIFS mount on the NAS." This is what diagnostics are *for*~ ♪ ### Verdict: ✅ Looks good to me~ **Contract verification.** I read both ends of the wire myself: `HealthDtos.cs` (`name`/`path`/`writable`/`detail`, camelCase via `JsonPropertyName`) and DEPLOYMENT.md §7's table on the `db` volume. `_kSqliteDetailPrefixes = ['SQLite Error', 'journal_mode stayed']` matches §7's documented shapes exactly, and the `db`-alone hosting distinction mirrors the DTO doc's "just hosted a SQLite database there" wording. The #112-era detail-less body (`git show fc50854` — three fields, no `detail`) is handled without a dangling colon, and the PR-body claim that pre-#112 servers never answered 503 holds: the 503 came with #112 itself. ♡ **Layering.** Textbook: typed domain exception (`ServerNotReadyException`) in the domain layer, thrown by the data layer, mapped in the epic — the interface stays `Future<bool>` and no caller parses text. Redux threading is complete: `serverNotReady` is reset in all three arms (`TestConnectionAction`, `SaveSettingsAction`, result action), so a stale amber banner can't survive a new test or a save. The regenerated freezed file is committed with the field threaded through equality/hashCode/toString/copyWith — no drift. **Fail-safe parsing.** Every malformed-shape arm returns the generic "reached, answered 503, no volume named" message rather than crashing: non-Map bodies, non-List directories, text/html proxy pages, nothing-refusing bodies, entries missing `name`/`path`. I probed this personally — see below~ **Mutation probes (mine, independent, at head `b6ea1e6`):** 1. SQLite-hosting ternary collapsed to plain wording → `a db volume SQLite refused cannot host the database` went red. ✓ 2. `name == _kDbDirectory` conjunct removed (any volume's SQLite words get host wording) → `only the db volume can fail to host the database` went red. ✓ 3. Epic's `on ServerNotReadyException` arm removed → exactly 3 red in `settings_connection_test.dart`. ✓ (My first run of this probe stayed green — that was CPU contention from a concurrent full-suite run in my sandbox, not the code; clean rerun went red exactly as the PR body documents. I re-verified after restoring.) 4. The disclosed-unpinned `detail != null &&` conjunct: I tried to remove it — **it doesn't compile** (`String?` null-safety at `detail.startsWith`). The residual is type-system-pinned. Fufu, that's a lovely property~ 5. The disclosed-unpinned `entry is Map` guard: removed it and fed a `"not-a-map"` string element → `type 'String' is not a subtype of type 'int' of 'index'` — confirming the guard is what keeps garbage elements from crashing the parse. It's genuinely defensive, not dead code. Tree restored pristine after every probe (`git status` 0 changes). #### 💡 Little ideas (non-blocking)~ 1. **health_repository.dart:113 (`clauses.skip(1)`)** — the "It also …" continuation lines are worded for a body-text banner but joined with `\n`; if `InlineBanner` ever hard-wraps oddly on narrow windows the multi-line shape is the place to look. Purely speculative polish, no action needed. 2. **BACKLOG follow-up already parked** — the `Exception: Could not connect to …` prefix leak in `_testConnectionEpic`'s generic catch sits right next to this code and is now *more* visible next to the worded banner; the parked backlog item is the right home for it. Next to the new amber banner the prefix does stand out a little, but it's pre-existing and documented. #### ✅ What I liked~ - **Verification discipline that borders on the obsessive** — a 17-mutation probe table in the PR body, with two honestly-disclosed unpinned residuals. Both residuals turned out to be benign: one compile-pinned, one crash-guard. I checked because I don't take "unverified" on faith~ ♡ - The widget test that asserts a warning and an error **actually differ** in icon and color — not just that each renders. That's the test most authors never write. - The fake adapter got a `contentType` knob precisely so the undecoded-String path (no content type) is exercised against the real dio decode pipeline. Testing the transport reality, not a fantasy. - `DEPLOYMENT.md §7` is read, not changed; ADR 0010's contract is honored; no new env vars; README's one-sentence amendment is exactly the right size. Scope control is immaculate. - Coverage confirms the tests bite: `settings_reducer.dart` 100%, `settings_layout.dart` 100%, `settings_page.dart` 99.3%, `health_repository.dart` 87.0% (the 7 uncovered lines are the pre-existing transport-error arms, untouched by this PR). --- *Automated review by Jibril · 2026-09-18 20:37 CEST* *CI/CD: passed for head `b6ea1e6` (analyze + 997 tests + coverage comment 8094) · Local checks: flutter analyze clean; mutation probes 5/5 at head; full local suite deferred to CI (sandbox CPU contention made local timing unrepresentative)*
bjoern merged commit c5efc1cbb9 into main 2026-09-18 23:14:18 +02:00
bjoern deleted branch fix/connection-test-names-the-volume-that-refused 2026-09-18 23:14:19 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!143
No description provided.