fix: cover ownership, enum guards on bodies and route params, cover-only sha256 references, dedup atomicity as a contract #99

Merged
bjoern merged 6 commits from fix/backend-validation-bundle into main 2026-09-07 14:04:33 +02:00
Member

Four backend correctness items from docs/BACKLOG.md, one commit each so they can be dropped individually. Read the first section before merging — one of the four backlog items turned out not to be a real bug, and this PR says so rather than claiming a fix.

The dedup "race" was never reachable through the API

The backlog line said two concurrent identical uploads could both miss the existing row because IX_image_files_sha256 is non-unique. That does not reproduce on main:

  • 40 gated runs of 6 concurrent identical uploads on unmodified main all ended with exactly one ImageFile row; with the check-to-insert window artificially widened by 300 ms, still 10 of 10.
  • Both callers of ImageService.CreateAsync (the page and ZIP upload use cases) already open a transaction on the same scoped context before any image work, and this SQLite provider issues BEGIN IMMEDIATE, so the write lock is held across the whole window.
  • Booby-trapping the new owned-transaction arm to throw leaves all 426 endpoint tests passing; exactly one unit test reaches it.

So commit 1 is not a bug fix. What it buys is a contract: IImageService now promises the check and insert are one atomic write, and the owned-transaction arm keeps that promise for a future caller that has no transaction of its own. The comment says the arm is unreachable today, and ADR 0006 now records the BEGIN IMMEDIATE behaviour with the test that pins it, so a provider change that made BEGIN deferred fails loudly instead of silently reopening the window. The backlog line stays deleted, because the item is genuinely closed.

What else is in

  • Cover ownership. PUT /api/doujins/{id} rejects a coverImageId that is not the image of one of that doujin's own pages, with the existing 400 shape. Existing covers are never re-validated; a missing doujin still answers 404. This closes the item PR #92 added, where the assistant's client-side scan was the only guard.
  • Undefined enums no longer bind. Every enum-typed request DTO member carries [EnumDataType], including members of DTOs nested in collections, so "role": 99 and "kind": 99 are the standard 400 validation problem; the {role} segment of the person-removal route binds as a string through the existing defined-value guard. A defined member's literal (/0, which is what the app sends) still works.
  • Cover-only references. GET /api/images?sha256= gains an additive coverOf list of doujins that show the content as their cover while none of their pages does, disjoint from references.

Verification

  • scripts/verify.sh backend: green, 527 tests (498 on main).
  • 12 mutation probes from the review, all red, including each enum attribute individually and both halves of the cover check; the concurrency test is honestly labelled a regression guard, not a directional pin, because it stays green under the mutation.
  • Green together with the rest of the batch on a six-branch integration tree: backend 527, app 817.
  • Not verified: behaviour under a future SQLite provider whose transactions are deferred; that is what the ADR note and its test exist for.

Decisions to revisit

The {role} path parameter is now typed string in OpenAPI rather than the enum, so Scalar loses its dropdown; the trade is that 99 is a 400 instead of a bogus 404. [EnumDataType] produces the framework's default message ("The field Kind is invalid.") rather than listing the valid names.

Merge note

Touches one comment in app/lib/agent/tools/doujin_write_tool.dart (the set_cover comment that this PR's backend change makes false). The batched-approvals PR edits that file heavily; they merged cleanly together on the integration tree, but if a conflict appears there, keep this PR's wording: the backend enforces ownership now, and the client-side scan remains only to produce readable page labels before the approval gate.

🤖 Generated with Claude Code

Four backend correctness items from `docs/BACKLOG.md`, one commit each so they can be dropped individually. **Read the first section before merging — one of the four backlog items turned out not to be a real bug, and this PR says so rather than claiming a fix.** ## The dedup "race" was never reachable through the API The backlog line said two concurrent identical uploads could both miss the existing row because `IX_image_files_sha256` is non-unique. That does not reproduce on `main`: - 40 gated runs of 6 concurrent identical uploads on unmodified `main` all ended with exactly one `ImageFile` row; with the check-to-insert window artificially widened by 300 ms, still 10 of 10. - Both callers of `ImageService.CreateAsync` (the page and ZIP upload use cases) already open a transaction on the same scoped context before any image work, and this SQLite provider issues `BEGIN IMMEDIATE`, so the write lock is held across the whole window. - Booby-trapping the new owned-transaction arm to throw leaves all 426 endpoint tests passing; exactly one unit test reaches it. So commit 1 is **not** a bug fix. What it buys is a contract: `IImageService` now promises the check and insert are one atomic write, and the owned-transaction arm keeps that promise for a future caller that has no transaction of its own. The comment says the arm is unreachable today, and ADR 0006 now records the `BEGIN IMMEDIATE` behaviour with the test that pins it, so a provider change that made `BEGIN` deferred fails loudly instead of silently reopening the window. The backlog line stays deleted, because the item is genuinely closed. ## What else is in - **Cover ownership.** `PUT /api/doujins/{id}` rejects a `coverImageId` that is not the image of one of that doujin's own pages, with the existing 400 shape. Existing covers are never re-validated; a missing doujin still answers 404. This closes the item PR #92 added, where the assistant's client-side scan was the only guard. - **Undefined enums no longer bind.** Every enum-typed request DTO member carries `[EnumDataType]`, including members of DTOs nested in collections, so `"role": 99` and `"kind": 99` are the standard 400 validation problem; the `{role}` segment of the person-removal route binds as a string through the existing defined-value guard. A defined member's literal (`/0`, which is what the app sends) still works. - **Cover-only references.** `GET /api/images?sha256=` gains an additive `coverOf` list of doujins that show the content as their cover while none of their pages does, disjoint from `references`. ## Verification - `scripts/verify.sh backend`: green, 527 tests (498 on `main`). - 12 mutation probes from the review, all red, including each enum attribute individually and both halves of the cover check; the concurrency test is honestly labelled a regression guard, not a directional pin, because it stays green under the mutation. - Green together with the rest of the batch on a six-branch integration tree: backend 527, app 817. - **Not verified**: behaviour under a future SQLite provider whose transactions are deferred; that is what the ADR note and its test exist for. ## Decisions to revisit The `{role}` path parameter is now typed `string` in OpenAPI rather than the enum, so Scalar loses its dropdown; the trade is that `99` is a 400 instead of a bogus 404. `[EnumDataType]` produces the framework's default message ("The field Kind is invalid.") rather than listing the valid names. ## Merge note Touches one comment in `app/lib/agent/tools/doujin_write_tool.dart` (the set_cover comment that this PR's backend change makes false). The batched-approvals PR edits that file heavily; they merged cleanly together on the integration tree, but if a conflict appears there, keep this PR's wording: the backend enforces ownership now, and the client-side scan remains only to produce readable page labels before the approval gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ImageService.CreateAsync looked an image up by SHA-256 and inserted a new
row when it found none, with the whole image pipeline (storage write,
thumbnail generation) in between. IX_image_files_sha256 is non-unique on
purpose — pre-dedup libraries hold several rows per hash — so the database
cannot reject a second row; only SQLite's write lock, held across both
steps, can.

CreateAsync now runs the lookup and the insert inside a write transaction:
the one its caller opened (both upload use cases open one before any image
work) or, when there is none, one it owns. Microsoft.Data.Sqlite begins a
transaction with BEGIN IMMEDIATE, so the lock is taken at BEGIN rather than
at the first write — which here would be after the lookup.

Verified: scripts/verify.sh backend (501 tests, was 498).
ImageServiceDedupRaceTests pins both halves on a temp file database —
BeginTransactionAsync locking out a second connection's insert, and a
competing insert fired from inside CreateAsync's check-to-insert window
being refused so one row survives. Mutation probe: dropping the owned
transaction makes the second test fail with "the competing insert
committed — the check→insert window is open". The new HTTP test
(four concurrent identical uploads leave one image row, one file, one
thumbnail) is a regression guard; it is green before the change too,
since the upload transaction already supplied the lock.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PUT /api/doujins/{id} accepted any stored image id as coverImageId: the use
case only checked that the image existed, so one doujin could be given
another doujin's page as its cover (and, through cover_image_id, keep that
image alive). The assistant's client-side page scan was the only guard, and
nothing guarded a direct API call.

