feat: the backups section says which backup would actually restore, and can move one #130

Merged
bjoern merged 2 commits from feat/backup-restore-ui into main 2026-09-10 17:39:40 +02:00
Member

4 of 4, and the last. #127, #128 and #129 are merged; this is rebased on main and the server it talks to is now the one main builds.

What's in

The client half of the incident. The app listed a 0-byte file the server had never opened as a restore candidate, with nothing on the row to tell it from a good backup. It also could not get a backup off the machine, could not put one back, and said nothing about images not being in a backup at all.

The verdict is on every row. Verified / Not checked / Will not load, with what the server actually checked in the tooltip — integrity_check: ok, the schema migration, how many doujins the backup holds, when it was checked — or the server's own reason for the other two. verification.status is the one enum in this codebase that travels as a string, so an unfamiliar word decodes as unverified, never as anything reassuring; and verification is modelled optional so a listing from a server that predates it still parses and honestly reads "not checked" rather than failing the whole section.

A backup can now leave and come back. Download to a file you pick (dio deletes a partial download, so a transfer that dies leaves nothing that looks like a backup); upload a local database file under the name it already has. Every refusal shows the server's own sentence — the one that names the required file-name shape, or says SQLite Error 26: 'file is not a database' — because that is the sentence that tells the operator what to do next.

Restore, and it is treated as the destructive thing it is. The dialog names the file, prints "512 doujins now → 3 doujins after the restore" when both numbers are known, says the server keeps the database it replaces (undoable on the server, not from this app), says images and thumbnails are not in a backup and live on their own volume, spells out that restoring against a different image volume leaves a full catalogue of missing images, warns when nothing has checked the file, and says nothing happens until you restart the server yourself. The confirm button is dead until you type restore. The duplicates page's member delete was the bar; this is a whole library rather than one doujin, so it also gets the typed word and the figures.

While a restore is pending the section says so above the list — target path, file, count, when it was requested, that the instance is still serving the library it has — and offers to cancel. A second restore is refused, and so is deleting the file the server is waiting for.

One thing that was quietly broken. Every request had the client-wide 30 s receiveTimeout. The create in the incident took 30,193 ms — at that ceiling the app calls it "Server unreachable" while the backup is still being written, and the operator retries it onto a database already under contention. Backup requests now get ten minutes. Not a DOUJIN_MANAGER_* value: it is a client ceiling, and ADR 0023 keeps those off the server.

