feat: Phase 1 · 5/7 — server wiring, page-image endpoint & the seeder #9

Merged
bjoern merged 2 commits from feat/p1-server into main 2026-07-24 20:36:10 +02:00
Member

Cut 5 of the Phase-1 stack (after #8): the composition root comes alive.

Scope

  • Program.cs: ORIHON_DB_PATH / ORIHON_PROJECTS_DIR (each its own Docker volume — ADR 0008), AddUseCases() + AddInfrastructure(), and migrate-at-startup (single user, single node — ADR 0005).
  • The single authorized page-image route (ADR 0005): minimal-API MapGet (a Blazor circuit can't write response bytes), gated by the fallback policy, 404 for unknown pages and for cross-project reads, streaming with the right content type, range support, and private, max-age=1d caching (raw scans never change in place — re-uploads dedupe).
  • SeedDevData (AGENTS.md's sample world): a ready project with metadata, six pages across the kinds (some annotated, images deliberately absent so empty states show), regions across the taxonomy in RTL order with mixed translation states, one rough box, one recorded-not-typeset document, every bible table, summaries on only some pages. Real use cases wherever one exists; ports only where none does (imageless pages, wizard fast-forward) — documented on the class. Opt-in via ORIHON_SEED_DEV_DATA, no-op unless the DB has no project. AGENTS.md's seed section updated to describe what exists (the run/model-settings content arrives with its phases).
  • The Server host adds SkiaSharp.NativeAssets.Linux.NoDependencies — the container's libSkiaSharp.so, completing #8's packaging split.

Coverage

11 new tests. The endpoint suite drives the real host via the existing GatedAppFactory: unauthenticated → gate redirect; unknown page → 404; wrong project + right page → 404; stored page → byte-identical PNG with type/caching asserted structurally. The seed hook is proven at Program level (boot with the flag → sample world; second run → still exactly one project) plus the table-by-table contract test. 181 tests green solution-wide.

Final stretch: cut 6 — UI: project list + creation wizard (bUnit); cut 7 — UI: workspace/bible/page editor (bUnit).

🤖 Generated with Claude Code

Cut 5 of the Phase-1 stack (after #8): the composition root comes alive. ## Scope - **Program.cs**: `ORIHON_DB_PATH` / `ORIHON_PROJECTS_DIR` (each its own Docker volume — ADR 0008), `AddUseCases()` + `AddInfrastructure()`, and **migrate-at-startup** (single user, single node — ADR 0005). - **The single authorized page-image route** (ADR 0005): minimal-API `MapGet` (a Blazor circuit can't write response bytes), gated by the fallback policy, `404` for unknown pages **and for cross-project reads**, streaming with the right content type, range support, and `private, max-age=1d` caching (raw scans never change in place — re-uploads dedupe). - **`SeedDevData`** (AGENTS.md's sample world): a ready project with metadata, six pages across the kinds (some annotated, images deliberately absent so empty states show), regions across the taxonomy in RTL order with mixed translation states, one rough box, one recorded-not-typeset document, every bible table, summaries on only some pages. Real use cases wherever one exists; ports only where none does (imageless pages, wizard fast-forward) — documented on the class. Opt-in via `ORIHON_SEED_DEV_DATA`, no-op unless the DB has no project. AGENTS.md's seed section updated to describe what exists (the run/model-settings content arrives with its phases). - The Server host adds `SkiaSharp.NativeAssets.Linux.NoDependencies` — the container's `libSkiaSharp.so`, completing #8's packaging split. ## Coverage 11 new tests. The endpoint suite drives the **real host** via the existing `GatedAppFactory`: unauthenticated → gate redirect; unknown page → 404; wrong project + right page → 404; stored page → byte-identical PNG with type/caching asserted structurally. The seed hook is proven at Program level (boot with the flag → sample world; second run → still exactly one project) plus the table-by-table contract test. 181 tests green solution-wide. Final stretch: cut 6 — UI: project list + creation wizard (bUnit); cut 7 — UI: workspace/bible/page editor (bUnit). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: composition-root wiring, the page-image endpoint, and the sample-world seeder
All checks were successful
CI / build (pull_request) Successful in 21s
CI / test (pull_request) Successful in 29s
fe0cf1f2a5
Phase 1, cut 5 of 7: Program.cs gains its data paths (ORIHON_DB_PATH / ORIHON_PROJECTS_DIR under
the Docker-volume convention of ADR 0008), AddUseCases + AddInfrastructure, migrate-at-startup
(single user, single node — ADR 0005), and the one authorized page-image route: a minimal-API
MapGet behind the fallback policy, refusing cross-project reads, streaming with content type,
range support, and a day of private caching. The Server host carries the Linux SkiaSharp natives
the Infrastructure library deliberately leaves out. SeedDevData builds AGENTS.md's sample world
through the real use cases (ports only where no user-facing operation exists — imageless pages,
the wizard fast-forward), opt-in via ORIHON_SEED_DEV_DATA and a no-op unless the DB is empty;
AGENTS.md's seed section now describes what exists rather than what is planned.

Tests drive the real host via GatedAppFactory: the route is gated, 404s on unknown pages and
cross-project reads, streams byte-identical PNGs; the seed hook boots seeded exactly once
(contract-tested table by table). 181 tests green.

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

Summary

Summary
Generated on: 07/24/2026 - 18:29:18
Coverage date: 07/24/2026 - 18:29:13 - 07/24/2026 - 18:29:16
Parser: MultiReport (4x Cobertura)
Assemblies: 6
Classes: 102
Files: 75
Line coverage: 98.5% (2740 of 2781)
Covered lines: 2740
Uncovered lines: 41
Coverable lines: 2781
Total lines: 5114
Branch coverage: 94.1% (292 of 310)
Covered branches: 292
Total branches: 310
Method coverage: Feature is only available for sponsors

Coverage

Orihon.BlazorAdapter - 100%
Name Line Branch
Orihon.BlazorAdapter 100% ****
Orihon.BlazorAdapter.BlazorAdapterAssembly 100%
Orihon.Domain - 100%
Name Line Branch
Orihon.Domain 100% 100%
Orihon.Domain.Bible.Character 100% 100%
Orihon.Domain.Bible.GlossaryEntry 100% 100%
Orihon.Domain.Bible.LoreEntry 100% 100%
Orihon.Domain.Bible.PageSummary 100%
Orihon.Domain.Bible.StoryBeat 100%
Orihon.Domain.Projects.Project 100% 100%
Orihon.Domain.Projects.ProjectProfile 100%
Orihon.Domain.Text 100% 100%
Orihon.Domain.Translation.BoundingBox 100%
Orihon.Domain.Translation.Chapter 100%
Orihon.Domain.Translation.Page 100%
Orihon.Domain.Translation.Region 100% 100%
Orihon.Domain.Translation.RegionProfile 100%
Orihon.Infrastructure - 98.8%
Name Line Branch
Orihon.Infrastructure 98.8% 100%
Orihon.Infrastructure.Bible.EfBibleStore 100% 100%
Orihon.Infrastructure.DependencyInjection 100%
Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.GlossaryEntryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.JsonColumnMapper 100%
Orihon.Infrastructure.Persistence.Configurations.LoreEntryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.PageConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.PageSummaryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.RegionConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration 100%
Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Orihon.Infrastructure.Persistence.Migrations.InitialTranslationDomain 97.3%
Orihon.Infrastructure.Persistence.Migrations.OrihonDbContextModelSnapshot 100%
Orihon.Infrastructure.Persistence.OrihonDbContext 100%
Orihon.Infrastructure.Persistence.OrihonDbContextFactory 100%
Orihon.Infrastructure.Projects.EfProjectStore 100% 100%
Orihon.Infrastructure.Projects.FileSystemPageImageStore 100% 100%
Orihon.Infrastructure.Translation.EfChapterStore 100% 100%
Orihon.Infrastructure.Translation.EfPageStore 100% 100%
Orihon.Infrastructure.Translation.EfRegionStore 100% 100%
Orihon.Infrastructure.Translation.Ordering 100% 100%
Orihon.Kernel - 90.9%
Name Line Branch
Orihon.Kernel 90.9% 75%
Orihon.Kernel.Err`1 100%
Orihon.Kernel.Ok`1 100%
Orihon.Kernel.Result`1 88.8% 75%
Orihon.Server - 91.3%
Name Line Branch
Orihon.Server 91.3% 64.7%
Orihon.Server.Components.App 100%
Orihon.Server.Components.Layout.MainLayout 100%
Orihon.Server.Components.Pages.Gate 64.2% 66.6%
Orihon.Server.Security.AccessGate 91.8% 41.6%
Orihon.Server.Security.AccessSecret 100% 50%
Program 94.5% 85.7%
Orihon.UseCases - 98.7%
Name Line Branch
Orihon.UseCases 98.7% 96.9%
Orihon.UseCases.Bible.AddCharacter 100% 100%
Orihon.UseCases.Bible.AddGlossaryEntry 100% 100%
Orihon.UseCases.Bible.AddLoreEntry 100% 100%
Orihon.UseCases.Bible.AddStoryBeat 100% 100%
Orihon.UseCases.Bible.BibleDto 100%
Orihon.UseCases.Bible.CharacterDto 100%
Orihon.UseCases.Bible.DeleteCharacter 100% 100%
Orihon.UseCases.Bible.DeleteGlossaryEntry 100% 100%
Orihon.UseCases.Bible.DeleteLoreEntry 100% 100%
Orihon.UseCases.Bible.DeletePageSummary 100% 100%
Orihon.UseCases.Bible.DeleteStoryBeat 100% 100%
Orihon.UseCases.Bible.GetBible 100% 100%
Orihon.UseCases.Bible.GlossaryEntryDto 100%
Orihon.UseCases.Bible.LoreEntryDto 100%
Orihon.UseCases.Bible.PageSummaryDto 100%
Orihon.UseCases.Bible.ReorderStoryBeats 100%
Orihon.UseCases.Bible.SetPageSummary 100% 100%
Orihon.UseCases.Bible.StoryBeatDto 100%
Orihon.UseCases.Bible.UpdateCharacter 100% 100%
Orihon.UseCases.Bible.UpdateGlossaryEntry 100% 100%
Orihon.UseCases.Bible.UpdateLoreEntry 100% 100%
Orihon.UseCases.Bible.UpdateStoryBeat 100% 100%
Orihon.UseCases.Chapters.ChapterDto 100%
Orihon.UseCases.Chapters.CreateChapter 100% 100%
Orihon.UseCases.Chapters.DeleteChapter 100% 100%
Orihon.UseCases.Chapters.RenameChapter 100% 100%
Orihon.UseCases.Chapters.ReorderChapters 100%
Orihon.UseCases.DependencyInjection 100%
Orihon.UseCases.Diagnostics.SeedDevData 99% 92.8%
Orihon.UseCases.NextOrder 100%
Orihon.UseCases.Pages.DeletePage 100% 100%
Orihon.UseCases.Pages.GetPage 100% 100%
Orihon.UseCases.Pages.GetProjectWorkspace 100% 100%
Orihon.UseCases.Pages.ImportPages 98.4% 96.4%
Orihon.UseCases.Pages.ImportPagesResult 100%
Orihon.UseCases.Pages.MovePage 100% 92.8%
Orihon.UseCases.Pages.PageDetailDto 75%
Orihon.UseCases.Pages.PageDto 95.6%
Orihon.UseCases.Pages.PageUpload 100%
Orihon.UseCases.Pages.ProjectWorkspaceDto 75%
Orihon.UseCases.Pages.ReorderPages 100%
Orihon.UseCases.Pages.SetPageMeta 100% 100%
Orihon.UseCases.Pages.WorkspaceChapterDto 100%
Orihon.UseCases.Projects.CompleteProjectSetup 92.8% 83.3%
Orihon.UseCases.Projects.CreateProject 100% 100%
Orihon.UseCases.Projects.DeleteProject 100% 100%
Orihon.UseCases.Projects.GetProject 100% 100%
Orihon.UseCases.Projects.ListProjects 100%
Orihon.UseCases.Projects.ProjectDto 95.8%
Orihon.UseCases.Projects.StoredPageImage 100%
Orihon.UseCases.Projects.UpdateProjectMetadata 100% 100%
Orihon.UseCases.Regions.CreateRegion 100% 100%
Orihon.UseCases.Regions.DeleteRegion 100% 100%
Orihon.UseCases.Regions.RegionDto 97%
Orihon.UseCases.Regions.ReorderRegions 100%
Orihon.UseCases.Regions.UpdateRegion 100% 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/24/2026 - 18:29:18 | | Coverage date: | 07/24/2026 - 18:29:13 - 07/24/2026 - 18:29:16 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 6 | | Classes: | 102 | | Files: | 75 | | **Line coverage:** | 98.5% (2740 of 2781) | | Covered lines: | 2740 | | Uncovered lines: | 41 | | Coverable lines: | 2781 | | Total lines: | 5114 | | **Branch coverage:** | 94.1% (292 of 310) | | Covered branches: | 292 | | Total branches: | 310 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Orihon.BlazorAdapter - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.BlazorAdapter**|**100%**|****| |Orihon.BlazorAdapter.BlazorAdapterAssembly|100%|| </details> <details><summary>Orihon.Domain - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Domain**|**100%**|**100%**| |Orihon.Domain.Bible.Character|100%|100%| |Orihon.Domain.Bible.GlossaryEntry|100%|100%| |Orihon.Domain.Bible.LoreEntry|100%|100%| |Orihon.Domain.Bible.PageSummary|100%|| |Orihon.Domain.Bible.StoryBeat|100%|| |Orihon.Domain.Projects.Project|100%|100%| |Orihon.Domain.Projects.ProjectProfile|100%|| |Orihon.Domain.Text|100%|100%| |Orihon.Domain.Translation.BoundingBox|100%|| |Orihon.Domain.Translation.Chapter|100%|| |Orihon.Domain.Translation.Page|100%|| |Orihon.Domain.Translation.Region|100%|100%| |Orihon.Domain.Translation.RegionProfile|100%|| </details> <details><summary>Orihon.Infrastructure - 98.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Infrastructure**|**98.8%**|**100%**| |Orihon.Infrastructure.Bible.EfBibleStore|100%|100%| |Orihon.Infrastructure.DependencyInjection|100%|| |Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.GlossaryEntryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.JsonColumnMapper|100%|| |Orihon.Infrastructure.Persistence.Configurations.LoreEntryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.PageConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.PageSummaryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.RegionConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration|100%|| |Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Orihon.Infrastructure.Persistence.Migrations.InitialTranslationDomain|97.3%|| |Orihon.Infrastructure.Persistence.Migrations.OrihonDbContextModelSnapshot|100%|| |Orihon.Infrastructure.Persistence.OrihonDbContext|100%|| |Orihon.Infrastructure.Persistence.OrihonDbContextFactory|100%|| |Orihon.Infrastructure.Projects.EfProjectStore|100%|100%| |Orihon.Infrastructure.Projects.FileSystemPageImageStore|100%|100%| |Orihon.Infrastructure.Translation.EfChapterStore|100%|100%| |Orihon.Infrastructure.Translation.EfPageStore|100%|100%| |Orihon.Infrastructure.Translation.EfRegionStore|100%|100%| |Orihon.Infrastructure.Translation.Ordering|100%|100%| </details> <details><summary>Orihon.Kernel - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Kernel**|**90.9%**|**75%**| |Orihon.Kernel.Err`1|100%|| |Orihon.Kernel.Ok`1|100%|| |Orihon.Kernel.Result`1|88.8%|75%| </details> <details><summary>Orihon.Server - 91.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Server**|**91.3%**|**64.7%**| |Orihon.Server.Components.App|100%|| |Orihon.Server.Components.Layout.MainLayout|100%|| |Orihon.Server.Components.Pages.Gate|64.2%|66.6%| |Orihon.Server.Security.AccessGate|91.8%|41.6%| |Orihon.Server.Security.AccessSecret|100%|50%| |Program|94.5%|85.7%| </details> <details><summary>Orihon.UseCases - 98.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.UseCases**|**98.7%**|**96.9%**| |Orihon.UseCases.Bible.AddCharacter|100%|100%| |Orihon.UseCases.Bible.AddGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.AddLoreEntry|100%|100%| |Orihon.UseCases.Bible.AddStoryBeat|100%|100%| |Orihon.UseCases.Bible.BibleDto|100%|| |Orihon.UseCases.Bible.CharacterDto|100%|| |Orihon.UseCases.Bible.DeleteCharacter|100%|100%| |Orihon.UseCases.Bible.DeleteGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.DeleteLoreEntry|100%|100%| |Orihon.UseCases.Bible.DeletePageSummary|100%|100%| |Orihon.UseCases.Bible.DeleteStoryBeat|100%|100%| |Orihon.UseCases.Bible.GetBible|100%|100%| |Orihon.UseCases.Bible.GlossaryEntryDto|100%|| |Orihon.UseCases.Bible.LoreEntryDto|100%|| |Orihon.UseCases.Bible.PageSummaryDto|100%|| |Orihon.UseCases.Bible.ReorderStoryBeats|100%|| |Orihon.UseCases.Bible.SetPageSummary|100%|100%| |Orihon.UseCases.Bible.StoryBeatDto|100%|| |Orihon.UseCases.Bible.UpdateCharacter|100%|100%| |Orihon.UseCases.Bible.UpdateGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.UpdateLoreEntry|100%|100%| |Orihon.UseCases.Bible.UpdateStoryBeat|100%|100%| |Orihon.UseCases.Chapters.ChapterDto|100%|| |Orihon.UseCases.Chapters.CreateChapter|100%|100%| |Orihon.UseCases.Chapters.DeleteChapter|100%|100%| |Orihon.UseCases.Chapters.RenameChapter|100%|100%| |Orihon.UseCases.Chapters.ReorderChapters|100%|| |Orihon.UseCases.DependencyInjection|100%|| |Orihon.UseCases.Diagnostics.SeedDevData|99%|92.8%| |Orihon.UseCases.NextOrder|100%|| |Orihon.UseCases.Pages.DeletePage|100%|100%| |Orihon.UseCases.Pages.GetPage|100%|100%| |Orihon.UseCases.Pages.GetProjectWorkspace|100%|100%| |Orihon.UseCases.Pages.ImportPages|98.4%|96.4%| |Orihon.UseCases.Pages.ImportPagesResult|100%|| |Orihon.UseCases.Pages.MovePage|100%|92.8%| |Orihon.UseCases.Pages.PageDetailDto|75%|| |Orihon.UseCases.Pages.PageDto|95.6%|| |Orihon.UseCases.Pages.PageUpload|100%|| |Orihon.UseCases.Pages.ProjectWorkspaceDto|75%|| |Orihon.UseCases.Pages.ReorderPages|100%|| |Orihon.UseCases.Pages.SetPageMeta|100%|100%| |Orihon.UseCases.Pages.WorkspaceChapterDto|100%|| |Orihon.UseCases.Projects.CompleteProjectSetup|92.8%|83.3%| |Orihon.UseCases.Projects.CreateProject|100%|100%| |Orihon.UseCases.Projects.DeleteProject|100%|100%| |Orihon.UseCases.Projects.GetProject|100%|100%| |Orihon.UseCases.Projects.ListProjects|100%|| |Orihon.UseCases.Projects.ProjectDto|95.8%|| |Orihon.UseCases.Projects.StoredPageImage|100%|| |Orihon.UseCases.Projects.UpdateProjectMetadata|100%|100%| |Orihon.UseCases.Regions.CreateRegion|100%|100%| |Orihon.UseCases.Regions.DeleteRegion|100%|100%| |Orihon.UseCases.Regions.RegionDto|97%|| |Orihon.UseCases.Regions.ReorderRegions|100%|| |Orihon.UseCases.Regions.UpdateRegion|100%|100%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! The composition root comes ALIVE~ The heart starts beating and the sample world breathes! I do love a PR that turns a skeleton into a living thing — migrate-at-startup, the page-image route, and a seeder that drives the REAL use cases instead of poking raw rows. That's craft, that is~ ♡

But fufu~ even a beautiful heart has a leaky valve or two, and you wouldn't leave THIS in production, would you? ♡

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. [tests/Orihon.Integration.Tests/PageImageEndpointTests.cs:114-132 & AppChromeTests.cs:67-87]DRY violation: SignInAsync + HiddenFields are copy-pasted verbatim across two test classes. The entire SignInAsync(HttpClient) method (fetch /gate, parse hidden fields, set Input.Secret, POST, assert Found) and the HiddenFields(string html) helper (the <input[^>]*type="hidden" regex + name=/value= extraction) are byte-identical between AppChromeTests and the new PageImageEndpointTests. This is exactly the kind of duplication that metastasizes: cuts 6 and 7 bring bUnit UI tests behind the same gate, and each new test class will either clone these helpers a third/fourth time or — finally — extract them. Extract them NOW. ♡
    Fix: Move both helpers to a shared location. The natural home is GatedAppFactory itself (it already owns Secret), or a small static class GateTestHelper { public static async Task SignInAsync(HttpClient client) { ... } }. Then both test classes call the shared version. Two copies become one; cuts 6/7 just call it.

💡 Little ideas (non-blocking)~

  1. [src/Orihon.UseCases/Diagnostics/SeedDevData.cs:34]Result<Unit>.Fail(created.Match(_ => "", message => message)) is a little awkward: the onOk arm (_ => "") is unreachable because you just checked created is not Ok<ProjectDto> ok on the line above. A cleaner read: if (created is Err<ProjectDto> err) return Result<Unit>.Fail(err.Error);. Not wrong, just a tiny wobble~

What I liked~

  • The endpoint is textbook. The page row is the sole source of the filename — nothing user-typed ever reaches the filesystem. The store canonicalizes on save, the cross-project check (page.ProjectId != projectId) returns 404 (not 403 — no information leak), and the null-filename arm (RawImageFileName is not { } fileName) handles imageless pages cleanly. Range processing for large scans, private, max-age=1d with the "raw scans never change in place" rationale — chef's kiss~
  • The seed is honest about its shortcuts. It drives real use cases (CreateProject, UpdateProjectMetadata, CreateRegion, UpdateRegion, every bible Add) and only goes through ports where no user-facing operation fits (imageless pages, wizard fast-forward) — and documents exactly that on the class. The idempotency guard (Count > 0 → no-op) is proven by both the contract test and the Program-level SeedHookTests.
  • The endpoint tests are genuinely behavioral, not tautologies. Gate redirect, unknown-page 404, cross-project 404, and byte-identical PNG with content-type + caching asserted structurally — driving the real host through GatedAppFactory. This is how you test a route. ♪
  • 181/181 green (3 BlazorAdapter + 56 Domain + 44 Integration + 78 UseCases) — verified locally, matches the PR claim exactly. Build clean: 0 warnings, 0 errors. SkiaSharp.NativeAssets.Linux.NoDependencies version pinned at 3.119.4 in Directory.Packages.props, matching the sibling SkiaSharp — the packaging split from #8 is properly completed.

Automated review by Jibril · 2026-07-24
CI/CD: absent for head fe0cf1f (PR just opened, no bot comment yet) · Local checks: build 0/0, 181/181 tests pass

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! The composition root comes ALIVE~ The heart starts beating and the sample world breathes! I do love a PR that turns a skeleton into a living thing — migrate-at-startup, the page-image route, and a seeder that drives the REAL use cases instead of poking raw rows. That's craft, that is~ ♡ But fufu~ even a beautiful heart has a leaky valve or two, and you wouldn't leave THIS in production, would you? ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **[tests/Orihon.Integration.Tests/PageImageEndpointTests.cs:114-132 & AppChromeTests.cs:67-87]** — **DRY violation: `SignInAsync` + `HiddenFields` are copy-pasted verbatim across two test classes.** The entire `SignInAsync(HttpClient)` method (fetch `/gate`, parse hidden fields, set `Input.Secret`, POST, assert `Found`) and the `HiddenFields(string html)` helper (the `<input[^>]*type="hidden"` regex + `name=`/`value=` extraction) are **byte-identical** between `AppChromeTests` and the new `PageImageEndpointTests`. This is exactly the kind of duplication that metastasizes: cuts 6 and 7 bring bUnit UI tests behind the same gate, and each new test class will either clone these helpers a third/fourth time or — finally — extract them. Extract them NOW. ♡ **Fix:** Move both helpers to a shared location. The natural home is `GatedAppFactory` itself (it already owns `Secret`), or a small `static class GateTestHelper { public static async Task SignInAsync(HttpClient client) { ... } }`. Then both test classes call the shared version. Two copies become one; cuts 6/7 just call it. #### 💡 Little ideas (non-blocking)~ 1. **[src/Orihon.UseCases/Diagnostics/SeedDevData.cs:34]** — `Result<Unit>.Fail(created.Match(_ => "", message => message))` is a little awkward: the `onOk` arm (`_ => ""`) is unreachable because you just checked `created is not Ok<ProjectDto> ok` on the line above. A cleaner read: `if (created is Err<ProjectDto> err) return Result<Unit>.Fail(err.Error);`. Not wrong, just a tiny wobble~ #### ✅ What I liked~ - **The endpoint is textbook.** The page row is the sole source of the filename — nothing user-typed ever reaches the filesystem. The store canonicalizes on save, the cross-project check (`page.ProjectId != projectId`) returns 404 (not 403 — no information leak), and the null-filename arm (`RawImageFileName is not { } fileName`) handles imageless pages cleanly. Range processing for large scans, `private, max-age=1d` with the "raw scans never change in place" rationale — *chef's kiss*~ - **The seed is honest about its shortcuts.** It drives real use cases (CreateProject, UpdateProjectMetadata, CreateRegion, UpdateRegion, every bible Add) and only goes through ports where no user-facing operation fits (imageless pages, wizard fast-forward) — **and documents exactly that on the class**. The idempotency guard (`Count > 0 → no-op`) is proven by both the contract test and the Program-level `SeedHookTests`. - **The endpoint tests are genuinely behavioral, not tautologies.** Gate redirect, unknown-page 404, cross-project 404, and **byte-identical PNG with content-type + caching asserted structurally** — driving the real host through `GatedAppFactory`. This is how you test a route. ♪ - **181/181 green** (3 BlazorAdapter + 56 Domain + 44 Integration + 78 UseCases) — verified locally, matches the PR claim exactly. Build clean: 0 warnings, 0 errors. `SkiaSharp.NativeAssets.Linux.NoDependencies` version pinned at 3.119.4 in `Directory.Packages.props`, matching the sibling `SkiaSharp` — the packaging split from #8 is properly completed. --- *Automated review by Jibril · 2026-07-24* *CI/CD: absent for head fe0cf1f (PR just opened, no bot comment yet) · Local checks: build 0/0, 181/181 tests pass*
fix: one gate sign-in helper, shared by every host-driven test
All checks were successful
CI / build (pull_request) Successful in 21s
CI / test (pull_request) Successful in 30s
ec0c1815ab
Jibril's blocker on #9: SignInAsync + HiddenFields were byte-identical between AppChromeTests and
PageImageEndpointTests, and the UI cuts would have cloned them again. Both now live on
GatedAppFactory — which already owns the secret — and both test classes call the shared copy.
Also her non-blocking nit: SeedDevData's unreachable Match arm replaced with a plain Err pattern.

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

Blocker fixed in ec0c181: SignInAsync and HiddenFields now live once on GatedAppFactory — which already owns Secret, as you said — with the https-base-address requirement documented on the helper. Both test classes call the shared copy; AppChromeTests keeps only its own LocationOf. Cuts 6/7 will call the same helper instead of cloning a third time.

Non-blocking nit taken too: SeedDevData's unreachable Match arm is gone — a plain is Err<ProjectDto> err → Fail(err.Error) guard.

181/181 tests green.

🤖 Generated with Claude Code

Blocker fixed in `ec0c181`: `SignInAsync` and `HiddenFields` now live once on `GatedAppFactory` — which already owns `Secret`, as you said — with the https-base-address requirement documented on the helper. Both test classes call the shared copy; `AppChromeTests` keeps only its own `LocationOf`. Cuts 6/7 will call the same helper instead of cloning a third time. Non-blocking nit taken too: `SeedDevData`'s unreachable `Match` arm is gone — a plain `is Err<ProjectDto> err → Fail(err.Error)` guard. 181/181 tests green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Oh~ back already, and with such a tidy little fix in tow! One commit, four files, and the leaky valve is sealed. That's how you answer a review — surgically, no scope creep, no drama~ ♡

Verdict: Looks good to me~

The blocker is closed~

  1. DRY violation — RESOLVED. SignInAsync(HttpClient) and HiddenFields(string) now live exactly where I hoped they would: on GatedAppFactory itself as public static members. Both AppChromeTests (6 call sites) and PageImageEndpointTests (3 call sites) now call GatedAppFactory.SignInAsync(client). Two copies became one. Cuts 6 and 7 will just call it — no third clone, no metastasis. ♪

  2. Non-blocking Result.Match wobble — also RESOLVED. The unreachable onOk arm (_ => "") is gone. Now: if (created is Err<ProjectDto> err) return Result<Unit>.Fail(err.Error); then var project = ((Ok<ProjectDto>)created).Value;. I traced Result<T> — it's a closed two-variant type (Ok<T> + Err<T>, both sealed, the only two subtypes of the abstract Result<T>), so the is Err check is exhaustive and the ((Ok<>)created) cast can never throw. Clean and correct.

What I liked~

  • The home for the helper is the right one. Not a separate GateTestHelper class, not a static utility — GatedAppFactory, which already owns Secret and is the IClassFixture<> every gated test class shares. The doc comment even documents why the helper lives here ("Shared by every test class that talks through the gate") and the Secure-cookie gotcha ("the client needs an https base address, or the Secure-only cookie never rides along"). That's forward-thinking.
  • Zero behavioral drift. The production fix in SeedDevData is semantically identical to the old Match — both extract the error message from an Err and return Fail. I diffed the stripped logic: byte-equivalent behavior. And the test-only changes just re-route the same calls through the shared helper.
  • 181/181 green — 3 BlazorAdapter + 56 Domain + 44 Integration + 78 UseCases, all verified locally. Build clean: 0 warnings, 0 errors. No new code paths from this fix commit, so the architectural review from fe0cf1f stands unchanged: textbook endpoint (filename from page row only, cross-project 404 not 403, imageless-page null arm, range support, private, max-age=1d), honest seeder driving real use cases, migrate-at-startup for single-user single-node.

Automated review by Jibril · 2026-07-24
CI/CD: stale for ec0c181 (coverage bot 3494 covers fe0cf1f only) · Local checks: build 0/0, 181/181 tests pass

## 🔮 fufu~ Jibril reviewed your code! Oh~ back already, and with such a tidy little fix in tow! One commit, four files, and the leaky valve is sealed. That's how you answer a review — surgically, no scope creep, no drama~ ♡ ### Verdict: ✅ Looks good to me~ #### ✅ The blocker is closed~ 1. **DRY violation — RESOLVED.** `SignInAsync(HttpClient)` and `HiddenFields(string)` now live exactly where I hoped they would: on `GatedAppFactory` itself as `public static` members. Both `AppChromeTests` (6 call sites) and `PageImageEndpointTests` (3 call sites) now call `GatedAppFactory.SignInAsync(client)`. Two copies became one. Cuts 6 and 7 will just call it — no third clone, no metastasis. ♪ 2. **Non-blocking `Result.Match` wobble — also RESOLVED.** The unreachable `onOk` arm (`_ => ""`) is gone. Now: `if (created is Err<ProjectDto> err) return Result<Unit>.Fail(err.Error);` then `var project = ((Ok<ProjectDto>)created).Value;`. I traced `Result<T>` — it's a closed two-variant type (`Ok<T>` + `Err<T>`, both sealed, the only two subtypes of the abstract `Result<T>`), so the `is Err` check is exhaustive and the `((Ok<>)created)` cast can never throw. Clean and correct. #### ✅ What I liked~ - **The home for the helper is the *right* one.** Not a separate `GateTestHelper` class, not a static utility — `GatedAppFactory`, which already owns `Secret` and is the `IClassFixture<>` every gated test class shares. The doc comment even documents *why* the helper lives here ("Shared by every test class that talks through the gate") and the Secure-cookie gotcha ("the client needs an https base address, or the Secure-only cookie never rides along"). That's forward-thinking. - **Zero behavioral drift.** The production fix in `SeedDevData` is semantically identical to the old `Match` — both extract the error message from an `Err` and return `Fail`. I diffed the stripped logic: byte-equivalent behavior. And the test-only changes just re-route the same calls through the shared helper. - **181/181 green** — 3 BlazorAdapter + 56 Domain + 44 Integration + 78 UseCases, all verified locally. Build clean: 0 warnings, 0 errors. No new code paths from this fix commit, so the architectural review from `fe0cf1f` stands unchanged: textbook endpoint (filename from page row only, cross-project 404 not 403, imageless-page null arm, range support, `private, max-age=1d`), honest seeder driving real use cases, migrate-at-startup for single-user single-node. --- *Automated review by Jibril · 2026-07-24* *CI/CD: stale for ec0c181 (coverage bot 3494 covers fe0cf1f only) · Local checks: build 0/0, 181/181 tests pass*
bjoern merged commit 9a95fe5804 into main 2026-07-24 20:36:10 +02:00
bjoern deleted branch feat/p1-server 2026-07-24 20:36:10 +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/Orihon!9
No description provided.