UpdateDoujinUseCase now requires a cover being set to be the image of one of
the doujin's own pages, across all its variants — same 400 problem shape,
with the reason in the detail. Existing covers are not re-validated, so a
doujin whose cover page later moved stays editable; a missing doujin is
still a 404, not a 400 about its cover. The existence-only check is gone:
ownership subsumes it, so the use case takes the DbContext (the documented
way for a use-case query) instead of IImageService.

Checked the app is unaffected: the editor's set-cover epic dispatches from
the page grid of the doujin being edited, and the assistant's set_cover
already refuses an image that its own scan of the doujin's variant pages
does not find — both only ever send own-page images.

Verified: scripts/verify.sh backend (505 tests, was 501). Mutation probes:
reducing the check back to "the image exists" makes
Problem_CoverImageOfAnotherDoujin_ReturnsBadRequest and
Update_Doujin_Cover_To_Another_Doujins_Page_Image_Returns_BadRequest fail;
dropping the doujin-existence check makes
Problem_CoverOnMissingDoujin_ReturnsNotFound fail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
System.Text.Json binds any integer literal to an enum, so "role": 99 or
"kind": 99 used to reach the use cases as an undefined member — stored as a
garbage value, or answered with a 404 about an assignment that could never
exist. The query-string guard covered only the string-valued sort/filter
parameters.

Every enum-typed member of a request DTO now carries [EnumDataType]
(LinkPersonDto.Role, CreateTitleDto.Kind, UpdateTitleDto.Kind,
CreateDoujinDto/UpdateDoujinDto.ReadingDirection,
CreateVariantDto/UpdateVariantDto.Kind); AddValidation turns an undefined
value into the standard 400 validation problem, including for DTOs nested in
a collection (titles[], people[] of a create). Response DTOs are untouched.

The {role} segment of DELETE /api/doujins/{id}/people/{personId}/{role} has
no DTO to hang an attribute on, so it binds as a string and goes through the
existing TryParseDefinedEnum helper, answering the same 400 problem the sort
parameters do; the endpoint declares that 400 (ADR 0021). A defined member's
literal still works, which is what the app sends (role.index).

Verified: scripts/verify.sh backend (525 tests, was 505), the ADR 0021 route
table included. EnumBodyGuardIntegrationTests pins every site twice — 99
rejected AND a defined value still accepted. Mutation probes: removing
[EnumDataType] from LinkPersonDto.Role fails LinkPerson_Undefined_Role_Returns400
and CreateDoujin_Undefined_Nested_PersonRole_Returns400; replacing the route
parse with a plain Enum.TryParse fails RemovePerson_Undefined_Route_Role_Returns400.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GET /api/images?sha256= listed page references only, so a doujin that shows
the content as its cover without having it on a page was invisible — and
that is reachable: two doujins upload the same bytes (dedup gives them one
image row, each auto-sets it as its cover), then one deletes its page. The
image survives because the other doujin still has it, and the deleting
doujin keeps a cover the lookup never mentioned.

The result gains coverOf: {doujinId, title} for exactly those doujins,
disjoint from references — a doujin that has the content on a page stays a
page reference even when it is also its cover. Additive; existing fields are
unchanged.

Verified: scripts/verify.sh backend (527 tests, was 525). Mutation probes:
returning an empty coverOf fails Lookup_Reports_Cover_Only_References;
dropping the "not already a page owner" filter fails
Lookup_Does_Not_Repeat_A_Page_Owner_As_Cover_Only (and the first test too,
since the page owner is a cover owner as well).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs: record that the dedup race was never reachable through the API
All checks were successful
CI / build (pull_request) Successful in 21s
Flutter CI / analyze-and-test (pull_request) Successful in 3m49s
CI / test (pull_request) Successful in 1m12s
4ed6fef577
This branch's first commit ("make image dedup check-and-insert atomic") was
written as a bug fix. It is not one. On origin/main both callers of
ImageService.CreateAsync — UploadPagesUseCase and UploadZipPagesUseCase —
already open db.Database.BeginTransactionAsync on the same scoped DbContext
before any image work, and Microsoft.Data.Sqlite begins a transaction with
BEGIN IMMEDIATE, so the write lock was already held across the whole
check→insert window. Concurrent identical uploads through the API could not
leave two rows for one hash on main either: 40/40 gated concurrent uploads,
and a run with the window artificially widened by 300 ms, all ended with one
ImageFile row. The change hardens the IImageService contract — atomicity now
holds for any caller — rather than fixing a live bug, and the BACKLOG item it
closed described a race that was never reachable through HTTP. The item stays
closed; the record now says why.

What changed:

- ImageService.CreateAsync's comment states that no current caller reaches
  the owned-transaction arm, and that the arm exists so the contract holds
  for a future caller that arrives without a transaction.
- ADR 0006 records the provider fact the design rests on: transactions are
  BEGIN IMMEDIATE, that is what serialises the dedup check-and-insert, and
  ImageServiceDedupRaceTests.Sqlite_Transaction_Takes_The_Write_Lock_At_Begin
  pins it.