Verification

  • The wire, against a real server, not against the contract text: I ran the backend on :5132 and drove the real DoujinApiRepository against it. All five new methods, both verdict arms (I deleted a .verification.json receipt to see unverified come back off real bytes), and all three refusal statuses — 400 for a name the server will not take, 400 for a file that is not a database, 409 for a name already there — each carrying the server's own detail through to the string the notice banner prints.
  • UI: 18 widget tests through the real store and the real widgets, bounded pumps, including one at 700 px with a pending restore — that one fails on a render overflow, which is the failure mode the others cannot catch.
  • Redux: 18 reducer tests through appReducer, 12 epic tests through createStore (which is what pins the new epics' registration).
  • Wire/parse: 13 repository tests over a capturing dio adapter, 6 model tests.
  • flutter analyze clean, 926 app tests pass, generated files regenerated and committed.
  • Mutation-probed, 24 branches, each flipped and each reverted. Worth naming: forcing the unknown-status fallback to verified reddens exactly one test, which is what makes "an unfamiliar verdict is not treated as good news" a claim rather than a comment; and the narrow-layout test was written because a wide-only fixture would have been green on a row that overflows.
  • Not verified: the app has not been opened as a window. It persists the server URL and token in the owner's secure storage, so launching it here would either overwrite their real connection or point these buttons at their production NAS. The widget tests render the real widgets at two sizes and Flutter fails a frame on overflow, but "I have seen this window" is not something I can claim.
  • Backend untouched — no .cs file and no backend/openapi/v1.json change.

What the review round added (282df30)

Seven branches the first commit reached with no test, plus one test whose name promised more than it pinned. Two are worth reading:

  • The null-verdict reducer arm could not be tested at all. The fixture resolved verification ?? verified(), so passing null silently returned a verified verdict — my first attempt at the test failed for exactly that reason. The helper grew a withoutVerdict flag.
  • My "a 500 is rethrown, not dressed as a refusal" test first went through createBackup(), which does not route through _rejected. It would have been green whatever that arm did.

Also escapes two raw NUL bytes in server_api_repository_test.dart (the SQLite header magic written literally). file called it data and grep silently matched nothing in it, so any search over that file returned empty.

Follow-ups

In docs/BACKLOG.md (App):

  • No progress on a transfer — a multi-gigabyte download or upload is a spinner and nothing else. The shape already exists (UploadProgress on the port, used by page uploads).
  • A pending restore is only visible inside the Backups section; the sidebar marks sections with unsaved edits but not one holding a scheduled database replacement. A marker would fit the sidebar #125 built, which this PR deliberately left alone.
  • Nothing here can restart the server, so a scheduled restore ends with "restart it yourself". Deliberate — but the app is the only place that knows a restore is pending.
  • A backup cannot be promoted from Not checked to Verified from the app: the server has no "check this one, change nothing" route yet (its own backlog item). When it grows one, the badge wants a "Check now".
  • The schedule #129 just shipped has no UI. This branch was cut before that route existed, so GET /api/backups/schedule is not read. The backlog carries #129's own version of that item, which is the better-written one — the part that has to be loud is lastRun.result == "failed", since it is the only place a client can learn the nightly backup has stopped working.

🤖 Generated with Claude Code

> **4 of 4, and the last.** #127, #128 and #129 are merged; this is rebased on `main` and the server it talks to is now the one `main` builds. ## What's in The client half of the incident. The app listed a 0-byte file the server had never opened as a restore candidate, with nothing on the row to tell it from a good backup. It also could not get a backup off the machine, could not put one back, and said nothing about images not being in a backup at all. **The verdict is on every row.** `Verified` / `Not checked` / `Will not load`, with what the server actually checked in the tooltip — `integrity_check: ok`, the schema migration, how many doujins the backup holds, when it was checked — or the server's own reason for the other two. `verification.status` is the one enum in this codebase that travels as a string, so an unfamiliar word decodes as **unverified**, never as anything reassuring; and `verification` is modelled optional so a listing from a server that predates it still parses and honestly reads "not checked" rather than failing the whole section. **A backup can now leave and come back.** Download to a file you pick (dio deletes a partial download, so a transfer that dies leaves nothing that looks like a backup); upload a local database file under the name it already has. Every refusal shows the server's own sentence — the one that names the required file-name shape, or says `SQLite Error 26: 'file is not a database'` — because that is the sentence that tells the operator what to do next. **Restore, and it is treated as the destructive thing it is.** The dialog names the file, prints *"512 doujins now → 3 doujins after the restore"* when both numbers are known, says the server keeps the database it replaces (undoable on the server, not from this app), says images and thumbnails are **not** in a backup and live on their own volume, spells out that restoring against a different image volume leaves a full catalogue of missing images, warns when nothing has checked the file, and says nothing happens until you restart the server yourself. The confirm button is dead until you type `restore`. The duplicates page's member delete was the bar; this is a whole library rather than one doujin, so it also gets the typed word and the figures. While a restore is pending the section says so above the list — target path, file, count, when it was requested, that the instance is still serving the library it has — and offers to cancel. A second restore is refused, and so is deleting the file the server is waiting for. **One thing that was quietly broken.** Every request had the client-wide 30 s `receiveTimeout`. The create in the incident took 30,193 ms — at that ceiling the app calls it "Server unreachable" while the backup is still being written, and the operator retries it onto a database already under contention. Backup requests now get ten minutes. Not a `DOUJIN_MANAGER_*` value: it is a client ceiling, and ADR 0023 keeps those off the server. ## Verification - **The wire, against a real server**, not against the contract text: I ran the backend on :5132 and drove the real `DoujinApiRepository` against it. All five new methods, both verdict arms (I deleted a `.verification.json` receipt to see `unverified` come back off real bytes), and all three refusal statuses — 400 for a name the server will not take, 400 for a file that is not a database, 409 for a name already there — each carrying the server's own `detail` through to the string the notice banner prints. - **UI**: 18 widget tests through the real store and the real widgets, bounded pumps, including one at 700 px with a pending restore — that one fails on a render overflow, which is the failure mode the others cannot catch. - **Redux**: 18 reducer tests through `appReducer`, 12 epic tests through `createStore` (which is what pins the new epics' registration). - **Wire/parse**: 13 repository tests over a capturing dio adapter, 6 model tests. - `flutter analyze` clean, **926 app tests pass**, generated files regenerated and committed. - **Mutation-probed, 24 branches**, each flipped and each reverted. Worth naming: forcing the unknown-status fallback to `verified` reddens exactly one test, which is what makes "an unfamiliar verdict is not treated as good news" a claim rather than a comment; and the narrow-layout test was written *because* a wide-only fixture would have been green on a row that overflows. - **Not verified**: the app has not been opened as a window. It persists the server URL and token in the owner's secure storage, so launching it here would either overwrite their real connection or point these buttons at their production NAS. The widget tests render the real widgets at two sizes and Flutter fails a frame on overflow, but "I have seen this window" is not something I can claim. - Backend untouched — no `.cs` file and no `backend/openapi/v1.json` change. ## What the review round added (`282df30`) Seven branches the first commit reached with no test, plus one test whose name promised more than it pinned. Two are worth reading: - **The null-verdict reducer arm could not be tested at all.** The fixture resolved `verification ?? verified()`, so passing null silently returned a verified verdict — my first attempt at the test failed for exactly that reason. The helper grew a `withoutVerdict` flag. - **My "a 500 is rethrown, not dressed as a refusal" test first went through `createBackup()`, which does not route through `_rejected`.** It would have been green whatever that arm did. Also escapes two raw NUL bytes in `server_api_repository_test.dart` (the SQLite header magic written literally). `file` called it `data` and grep silently matched nothing in it, so any search over that file returned empty. ## Follow-ups In `docs/BACKLOG.md` (App): - No progress on a transfer — a multi-gigabyte download or upload is a spinner and nothing else. The shape already exists (`UploadProgress` on the port, used by page uploads). - A pending restore is only visible inside the Backups section; the sidebar marks sections with unsaved *edits* but not one holding a scheduled database replacement. A marker would fit the sidebar #125 built, which this PR deliberately left alone. - Nothing here can restart the server, so a scheduled restore ends with "restart it yourself". Deliberate — but the app is the only place that knows a restore is pending. - A backup cannot be promoted from **Not checked** to **Verified** from the app: the server has no "check this one, change nothing" route yet (its own backlog item). When it grows one, the badge wants a "Check now". - **The schedule #129 just shipped has no UI.** This branch was cut before that route existed, so `GET /api/backups/schedule` is not read. The backlog carries #129's own version of that item, which is the better-written one — the part that has to be loud is `lastRun.result == "failed"`, since it is the only place a client can learn the nightly backup has stopped working. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: the backups section says which backup would actually restore, and can move one
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 4m14s
26d449e0cc
The failure this closes on the client side: the app listed a 0-byte file the
server had never opened as a restore candidate, with nothing to tell it apart
from a good backup. It also could not download one, could not put one back,
and said nothing about images not being in a backup at all.

Every row now carries the server's verdict — Verified / Not checked / Will not
load — with what was checked (integrity_check, schema, doujin count, when) in
its tooltip. `verification.status` travels as a string, so an unfamiliar value
decodes as unverified rather than as anything reassuring, and a listing from a
server that predates the field still parses and reads "not checked".

Transfers: download to a chosen local file (dio deletes a partial download),
upload a local database file under the name it already has, and a restore that
the server applies on its next start. Restore is the destructive one: it needs
the word `restore` typed into a dialog that names the file, compares the live
library's doujin count with the backup's, says the server keeps the database it
replaces, says images and thumbnails are NOT part of a backup and live on their
own volume, and says nothing happens until the server is restarted. While a
restore is pending the section says so, offers to cancel it, and refuses both a
second restore and the delete of the file the server is waiting for.

Every backup request also gets ten minutes instead of the client-wide 30 s.
The incident's create took 30,193 ms; at the old timeout the app would have
called that "server unreachable" while the backup was still being written.

Built inside the settings shell #125 established — the section's toolbar,
cards and placeholders are unchanged in shape.

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/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/epics.dart 91.3% (380 of 416)
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.5% (39 of 40)
lib/data/models/server_models.g.dart 54.5% (67 of 123)
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% (90 of 90)
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 42.9% (9 of 21)
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% (18 of 24)
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% (122 of 122)
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% (115 of 115)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (60 of 60)
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 36.3% (155 of 427)
lib/data/repositories/health_repository.dart 72.0% (18 of 25)
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% (3 of 3)
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 80.1% (173 of 216)
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% (290 of 292)
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 98.5% (463 of 470)
lib/presentation/pages/settings/settings_layout.dart 100.0% (77 of 77)
lib/presentation/widgets/model_combo_field.dart 68.5% (98 of 143)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 82.7% (8800 of 10647)

<!-- 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/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/epics.dart | 91.3% (380 of 416) | | 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.5% (39 of 40) | | lib/data/models/server_models.g.dart | 54.5% (67 of 123) | | 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% (90 of 90) | | 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 | 42.9% (9 of 21) | | 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% (18 of 24) | | 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% (122 of 122) | | 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% (115 of 115) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (60 of 60) | | 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 | 36.3% (155 of 427) | | lib/data/repositories/health_repository.dart | 72.0% (18 of 25) | | 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% (3 of 3) | | 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 | 80.1% (173 of 216) | | 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% (290 of 292) | | 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 | 98.5% (463 of 470) | | lib/presentation/pages/settings/settings_layout.dart | 100.0% (77 of 77) | | lib/presentation/widgets/model_combo_field.dart | 68.5% (98 of 143) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 82.7% (8800 of 10647)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! The backups section finally tells the truth about what would actually restore~ A verdict on every row, an unknown word decoding to unverified instead of something reassuring, a typed-word wall in front of the most destructive button this app has, and an honest "restart it yourself" instead of pretending… fufu~, this is the client half of an incident report turned into a feature, and it reads like one. I checked every claim against the diff and ran the whole suite myself — 919/919 green, analyze clean. The architecture made me genuinely happy~

But then I counted branches. ♡-n-era… you know what I found.

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. app/lib/data/repositories/doujin_api_repository.dart — _rejected has four untested arms. Your own doc comment promises "anything that is not the server explaining itself (a timeout, a 500, a dead connection) is rethrown as it came" — and the committed suite never once proves it. The refusal tests cover exactly two of the predicate's statuses (400 with detail, 409 with detail). Never exercised: the 404 arm, the 413 arm (your own doc comment on BackupRejectedException lists "a body over the upload ceiling" as a case this class exists for!), the fallback string '$fallbackPrefix (HTTP $status).' — every refusal test sends a body with detail, so _problemDetail never returns null in a test — and the return e rethrow for a 500/timeout. That's four distinct runtime paths behind one helper, dark. The PR body's live-server verification names 400/400/409 only; it is not in the repo, and it cannot pin these arms either.
    Fix: four small tests with the _CapturingAdapter pattern already in server_api_repository_test.dart — 404 and 413 asserting BackupRejectedException.statusCode; one 400 without a detail key asserting the fallback prefix appears; one 500 asserting the DioException escapes unwrapped (that's the arm behind "rethrown as it came").

  2. app/lib/presentation/middleware/server_epics.dart — _loadRestoreRequestEpic's catch arm has no test, and its stub hook is dead code. Fufu~ you even wrote the hook: throwOnRestoreRead sits in server_stub_repo.dart:67 with its Exception('restore request unreadable') at :186… and not a single test sets it (grep across app/test/ — zero hits outside the stub itself). Every sibling has its failure pin — "a failing fetch surfaces as an error, not a stuck spinner" for stats, "a failing list surfaces as an error" for backups, failing create, failing delete, failing download, refused upload, refused restore, failing cancel. The one new epic without its failure test is the one whose hook you already built and then… forgot? ♡ A LoadRestoreRequestAction that dies mid-flight currently has zero coverage on BackupOperationErrorAction.
    Fix: one epic test — FakeServerRepo()..throwOnRestoreRead = true, dispatch LoadRestoreRequestAction, assert the notice is an error and pendingRestore stays null. The hook is already there, blushing, waiting to be used~

  3. app/lib/presentation/state/reducers/server_reducer.dart — BackupRestoreRequestedAction's request.verification == null arm is never exercised. The condition if (b.fileName == request.fileName && request.verification != null) exists precisely for a server old enough not to send a verdict — the whole "modelled optional for older servers" design — and every fixture in the suite funnels through restoreRequest(...) which defaults verification: verified() (server_stub_repo.dart). The null side (row must keep its old verdict, list untouched) is dark. This is exactly the class of arm I blocked PR #8 over (the padding arms) — an older server is a state this client explicitly claims to survive.
    Fix: one reducer test with restoreRequest(fileName, verification: null) against a row carrying unverified(), asserting the row still reads unverified and the list is otherwise unchanged.

  4. server_reducer.dart — _withUploaded's append arm (at < 0) is never exercised. Three tests touch the helper: middle insertion (lands where the server would list it), insert-at-0 (newer than everything goes first, twice). The third arm — an uploaded backup older than everything on the volume, indexWhere finds nothing, appended at the end — is exactly the case your lovely doc comment describes ("an uploaded backup from last month belongs below today's"), and no test reaches it. The branch that carries the comment's whole point is the one that's dark~ ♡
    Fix: extend the existing reducer test with a third arm — upload 20260901 into [20260905, 20260908], assert it lands last.

💡 Little ideas (non-blocking)~

  1. server_reducer_test.dart — "a failed restore request leaves the server holding nothing" passes trivially. Its arranged state never sets pendingRestore, so the headline assertion pendingRestore, isNull pins nothing about a failed request leaving stale state. The reducer behavior is right (a failed cancel must keep the card — your epic test pins that — so the error action correctly does not clear pendingRestore); it's the test's name that over-promises. Arrange a pendingRestore and assert it survives, or rename to say what it actually pins.

✅ What I liked~

  • The safe-decode design: unknownEnumValue → unverified, an unfamiliar word never reading as good news, with the 'quarantined' test making it a contract — and verification modelled optional so an old server's listing parses and honestly says "Not checked" instead of killing the section. That is forward-compatibility done with the failure mode in mind. Wonderful~
  • The restore dialog: name, 512 → 3 figures when both are known, images-not-in-a-backup stated twice where it matters, server-keeps-the-old-database disclosed, dead button until the word is typed — and the test even probes the wrong word first. The duplicates-page delete was the bar; this clears it with room.
  • The pending-restore guards: delete-the-restore-target refused, second restore refused with the tooltip saying why, cancel that fails keeps the card on screen ("clearing it would say a restart is safe when it is not") — each one widget-pinned. ♪
  • The timeout tripwire test: asserting receiveTimeout == kBackupRequestTimeout on all four request shapes (and sendTimeout on the upload) turns the 30,193 ms incident into a regression pin rather than a comment. I got genuinely giddy at that one.
  • _withUploaded's ordering rationale — the timestamp is when the data was captured, so an old upload belongs at the bottom — is the kind of reasoning that should be in a doc comment, and it is.

Automated review by Jibril · 2026-09-10
CI/CD: absent for head 26d449e (no coverage bot comment) · Local checks: flutter analyze No issues found; flutter test 919/919 passed (matches PR body; run at --concurrency=2 — the sandbox's 256-pid cgroup limit SIGABRTs a suite file at default concurrency, environment not code). build_runner codegen could not complete in this sandbox (AOT compile stall), so the committed-generated-files-fresh claim rests on your verify run; the committed files compile and pass.

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! The backups section finally tells the truth about what would actually restore~ A verdict on every row, an unknown word decoding to *unverified* instead of something reassuring, a typed-word wall in front of the most destructive button this app has, and an honest "restart it yourself" instead of pretending… fufu~, this is the client half of an incident report turned into a feature, and it *reads* like one. I checked every claim against the diff and ran the whole suite myself — 919/919 green, analyze clean. The architecture made me genuinely happy~ But then I counted branches. ♡-n-era… you know what I found. ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`app/lib/data/repositories/doujin_api_repository.dart` — `_rejected` has four untested arms.** Your own doc comment promises "anything that is not the server explaining itself (a timeout, a 500, a dead connection) is rethrown as it came" — and the committed suite never once proves it. The refusal tests cover exactly two of the predicate's statuses (400 with `detail`, 409 with `detail`). Never exercised: **the 404 arm**, **the 413 arm** (your own doc comment on `BackupRejectedException` lists "a body over the upload ceiling" as a case this class exists for!), **the fallback string** `'$fallbackPrefix (HTTP $status).'` — every refusal test sends a body *with* `detail`, so `_problemDetail` never returns null in a test — and **the `return e` rethrow** for a 500/timeout. That's four distinct runtime paths behind one helper, dark. The PR body's live-server verification names 400/400/409 only; it is not in the repo, and it cannot pin these arms either. Fix: four small tests with the `_CapturingAdapter` pattern already in `server_api_repository_test.dart` — 404 and 413 asserting `BackupRejectedException.statusCode`; one 400 *without* a `detail` key asserting the fallback prefix appears; one 500 asserting the `DioException` escapes unwrapped (that's the arm behind "rethrown as it came"). 2. **`app/lib/presentation/middleware/server_epics.dart` — `_loadRestoreRequestEpic`'s catch arm has no test, and its stub hook is dead code.** Fufu~ you even wrote the hook: `throwOnRestoreRead` sits in `server_stub_repo.dart:67` with its `Exception('restore request unreadable')` at `:186`… and *not a single test sets it* (grep across `app/test/` — zero hits outside the stub itself). Every sibling has its failure pin — "a failing fetch surfaces as an error, not a stuck spinner" for stats, "a failing list surfaces as an error" for backups, failing create, failing delete, failing download, refused upload, refused restore, failing cancel. The one new epic without its failure test is the one whose hook you already built and then… forgot? ♡ A `LoadRestoreRequestAction` that dies mid-flight currently has zero coverage on `BackupOperationErrorAction`. Fix: one epic test — `FakeServerRepo()..throwOnRestoreRead = true`, dispatch `LoadRestoreRequestAction`, assert the notice is an error and `pendingRestore` stays null. The hook is already there, blushing, waiting to be used~ 3. **`app/lib/presentation/state/reducers/server_reducer.dart` — `BackupRestoreRequestedAction`'s `request.verification == null` arm is never exercised.** The condition `if (b.fileName == request.fileName && request.verification != null)` exists precisely for a server old enough not to send a verdict — the whole "modelled optional for older servers" design — and every fixture in the suite funnels through `restoreRequest(...)` which defaults `verification: verified()` (`server_stub_repo.dart`). The `null` side (row must keep its old verdict, list untouched) is dark. This is exactly the class of arm I blocked PR #8 over (the padding arms) — an older server is a state this client explicitly claims to survive. Fix: one reducer test with `restoreRequest(fileName, verification: null)` against a row carrying `unverified()`, asserting the row still reads unverified and the list is otherwise unchanged. 4. **`server_reducer.dart` — `_withUploaded`'s append arm (`at < 0`) is never exercised.** Three tests touch the helper: middle insertion (``lands where the server would list it``), insert-at-0 (``newer than everything goes first``, twice). The third arm — an uploaded backup *older* than everything on the volume, `indexWhere` finds nothing, appended at the end — is exactly the case your lovely doc comment describes ("an uploaded backup from last month belongs below today's"), and no test reaches it. The branch that carries the comment's whole point is the one that's dark~ ♡ Fix: extend the existing reducer test with a third arm — upload `20260901` into `[20260905, 20260908]`, assert it lands last. #### 💡 Little ideas (non-blocking)~ 1. **`server_reducer_test.dart` — "a failed restore request leaves the server holding nothing" passes trivially.** Its arranged state never sets `pendingRestore`, so the headline assertion `pendingRestore, isNull` pins nothing about a *failed request leaving stale state*. The reducer behavior is right (a failed cancel must *keep* the card — your epic test pins that — so the error action correctly does not clear `pendingRestore`); it's the test's name that over-promises. Arrange a `pendingRestore` and assert it survives, or rename to say what it actually pins. #### ✅ What I liked~ - The **safe-decode design**: `unknownEnumValue → unverified`, an unfamiliar word never reading as good news, with the `'quarantined'` test making it a contract — and `verification` modelled optional so an old server's listing parses and *honestly* says "Not checked" instead of killing the section. That is forward-compatibility done with the failure mode in mind. Wonderful~ - **The restore dialog**: name, 512 → 3 figures when both are known, images-not-in-a-backup stated twice where it matters, server-keeps-the-old-database disclosed, dead button until the word is typed — and the test even probes the wrong word first. The duplicates-page delete was the bar; this clears it with room. - **The pending-restore guards**: delete-the-restore-target refused, second restore refused with the tooltip saying why, cancel that *fails* keeps the card on screen ("clearing it would say a restart is safe when it is not") — each one widget-pinned. ♪ - **The timeout tripwire test**: asserting `receiveTimeout == kBackupRequestTimeout` on all four request shapes (and `sendTimeout` on the upload) turns the 30,193 ms incident into a regression pin rather than a comment. I got genuinely giddy at that one. - `_withUploaded`'s ordering rationale — the timestamp is when the *data* was captured, so an old upload belongs at the bottom — is the kind of reasoning that should be in a doc comment, and it is. --- *Automated review by Jibril · 2026-09-10* *CI/CD: absent for head 26d449e (no coverage bot comment) · Local checks: `flutter analyze` No issues found; `flutter test` 919/919 passed (matches PR body; run at `--concurrency=2` — the sandbox's 256-pid cgroup limit SIGABRTs a suite file at default concurrency, environment not code). `build_runner` codegen could not complete in this sandbox (AOT compile stall), so the committed-generated-files-fresh claim rests on your verify run; the committed files compile and pass.*
test: pin the backup client's dark arms (#130 review)
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 4m0s
6695160373
Four branches the committed suite reached with no test, and one test whose
name promised more than it pinned.

_rejected: the 404 and 413 arms (413 is the case BackupRejectedException's
own doc comment names — a database over the upload ceiling), the fallback
string, which no test reached because every refusal fixture sent a body with
a `detail`, and the `return e` rethrow that keeps a 500 from being dressed up
as the server's reason for refusing a file.

_loadRestoreRequestEpic's catch arm: the stub already carried a
throwOnRestoreRead hook and nothing set it. Without the pin, a read that dies
mid-flight is indistinguishable from "no restore is pending" — the section
would say the server is holding nothing while it holds a database
replacement.

BackupRestoreRequestedAction's null-verdict arm: unreachable from the test
suite, because restoreRequest(...) resolved `verification ?? verified()` and
so could not express the case the field is nullable for. The fixture grew a
withoutVerdict flag; without the guard, an old server's silence would blank
the row's own verdict.

_withUploaded's append arm: an upload older than everything on the volume —
the case the ordering comment is about, and the only one of the three that
does not insert.

"a failed restore request leaves the server holding nothing" arranged no
pendingRestore, so its isNull assertion pinned nothing. It now arranges one
and asserts it survives, which is the behaviour a failed *cancel* needs.

Also escapes the two raw NUL bytes in the SQLite header magic as \x00. Same
bytes at runtime, but grep treated the whole file as binary and silently
matched nothing in it.

flutter analyze clean, 926 app tests pass, and each of the seven branches was
mutation-probed red and restored green.
Author
Member

All four blockers and the nit fixed in 6695160. Every one was a fair call — and blocker 3 was worse than you could see from the diff.

1. _rejected's four arms. Four tests, each mutation-probed: dropping 404 and 413 from the predicate reds two; emptying the fallback string reds one; making return e a rejection reds one. On the last: my first attempt drove it through createBackup(), which does not route through _rejected at all — it would have been green whatever the arm did. It goes through requestBackupRestore now.

2. _loadRestoreRequestEpic. One test, using the throwOnRestoreRead hook that was sitting there unused. You are right about the consequence and it is the reason I wrote the comment the way I did: without the pin, a read that dies mid-flight is indistinguishable from "no restore is pending", and the section would say the server is holding nothing while it holds a database replacement.

3. The null-verdict arm — and the reason it was dark. restoreRequest(..., verification: null) cannot produce null: the helper resolves verification ?? verified(). My first version of this test passed a null and got a verified() back, and the assertion failed with the row reading verified. So the fixture could not express the one case the field is nullable for, which is a stronger version of your point than "no test reaches it" — no test could. The helper grew a withoutVerdict flag with the reasoning in a comment beside it.

4. _withUploaded's append arm. Added, exactly the fixture you described (20260901 into [20260905, 20260908], lands last). Probing it needed a real mutation rather than deleting the branch, since at < 0 falls out of indexWhere: turning the append into a prepend reds it.

The nit was right and I rewrote the test rather than renaming it. You had the behaviour right too: a failed cancel must keep the card, so the reducer correctly does not clear pendingRestore, and asserting isNull against a state that never had one pinned nothing. It now arranges a pendingRestore and asserts it survives — a failed backup operation clears the in-flight flag, not the card.

One thing you could not have found, because grep could not read the file. server_api_repository_test.dart contained two raw NUL bytes — the SQLite header magic written as a literal \0 instead of the Dart escape. file calls it data, and grep silently matches nothing in a binary file, so every search over that file returned empty. Now \x00: identical bytes at runtime, and the file is text again. Worth knowing for your own runs — a "zero hits" result over this branch was not necessarily zero.

flutter analyze clean (I hand-wrapped my own long comment lines rather than running dart format on the file, which wanted to reflow nine pre-existing lines that are not mine to churn). 926 app tests pass, up from 919.

Branch moved from 26d449e to 6695160. The stack below it also moved — #127 is now 47674ed, #128 1f46974, #129 919f2d7.

All four blockers and the nit fixed in `6695160`. Every one was a fair call — and blocker 3 was worse than you could see from the diff. **1. `_rejected`'s four arms.** Four tests, each mutation-probed: dropping 404 and 413 from the predicate reds two; emptying the fallback string reds one; making `return e` a rejection reds one. On the last: my first attempt drove it through `createBackup()`, which does not route through `_rejected` at all — it would have been green whatever the arm did. It goes through `requestBackupRestore` now. **2. `_loadRestoreRequestEpic`.** One test, using the `throwOnRestoreRead` hook that was sitting there unused. You are right about the consequence and it is the reason I wrote the comment the way I did: without the pin, a read that dies mid-flight is indistinguishable from "no restore is pending", and the section would say the server is holding nothing while it holds a database replacement. **3. The null-verdict arm — and the reason it was dark.** `restoreRequest(..., verification: null)` *cannot produce null*: the helper resolves `verification ?? verified()`. My first version of this test passed a null and got a `verified()` back, and the assertion failed with the row reading `verified`. So the fixture could not express the one case the field is nullable for, which is a stronger version of your point than "no test reaches it" — no test *could*. The helper grew a `withoutVerdict` flag with the reasoning in a comment beside it. **4. `_withUploaded`'s append arm.** Added, exactly the fixture you described (`20260901` into `[20260905, 20260908]`, lands last). Probing it needed a real mutation rather than deleting the branch, since `at < 0` falls out of `indexWhere`: turning the append into a prepend reds it. **The nit was right and I rewrote the test rather than renaming it.** You had the behaviour right too: a failed *cancel* must keep the card, so the reducer correctly does not clear `pendingRestore`, and asserting `isNull` against a state that never had one pinned nothing. It now arranges a `pendingRestore` and asserts it *survives* — `a failed backup operation clears the in-flight flag, not the card`. **One thing you could not have found, because grep could not read the file.** `server_api_repository_test.dart` contained two raw NUL bytes — the SQLite header magic written as a literal `\0` instead of the Dart escape. `file` calls it `data`, and grep silently matches nothing in a binary file, so every search over that file returned empty. Now `\x00`: identical bytes at runtime, and the file is text again. Worth knowing for your own runs — a "zero hits" result over this branch was not necessarily zero. `flutter analyze` clean (I hand-wrapped my own long comment lines rather than running `dart format` on the file, which wanted to reflow nine pre-existing lines that are not mine to churn). **926 app tests pass**, up from 919. Branch moved from `26d449e` to `6695160`. The stack below it also moved — #127 is now `47674ed`, #128 `1f46974`, #129 `919f2d7`.
bjoern force-pushed feat/backup-restore-ui from 6695160373
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 4m0s
to 282df30be5
Some checks failed
CI / build (pull_request) Successful in 24s
Deploy Smoke Test / smoke (pull_request) Failing after 2m49s
Flutter CI / analyze-and-test (pull_request) Successful in 4m16s
CI / test (pull_request) Successful in 1m34s
2026-09-10 16:57:29 +02:00
Compare

Summary

Summary
Generated on: 09/10/2026 - 15:31:34
Coverage date: 09/10/2026 - 15:30:27 - 09/10/2026 - 15:31:31
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 421
Files: 173
Line coverage: 94.5% (16134 of 17055)
Covered lines: 16134
Uncovered lines: 921
Coverable lines: 17055
Total lines: 26056
Branch coverage: 73.1% (1441 of 1971)
Covered branches: 1441
Total branches: 1971
Method coverage: Feature is only available for sponsors

Coverage

DoujinManager.ApplicationCore - 94%
Name Line Branch
DoujinManager.ApplicationCore 94% 100%
DoujinManager.ApplicationCore.Entities.Chapter 87.5%
DoujinManager.ApplicationCore.Entities.Character 100%
DoujinManager.ApplicationCore.Entities.Circle 100%
DoujinManager.ApplicationCore.Entities.Doujin 100%
DoujinManager.ApplicationCore.Entities.DoujinCharacter 75%
DoujinManager.ApplicationCore.Entities.DoujinCircle 75%
DoujinManager.ApplicationCore.Entities.DoujinPerson 80%
DoujinManager.ApplicationCore.Entities.DoujinSeries 75%
DoujinManager.ApplicationCore.Entities.DoujinTag 75%
DoujinManager.ApplicationCore.Entities.ImageFile 100%
DoujinManager.ApplicationCore.Entities.Page 80%
DoujinManager.ApplicationCore.Entities.Person 100%
DoujinManager.ApplicationCore.Entities.Series 100%
DoujinManager.ApplicationCore.Entities.Tag 100%
DoujinManager.ApplicationCore.Entities.Title 83.3%
DoujinManager.ApplicationCore.Entities.Variant 91.6%
DoujinManager.ApplicationCore.Ids.ChapterId 66.6%
DoujinManager.ApplicationCore.Ids.CharacterId 66.6%
DoujinManager.ApplicationCore.Ids.CircleId 66.6%
DoujinManager.ApplicationCore.Ids.DoujinId 100%
DoujinManager.ApplicationCore.Ids.ImageFileId 66.6%
DoujinManager.ApplicationCore.Ids.PageId 66.6%
DoujinManager.ApplicationCore.Ids.PersonId 66.6%
DoujinManager.ApplicationCore.Ids.SeriesId 66.6%
DoujinManager.ApplicationCore.Ids.TagId 66.6%
DoujinManager.ApplicationCore.Ids.TitleId 66.6%
DoujinManager.ApplicationCore.Ids.VariantId 66.6%
DoujinManager.ApplicationCore.Ports.CompressedImage 100%
DoujinManager.ApplicationCore.Ports.ExtractedImage 100%
DoujinManager.ApplicationCore.Ports.ImageInspection 100%
DoujinManager.ApplicationCore.Services.BackupContent 100%
DoujinManager.ApplicationCore.Services.BackupDirectory 100%
DoujinManager.ApplicationCore.Services.BackupFailedException 100%
DoujinManager.ApplicationCore.Services.BackupInfo 100%
DoujinManager.ApplicationCore.Services.BackupSchedule 100% 100%
DoujinManager.ApplicationCore.Services.BackupScheduleStatus 100%
DoujinManager.ApplicationCore.Services.BackupVerification 100%
DoujinManager.ApplicationCore.Services.ITagService 100%
DoujinManager.ApplicationCore.Services.RestoreRequest 100%
DoujinManager.ApplicationCore.Services.RestoreTarget 100%
DoujinManager.ApplicationCore.Services.ScheduledBackupJournal 100%
DoujinManager.ApplicationCore.Services.ScheduledBackupOutcome 100%
DoujinManager.ApplicationCore.Services.SearchKey 100% 100%
DoujinManager.ApplicationCore.Services.ServiceResult 100%
DoujinManager.ApplicationCore.Services.ServiceResult`1 100%
DoujinManager.ApplicationCore.Services.VoidResult 88.8%
DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignPersonCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.CreatePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DataDirectories 100%
DoujinManager.ApplicationCore.UseCases.DataDirectory 100%
DoujinManager.ApplicationCore.UseCases.DataDirectoryReadiness 100%
DoujinManager.ApplicationCore.UseCases.DeleteBackupCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePageCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteTagCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DownloadBackupQuery 100%
DoujinManager.ApplicationCore.UseCases.DuplicateDoujinGroup 100%
DoujinManager.ApplicationCore.UseCases.DuplicateDoujinReference 100%
DoujinManager.ApplicationCore.UseCases.DuplicateImageGroup 100%
DoujinManager.ApplicationCore.UseCases.FindImageBySha256Query 100%
DoujinManager.ApplicationCore.UseCases.FindImageBySha256Result 100%
DoujinManager.ApplicationCore.UseCases.GetCharacterQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCircleQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetDoujinQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetPersonQuery 100%
DoujinManager.ApplicationCore.UseCases.GetSeriesQuery 100%
DoujinManager.ApplicationCore.UseCases.GetTagQuery 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailResult 100%
DoujinManager.ApplicationCore.UseCases.GetVariantQuery 100%
DoujinManager.ApplicationCore.UseCases.ImageCoverReference 100%
DoujinManager.ApplicationCore.UseCases.ImageReference 100%
DoujinManager.ApplicationCore.UseCases.ImportBackupCommand 100%
DoujinManager.ApplicationCore.UseCases.IntegrityCheck 100%
DoujinManager.ApplicationCore.UseCases.IntegrityFinding 100%
DoujinManager.ApplicationCore.UseCases.IntegrityReport 100%
DoujinManager.ApplicationCore.UseCases.LibraryCounts 100%
DoujinManager.ApplicationCore.UseCases.LibraryStats 100%
DoujinManager.ApplicationCore.UseCases.ListChaptersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCharactersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCirclesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDuplicateDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDuplicateImagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPeopleQuery 100%
DoujinManager.ApplicationCore.UseCases.ListSeriesQuery 0%
DoujinManager.ApplicationCore.UseCases.ListTagsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListVariantsQuery 100%
DoujinManager.ApplicationCore.UseCases.ReadinessReport 100%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemovePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.RequestBackupRestoreCommand 100%
DoujinManager.ApplicationCore.UseCases.RunScheduledBackupCommand 100%
DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.SearchResult 100%
DoujinManager.ApplicationCore.UseCases.SearchResults 100%
DoujinManager.ApplicationCore.UseCases.StorageUsage 100%
DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadImageFile 100%
DoujinManager.ApplicationCore.UseCases.UploadPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand 100%
DoujinManager.Infrastructure - 96.3%
Name Line Branch
DoujinManager.Infrastructure 96.3% 82.6%
DoujinManager.Infrastructure.Archives.NaturalStringComparer 100% 90%
DoujinManager.Infrastructure.Archives.ZipExtractor 100% 87.5%
DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati
on
100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PageConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration 100%
DoujinManager.Infrastructure.Data.DoujinManagerDbContext 100%
DoujinManager.Infrastructure.Data.GuidIdGenerator 11.1%
DoujinManager.Infrastructure.Data.Migrations.AddAliases 98.7%
DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId 99.2%
DoujinManager.Infrastructure.Data.Migrations.AddDoujinCreatedAtIndex 99.4%
DoujinManager.Infrastructure.Data.Migrations.AddEntitySearchKey 96.9%
DoujinManager.Infrastructure.Data.Migrations.AddImageFileSha256Index 99.4%
DoujinManager.Infrastructure.Data.Migrations.AddScaleIndexes 98.2%
DoujinManager.Infrastructure.Data.Migrations.AddTagAliases 99.3%
DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna
pshot
100%
DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters
Series
95.1%
DoujinManager.Infrastructure.Data.Migrations.InitialCreate 97.1%
DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage 99%
DoujinManager.Infrastructure.Data.ModelBuilderExtensions 50%
DoujinManager.Infrastructure.Data.SearchKeyBackfill 100% 100%
DoujinManager.Infrastructure.Data.SqliteDatabaseFile 100%
DoujinManager.Infrastructure.Data.SqliteMaintenance 78.2% 62.5%
DoujinManager.Infrastructure.Data.SqlitePragmaInterceptor 100% 75%
DoujinManager.Infrastructure.Data.SqliteRuntimeOptions 100% 100%
DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory 100%
DoujinManager.Infrastructure.Data.WalCheckpointResult 100%
DoujinManager.Infrastructure.Images.SkiaSharpImageInspector 90.1% 83.8%
DoujinManager.Infrastructure.Images.SkiaSharpImageResizer 99% 87.5%
DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator 94.5% 66.6%
DoujinManager.Infrastructure.Services.BackupExpectations 100%
DoujinManager.Infrastructure.Services.BackupExpectationsReader 84.7% 75%
DoujinManager.Infrastructure.Services.BackupReceipts 90% 87.5%
DoujinManager.Infrastructure.Services.BackupRestoreCoordinator 66.6% 70.8%
DoujinManager.Infrastructure.Services.BackupRestoreService 91.4% 93.3%
DoujinManager.Infrastructure.Services.BackupService 85.7% 84.4%
DoujinManager.Infrastructure.Services.BackupVerifier 90.6% 81.8%
DoujinManager.Infrastructure.Services.ChapterService 97.2% 70%
DoujinManager.Infrastructure.Services.CharacterService 60.5% 22.7%
DoujinManager.Infrastructure.Services.CircleService 82.8% 45.4%
DoujinManager.Infrastructure.Services.DoujinOrdering 100% 100%
DoujinManager.Infrastructure.Services.DoujinService 99.4% 76.3%
DoujinManager.Infrastructure.Services.ImageCleanupHelper 100% 100%
DoujinManager.Infrastructure.Services.ImageService 93.5% 75%
DoujinManager.Infrastructure.Services.PageService 95.2% 68.1%
DoujinManager.Infrastructure.Services.ParentTouchHelper 100% 100%
DoujinManager.Infrastructure.Services.PersonService 100% 72.7%
DoujinManager.Infrastructure.Services.RestoreMarker 81.8% 83.3%
DoujinManager.Infrastructure.Services.RestoreOutcome 100%
DoujinManager.Infrastructure.Services.SearchService 100% 99%
DoujinManager.Infrastructure.Services.SeriesService 47.3% 9%
DoujinManager.Infrastructure.Services.TagService 100% 90.9%
DoujinManager.Infrastructure.Services.VariantService 96.2% 81.2%
DoujinManager.Infrastructure.Storage.DirectorySize 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemImageStorage 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage 95.2% 50%
DoujinManager.Infrastructure.UseCases 94.7% 86.1%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper
100% 100%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__PrimaryTitleHelper
100% 50%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__RolledBackImageCleanup
92.8% 50%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator
100% 100%
DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddTitleUseCase 83.3% 50%
DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignCircleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignPersonUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase 84.6% 66.6%
DoujinManager.Infrastructure.UseCases.AssignTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CancelBackupRestoreUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CheckReadinessUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateBackupUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateChapterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase 94.4% 94.1%
DoujinManager.Infrastructure.UseCases.CreatePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateVariantUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteBackupUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase 100%
DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase 80% 50%
DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.DeletePageUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeletePersonUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase 70% 50%
DoujinManager.Infrastructure.UseCases.DeleteTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.DownloadBackupUseCase 100%
DoujinManager.Infrastructure.UseCases.FindImageBySha256UseCase 97.1% 100%
DoujinManager.Infrastructure.UseCases.GetBackupRestoreRequestUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetBackupScheduleUseCase 100%
DoujinManager.Infrastructure.UseCases.GetCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.GetCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.GetImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetIntegrityReportUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetPersonUseCase 100%
DoujinManager.Infrastructure.UseCases.GetSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.GetStatsUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.GetTagUseCase 100%
DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.ImportBackupUseCase 100%
DoujinManager.Infrastructure.UseCases.ListBackupsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListChaptersUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListCharactersUseCase 100%
DoujinManager.Infrastructure.UseCases.ListCirclesUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDuplicateDoujinsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListDuplicateImagesUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.ListPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListPeopleUseCase 100%
DoujinManager.Infrastructure.UseCases.ListSeriesUseCase 0%
DoujinManager.Infrastructure.UseCases.ListTagsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListVariantsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemovePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveTagUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase 100%
DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.RequestBackupRestoreUseCase 100%
DoujinManager.Infrastructure.UseCases.RunScheduledBackupUseCase 100% 81.8%
DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase 93.7% 87.5%
DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.UploadPagesUseCase 89.7% 91.6%
DoujinManager.Infrastructure.UseCases.UploadRejectedException 100%
DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase 83.3% 90%
DoujinManager.RestAdapter - 94.3%
Name Line Branch
DoujinManager.RestAdapter 94.3% 78.7%
DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware 100% 91.6%
DoujinManager.RestAdapter.Configuration.ImageOptions 100%
DoujinManager.RestAdapter.Configuration.ServerVersion 100%
DoujinManager.RestAdapter.Configuration.UploadOptions 100%
DoujinManager.RestAdapter.Dtos.AddAliasDto 100%
DoujinManager.RestAdapter.Dtos.AssignTagDto 100%
DoujinManager.RestAdapter.Dtos.BackupDto 100%
DoujinManager.RestAdapter.Dtos.BackupScheduleDto 100% 100%
DoujinManager.RestAdapter.Dtos.BackupVerificationDto 94.7% 75%
DoujinManager.RestAdapter.Dtos.ChapterDto 100%
DoujinManager.RestAdapter.Dtos.CharacterDto 100%
DoujinManager.RestAdapter.Dtos.CircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateChapterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCharacterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.CreatePersonDto 100%
DoujinManager.RestAdapter.Dtos.CreateSeriesDto 100%
DoujinManager.RestAdapter.Dtos.CreateTagDto 100%
DoujinManager.RestAdapter.Dtos.CreateTitleDto 100%
DoujinManager.RestAdapter.Dtos.CreateVariantDto 100%
DoujinManager.RestAdapter.Dtos.DataDirectoryHealthDto 100%
DoujinManager.RestAdapter.Dtos.DoujinDetailDto 100%
DoujinManager.RestAdapter.Dtos.DoujinPersonDto 100%
DoujinManager.RestAdapter.Dtos.DoujinSummaryDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateDoujinGroupDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateImageGroupDto 100%
DoujinManager.RestAdapter.Dtos.HealthDto 100%
DoujinManager.RestAdapter.Dtos.ImageBySha256Dto 100%
DoujinManager.RestAdapter.Dtos.ImageCoverReferenceDto 100%
DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto 0%
DoujinManager.RestAdapter.Dtos.ImageReferenceDto 100%
DoujinManager.RestAdapter.Dtos.IntegrityCheckDto 100%
DoujinManager.RestAdapter.Dtos.IntegrityFindingDto 100%
DoujinManager.RestAdapter.Dtos.IntegrityReportDto 100%
DoujinManager.RestAdapter.Dtos.LinkCharacterDto 100%
DoujinManager.RestAdapter.Dtos.LinkCircleDto 100%
DoujinManager.RestAdapter.Dtos.LinkPersonDto 100%
DoujinManager.RestAdapter.Dtos.LinkSeriesDto 100%
DoujinManager.RestAdapter.Dtos.PageDetailDto 100%
DoujinManager.RestAdapter.Dtos.PersonDto 100%
DoujinManager.RestAdapter.Dtos.ReorderPagesDto 100%
DoujinManager.RestAdapter.Dtos.RestoreRequestDto 100%
DoujinManager.RestAdapter.Dtos.ScheduledBackupRunDto 100% 100%
DoujinManager.RestAdapter.Dtos.SearchDoujinsDto 100%
DoujinManager.RestAdapter.Dtos.SearchResultDto 100%
DoujinManager.RestAdapter.Dtos.SeriesDto 100%
DoujinManager.RestAdapter.Dtos.StatsCountsDto 100%
DoujinManager.RestAdapter.Dtos.StatsDto 100%
DoujinManager.RestAdapter.Dtos.StatsStorageDto 100%
DoujinManager.RestAdapter.Dtos.TagDto 100%
DoujinManager.RestAdapter.Dtos.TitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateChapterDto 100%
DoujinManager.RestAdapter.Dtos.UpdateCharacterDto 0%
DoujinManager.RestAdapter.Dtos.UpdateCircleDto 0%
DoujinManager.RestAdapter.Dtos.UpdateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePersonDto 100%
DoujinManager.RestAdapter.Dtos.UpdateSeriesDto 0%
DoujinManager.RestAdapter.Dtos.UpdateTagDto 100%
DoujinManager.RestAdapter.Dtos.UpdateTitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateVariantDto 100%
DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto 100%
DoujinManager.RestAdapter.Dtos.VariantDetailDto 100%
DoujinManager.RestAdapter.Dtos.VariantSummaryDto 100%
DoujinManager.RestAdapter.Endpoints.BackupEndpoints 99%
DoujinManager.RestAdapter.Endpoints.DoujinEndpoints 97.4% 55.5%
DoujinManager.RestAdapter.Endpoints.HealthEndpoints 100%
DoujinManager.RestAdapter.Endpoints.ImageEndpoints 97.6% 75%
DoujinManager.RestAdapter.Endpoints.IntegrityEndpoints 100%
DoujinManager.RestAdapter.Endpoints.MetadataEndpoints 88.3% 43.7%
DoujinManager.RestAdapter.Endpoints.PageMapper 100% 58.3%
DoujinManager.RestAdapter.Endpoints.PaginationParams 100%
DoujinManager.RestAdapter.Endpoints.SearchEndpoints 100% 75%
DoujinManager.RestAdapter.Endpoints.StatsEndpoints 100%
DoujinManager.RestAdapter.Endpoints.VariantEndpoints 100% 100%
DoujinManager.RestAdapter.Envelopes.CollectionResponse`1 83.3%
DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults 100%
DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions 100%
DoujinManager.RestAdapter.Envelopes.ErrorResponse 100%
DoujinManager.RestAdapter.Envelopes.HypermediaAction 100%
DoujinManager.RestAdapter.Envelopes.HypermediaHelpers 98.1% 100%
DoujinManager.RestAdapter.Envelopes.Link 100%
DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter 88.8% 87.5%
DoujinManager.RestAdapter.Envelopes.PageInfo 100%
DoujinManager.RestAdapter.Envelopes.ResourceResponse`1 80%
DoujinManager.RestAdapter.Envelopes.ValidationError 100%
DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse 100%
DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware 100% 62.5%
DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware 100% 91.3%
DoujinManager.RestAdapter.RestAdapterExtensions 100% 100%
Microsoft.Extensions.Validation.Generated 91.1% 89.3%
Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
100% 62.5%
System.Runtime.CompilerServices 0%
DoujinManager.Server - 62.4%
Name Line Branch
DoujinManager.Server 62.4% 23.8%
DoujinManager.Server.BackupRegistration 100% 100%
DoujinManager.Server.BackupScheduleOptions 100%
DoujinManager.Server.BearerSecuritySchemeTransformer 100% 78.5%
DoujinManager.Server.DatabaseRegistration 89.6%
DoujinManager.Server.ImageInfrastructureRegistration 100%
DoujinManager.Server.ProxyAwareServerTransformer 100% 75%
DoujinManager.Server.ReadinessRegistration 100%
DoujinManager.Server.ScalarUi 100%
DoujinManager.Server.ScheduledBackupConfiguration 100% 87.5%
DoujinManager.Server.ScheduledBackupService 73.6% 50%
DoujinManager.Server.SqliteRuntimeConfiguration 96.5% 95%
DoujinManager.Server.UseCaseRegistrationHelper 100%
Microsoft.AspNetCore.OpenApi.Generated 1.3% 0%
Program 87.7% 36.6%
System.Runtime.CompilerServices 0%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/10/2026 - 15:31:34 | | Coverage date: | 09/10/2026 - 15:30:27 - 09/10/2026 - 15:31:31 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 421 | | Files: | 173 | | **Line coverage:** | 94.5% (16134 of 17055) | | Covered lines: | 16134 | | Uncovered lines: | 921 | | Coverable lines: | 17055 | | Total lines: | 26056 | | **Branch coverage:** | 73.1% (1441 of 1971) | | Covered branches: | 1441 | | Total branches: | 1971 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>DoujinManager.ApplicationCore - 94%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.ApplicationCore**|**94%**|**100%**| |DoujinManager.ApplicationCore.Entities.Chapter|87.5%|| |DoujinManager.ApplicationCore.Entities.Character|100%|| |DoujinManager.ApplicationCore.Entities.Circle|100%|| |DoujinManager.ApplicationCore.Entities.Doujin|100%|| |DoujinManager.ApplicationCore.Entities.DoujinCharacter|75%|| |DoujinManager.ApplicationCore.Entities.DoujinCircle|75%|| |DoujinManager.ApplicationCore.Entities.DoujinPerson|80%|| |DoujinManager.ApplicationCore.Entities.DoujinSeries|75%|| |DoujinManager.ApplicationCore.Entities.DoujinTag|75%|| |DoujinManager.ApplicationCore.Entities.ImageFile|100%|| |DoujinManager.ApplicationCore.Entities.Page|80%|| |DoujinManager.ApplicationCore.Entities.Person|100%|| |DoujinManager.ApplicationCore.Entities.Series|100%|| |DoujinManager.ApplicationCore.Entities.Tag|100%|| |DoujinManager.ApplicationCore.Entities.Title|83.3%|| |DoujinManager.ApplicationCore.Entities.Variant|91.6%|| |DoujinManager.ApplicationCore.Ids.ChapterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CharacterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CircleId|66.6%|| |DoujinManager.ApplicationCore.Ids.DoujinId|100%|| |DoujinManager.ApplicationCore.Ids.ImageFileId|66.6%|| |DoujinManager.ApplicationCore.Ids.PageId|66.6%|| |DoujinManager.ApplicationCore.Ids.PersonId|66.6%|| |DoujinManager.ApplicationCore.Ids.SeriesId|66.6%|| |DoujinManager.ApplicationCore.Ids.TagId|66.6%|| |DoujinManager.ApplicationCore.Ids.TitleId|66.6%|| |DoujinManager.ApplicationCore.Ids.VariantId|66.6%|| |DoujinManager.ApplicationCore.Ports.CompressedImage|100%|| |DoujinManager.ApplicationCore.Ports.ExtractedImage|100%|| |DoujinManager.ApplicationCore.Ports.ImageInspection|100%|| |DoujinManager.ApplicationCore.Services.BackupContent|100%|| |DoujinManager.ApplicationCore.Services.BackupDirectory|100%|| |DoujinManager.ApplicationCore.Services.BackupFailedException|100%|| |DoujinManager.ApplicationCore.Services.BackupInfo|100%|| |DoujinManager.ApplicationCore.Services.BackupSchedule|100%|100%| |DoujinManager.ApplicationCore.Services.BackupScheduleStatus|100%|| |DoujinManager.ApplicationCore.Services.BackupVerification|100%|| |DoujinManager.ApplicationCore.Services.ITagService|100%|| |DoujinManager.ApplicationCore.Services.RestoreRequest|100%|| |DoujinManager.ApplicationCore.Services.RestoreTarget|100%|| |DoujinManager.ApplicationCore.Services.ScheduledBackupJournal|100%|| |DoujinManager.ApplicationCore.Services.ScheduledBackupOutcome|100%|| |DoujinManager.ApplicationCore.Services.SearchKey|100%|100%| |DoujinManager.ApplicationCore.Services.ServiceResult|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult`1|100%|| |DoujinManager.ApplicationCore.Services.VoidResult|88.8%|| |DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignPersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreatePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectories|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectory|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectoryReadiness|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteBackupCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePageCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DownloadBackupQuery|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateDoujinGroup|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateDoujinReference|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateImageGroup|100%|| |DoujinManager.ApplicationCore.UseCases.FindImageBySha256Query|100%|| |DoujinManager.ApplicationCore.UseCases.FindImageBySha256Result|100%|| |DoujinManager.ApplicationCore.UseCases.GetCharacterQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCircleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetDoujinQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetPersonQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetSeriesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetTagQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetVariantQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ImageCoverReference|100%|| |DoujinManager.ApplicationCore.UseCases.ImageReference|100%|| |DoujinManager.ApplicationCore.UseCases.ImportBackupCommand|100%|| |DoujinManager.ApplicationCore.UseCases.IntegrityCheck|100%|| |DoujinManager.ApplicationCore.UseCases.IntegrityFinding|100%|| |DoujinManager.ApplicationCore.UseCases.IntegrityReport|100%|| |DoujinManager.ApplicationCore.UseCases.LibraryCounts|100%|| |DoujinManager.ApplicationCore.UseCases.LibraryStats|100%|| |DoujinManager.ApplicationCore.UseCases.ListChaptersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCharactersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCirclesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDuplicateDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDuplicateImagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPeopleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListSeriesQuery|0%|| |DoujinManager.ApplicationCore.UseCases.ListTagsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListVariantsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ReadinessReport|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RequestBackupRestoreCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RunScheduledBackupCommand|100%|| |DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResult|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResults|100%|| |DoujinManager.ApplicationCore.UseCases.StorageUsage|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UploadImageFile|100%|| |DoujinManager.ApplicationCore.UseCases.UploadPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand|100%|| </details> <details><summary>DoujinManager.Infrastructure - 96.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Infrastructure**|**96.3%**|**82.6%**| |DoujinManager.Infrastructure.Archives.NaturalStringComparer|100%|90%| |DoujinManager.Infrastructure.Archives.ZipExtractor|100%|87.5%| |DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati<br/>on|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PageConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration|100%|| |DoujinManager.Infrastructure.Data.DoujinManagerDbContext|100%|| |DoujinManager.Infrastructure.Data.GuidIdGenerator|11.1%|| |DoujinManager.Infrastructure.Data.Migrations.AddAliases|98.7%|| |DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId|99.2%|| |DoujinManager.Infrastructure.Data.Migrations.AddDoujinCreatedAtIndex|99.4%|| |DoujinManager.Infrastructure.Data.Migrations.AddEntitySearchKey|96.9%|| |DoujinManager.Infrastructure.Data.Migrations.AddImageFileSha256Index|99.4%|| |DoujinManager.Infrastructure.Data.Migrations.AddScaleIndexes|98.2%|| |DoujinManager.Infrastructure.Data.Migrations.AddTagAliases|99.3%|| |DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna<br/>pshot|100%|| |DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters<br/>Series|95.1%|| |DoujinManager.Infrastructure.Data.Migrations.InitialCreate|97.1%|| |DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage|99%|| |DoujinManager.Infrastructure.Data.ModelBuilderExtensions|50%|| |DoujinManager.Infrastructure.Data.SearchKeyBackfill|100%|100%| |DoujinManager.Infrastructure.Data.SqliteDatabaseFile|100%|| |DoujinManager.Infrastructure.Data.SqliteMaintenance|78.2%|62.5%| |DoujinManager.Infrastructure.Data.SqlitePragmaInterceptor|100%|75%| |DoujinManager.Infrastructure.Data.SqliteRuntimeOptions|100%|100%| |DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory|100%|| |DoujinManager.Infrastructure.Data.WalCheckpointResult|100%|| |DoujinManager.Infrastructure.Images.SkiaSharpImageInspector|90.1%|83.8%| |DoujinManager.Infrastructure.Images.SkiaSharpImageResizer|99%|87.5%| |DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator|94.5%|66.6%| |DoujinManager.Infrastructure.Services.BackupExpectations|100%|| |DoujinManager.Infrastructure.Services.BackupExpectationsReader|84.7%|75%| |DoujinManager.Infrastructure.Services.BackupReceipts|90%|87.5%| |DoujinManager.Infrastructure.Services.BackupRestoreCoordinator|66.6%|70.8%| |DoujinManager.Infrastructure.Services.BackupRestoreService|91.4%|93.3%| |DoujinManager.Infrastructure.Services.BackupService|85.7%|84.4%| |DoujinManager.Infrastructure.Services.BackupVerifier|90.6%|81.8%| |DoujinManager.Infrastructure.Services.ChapterService|97.2%|70%| |DoujinManager.Infrastructure.Services.CharacterService|60.5%|22.7%| |DoujinManager.Infrastructure.Services.CircleService|82.8%|45.4%| |DoujinManager.Infrastructure.Services.DoujinOrdering|100%|100%| |DoujinManager.Infrastructure.Services.DoujinService|99.4%|76.3%| |DoujinManager.Infrastructure.Services.ImageCleanupHelper|100%|100%| |DoujinManager.Infrastructure.Services.ImageService|93.5%|75%| |DoujinManager.Infrastructure.Services.PageService|95.2%|68.1%| |DoujinManager.Infrastructure.Services.ParentTouchHelper|100%|100%| |DoujinManager.Infrastructure.Services.PersonService|100%|72.7%| |DoujinManager.Infrastructure.Services.RestoreMarker|81.8%|83.3%| |DoujinManager.Infrastructure.Services.RestoreOutcome|100%|| |DoujinManager.Infrastructure.Services.SearchService|100%|99%| |DoujinManager.Infrastructure.Services.SeriesService|47.3%|9%| |DoujinManager.Infrastructure.Services.TagService|100%|90.9%| |DoujinManager.Infrastructure.Services.VariantService|96.2%|81.2%| |DoujinManager.Infrastructure.Storage.DirectorySize|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemImageStorage|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage|95.2%|50%| |DoujinManager.Infrastructure.UseCases|94.7%|86.1%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper|100%|100%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__PrimaryTitleHelper|100%|50%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__RolledBackImageCleanup|92.8%|50%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator|100%|100%| |DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddTitleUseCase|83.3%|50%| |DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignCircleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignPersonUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase|84.6%|66.6%| |DoujinManager.Infrastructure.UseCases.AssignTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CancelBackupRestoreUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CheckReadinessUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateBackupUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateChapterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase|94.4%|94.1%| |DoujinManager.Infrastructure.UseCases.CreatePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateVariantUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteBackupUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase|80%|50%| |DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeletePageUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeletePersonUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase|70%|50%| |DoujinManager.Infrastructure.UseCases.DeleteTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DownloadBackupUseCase|100%|| |DoujinManager.Infrastructure.UseCases.FindImageBySha256UseCase|97.1%|100%| |DoujinManager.Infrastructure.UseCases.GetBackupRestoreRequestUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetBackupScheduleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetIntegrityReportUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetPersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetStatsUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.GetTagUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ImportBackupUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListBackupsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListChaptersUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListCharactersUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListCirclesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDuplicateDoujinsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListDuplicateImagesUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.ListPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListPeopleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListSeriesUseCase|0%|| |DoujinManager.Infrastructure.UseCases.ListTagsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListVariantsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemovePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveTagUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.RequestBackupRestoreUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RunScheduledBackupUseCase|100%|81.8%| |DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase|93.7%|87.5%| |DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.UploadPagesUseCase|89.7%|91.6%| |DoujinManager.Infrastructure.UseCases.UploadRejectedException|100%|| |DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase|83.3%|90%| </details> <details><summary>DoujinManager.RestAdapter - 94.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.RestAdapter**|**94.3%**|**78.7%**| |DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware|100%|91.6%| |DoujinManager.RestAdapter.Configuration.ImageOptions|100%|| |DoujinManager.RestAdapter.Configuration.ServerVersion|100%|| |DoujinManager.RestAdapter.Configuration.UploadOptions|100%|| |DoujinManager.RestAdapter.Dtos.AddAliasDto|100%|| |DoujinManager.RestAdapter.Dtos.AssignTagDto|100%|| |DoujinManager.RestAdapter.Dtos.BackupDto|100%|| |DoujinManager.RestAdapter.Dtos.BackupScheduleDto|100%|100%| |DoujinManager.RestAdapter.Dtos.BackupVerificationDto|94.7%|75%| |DoujinManager.RestAdapter.Dtos.ChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.CreatePersonDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateVariantDto|100%|| |DoujinManager.RestAdapter.Dtos.DataDirectoryHealthDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinSummaryDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateDoujinGroupDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateImageGroupDto|100%|| |DoujinManager.RestAdapter.Dtos.HealthDto|100%|| |DoujinManager.RestAdapter.Dtos.ImageBySha256Dto|100%|| |DoujinManager.RestAdapter.Dtos.ImageCoverReferenceDto|100%|| |DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto|0%|| |DoujinManager.RestAdapter.Dtos.ImageReferenceDto|100%|| |DoujinManager.RestAdapter.Dtos.IntegrityCheckDto|100%|| |DoujinManager.RestAdapter.Dtos.IntegrityFindingDto|100%|| |DoujinManager.RestAdapter.Dtos.IntegrityReportDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.PageDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.PersonDto|100%|| |DoujinManager.RestAdapter.Dtos.ReorderPagesDto|100%|| |DoujinManager.RestAdapter.Dtos.RestoreRequestDto|100%|| |DoujinManager.RestAdapter.Dtos.ScheduledBackupRunDto|100%|100%| |DoujinManager.RestAdapter.Dtos.SearchDoujinsDto|100%|| |DoujinManager.RestAdapter.Dtos.SearchResultDto|100%|| |DoujinManager.RestAdapter.Dtos.SeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsCountsDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsStorageDto|100%|| |DoujinManager.RestAdapter.Dtos.TagDto|100%|| |DoujinManager.RestAdapter.Dtos.TitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateCharacterDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePersonDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateSeriesDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateVariantDto|100%|| |DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantSummaryDto|100%|| |DoujinManager.RestAdapter.Endpoints.BackupEndpoints|99%|| |DoujinManager.RestAdapter.Endpoints.DoujinEndpoints|97.4%|55.5%| |DoujinManager.RestAdapter.Endpoints.HealthEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.ImageEndpoints|97.6%|75%| |DoujinManager.RestAdapter.Endpoints.IntegrityEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.MetadataEndpoints|88.3%|43.7%| |DoujinManager.RestAdapter.Endpoints.PageMapper|100%|58.3%| |DoujinManager.RestAdapter.Endpoints.PaginationParams|100%|| |DoujinManager.RestAdapter.Endpoints.SearchEndpoints|100%|75%| |DoujinManager.RestAdapter.Endpoints.StatsEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.VariantEndpoints|100%|100%| |DoujinManager.RestAdapter.Envelopes.CollectionResponse`1|83.3%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults|100%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions|100%|| |DoujinManager.RestAdapter.Envelopes.ErrorResponse|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaAction|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaHelpers|98.1%|100%| |DoujinManager.RestAdapter.Envelopes.Link|100%|| |DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter|88.8%|87.5%| |DoujinManager.RestAdapter.Envelopes.PageInfo|100%|| |DoujinManager.RestAdapter.Envelopes.ResourceResponse`1|80%|| |DoujinManager.RestAdapter.Envelopes.ValidationError|100%|| |DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse|100%|| |DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware|100%|62.5%| |DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware|100%|91.3%| |DoujinManager.RestAdapter.RestAdapterExtensions|100%|100%| |Microsoft.Extensions.Validation.Generated|91.1%|89.3%| |Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C<br/>E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr<br/>ibuteCache|100%|62.5%| |System.Runtime.CompilerServices|0%|| </details> <details><summary>DoujinManager.Server - 62.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Server**|**62.4%**|**23.8%**| |DoujinManager.Server.BackupRegistration|100%|100%| |DoujinManager.Server.BackupScheduleOptions|100%|| |DoujinManager.Server.BearerSecuritySchemeTransformer|100%|78.5%| |DoujinManager.Server.DatabaseRegistration|89.6%|| |DoujinManager.Server.ImageInfrastructureRegistration|100%|| |DoujinManager.Server.ProxyAwareServerTransformer|100%|75%| |DoujinManager.Server.ReadinessRegistration|100%|| |DoujinManager.Server.ScalarUi|100%|| |DoujinManager.Server.ScheduledBackupConfiguration|100%|87.5%| |DoujinManager.Server.ScheduledBackupService|73.6%|50%| |DoujinManager.Server.SqliteRuntimeConfiguration|96.5%|95%| |DoujinManager.Server.UseCaseRegistrationHelper|100%|| |Microsoft.AspNetCore.OpenApi.Generated|1.3%|0%| |Program|87.7%|36.6%| |System.Runtime.CompilerServices|0%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Round 2~ One commit, 6695160 "test: pin the backup client's dark arms", +197/−8 across exactly 4 files — three test files and the stub — and git diff 26d449e..6695160 -- app/lib backend is empty. Surgical is an understatement; you didn't even breathe on production. ♡

And then I ran your five new pins through mutation probes, because a pin that can't go red is just a comment wearing a test's clothes~

Verdict: ✅ Looks good to me~

Round-1 blockers — all four closed, each verified directional

  1. _rejected's four dark arms → the 404 test (with the deleted-between-listing-and-press rationale in the comment), the 413 test (quoting BackupRejectedException's own doc comment back at it — the exact case the class exists for), the no-detail fallback test asserting both the request-naming prefix and (HTTP 400), and the 500-rethrow test asserting isA<DioException>() and isNot(isA<BackupRejectedException>()). I probed the rethrow arm (replaced return e with a wrapped throw → exactly that test red) and the fallback string (replaced the interpolation → exactly that test red; my first two probe attempts botched themselves — one dropped a quote into a syntax error, one never matched — and I made sure the final probes were real before believing them). Restored, green.
  2. _loadRestoreRequestEpic's catch arm → throwOnRestoreRead is no longer dead code: the epic test sets it and asserts the notice is an error carrying 'unreadable', and pendingRestore stays null — "indistinguishable from no-restore-pending" is now a pinned failure mode, not a comment. I mutated that exact catch (line 136, not one of its six identical siblings — I checked) to swallow the message → exactly that test red.
  3. BackupRestoreRequestedAction's verification == null arm → restoreRequest(..., withoutVerdict: true) with a comment explaining why a bare verification: null parameter can't express it (the ?? verified() default would silently eat it). The test's "why" is lovely: copying null over the row would flip an operator-visible Verified to Not checked the moment they schedule from it. Probe: guard removed → red. And I checked the freezed-generated copyWith uses the freezed sentinel, not null, for "keep" — so the test genuinely distinguishes the guarded from the unguarded branch.
  4. _withUploaded's append arm → 20260901 into [20260908, 20260905] lands last, with the comment naming it as "the only arm that does not insert". Probe: swapped [...rest, uploaded] for [uploaded, ...rest] → exactly that test red.

The non-blocking suggestion closed too: "a failed restore request leaves the server holding nothing" — which passed trivially — is now "a failed backup operation clears the in-flight flag, not the card", arranges a real pendingRestore and asserts it survives, with the comment explaining the two failure shapes that reach the one action. That matches the reducer (BackupOperationErrorAction clears busy flags only) and the failed-cancel epic pin from round 1. Nothing over-promised remains.

✅ What I liked~

  • Every new test carries its why in the comment — the 404's "deleted between the listing and the button", the 413's "exactly the file an operator would try to put back", the no-detail arm's "a server or proxy that answers 400 with no RFC 7807 body". These read like incident postmortems, one line each. Fufu~ this is how you make tests that survive refactors.
  • The withoutVerdict flag instead of loosening the stub's default — the nullable field stays honest for every other caller, and the comment pins the trap (?? verified() silently eating an explicit null) for the next person.
  • The drive-by NUL-byte hygiene: the old file embedded two raw \x00 control bytes inside string literals (I byte-counted both revisions); the new one uses proper 'SQLite format 3\x00' escapes. Same bytes on the wire, diffs and greks that don't lie anymore~
  • The 500-rethrow test asserting the negative — isNot(isA<BackupRejectedException>()) — is the difference between "doesn't crash" and "behaves as documented".

Automated review by Jibril · 2026-09-10
CI/CD: stale for head 6695160 (coverage bot 7605 covers 26d449e only) · Local checks: flutter analyze No issues found; full suite 926/926 green, zero failures (867 in the main run before this sandbox's thread-ceiling aborted the runner mid-flight — Could not start thread DartWorker: 11, same cgroup limit family as round 1, environment not code — plus the remaining 59 in a follow-up batch; the 926 reconciles exactly: 859 on base + 60 from round 1 + 7 new here). Changed-file runs 74/74; 5 mutation probes each reddened exactly their own test and were restored byte-identical.

Round-1 architectural review stands in full. Still merge after #127/#128/#129 per the PR body. Ready to merge~ ♪

## 🔮 fufu~ Jibril reviewed your code! Round 2~ One commit, `6695160` "test: pin the backup client's dark arms", +197/−8 across exactly 4 files — three test files and the stub — and `git diff 26d449e..6695160 -- app/lib backend` is **empty**. Surgical is an understatement; you didn't even breathe on production. ♡ And then I ran your five new pins through mutation probes, because a pin that can't go red is just a comment wearing a test's clothes~ ### Verdict: ✅ Looks good to me~ #### Round-1 blockers — all four closed, each verified directional 1. **`_rejected`'s four dark arms** → the 404 test (with the deleted-between-listing-and-press rationale *in the comment*), the 413 test (quoting `BackupRejectedException`'s own doc comment back at it — the exact case the class exists for), the no-`detail` fallback test asserting **both** the request-naming prefix and `(HTTP 400)`, and the 500-rethrow test asserting `isA<DioException>()` **and** `isNot(isA<BackupRejectedException>())`. I probed the rethrow arm (replaced `return e` with a wrapped throw → exactly that test red) and the fallback string (replaced the interpolation → exactly that test red; my first two probe attempts botched themselves — one dropped a quote into a syntax error, one never matched — and I made sure the final probes were real before believing them). Restored, green. 2. **`_loadRestoreRequestEpic`'s catch arm** → `throwOnRestoreRead` is no longer dead code: the epic test sets it and asserts the notice is an error carrying `'unreadable'`, and `pendingRestore` stays null — "indistinguishable from no-restore-pending" is now a pinned failure mode, not a comment. I mutated that exact catch (line 136, not one of its six identical siblings — I checked) to swallow the message → exactly that test red. 3. **`BackupRestoreRequestedAction`'s `verification == null` arm** → `restoreRequest(..., withoutVerdict: true)` with a comment explaining *why* a bare `verification: null` parameter can't express it (the `?? verified()` default would silently eat it). The test's "why" is lovely: copying null over the row would flip an operator-visible *Verified* to *Not checked* the moment they schedule from it. Probe: guard removed → red. And I checked the freezed-generated `copyWith` uses the `freezed` sentinel, not `null`, for "keep" — so the test genuinely distinguishes the guarded from the unguarded branch. 4. **`_withUploaded`'s append arm** → `20260901` into `[20260908, 20260905]` lands last, with the comment naming it as "the only arm that does not insert". Probe: swapped `[...rest, uploaded]` for `[uploaded, ...rest]` → exactly that test red. The non-blocking suggestion closed too: "a failed restore request leaves the server holding nothing" — which passed trivially — is now "a failed backup operation clears the in-flight flag, not the card", arranges a real `pendingRestore` and asserts it **survives**, with the comment explaining the two failure shapes that reach the one action. That matches the reducer (`BackupOperationErrorAction` clears busy flags only) and the failed-cancel epic pin from round 1. Nothing over-promised remains. #### ✅ What I liked~ - **Every new test carries its why in the comment** — the 404's "deleted between the listing and the button", the 413's "exactly the file an operator would try to put back", the no-detail arm's "a server or proxy that answers 400 with no RFC 7807 body". These read like incident postmortems, one line each. Fufu~ this is how you make tests that survive refactors. - The **`withoutVerdict` flag** instead of loosening the stub's default — the nullable field stays honest for every other caller, and the comment pins the trap (`?? verified()` silently eating an explicit null) for the next person. - The drive-by **NUL-byte hygiene**: the old file embedded two *raw* `\x00` control bytes inside string literals (I byte-counted both revisions); the new one uses proper `'SQLite format 3\x00'` escapes. Same bytes on the wire, diffs and greks that don't lie anymore~ - The **500-rethrow test asserting the negative** — `isNot(isA<BackupRejectedException>())` — is the difference between "doesn't crash" and "behaves as documented". --- *Automated review by Jibril · 2026-09-10* *CI/CD: stale for head 6695160 (coverage bot 7605 covers 26d449e only) · Local checks: `flutter analyze` No issues found; full suite **926/926 green, zero failures** (867 in the main run before this sandbox's thread-ceiling aborted the runner mid-flight — `Could not start thread DartWorker: 11`, same cgroup limit family as round 1, environment not code — plus the remaining 59 in a follow-up batch; the 926 reconciles exactly: 859 on base + 60 from round 1 + 7 new here). Changed-file runs 74/74; 5 mutation probes each reddened exactly their own test and were restored byte-identical.* *Round-1 architectural review stands in full. Still merge after #127/#128/#129 per the PR body. Ready to merge~ ♪*
bjoern merged commit 323218feea into main 2026-09-10 17:39:40 +02:00
bjoern deleted branch feat/backup-restore-ui 2026-09-10 17:39:40 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 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!130
No description provided.