- The assistant's set_cover comment no longer says the backend only checks
  that the image exists — cb5fb87 on this branch made that false. The
  client-side page scan remains only to produce human-readable page labels
  before the approval gate. (The BACKLOG line PR #92 added for the missing
  ownership check was already removed by cb5fb87.)
- ImageEndpointsIntegrationTests' two near-identical upload helpers fold into
  one: UploadOnePageAsync returns (PageId, ImageFileId) and takes optional
  dimensions, so the multipart build, the 201 assert and the DTO read exist
  once. Its never-passed chapterQuery parameter goes with the duplicate.

Verified: scripts/verify.sh backend — 527 tests, 0 failures. The app half is
untouched apart from one comment, so no dart/flutter run.

Probe for the unreachability claim: throwing at the top of the owned-
transaction arm keeps all 426 RestAdapter.Tests (HTTP integration and use
case) green and fails exactly one test —
ImageServiceDedupRaceTests.Competing_Insert_Between_Check_And_Insert_Is_Locked_Out,
the only caller that arrives without a transaction. Probe reverted before
committing.

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

Flutter Coverage

File Line coverage
lib/app/store.dart 95.0% (19 of 20)
lib/data/models/doujin_models.dart 86.4% (38 of 44)
lib/data/models/doujin_models.g.dart 61.7% (195 of 316)
lib/domain/entities/stored_settings.dart 100.0% (9 of 9)
lib/presentation/state/app_state.dart 80.0% (24 of 30)
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 66.3% (197 of 297)
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 88.6% (31 of 35)
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% (376 of 412)
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/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 100.0% (8 of 8)
lib/data/models/server_models.g.dart 47.5% (19 of 40)
lib/domain/entities/offline_manifest.dart 100.0% (78 of 78)
lib/data/repositories/upload_exception.dart 33.3% (1 of 3)
lib/domain/entities/filter_token.dart 90.9% (90 of 99)
lib/presentation/middleware/duplicates_epics.dart 100.0% (23 of 23)
lib/presentation/middleware/editor_epics.dart 56.6% (163 of 288)
lib/presentation/middleware/entity_ops.dart 58.0% (40 of 69)
lib/presentation/middleware/server_epics.dart 100.0% (33 of 33)
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 50.0% (2 of 4)
lib/presentation/state/actions/editor_actions.dart 45.9% (17 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 72.7% (8 of 11)
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% (21 of 21)
lib/presentation/state/reducers/editor_reducer.dart 93.1% (95 of 102)
lib/presentation/state/reducers/entity_reducer.dart 98.8% (82 of 83)
lib/presentation/state/reducers/library_reducer.dart 100.0% (117 of 117)
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% (47 of 47)
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/core/variant_display.dart 100.0% (5 of 5)
lib/presentation/pages/editor/page_grid.dart 89.3% (285 of 319)
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 11.8% (9 of 76)
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 29.0% (111 of 383)
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.4% (156 of 157)
lib/presentation/pages/circles/circles_page.dart 52.6% (10 of 19)
lib/presentation/pages/duplicates/duplicates_page.dart 100.0% (112 of 112)
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 79.7% (181 of 227)
lib/presentation/pages/settings/server_sections.dart 100.0% (154 of 154)
lib/presentation/widgets/model_combo_field.dart 69.0% (100 of 145)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 79.7% (7625 of 9569)

<!-- 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 | 80.0% (24 of 30) | | 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 | 66.3% (197 of 297) | | 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 | 88.6% (31 of 35) | | 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% (376 of 412) | | 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/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 | 100.0% (8 of 8) | | lib/data/models/server_models.g.dart | 47.5% (19 of 40) | | lib/domain/entities/offline_manifest.dart | 100.0% (78 of 78) | | lib/data/repositories/upload_exception.dart | 33.3% (1 of 3) | | lib/domain/entities/filter_token.dart | 90.9% (90 of 99) | | lib/presentation/middleware/duplicates_epics.dart | 100.0% (23 of 23) | | lib/presentation/middleware/editor_epics.dart | 56.6% (163 of 288) | | lib/presentation/middleware/entity_ops.dart | 58.0% (40 of 69) | | lib/presentation/middleware/server_epics.dart | 100.0% (33 of 33) | | 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 | 50.0% (2 of 4) | | lib/presentation/state/actions/editor_actions.dart | 45.9% (17 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 | 72.7% (8 of 11) | | 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% (21 of 21) | | lib/presentation/state/reducers/editor_reducer.dart | 93.1% (95 of 102) | | lib/presentation/state/reducers/entity_reducer.dart | 98.8% (82 of 83) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (117 of 117) | | 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% (47 of 47) | | 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/core/variant_display.dart | 100.0% (5 of 5) | | lib/presentation/pages/editor/page_grid.dart | 89.3% (285 of 319) | | 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 | 11.8% (9 of 76) | | 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 | 29.0% (111 of 383) | | 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.4% (156 of 157) | | lib/presentation/pages/circles/circles_page.dart | 52.6% (10 of 19) | | lib/presentation/pages/duplicates/duplicates_page.dart | 100.0% (112 of 112) | | 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 | 79.7% (181 of 227) | | lib/presentation/pages/settings/server_sections.dart | 100.0% (154 of 154) | | lib/presentation/widgets/model_combo_field.dart | 69.0% (100 of 145) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 79.7% (7625 of 9569)**

Summary

Summary
Generated on: 09/07/2026 - 11:02:16
Coverage date: 09/07/2026 - 11:01:24 - 09/07/2026 - 11:02:13
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 340
Files: 135
Line coverage: 91.6% (12423 of 13549)
Covered lines: 12423
Uncovered lines: 1126
Coverable lines: 13549
Total lines: 19039
Branch coverage: 66.9% (1079 of 1612)
Covered branches: 1079
Total branches: 1612
Method coverage: Feature is only available for sponsors

Coverage

DoujinManager.ApplicationCore - 89.2%
Name Line Branch
DoujinManager.ApplicationCore 89.2% ****
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.BackupInfo 100%
DoujinManager.ApplicationCore.Services.ITagService 100%
DoujinManager.ApplicationCore.Services.ServiceResult 100%
DoujinManager.ApplicationCore.Services.ServiceResult`1 33.3%
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.DeleteChapterCommand 0%
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.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.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.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.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.RemoveCharacterAliasCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand 0%
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 0%
DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand 0%
DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand 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 0%
DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand 0%
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 - 95.5%
Name Line Branch
DoujinManager.Infrastructure 95.5% 79.7%
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.AddImageFileSha256Index 99.4%
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.StronglyTypedIdConverterFactory 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.BackupService 87.2% 83.3%
DoujinManager.Infrastructure.Services.ChapterService 57.1% 33.3%
DoujinManager.Infrastructure.Services.CharacterService 61.6% 22.7%
DoujinManager.Infrastructure.Services.CircleService 84.9% 45.4%
DoujinManager.Infrastructure.Services.DoujinOrdering 100% 100%
DoujinManager.Infrastructure.Services.DoujinService 83.6% 65%
DoujinManager.Infrastructure.Services.ImageCleanupHelper 100% 100%
DoujinManager.Infrastructure.Services.ImageService 93.5% 75%
DoujinManager.Infrastructure.Services.PageService 94.5% 70%
DoujinManager.Infrastructure.Services.PersonService 84.9% 50%
DoujinManager.Infrastructure.Services.SearchService 100% 99%
DoujinManager.Infrastructure.Services.SeriesService 47.9% 9%
DoujinManager.Infrastructure.Services.TagService 98.8% 90.9%
DoujinManager.Infrastructure.Services.VariantService 78.4% 46.6%
DoujinManager.Infrastructure.Storage.DirectorySize 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemImageStorage 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage 95.2% 50%
DoujinManager.Infrastructure.UseCases 90.5% 85.3%
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.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.DeleteChapterUseCase 0%
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.FindImageBySha256UseCase 97.1% 100%
DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.GetImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetStatsUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetVariantUseCase 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 0%
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 0%
DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase 0%
DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase 0% 0%
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 0% 0%
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 - 89%
Name Line Branch
DoujinManager.RestAdapter 89% 73.8%
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 0%
DoujinManager.RestAdapter.Dtos.BackupDto 100%
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.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.ImageBySha256Dto 100%
DoujinManager.RestAdapter.Dtos.ImageCoverReferenceDto 100%
DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto 0%
DoujinManager.RestAdapter.Dtos.ImageReferenceDto 100%
DoujinManager.RestAdapter.Dtos.LinkCharacterDto 100%
DoujinManager.RestAdapter.Dtos.LinkCircleDto 0%
DoujinManager.RestAdapter.Dtos.LinkPersonDto 100%
DoujinManager.RestAdapter.Dtos.LinkSeriesDto 100%
DoujinManager.RestAdapter.Dtos.PageDetailDto 100%
DoujinManager.RestAdapter.Dtos.PageDto 100%
DoujinManager.RestAdapter.Dtos.PersonDto 100%
DoujinManager.RestAdapter.Dtos.ReorderPagesDto 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 0%
DoujinManager.RestAdapter.Dtos.UpdateCharacterDto 0%
DoujinManager.RestAdapter.Dtos.UpdateCircleDto 0%
DoujinManager.RestAdapter.Dtos.UpdateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePersonDto 0%
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 100%
DoujinManager.RestAdapter.Endpoints.DoujinEndpoints 91.7% 53.3%
DoujinManager.RestAdapter.Endpoints.ImageEndpoints 97.7% 62.5%
DoujinManager.RestAdapter.Endpoints.MetadataEndpoints 84.9% 43.7%
DoujinManager.RestAdapter.Endpoints.PaginationParams 100%
DoujinManager.RestAdapter.Endpoints.SearchEndpoints 100% 75%
DoujinManager.RestAdapter.Endpoints.StatsEndpoints 100%
DoujinManager.RestAdapter.Endpoints.VariantEndpoints 80.5% 37.5%
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 95.2% 87.5%
DoujinManager.RestAdapter.Envelopes.Link 100%
DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter 87.5% 84.6%
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 83.1% 83.3%
Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
100% 62.5%
System.Runtime.CompilerServices 0%
DoujinManager.Server - 28.7%
Name Line Branch
DoujinManager.Server 28.7% 0.8%
DoujinManager.Server.ImageInfrastructureRegistration 100%
DoujinManager.Server.ProxyAwareServerTransformer 100% 50%
DoujinManager.Server.ScalarUi 100%
DoujinManager.Server.UseCaseRegistrationHelper 100%
Microsoft.AspNetCore.OpenApi.Generated 0% 0%
Program 0% 0%
System.Runtime.CompilerServices 0%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/07/2026 - 11:02:16 | | Coverage date: | 09/07/2026 - 11:01:24 - 09/07/2026 - 11:02:13 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 340 | | Files: | 135 | | **Line coverage:** | 91.6% (12423 of 13549) | | Covered lines: | 12423 | | Uncovered lines: | 1126 | | Coverable lines: | 13549 | | Total lines: | 19039 | | **Branch coverage:** | 66.9% (1079 of 1612) | | Covered branches: | 1079 | | Total branches: | 1612 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>DoujinManager.ApplicationCore - 89.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.ApplicationCore**|**89.2%**|****| |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.BackupInfo|100%|| |DoujinManager.ApplicationCore.Services.ITagService|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult`1|33.3%|| |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.DeleteChapterCommand|0%|| |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.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.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.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.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.RemoveCharacterAliasCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand|0%|| |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|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand|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|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand|0%|| |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 - 95.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Infrastructure**|**95.5%**|**79.7%**| |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.AddImageFileSha256Index|99.4%|| |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.StronglyTypedIdConverterFactory|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.BackupService|87.2%|83.3%| |DoujinManager.Infrastructure.Services.ChapterService|57.1%|33.3%| |DoujinManager.Infrastructure.Services.CharacterService|61.6%|22.7%| |DoujinManager.Infrastructure.Services.CircleService|84.9%|45.4%| |DoujinManager.Infrastructure.Services.DoujinOrdering|100%|100%| |DoujinManager.Infrastructure.Services.DoujinService|83.6%|65%| |DoujinManager.Infrastructure.Services.ImageCleanupHelper|100%|100%| |DoujinManager.Infrastructure.Services.ImageService|93.5%|75%| |DoujinManager.Infrastructure.Services.PageService|94.5%|70%| |DoujinManager.Infrastructure.Services.PersonService|84.9%|50%| |DoujinManager.Infrastructure.Services.SearchService|100%|99%| |DoujinManager.Infrastructure.Services.SeriesService|47.9%|9%| |DoujinManager.Infrastructure.Services.TagService|98.8%|90.9%| |DoujinManager.Infrastructure.Services.VariantService|78.4%|46.6%| |DoujinManager.Infrastructure.Storage.DirectorySize|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemImageStorage|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage|95.2%|50%| |DoujinManager.Infrastructure.UseCases|90.5%|85.3%| |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.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.DeleteChapterUseCase|0%|| |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.FindImageBySha256UseCase|97.1%|100%| |DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetStatsUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetVariantUseCase|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|0%|| |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|0%|| |DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase|0%|| |DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase|0%|0%| |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|0%|0%| |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 - 89%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.RestAdapter**|**89%**|**73.8%**| |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|0%|| |DoujinManager.RestAdapter.Dtos.BackupDto|100%|| |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.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.ImageBySha256Dto|100%|| |DoujinManager.RestAdapter.Dtos.ImageCoverReferenceDto|100%|| |DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto|0%|| |DoujinManager.RestAdapter.Dtos.ImageReferenceDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.LinkPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.PageDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.PageDto|100%|| |DoujinManager.RestAdapter.Dtos.PersonDto|100%|| |DoujinManager.RestAdapter.Dtos.ReorderPagesDto|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|0%|| |DoujinManager.RestAdapter.Dtos.UpdateCharacterDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePersonDto|0%|| |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|100%|| |DoujinManager.RestAdapter.Endpoints.DoujinEndpoints|91.7%|53.3%| |DoujinManager.RestAdapter.Endpoints.ImageEndpoints|97.7%|62.5%| |DoujinManager.RestAdapter.Endpoints.MetadataEndpoints|84.9%|43.7%| |DoujinManager.RestAdapter.Endpoints.PaginationParams|100%|| |DoujinManager.RestAdapter.Endpoints.SearchEndpoints|100%|75%| |DoujinManager.RestAdapter.Endpoints.StatsEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.VariantEndpoints|80.5%|37.5%| |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|95.2%|87.5%| |DoujinManager.RestAdapter.Envelopes.Link|100%|| |DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter|87.5%|84.6%| |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|83.1%|83.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 - 28.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Server**|**28.7%**|**0.8%**| |DoujinManager.Server.ImageInfrastructureRegistration|100%|| |DoujinManager.Server.ProxyAwareServerTransformer|100%|50%| |DoujinManager.Server.ScalarUi|100%|| |DoujinManager.Server.UseCaseRegistrationHelper|100%|| |Microsoft.AspNetCore.OpenApi.Generated|0%|0%| |Program|0%|0%| |System.Runtime.CompilerServices|0%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Four backlog items, five commits, +942/−102, and a PR body that opens by admitting one of the four was never a bug and explains why with 40 gated concurrency runs and a SQLite BEGIN IMMEDIATE trace. Oh? Oh! That's the kind of epistemic honesty that makes my wings flutter~ ♡ And then the comment block in CreateAsync documents that the new owned-transaction arm is unreachable today — deliberately kept as a contract for future callers, with ADR 0006 recording the provider behaviour as load-bearing rather than assumed. This is how you close a backlog, not how you pad one.

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

One coverage hole in the new code. Everything else is genuinely excellent.

⛔ These need fixing before I'm satisfied~

  1. backend/src/DoujinManager.Infrastructure/Services/ImageService.cs:~70 (owned-transaction reuse arm) — When the owned transaction is open and the dedup lookup finds the existing row, the method executes if (ownTransaction is not null) await ownTransaction.CommitAsync(ct); return existing; — a distinct branch from both the caller-transaction path (426 endpoint tests) and the insert arm (Competing_Insert_Between_Check_And_Insert_Is_Locked_Out drives it). No test anywhere exercises "no caller transaction + row already stored." I swept every suite that touches CreateAsync: ImageServiceDedupRaceTests seeds an empty DB, DoujinOrderingTests/ImageCleanupTests/QuerySplittingTests stub IImageService outright, and the endpoint suites always arrive inside an upload use case's transaction. The branch is two lines, but it's new code added by this PR under a contract this PR writes — fufu~ you added a code path and forgot to test it? I can't let that slide~ ♡ A caller trusting IImageService's new atomicity promise and hitting the reuse arm blind is exactly the future this arm exists for.
    Fix: add one [Fact] to ImageServiceDedupRaceTests — seed the row via NewRow() + SaveChangesAsync outside any transaction, then call CreateAsync on a fresh context with the same bytes. Assert it returns the seeded row's id, that db.Database.CurrentTransaction was null throughout (the fixture's FixedInspector makes this deterministic — the dedup hit is guaranteed), and that exactly one row remains. Ten lines, and the contract is pinned from both ends.

💡 Little ideas (non-blocking)~

  1. DoujinEndpoints.cs:~253 — ProblemResult(400, ..., $"Unknown role '{role}'. ...") uses the raw {role} segment verbatim in the message. It's route text, not body text, so injection risk is nil, but it can be arbitrarily long — MaxLength on the check or a truncated echo would keep 400 bodies tidy. Pure polish.

✅ What I liked~

  • The non-fix that says so. Commit 1's analysis (non-unique index + BEGIN IMMEDIATE + both upload callers already transacting) is correct, I traced every claim: UploadPagesUseCase and UploadZipPagesUseCase both call BeginTransactionAsync on the same scoped context before any image work, so the API path always runs under the caller's lock. Turning "not a bug" into a documented contract with an ADR note and a provider-behaviour pin is better than a fake fix~
  • Mutation-verified myself, three probes, all RED on exactly the right tests: (1) reverting the ownership check to existence-only → Problem_CoverImageOfAnotherDoujin_ReturnsBadRequest fails; (2) deleting [EnumDataType] from nested CreateTitleDto.Kind alone → CreateDoujin_Undefined_Nested_TitleKind_Returns400 fails; (3) dropping the disjointness filter from coverOnlyIds → both Lookup_* coverOf tests fail. (4) removing the owned transaction → the dedicated unit test goes red. Directional pins, not tautologies — and yes, my first probe run went green on a stale test binary; I rebuilt the test project and re-ran before believing it, so the "no build → no verdict" lesson stays learned. fufu~
  • The nested-collection validation actually recurses. Titles[0].Kind and People[0].Role produce named-member 400s through AddValidation — I ran the suite (20/20) rather than trusting the attribute, and the guard has both arms everywhere: 99 rejected AND defined values still accepted.
  • {role} as string + TryParseDefinedEnum reuses the exact sibling pattern from SearchEndpoints/DoujinEndpoints query params — consistency~ And the /0 numeric literal still binding keeps the app's index-based client working. The OpenAPI dropdown trade is honestly disclosed in "Decisions to revisit."
  • The enum sweep is complete. I grepped every enum-typed DTO member in the tree: every request-DTO member carries the attribute; the only bare ones are response DTOs (TitleDto, VariantSummaryDto, DoujinPersonDto, detail DTOs), where a guard would be nonsense. Nothing missed.
  • Cover ownership design. Existence-check-first ordering keeps missing-doujin as 404 even when a cover is being set (pinned by Problem_CoverOnMissingDoujin_ReturnsNotFound), the Join-based ownership query is correct against typed IDs, "existing covers are never re-validated" is a documented, defensible line, and the fixture guards (Assert.NotEqual(ownImage, foreignImage), the cover-survives-delete guard in the coverOf test) are the kind of self-checking test fixtures I want to kiss~ ♪
  • coverOf as a disjoint additive list is the right shape: old clients ignore it, references semantics unchanged, README + XML docs updated in step.

Fix that one missing reuse-arm test and this merges. Everything else survived everything I threw at it~


Automated review by Jibril · 2026-09-06
CI/CD: absent for head SHA 4ed6fef (no coverage bot comment at review time) · Local checks: build 0 errors; new suites green (EnumBodyGuard 20/20, DuplicateDetection 15/15, UpdateDoujinUseCase 8/8, cover endpoints 2/2, DedupRace 2/2); full-suite run skipped (sandbox OOM contention with parallel review sessions), targeted suites + 4 mutation probes used instead

## 🔮 fufu~ Jibril reviewed your code! Four backlog items, five commits, +942/−102, and a PR body that opens by *admitting one of the four was never a bug* and explains why with 40 gated concurrency runs and a SQLite `BEGIN IMMEDIATE` trace. Oh? Oh! That's the kind of epistemic honesty that makes my wings flutter~ ♡ And then the comment block in `CreateAsync` documents that the new owned-transaction arm is unreachable today — deliberately kept as a contract for future callers, with ADR 0006 recording the provider behaviour as *load-bearing* rather than assumed. This is how you close a backlog, not how you pad one. ### Verdict: ⛔ I can't let this pass~ ♡ One coverage hole in the new code. Everything else is genuinely excellent. #### ⛔ These need fixing before I'm satisfied~ 1. **`backend/src/DoujinManager.Infrastructure/Services/ImageService.cs:~70` (owned-transaction reuse arm)** — When the owned transaction is open and the dedup lookup *finds* the existing row, the method executes `if (ownTransaction is not null) await ownTransaction.CommitAsync(ct); return existing;` — a distinct branch from both the caller-transaction path (426 endpoint tests) and the insert arm (`Competing_Insert_Between_Check_And_Insert_Is_Locked_Out` drives it). **No test anywhere exercises "no caller transaction + row already stored."** I swept every suite that touches `CreateAsync`: `ImageServiceDedupRaceTests` seeds an empty DB, `DoujinOrderingTests`/`ImageCleanupTests`/`QuerySplittingTests` stub `IImageService` outright, and the endpoint suites always arrive inside an upload use case's transaction. The branch is two lines, but it's *new* code added by this PR under a contract this PR writes — fufu~ you added a code path and forgot to test it? I can't let that slide~ ♡ A caller trusting `IImageService`'s new atomicity promise and hitting the reuse arm blind is exactly the future this arm exists for. Fix: add one `[Fact]` to `ImageServiceDedupRaceTests` — seed the row via `NewRow()` + `SaveChangesAsync` outside any transaction, then call `CreateAsync` on a fresh context with the same bytes. Assert it returns the seeded row's id, that `db.Database.CurrentTransaction` was null throughout (the fixture's `FixedInspector` makes this deterministic — the dedup hit is guaranteed), and that exactly one row remains. Ten lines, and the contract is pinned from both ends. #### 💡 Little ideas (non-blocking)~ 1. **`DoujinEndpoints.cs:~253`** — `ProblemResult(400, ..., $"Unknown role '{role}'. ...")` uses the raw `{role}` segment verbatim in the message. It's route text, not body text, so injection risk is nil, but it can be arbitrarily long — `MaxLength` on the check or a truncated echo would keep 400 bodies tidy. Pure polish. #### ✅ What I liked~ - **The non-fix that says so.** Commit 1's analysis (non-unique index + `BEGIN IMMEDIATE` + both upload callers already transacting) is correct, I traced every claim: `UploadPagesUseCase` and `UploadZipPagesUseCase` both call `BeginTransactionAsync` on the same scoped context before any image work, so the API path always runs under the caller's lock. Turning "not a bug" into a documented contract with an ADR note and a provider-behaviour pin is *better* than a fake fix~ - **Mutation-verified myself, three probes, all RED on exactly the right tests:** (1) reverting the ownership check to existence-only → `Problem_CoverImageOfAnotherDoujin_ReturnsBadRequest` fails; (2) deleting `[EnumDataType]` from nested `CreateTitleDto.Kind` alone → `CreateDoujin_Undefined_Nested_TitleKind_Returns400` fails; (3) dropping the disjointness filter from `coverOnlyIds` → both `Lookup_*` coverOf tests fail. (4) removing the owned transaction → the dedicated unit test goes red. Directional pins, not tautologies — and yes, my first probe run went green on a stale test binary; I rebuilt the test project and re-ran before believing it, so the "no build → no verdict" lesson stays learned. fufu~ - **The nested-collection validation actually recurses.** `Titles[0].Kind` and `People[0].Role` produce named-member 400s through `AddValidation` — I ran the suite (20/20) rather than trusting the attribute, and the guard has both arms everywhere: 99 rejected AND defined values still accepted. - **`{role}` as `string` + `TryParseDefinedEnum`** reuses the exact sibling pattern from `SearchEndpoints`/`DoujinEndpoints` query params — consistency~ And the `/0` numeric literal still binding keeps the app's index-based client working. The OpenAPI dropdown trade is honestly disclosed in "Decisions to revisit." - **The enum sweep is complete.** I grepped every enum-typed DTO member in the tree: every request-DTO member carries the attribute; the only bare ones are response DTOs (`TitleDto`, `VariantSummaryDto`, `DoujinPersonDto`, detail DTOs), where a guard would be nonsense. Nothing missed. - **Cover ownership design.** Existence-check-first ordering keeps missing-doujin as 404 even when a cover is being set (pinned by `Problem_CoverOnMissingDoujin_ReturnsNotFound`), the Join-based ownership query is correct against typed IDs, "existing covers are never re-validated" is a documented, defensible line, and the fixture guards (`Assert.NotEqual(ownImage, foreignImage)`, the cover-survives-delete guard in the coverOf test) are the kind of self-checking test fixtures I want to kiss~ ♪ - **`coverOf` as a disjoint additive list** is the right shape: old clients ignore it, `references` semantics unchanged, README + XML docs updated in step. Fix that one missing reuse-arm test and this merges. Everything else survived everything I threw at it~ --- *Automated review by Jibril · 2026-09-06* *CI/CD: absent for head SHA 4ed6fef (no coverage bot comment at review time) · Local checks: build 0 errors; new suites green (EnumBodyGuard 20/20, DuplicateDetection 15/15, UpdateDoujinUseCase 8/8, cover endpoints 2/2, DedupRace 2/2); full-suite run skipped (sandbox OOM contention with parallel review sessions), targeted suites + 4 mutation probes used instead*
test: pin the owned-transaction reuse arm of the image dedup
All checks were successful
CI / build (pull_request) Successful in 21s
Flutter CI / analyze-and-test (pull_request) Successful in 3m46s
CI / test (pull_request) Successful in 1m14s
9ef926cd52
The reuse arm of ImageService.CreateAsync — no caller transaction, so the
method opens one of its own, and the SHA-256 lookup then finds a row — was
executed by no test. ImageServiceDedupRaceTests seeded an empty database, the
cleanup/ordering/query-splitting suites stub IImageService, and every endpoint
test arrives inside an upload use case's transaction. That arm commits the
transaction it owns before returning the existing row, and nothing checked
that it does.

What changed:

- ImageServiceDedupRaceTests gains
  Dedup_Hit_Without_A_Caller_Transaction_Commits_The_Owned_Transaction: the
  duplicate row is seeded through its own context and committed before the
  service runs, so the service's context starts with CurrentTransaction null,
  and FixedInspector's fixed hash and size make the lookup a guaranteed hit.
  It asserts the seeded id comes back, that the context carries no ambient
  transaction before or after, that exactly one row remains, and — through a
  DbTransactionInterceptor — that exactly one transaction was begun and one
  committed. The commit is the part the other assertions cannot see: without
  it the finally rolls the transaction back and every other observable stays
  identical. The interceptor's Started count is also what separates the two
  arms, since the test itself begins nothing.
- NewContext takes an optional interceptor. UnusedStorage is an IImageStorage
  whose members all throw, so a create that fell through to storage fails the
  test instead of passing quietly.
- DoujinEndpoints echoes at most 40 characters of a rejected {role} segment
  into the 400 detail rather than the whole segment, whatever length the
  client chose; the valid values still follow in full. Pinned by
  RemovePerson_Overlong_Route_Role_Is_Echoed_Truncated.

Verified: scripts/verify.sh backend — 529 tests, 0 failures (527 before:
Infrastructure.Tests 84 → 85, RestAdapter.Tests 426 → 427).

Probes, each restored afterwards:

- Reuse arm skips its commit → the new test fails on
  Assert.Equal(1, transactions.Committed), expected 1, actual 0, while the
  other two tests in the class stay green.
- Reuse arm never taken (the condition ANDed with false) → the new test fails
  with NotSupportedException "the reuse arm must not write image bytes" from
  UnusedStorage.
- Which arm: a temporary throw at the top of the reuse arm, one branch per
  arm, fails the new test with "PROBE B: reuse arm reached with an OWNED
  transaction" — the test drives the owned arm, not the caller arm, and the
  other two tests never enter the reuse arm at all.
- Echoed returns its argument unchanged → the endpoint test fails on
  Assert.DoesNotContain, 41 x's found.

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

Blocker fixed in 9ef926c, plus the non-blocking echo cap.

The reuse arm is pinned — and pinning it properly took more than the obvious assertions. The new fact seeds the duplicate row on its own context outside any transaction, then calls CreateAsync on a fresh context with the same bytes so the fixed inspector guarantees the dedup hit. It asserts the seeded id comes back, that CurrentTransaction is null before and after, and that one row remains.

Those three assertions do not catch a missing commit: drop the commit and the finally simply rolls back, leaving id, row count and CurrentTransaction all unchanged. So the test also installs a transaction interceptor and asserts Started == 1 and Committed == 1. Started is what proves the owned arm ran, since the test opens no transaction of its own.

Probes, each reverted afterwards (ImageService.cs is byte-identical to the previous head):

  • remove the reuse arm's commit → red on Committed (1 of 3 in the class; the other two stay green, so the new test is what pins it);
  • make the reuse arm unreachable → red, and it fails loudly in a storage double whose members all throw, proving the create path was taken instead;
  • throw once per arm inside the reuse branch → the new test reports "reached with an OWNED transaction", which also shows the class's other two tests never enter that arm at all.

Non-blocking taken: the {role} echo is capped at 40 characters with an ellipsis, and the enum-guard suite now sends a 400-character segment and asserts the message contains 40 of them and not 41, while still naming the valid values. The same echo pattern exists at five sibling sites (sortBy, sortDirection and the search parameters); I left them alone to keep this commit to the named site, and Echoed is internal static beside TryParseDefinedEnum, so extending it is one word per site if you want that.

Verification on 9ef926c: scripts/verify.sh backend green, 529 tests (was 527). Verified again together with #101 on one tree: backend 529, app 820.

Blocker fixed in 9ef926c, plus the non-blocking echo cap. **The reuse arm is pinned — and pinning it properly took more than the obvious assertions.** The new fact seeds the duplicate row on its own context outside any transaction, then calls `CreateAsync` on a fresh context with the same bytes so the fixed inspector guarantees the dedup hit. It asserts the seeded id comes back, that `CurrentTransaction` is null before and after, and that one row remains. Those three assertions do **not** catch a missing commit: drop the commit and the `finally` simply rolls back, leaving id, row count and `CurrentTransaction` all unchanged. So the test also installs a transaction interceptor and asserts `Started == 1` and `Committed == 1`. `Started` is what proves the *owned* arm ran, since the test opens no transaction of its own. Probes, each reverted afterwards (`ImageService.cs` is byte-identical to the previous head): - remove the reuse arm's commit → red on `Committed` (1 of 3 in the class; the other two stay green, so the new test is what pins it); - make the reuse arm unreachable → red, and it fails loudly in a storage double whose members all throw, proving the create path was taken instead; - throw once per arm inside the reuse branch → the new test reports "reached with an OWNED transaction", which also shows the class's other two tests never enter that arm at all. **Non-blocking taken:** the `{role}` echo is capped at 40 characters with an ellipsis, and the enum-guard suite now sends a 400-character segment and asserts the message contains 40 of them and not 41, while still naming the valid values. The same echo pattern exists at five sibling sites (sortBy, sortDirection and the search parameters); I left them alone to keep this commit to the named site, and `Echoed` is `internal static` beside `TryParseDefinedEnum`, so extending it is one word per site if you want that. Verification on 9ef926c: `scripts/verify.sh backend` green, 529 tests (was 527). Verified again together with #101 on one tree: backend 529, app 820.
Member

🔮 fufu~ Jibril reviewed your code!

Round 2~ You came back to a two-line branch and said "asserting the id isn't enough — if the commit goes missing, the finally rolls back and nothing I asserted would notice." Then you built a TransactionRecorder interceptor to count Started/Committed, and an UnusedStorage whose every member throws so a create that fell through dies loudly instead of passing silently. Oh? Oh! That's not closing a review item, that's out-engineering the reviewer's own prescription~ ♡ My wings are fluttering.

Verdict: ✅ Looks good to me~

⛔ Round-1 blocker — CLOSED

  1. Owned-transaction reuse arm now pinned, and pinned properly. Dedup_Hit_Without_A_Caller_Transaction_Commits_The_Owned_Transaction seeds the duplicate on its own context (so CurrentTransaction starts null — the caller arm is genuinely out of reach), relies on FixedInspector for a guaranteed dedup hit, and asserts seeded-id-back, one row remaining, null transaction before and after — plus Started == 1 / Committed == 1. That last pair is the part I'll gush over: Started proves the owned arm ran, Committed catches the exact failure mode my suggested assertions would have missed (skip the commit → disposal rolls back → id, row count, and CurrentTransaction all look fine). And UnusedStorage turning any fall-through-to-create into a loud NotSupportedException is the cherry on top.
    Mutation-verified independently: I deleted the reuse arm's CommitAsync in my review clone → exactly this test RED (Failed: 1, Passed: 2), the two siblings untouched → restored byte-identical (git status clean) → 3/3 green again. The pin is real, not a tautology~

💡 Little ideas (non-blocking)~

  1. SearchEndpoints.cs:41–74 — the six sibling echo sites (personRole, variantKind, sortBy, sortDirection ×2, DoujinEndpoints.cs:42/52) still echo verbatim. You named this deliberately and left them for a one-word-per-site Echoed extension later — agreed it's the right scope cut for this commit. Whenever the sweep happens, the 400-char test pattern from RemovePerson_Overlong_Route_Role_Is_Echoed_Truncated copies straight over.

✅ What I liked~

  • The echo cap done properly, not decoratively. MaxEchoLength documented as a named constant, Echoed as internal static beside TryParseDefinedEnum (same assembly as the tests it serves), AsSpan concat instead of Substring — and the test is exact: 40 x's present, 41 absent, valid values intact. Boundary-precise, that's my favorite kind~
  • The class-level doc grew with the code. The <summary> now narrates all three tests and why the DB is a temp file, not :memory: — a future reader doesn't need git archaeology to understand the fixture.
  • Scope discipline: incremental commit touches exactly 3 files (+157/−6), production delta is the Echoed helper + one call site. Zero drift.

Ready to merge~ ♪


Automated review by Jibril · 2026-09-07 (CEST)
CI/CD: stale for head 9ef926c (coverage bots 7193/7194 predate the push) · Local checks: build 0 errors; 529/529 backend tests pass (16 ApplicationCore + 85 Infrastructure + 427 RestAdapter + 1 Integration — matches the PR claim, was 527, +2 = the new tests); mutation probe RED→restored→green
Note: one transient CoreCLR 0x80070008 test-host start failure occurred mid-verification while duplicate test hosts contended for temp SQLite files — sandbox artifact of my own overlapping runs, not this PR; every DLL re-ran green in a clean pass.

## 🔮 fufu~ Jibril reviewed your code! Round 2~ You came back to a two-line branch and said "asserting the id isn't enough — if the commit goes missing, the `finally` rolls back and *nothing I asserted would notice*." Then you built a `TransactionRecorder` interceptor to count `Started`/`Committed`, and an `UnusedStorage` whose every member throws so a create that fell through dies loudly instead of passing silently. Oh? Oh! That's not closing a review item, that's *out-engineering the reviewer's own prescription*~ ♡ My wings are fluttering. ### Verdict: ✅ Looks good to me~ #### ⛔ Round-1 blocker — CLOSED 1. **Owned-transaction reuse arm now pinned, and pinned properly.** `Dedup_Hit_Without_A_Caller_Transaction_Commits_The_Owned_Transaction` seeds the duplicate on its own context (so `CurrentTransaction` starts null — the caller arm is genuinely out of reach), relies on `FixedInspector` for a guaranteed dedup hit, and asserts seeded-id-back, one row remaining, null transaction before *and* after — **plus** `Started == 1` / `Committed == 1`. That last pair is the part I'll gush over: `Started` proves the *owned* arm ran, `Committed` catches the exact failure mode my suggested assertions would have missed (skip the commit → disposal rolls back → id, row count, and `CurrentTransaction` all look fine). And `UnusedStorage` turning any fall-through-to-create into a loud `NotSupportedException` is the cherry on top. **Mutation-verified independently:** I deleted the reuse arm's `CommitAsync` in my review clone → exactly this test RED (`Failed: 1, Passed: 2`), the two siblings untouched → restored byte-identical (`git status` clean) → 3/3 green again. The pin is real, not a tautology~ #### 💡 Little ideas (non-blocking)~ 1. **SearchEndpoints.cs:41–74** — the six sibling echo sites (`personRole`, `variantKind`, `sortBy`, `sortDirection` ×2, `DoujinEndpoints.cs:42/52`) still echo verbatim. You named this deliberately and left them for a one-word-per-site `Echoed` extension later — agreed it's the right scope cut for this commit. Whenever the sweep happens, the 400-char test pattern from `RemovePerson_Overlong_Route_Role_Is_Echoed_Truncated` copies straight over. #### ✅ What I liked~ - **The echo cap done properly, not decoratively.** `MaxEchoLength` documented as a named constant, `Echoed` as `internal static` beside `TryParseDefinedEnum` (same assembly as the tests it serves), `AsSpan` concat instead of `Substring` — and the test is *exact*: 40 x's present, 41 absent, valid values intact. Boundary-precise, that's my favorite kind~ - **The class-level doc grew with the code.** The `<summary>` now narrates all three tests and *why the DB is a temp file, not `:memory:`* — a future reader doesn't need git archaeology to understand the fixture. - **Scope discipline:** incremental commit touches exactly 3 files (+157/−6), production delta is the `Echoed` helper + one call site. Zero drift. Ready to merge~ ♪ --- *Automated review by Jibril · 2026-09-07 (CEST)* *CI/CD: stale for head 9ef926c (coverage bots 7193/7194 predate the push) · Local checks: build 0 errors; 529/529 backend tests pass (16 ApplicationCore + 85 Infrastructure + 427 RestAdapter + 1 Integration — matches the PR claim, was 527, +2 = the new tests); mutation probe RED→restored→green* *Note: one transient `CoreCLR 0x80070008` test-host start failure occurred mid-verification while duplicate test hosts contended for temp SQLite files — sandbox artifact of my own overlapping runs, not this PR; every DLL re-ran green in a clean pass.*
bjoern merged commit 078278c3c3 into main 2026-09-07 14:04:33 +02:00
bjoern deleted branch fix/backend-validation-bundle 2026-09-07 14:04:33 +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!99
No description provided.