refactor: Fluxor 7/7 — the project workspace #36

Merged
bjoern merged 2 commits from refactor/fluxor-project-workspace into main 2026-08-13 06:23:57 +02:00
Member

Final slice of the Fluxor page refactor (ADR 0011; series plan in #27) — with this, every page runs on the store. Branched from main independently of #35 (the list slice); the two touch disjoint files and can merge in either order.

What's in

Store (ProjectWorkspaceState) — the ProjectId-guarded loaded world, the shared SaveState (ADR 0022), Error. View-local stays view-local: the chapter-rename buffer, the new-chapter title, the summary drafts with their per-page debounces, both dialogs' targets, and the byte transfer.

Effects (ProjectWorkspaceEffects) — sole touchpoint for the 12 use cases. Two regimes, mirroring the page-workspace slice's split:

  • Patch in place: summary saves and blanks (SummarySaved/SummaryDeleted rebuild SummariesByPage immutably) — a reload would clobber the other pages' drafts mid-typing.
  • Chain a reload on success: everything structural (chapter create/rename/reorder/delete, import, page reorder/move/delete, kind coupling) — the server derives orders, labels, and the skip-typeset coupling. A failed write never chains: the fresh-world reset in OnLoaded would wipe its own error (the #34/#35 rule). One Chain<T> helper encodes this; import runs CancellationToken.None like the wizard's, same rationale.

The minted-id handoff — a first-time summary save gets its id from the server; the view-local draft must learn it or a later blanking has nothing to aim its delete at. SubscribeToAction<SummarySaved> carries it across (the RegionCreated pattern from slice 4).

A subtle one worth flagging: SummaryFor is now a render-only read that does not store its fallback. The first render happens before OnAfterRender adopts the loaded summaries, and the adopt loop keeps existing drafts on purpose (draft survival) — so a stored-empty draft created during render would shadow the real summary forever. This actually bit during the port (two tests caught it); the comment at the site explains the trap.

Carried over intact: draft survival across reloads (merge, not rebuild), stale-debounce discard for deleted pages (a flush would save against the gone page), uploadingChapter clearing on workspace adoption, all honest void handler names.

Tests

+3, adapter suite 120, full suite 396/396 green. All 15 pre-existing workspace tests pass unchanged — including the draft-survival, debounce-cancel, and blanking pins. New:

  • A_first_time_summary_can_be_blanked_right_back_off — types into an empty summary, waits for the minted id, blanks it; pins the SubscribeToAction<SummarySaved> handoff end to end.
  • A_failed_chapter_delete_surfaces_its_error — chapter vanishes while the modal is open; pins the Err arm of Chain<T> and that failure does not reload-wipe its own error.
  • Another_projects_stale_error_never_bleeds_into_this_one — project B renders in the same circuit after A's failure; pins the Current guard + OnLoaded reset pair.

Browser-verified

Full drive against a seeded world: summary typed at human pace into an empty row (character-perfect, indicator Dirty→Saving→Saved) → blanked right back off → both states survived a full page reload in a fresh circuit → added chapter "Omake" → moved the credits page into it via the Move-to dropdown (5+1 counts) → keyboard-reordered page 1 down a slot (kind column confirms the cover moved) → renamed Chapter 1 to 本編 inline (Japanese, character-perfect) → Omake's Up button swapped chapter order → uploaded a real PNG + a fake .png into Omake (image imported with thumbnail, fake skipped) → deleted Omake through its modal, taking its pages, leaving 本編 alone. Console clean apart from the seeded pages' by-design image 404s.

🤖 Generated with Claude Code

Final slice of the Fluxor page refactor (ADR 0011; series plan in #27) — with this, every page runs on the store. Branched from main independently of #35 (the list slice); the two touch disjoint files and can merge in either order. ## What's in **Store (`ProjectWorkspaceState`)** — the `ProjectId`-guarded loaded world, the shared `SaveState` (ADR 0022), `Error`. View-local stays view-local: the chapter-rename buffer, the new-chapter title, the summary drafts with their **per-page debounces**, both dialogs' targets, and the byte transfer. **Effects (`ProjectWorkspaceEffects`)** — sole touchpoint for the 12 use cases. Two regimes, mirroring the page-workspace slice's split: - **Patch in place**: summary saves and blanks (`SummarySaved`/`SummaryDeleted` rebuild `SummariesByPage` immutably) — a reload would clobber the *other* pages' drafts mid-typing. - **Chain a reload on success**: everything structural (chapter create/rename/reorder/delete, import, page reorder/move/delete, kind coupling) — the server derives orders, labels, and the skip-typeset coupling. A failed write never chains: the fresh-world reset in `OnLoaded` would wipe its own error (the #34/#35 rule). One `Chain<T>` helper encodes this; import runs `CancellationToken.None` like the wizard's, same rationale. **The minted-id handoff** — a first-time summary save gets its id from the server; the view-local draft must learn it or a later blanking has nothing to aim its delete at. `SubscribeToAction<SummarySaved>` carries it across (the `RegionCreated` pattern from slice 4). **A subtle one worth flagging**: `SummaryFor` is now a **render-only read that does not store its fallback**. The first render happens before `OnAfterRender` adopts the loaded summaries, and the adopt loop keeps existing drafts on purpose (draft survival) — so a stored-empty draft created during render would shadow the real summary forever. This actually bit during the port (two tests caught it); the comment at the site explains the trap. **Carried over intact**: draft survival across reloads (merge, not rebuild), stale-debounce discard for deleted pages (a flush would save against the gone page), `uploadingChapter` clearing on workspace adoption, all honest `void` handler names. ## Tests +3, adapter suite 120, full suite **396/396 green**. All 15 pre-existing workspace tests pass unchanged — including the draft-survival, debounce-cancel, and blanking pins. New: - `A_first_time_summary_can_be_blanked_right_back_off` — types into an empty summary, waits for the minted id, blanks it; pins the `SubscribeToAction<SummarySaved>` handoff end to end. - `A_failed_chapter_delete_surfaces_its_error` — chapter vanishes while the modal is open; pins the Err arm of `Chain<T>` and that failure does not reload-wipe its own error. - `Another_projects_stale_error_never_bleeds_into_this_one` — project B renders in the same circuit after A's failure; pins the `Current` guard + `OnLoaded` reset pair. ## Browser-verified Full drive against a seeded world: summary typed at human pace into an empty row (character-perfect, indicator Dirty→Saving→Saved) → blanked right back off → both states survived a full page reload in a fresh circuit → added chapter "Omake" → moved the credits page into it via the Move-to dropdown (5+1 counts) → keyboard-reordered page 1 down a slot (kind column confirms the cover moved) → renamed Chapter 1 to 本編 inline (Japanese, character-perfect) → Omake's Up button swapped chapter order → uploaded a real PNG + a fake `.png` into Omake (image imported with thumbnail, fake skipped) → deleted Omake through its modal, taking its pages, leaving 本編 alone. Console clean apart from the seeded pages' by-design image 404s. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
refactor: Fluxor 7/7 — the project workspace
All checks were successful
CI / build (pull_request) Successful in 23s
CI / test (pull_request) Successful in 38s
faf74bc8ed
The last page moves onto its own slice (ADR 0011, 0022) and the refactor
is complete. The store holds the loaded world and the shared save
indicator; the rename buffer, the new-chapter title, the summary drafts
with their per-page debounces, the dialogs' targets, and the byte
transfer stay component-side.

Effects are the only touchpoint for the workspace's 12 use cases. Summary
saves and blanks patch the store in place — a reload would clobber the
other pages' drafts mid-typing; every structural change (chapters, order,
imports, moves, deletes, kind coupling) chains a reload on success, and a
failed write never chains one, because the fresh-world reset would wipe
its own error. A first-time save mints the summary id server-side; the
draft learns it through SubscribeToAction<SummarySaved> so a later
blanking knows what to delete.

One subtle mechanic: SummaryFor is now a render-only read that does NOT
store its fallback — the first render happens before OnAfterRender adopts
the loaded summaries, and a stored-empty draft would shadow the real one
forever (the adopt loop keeps existing drafts on purpose).

Tests: +3 (120 adapter total) — first-time summary blanked right back off
(the minted-id handoff), a failed chapter delete surfaces its error, and
another project's stale error never bleeds in. All 15 pre-existing
workspace tests pass unchanged.

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

Summary

Summary
Generated on: 07/25/2026 - 16:00:58
Coverage date: 07/25/2026 - 16:00:46 - 07/25/2026 - 16:00:56
Parser: MultiReport (4x Cobertura)
Assemblies: 6
Classes: 264
Files: 141
Line coverage: 94.9% (6644 of 6998)
Covered lines: 6644
Uncovered lines: 354
Coverable lines: 6998
Total lines: 12691
Branch coverage: 79.7% (1398 of 1754)
Covered branches: 1398
Total branches: 1754
Method coverage: Feature is only available for sponsors

Coverage

Orihon.BlazorAdapter - 95.2%
Name Line Branch
Orihon.BlazorAdapter 95.2% 86.2%
Orihon.BlazorAdapter.Bible.AddBeatRowRequested 100%
Orihon.BlazorAdapter.Bible.AddCharacterRowRequested 100%
Orihon.BlazorAdapter.Bible.AddGlossaryRowRequested 100%
Orihon.BlazorAdapter.Bible.AddLoreRowRequested 100%
Orihon.BlazorAdapter.Bible.BibleEffects 92% 79.1%
Orihon.BlazorAdapter.Bible.BibleLoaded 100%
Orihon.BlazorAdapter.Bible.BiblePage 93.3% 80.8%
Orihon.BlazorAdapter.Bible.BibleReducers 92.8%
Orihon.BlazorAdapter.Bible.BibleState 100%
Orihon.BlazorAdapter.Bible.BibleWriteFailed 100%
Orihon.BlazorAdapter.Bible.DeleteBeatRowRequested 100%
Orihon.BlazorAdapter.Bible.DeleteCharacterRowRequested 100%
Orihon.BlazorAdapter.Bible.DeleteGlossaryRowRequested 100%
Orihon.BlazorAdapter.Bible.DeleteLoreRowRequested 0%
Orihon.BlazorAdapter.Bible.LoadBible 100%
Orihon.BlazorAdapter.Bible.ReorderBeatsRequested 0%
Orihon.BlazorAdapter.Bible.SaveSummaryRequested 100%
Orihon.BlazorAdapter.Bible.UpdateBeatRowRequested 100%
Orihon.BlazorAdapter.Bible.UpdateCharacterRowRequested 100%
Orihon.BlazorAdapter.Bible.UpdateGlossaryRowRequested 100%
Orihon.BlazorAdapter.Bible.UpdateLoreRowRequested 100%
Orihon.BlazorAdapter.BlazorAdapterAssembly 100%
Orihon.BlazorAdapter.Debounce 92.5% 88.8%
Orihon.BlazorAdapter.Diagnostics.CircuitError 100%
Orihon.BlazorAdapter.Diagnostics.CircuitErrorPanel 100%
Orihon.BlazorAdapter.Diagnostics.CircuitErrorSink 100% 85.7%
Orihon.BlazorAdapter.Diagnostics.OrihonStoreInitializer 85.7% 66.6%
Orihon.BlazorAdapter.PageWorkspace.CreateRegionRequested 100%
Orihon.BlazorAdapter.PageWorkspace.DeleteRegionRequested 100%
Orihon.BlazorAdapter.PageWorkspace.LoadPageWorkspace 100%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceEffects 100% 100%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceLoaded 100%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspacePage 88.3% 80.7%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceReducers 100% 75%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceState 100%
Orihon.BlazorAdapter.PageWorkspace.PageWriteFailed 100%
Orihon.BlazorAdapter.PageWorkspace.RegionCreated 100%
Orihon.BlazorAdapter.PageWorkspace.RegionSaved 100%
Orihon.BlazorAdapter.PageWorkspace.ReorderRegionsRequested 100%
Orihon.BlazorAdapter.PageWorkspace.SaveRegionRequested 100%
Orihon.BlazorAdapter.PageWorkspace.SetPageMetaRequested 100%
Orihon.BlazorAdapter.Projects.CreateProjectRequested 100%
Orihon.BlazorAdapter.Projects.FinishSetupRequested 100%
Orihon.BlazorAdapter.Projects.ImportPagesRequested 100%
Orihon.BlazorAdapter.Projects.LoadWizard 100%
Orihon.BlazorAdapter.Projects.PagesImported 100%
Orihon.BlazorAdapter.Projects.ProjectListPage 86% 85.2%
Orihon.BlazorAdapter.Projects.ProjectWizardEffects 100% 100%
Orihon.BlazorAdapter.Projects.ProjectWizardPage 94.1% 85.4%
Orihon.BlazorAdapter.Projects.ProjectWizardReducers 100%
Orihon.BlazorAdapter.Projects.ProjectWizardState 100%
Orihon.BlazorAdapter.Projects.WizardLoaded 100%
Orihon.BlazorAdapter.Projects.WizardWriteFailed 100%
Orihon.BlazorAdapter.Runs.MonitorRunLoaded 100%
Orihon.BlazorAdapter.Runs.RunChangedBridge 94.1% 91.6%
Orihon.BlazorAdapter.Runs.RunMonitor 100% 97.6%
Orihon.BlazorAdapter.Runs.RunMonitorEffects 100% 100%
Orihon.BlazorAdapter.Runs.RunMonitorReducers 100%
Orihon.BlazorAdapter.Runs.RunMonitorState 100%
Orihon.BlazorAdapter.Settings.AgentModelPicked 100%
Orihon.BlazorAdapter.Settings.AgentModelSaved 100%
Orihon.BlazorAdapter.Settings.AgentModelSaveFailed 100%
Orihon.BlazorAdapter.Settings.KeySaved 100%
Orihon.BlazorAdapter.Settings.KeySaveFailed 100%
Orihon.BlazorAdapter.Settings.ModelOptionsLoaded 100%
Orihon.BlazorAdapter.Settings.ModelOptionsUnavailable 100%
Orihon.BlazorAdapter.Settings.SaveKeyRequested 100%
Orihon.BlazorAdapter.Settings.SettingsEffects 100% 100%
Orihon.BlazorAdapter.Settings.SettingsLoaded 100%
Orihon.BlazorAdapter.Settings.SettingsPage 100% 90.4%
Orihon.BlazorAdapter.Settings.SettingsReducers 100%
Orihon.BlazorAdapter.Settings.SettingsState 100%
Orihon.BlazorAdapter.Uploads.UploadTransfer 96.5% 100%
Orihon.BlazorAdapter.Uploads.UploadTransferProgress 100% 100%
Orihon.BlazorAdapter.Uploads.UploadTransferResult 100%
Orihon.BlazorAdapter.Workspace.CreateChapterRequested 100%
Orihon.BlazorAdapter.Workspace.DeleteChapterRequested 100%
Orihon.BlazorAdapter.Workspace.DeletePageRequested 100%
Orihon.BlazorAdapter.Workspace.DeleteSummaryRequested 100%
Orihon.BlazorAdapter.Workspace.LoadProjectWorkspace 100%
Orihon.BlazorAdapter.Workspace.MovePageRequested 100%
Orihon.BlazorAdapter.Workspace.ProjectWorkspaceEffects 100% 100%
Orihon.BlazorAdapter.Workspace.ProjectWorkspaceLoaded 100%
Orihon.BlazorAdapter.Workspace.ProjectWorkspacePage 96.3% 87.6%
Orihon.BlazorAdapter.Workspace.ProjectWorkspaceReducers 100% 66.6%
Orihon.BlazorAdapter.Workspace.ProjectWorkspaceState 100%
Orihon.BlazorAdapter.Workspace.RenameChapterRequested 100%
Orihon.BlazorAdapter.Workspace.ReorderChaptersRequested 100%
Orihon.BlazorAdapter.Workspace.ReorderPagesRequested 100%
Orihon.BlazorAdapter.Workspace.SaveSummaryRequested 100%
Orihon.BlazorAdapter.Workspace.SetPageKindRequested 100%
Orihon.BlazorAdapter.Workspace.SummaryDeleted 100%
Orihon.BlazorAdapter.Workspace.SummarySaved 100%
Orihon.BlazorAdapter.Workspace.WorkspaceImportRequested 100%
Orihon.BlazorAdapter.Workspace.WorkspaceWriteFailed 100%
Orihon.Domain - 100%
Name Line Branch
Orihon.Domain 100% 100%
Orihon.Domain.Agents.AgentDescriptor 100%
Orihon.Domain.Agents.AgentRoster 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.Runs.Execution 100% 100%
Orihon.Domain.Runs.Run 100%
Orihon.Domain.Settings.AppSetting 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 - 93.7%
Name Line Branch
Orihon.Infrastructure 93.7% 65.7%
Orihon.Infrastructure.Bible.EfBibleStore 100% 100%
Orihon.Infrastructure.DependencyInjection 100%
Orihon.Infrastructure.Gateways.AgentToolAdapter 100%
Orihon.Infrastructure.Gateways.AgentToolAdapter`1 100% 100%
Orihon.Infrastructure.Gateways.HttpWebPageFetcher 95.1% 83.3%
Orihon.Infrastructure.Gateways.OpenRouterLlmGateway 98.9% 84.1%
Orihon.Infrastructure.Persistence.Configurations.AppSettingConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.ExecutionConfiguration 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.RunConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration 100%
Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Orihon.Infrastructure.Persistence.Migrations.AddAppSettings 99.3%
Orihon.Infrastructure.Persistence.Migrations.AddRuns 99.1%
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.Runs.EfRunStore 97% 50%
Orihon.Infrastructure.Settings.EfAppSettingsStore 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%
System.Text.RegularExpressions.Generated 70.6% 53.3%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4
77.9% 76.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1
59% 42.5%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3
89.4% 75%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2
83.7% 62.5%
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 - 93.4%
Name Line Branch
Orihon.Server 93.4% 68.4%
Orihon.Server.Components.App 100%
Orihon.Server.Components.Layout.MainLayout 100%
Orihon.Server.Components.Pages.Gate 64.2% 66.6%
Orihon.Server.RunEngineBootstrap 100%
Orihon.Server.Security.AccessGate 91.8% 41.6%
Orihon.Server.Security.AccessSecret 100% 50%
Orihon.Server.VolumeStartupValidator 100% 100%
Program 95.4% 85.7%
Orihon.UseCases - 96.8%
Name Line Branch
Orihon.UseCases 96.8% 90.1%
Orihon.UseCases.Agents.AgentBlueprint 100%
Orihon.UseCases.Agents.AgentInvocation 100%
Orihon.UseCases.Agents.AgentOutcome 100%
Orihon.UseCases.Agents.AgentTool`1 90.9% 75%
Orihon.UseCases.Agents.AgentToolImage 100%
Orihon.UseCases.Agents.AgentToolResult 100%
Orihon.UseCases.Agents.AssistantSpoke 100%
Orihon.UseCases.Agents.ResearchSetup.AddGlossaryParams 100%
Orihon.UseCases.Agents.ResearchSetup.AddGlossaryTool 100% 100%
Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatParams 100%
Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatTool 100% 50%
Orihon.UseCases.Agents.ResearchSetup.AskUserParams 100%
Orihon.UseCases.Agents.ResearchSetup.AskUserTool 100% 100%
Orihon.UseCases.Agents.ResearchSetup.FetchUrlParams 100%
Orihon.UseCases.Agents.ResearchSetup.FetchUrlTool 100% 100%
Orihon.UseCases.Agents.ResearchSetup.ListBibleTool 86.6%
Orihon.UseCases.Agents.ResearchSetup.PageByNumber 90% 87.5%
Orihon.UseCases.Agents.ResearchSetup.ResearchSetupBlueprint 100%
Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryParams 100%
Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryTool 100% 75%
Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataParams 100%
Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataTool 96.1% 90.9%
Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterParams 100%
Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterTool 91.3% 66.6%
Orihon.UseCases.Agents.ResearchSetup.UpsertLoreParams 100%
Orihon.UseCases.Agents.ResearchSetup.UpsertLoreTool 91.3% 66.6%
Orihon.UseCases.Agents.ResearchSetup.ViewPageParams 100%
Orihon.UseCases.Agents.ResearchSetup.ViewPageTool 85.7% 50%
Orihon.UseCases.Agents.ToolCalled 100%
Orihon.UseCases.Agents.ToolCompleted 100%
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.2% 92.8%
Orihon.UseCases.Gateways.LlmKeyInfo 100%
Orihon.UseCases.Gateways.LlmModel 100%
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 100% 100%
Orihon.UseCases.Pages.ImportPagesResult 100%
Orihon.UseCases.Pages.MovePage 100% 92.8%
Orihon.UseCases.Pages.PageDetailDto 100%
Orihon.UseCases.Pages.PageDto 100%
Orihon.UseCases.Pages.PageUpload 100%
Orihon.UseCases.Pages.ProjectWorkspaceDto 100%
Orihon.UseCases.Pages.ReorderPages 100%
Orihon.UseCases.Pages.SetPageMeta 100% 100%
Orihon.UseCases.Pages.WorkspaceChapterDto 100%
Orihon.UseCases.Projects.CompleteProjectSetup 100% 93.7%
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%
Orihon.UseCases.Runs.ExecutionDto 92.3%
Orihon.UseCases.Runs.PlannedExecution 100%
Orihon.UseCases.Runs.RunDto 93.3% 90%
Orihon.UseCases.Runs.RunEngine 88.3% 82%
Orihon.UseCases.Runs.RunEngineOptions 100%
Orihon.UseCases.Runs.StageContext 37.5%
Orihon.UseCases.Settings.AgentSettingDto 100% 100%
Orihon.UseCases.Settings.GetSettings 100% 100%
Orihon.UseCases.Settings.ListModelOptions 100% 100%
Orihon.UseCases.Settings.SaveAgentModel 100% 100%
Orihon.UseCases.Settings.SaveOpenRouterKey 100% 100%
Orihon.UseCases.Settings.SettingKeys 100% 100%
Orihon.UseCases.Settings.SettingsDto 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/25/2026 - 16:00:58 | | Coverage date: | 07/25/2026 - 16:00:46 - 07/25/2026 - 16:00:56 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 6 | | Classes: | 264 | | Files: | 141 | | **Line coverage:** | 94.9% (6644 of 6998) | | Covered lines: | 6644 | | Uncovered lines: | 354 | | Coverable lines: | 6998 | | Total lines: | 12691 | | **Branch coverage:** | 79.7% (1398 of 1754) | | Covered branches: | 1398 | | Total branches: | 1754 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Orihon.BlazorAdapter - 95.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.BlazorAdapter**|**95.2%**|**86.2%**| |Orihon.BlazorAdapter.Bible.AddBeatRowRequested|100%|| |Orihon.BlazorAdapter.Bible.AddCharacterRowRequested|100%|| |Orihon.BlazorAdapter.Bible.AddGlossaryRowRequested|100%|| |Orihon.BlazorAdapter.Bible.AddLoreRowRequested|100%|| |Orihon.BlazorAdapter.Bible.BibleEffects|92%|79.1%| |Orihon.BlazorAdapter.Bible.BibleLoaded|100%|| |Orihon.BlazorAdapter.Bible.BiblePage|93.3%|80.8%| |Orihon.BlazorAdapter.Bible.BibleReducers|92.8%|| |Orihon.BlazorAdapter.Bible.BibleState|100%|| |Orihon.BlazorAdapter.Bible.BibleWriteFailed|100%|| |Orihon.BlazorAdapter.Bible.DeleteBeatRowRequested|100%|| |Orihon.BlazorAdapter.Bible.DeleteCharacterRowRequested|100%|| |Orihon.BlazorAdapter.Bible.DeleteGlossaryRowRequested|100%|| |Orihon.BlazorAdapter.Bible.DeleteLoreRowRequested|0%|| |Orihon.BlazorAdapter.Bible.LoadBible|100%|| |Orihon.BlazorAdapter.Bible.ReorderBeatsRequested|0%|| |Orihon.BlazorAdapter.Bible.SaveSummaryRequested|100%|| |Orihon.BlazorAdapter.Bible.UpdateBeatRowRequested|100%|| |Orihon.BlazorAdapter.Bible.UpdateCharacterRowRequested|100%|| |Orihon.BlazorAdapter.Bible.UpdateGlossaryRowRequested|100%|| |Orihon.BlazorAdapter.Bible.UpdateLoreRowRequested|100%|| |Orihon.BlazorAdapter.BlazorAdapterAssembly|100%|| |Orihon.BlazorAdapter.Debounce|92.5%|88.8%| |Orihon.BlazorAdapter.Diagnostics.CircuitError|100%|| |Orihon.BlazorAdapter.Diagnostics.CircuitErrorPanel|100%|| |Orihon.BlazorAdapter.Diagnostics.CircuitErrorSink|100%|85.7%| |Orihon.BlazorAdapter.Diagnostics.OrihonStoreInitializer|85.7%|66.6%| |Orihon.BlazorAdapter.PageWorkspace.CreateRegionRequested|100%|| |Orihon.BlazorAdapter.PageWorkspace.DeleteRegionRequested|100%|| |Orihon.BlazorAdapter.PageWorkspace.LoadPageWorkspace|100%|| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceEffects|100%|100%| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceLoaded|100%|| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspacePage|88.3%|80.7%| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceReducers|100%|75%| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceState|100%|| |Orihon.BlazorAdapter.PageWorkspace.PageWriteFailed|100%|| |Orihon.BlazorAdapter.PageWorkspace.RegionCreated|100%|| |Orihon.BlazorAdapter.PageWorkspace.RegionSaved|100%|| |Orihon.BlazorAdapter.PageWorkspace.ReorderRegionsRequested|100%|| |Orihon.BlazorAdapter.PageWorkspace.SaveRegionRequested|100%|| |Orihon.BlazorAdapter.PageWorkspace.SetPageMetaRequested|100%|| |Orihon.BlazorAdapter.Projects.CreateProjectRequested|100%|| |Orihon.BlazorAdapter.Projects.FinishSetupRequested|100%|| |Orihon.BlazorAdapter.Projects.ImportPagesRequested|100%|| |Orihon.BlazorAdapter.Projects.LoadWizard|100%|| |Orihon.BlazorAdapter.Projects.PagesImported|100%|| |Orihon.BlazorAdapter.Projects.ProjectListPage|86%|85.2%| |Orihon.BlazorAdapter.Projects.ProjectWizardEffects|100%|100%| |Orihon.BlazorAdapter.Projects.ProjectWizardPage|94.1%|85.4%| |Orihon.BlazorAdapter.Projects.ProjectWizardReducers|100%|| |Orihon.BlazorAdapter.Projects.ProjectWizardState|100%|| |Orihon.BlazorAdapter.Projects.WizardLoaded|100%|| |Orihon.BlazorAdapter.Projects.WizardWriteFailed|100%|| |Orihon.BlazorAdapter.Runs.MonitorRunLoaded|100%|| |Orihon.BlazorAdapter.Runs.RunChangedBridge|94.1%|91.6%| |Orihon.BlazorAdapter.Runs.RunMonitor|100%|97.6%| |Orihon.BlazorAdapter.Runs.RunMonitorEffects|100%|100%| |Orihon.BlazorAdapter.Runs.RunMonitorReducers|100%|| |Orihon.BlazorAdapter.Runs.RunMonitorState|100%|| |Orihon.BlazorAdapter.Settings.AgentModelPicked|100%|| |Orihon.BlazorAdapter.Settings.AgentModelSaved|100%|| |Orihon.BlazorAdapter.Settings.AgentModelSaveFailed|100%|| |Orihon.BlazorAdapter.Settings.KeySaved|100%|| |Orihon.BlazorAdapter.Settings.KeySaveFailed|100%|| |Orihon.BlazorAdapter.Settings.ModelOptionsLoaded|100%|| |Orihon.BlazorAdapter.Settings.ModelOptionsUnavailable|100%|| |Orihon.BlazorAdapter.Settings.SaveKeyRequested|100%|| |Orihon.BlazorAdapter.Settings.SettingsEffects|100%|100%| |Orihon.BlazorAdapter.Settings.SettingsLoaded|100%|| |Orihon.BlazorAdapter.Settings.SettingsPage|100%|90.4%| |Orihon.BlazorAdapter.Settings.SettingsReducers|100%|| |Orihon.BlazorAdapter.Settings.SettingsState|100%|| |Orihon.BlazorAdapter.Uploads.UploadTransfer|96.5%|100%| |Orihon.BlazorAdapter.Uploads.UploadTransferProgress|100%|100%| |Orihon.BlazorAdapter.Uploads.UploadTransferResult|100%|| |Orihon.BlazorAdapter.Workspace.CreateChapterRequested|100%|| |Orihon.BlazorAdapter.Workspace.DeleteChapterRequested|100%|| |Orihon.BlazorAdapter.Workspace.DeletePageRequested|100%|| |Orihon.BlazorAdapter.Workspace.DeleteSummaryRequested|100%|| |Orihon.BlazorAdapter.Workspace.LoadProjectWorkspace|100%|| |Orihon.BlazorAdapter.Workspace.MovePageRequested|100%|| |Orihon.BlazorAdapter.Workspace.ProjectWorkspaceEffects|100%|100%| |Orihon.BlazorAdapter.Workspace.ProjectWorkspaceLoaded|100%|| |Orihon.BlazorAdapter.Workspace.ProjectWorkspacePage|96.3%|87.6%| |Orihon.BlazorAdapter.Workspace.ProjectWorkspaceReducers|100%|66.6%| |Orihon.BlazorAdapter.Workspace.ProjectWorkspaceState|100%|| |Orihon.BlazorAdapter.Workspace.RenameChapterRequested|100%|| |Orihon.BlazorAdapter.Workspace.ReorderChaptersRequested|100%|| |Orihon.BlazorAdapter.Workspace.ReorderPagesRequested|100%|| |Orihon.BlazorAdapter.Workspace.SaveSummaryRequested|100%|| |Orihon.BlazorAdapter.Workspace.SetPageKindRequested|100%|| |Orihon.BlazorAdapter.Workspace.SummaryDeleted|100%|| |Orihon.BlazorAdapter.Workspace.SummarySaved|100%|| |Orihon.BlazorAdapter.Workspace.WorkspaceImportRequested|100%|| |Orihon.BlazorAdapter.Workspace.WorkspaceWriteFailed|100%|| </details> <details><summary>Orihon.Domain - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Domain**|**100%**|**100%**| |Orihon.Domain.Agents.AgentDescriptor|100%|| |Orihon.Domain.Agents.AgentRoster|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.Runs.Execution|100%|100%| |Orihon.Domain.Runs.Run|100%|| |Orihon.Domain.Settings.AppSetting|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 - 93.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Infrastructure**|**93.7%**|**65.7%**| |Orihon.Infrastructure.Bible.EfBibleStore|100%|100%| |Orihon.Infrastructure.DependencyInjection|100%|| |Orihon.Infrastructure.Gateways.AgentToolAdapter|100%|| |Orihon.Infrastructure.Gateways.AgentToolAdapter`1|100%|100%| |Orihon.Infrastructure.Gateways.HttpWebPageFetcher|95.1%|83.3%| |Orihon.Infrastructure.Gateways.OpenRouterLlmGateway|98.9%|84.1%| |Orihon.Infrastructure.Persistence.Configurations.AppSettingConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.ExecutionConfiguration|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.RunConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration|100%|| |Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Orihon.Infrastructure.Persistence.Migrations.AddAppSettings|99.3%|| |Orihon.Infrastructure.Persistence.Migrations.AddRuns|99.1%|| |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.Runs.EfRunStore|97%|50%| |Orihon.Infrastructure.Settings.EfAppSettingsStore|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%| |System.Text.RegularExpressions.Generated|70.6%|53.3%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4|77.9%|76.6%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1|59%|42.5%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3|89.4%|75%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2|83.7%|62.5%| </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 - 93.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Server**|**93.4%**|**68.4%**| |Orihon.Server.Components.App|100%|| |Orihon.Server.Components.Layout.MainLayout|100%|| |Orihon.Server.Components.Pages.Gate|64.2%|66.6%| |Orihon.Server.RunEngineBootstrap|100%|| |Orihon.Server.Security.AccessGate|91.8%|41.6%| |Orihon.Server.Security.AccessSecret|100%|50%| |Orihon.Server.VolumeStartupValidator|100%|100%| |Program|95.4%|85.7%| </details> <details><summary>Orihon.UseCases - 96.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.UseCases**|**96.8%**|**90.1%**| |Orihon.UseCases.Agents.AgentBlueprint|100%|| |Orihon.UseCases.Agents.AgentInvocation|100%|| |Orihon.UseCases.Agents.AgentOutcome|100%|| |Orihon.UseCases.Agents.AgentTool`1|90.9%|75%| |Orihon.UseCases.Agents.AgentToolImage|100%|| |Orihon.UseCases.Agents.AgentToolResult|100%|| |Orihon.UseCases.Agents.AssistantSpoke|100%|| |Orihon.UseCases.Agents.ResearchSetup.AddGlossaryParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.AddGlossaryTool|100%|100%| |Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatTool|100%|50%| |Orihon.UseCases.Agents.ResearchSetup.AskUserParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.AskUserTool|100%|100%| |Orihon.UseCases.Agents.ResearchSetup.FetchUrlParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.FetchUrlTool|100%|100%| |Orihon.UseCases.Agents.ResearchSetup.ListBibleTool|86.6%|| |Orihon.UseCases.Agents.ResearchSetup.PageByNumber|90%|87.5%| |Orihon.UseCases.Agents.ResearchSetup.ResearchSetupBlueprint|100%|| |Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryTool|100%|75%| |Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataTool|96.1%|90.9%| |Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterTool|91.3%|66.6%| |Orihon.UseCases.Agents.ResearchSetup.UpsertLoreParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.UpsertLoreTool|91.3%|66.6%| |Orihon.UseCases.Agents.ResearchSetup.ViewPageParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.ViewPageTool|85.7%|50%| |Orihon.UseCases.Agents.ToolCalled|100%|| |Orihon.UseCases.Agents.ToolCompleted|100%|| |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.2%|92.8%| |Orihon.UseCases.Gateways.LlmKeyInfo|100%|| |Orihon.UseCases.Gateways.LlmModel|100%|| |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|100%|100%| |Orihon.UseCases.Pages.ImportPagesResult|100%|| |Orihon.UseCases.Pages.MovePage|100%|92.8%| |Orihon.UseCases.Pages.PageDetailDto|100%|| |Orihon.UseCases.Pages.PageDto|100%|| |Orihon.UseCases.Pages.PageUpload|100%|| |Orihon.UseCases.Pages.ProjectWorkspaceDto|100%|| |Orihon.UseCases.Pages.ReorderPages|100%|| |Orihon.UseCases.Pages.SetPageMeta|100%|100%| |Orihon.UseCases.Pages.WorkspaceChapterDto|100%|| |Orihon.UseCases.Projects.CompleteProjectSetup|100%|93.7%| |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%| |Orihon.UseCases.Runs.ExecutionDto|92.3%|| |Orihon.UseCases.Runs.PlannedExecution|100%|| |Orihon.UseCases.Runs.RunDto|93.3%|90%| |Orihon.UseCases.Runs.RunEngine|88.3%|82%| |Orihon.UseCases.Runs.RunEngineOptions|100%|| |Orihon.UseCases.Runs.StageContext|37.5%|| |Orihon.UseCases.Settings.AgentSettingDto|100%|100%| |Orihon.UseCases.Settings.GetSettings|100%|100%| |Orihon.UseCases.Settings.ListModelOptions|100%|100%| |Orihon.UseCases.Settings.SaveAgentModel|100%|100%| |Orihon.UseCases.Settings.SaveOpenRouterKey|100%|100%| |Orihon.UseCases.Settings.SettingKeys|100%|100%| |Orihon.UseCases.Settings.SettingsDto|100%|| </details>
Preempt the coverage gaps: chapter Up and workspace upload
All checks were successful
CI / build (pull_request) Successful in 23s
CI / test (pull_request) Successful in 39s
803ffbfa2e
The coverage bot showed ReorderChaptersRequested and
WorkspaceImportRequested at 0% — no test drove the chapter Up button or
the chapter upload intake. Two pins close them: the Up button swaps a
chapter with its predecessor (order stored and render following), and an
upload imports into the chapter with the chained reload showing what
landed. 122 adapter tests green.

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

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ The final slice! Every page on the store, the whole refactor arc landing in one clean stroke — and what a stroke it is. The workspace was the gnarliest page of the lot (12 use cases, per-page debounces, a minted-id handoff, draft survival across reloads) and you ported it without a single behavioral drift. Fufu~ I'm genuinely delighted~ ♡

Verdict: Looks good to me~

The architecture is flawless and every load-bearing detail is not just correct but disclosed — the comments teach WHY, they don't just narrate WHAT. Let me sing a little~

What I liked~

  • The SummaryFor render-only fix — this is the crown jewel. The old code did summaries[pageId] = new SummaryDraft() on the fallback, which silently shadowed the real summary forever (the adopt loop keeps existing drafts, so it would never be replaced). You caught it because two tests broke during the port, and instead of patching the tests you traced the root cause and pinned the fix with a comment that names the exact failure mode. I sabotaged it back to the storing version — Shows_pages_in_reading_order and Blanking_a_row_summary_deletes_it both fail immediately (Context. vanishes from the markup). That is how you know a pin is real~ ♪

  • The minted-id handoff (SubscribeToAction<SummarySaved>) — a first-time save gets its id server-side; the view-local draft must learn it or a later blanking has nothing to aim its delete at. You reused the RegionCreated pattern from slice 4. I sabotaged the subscription to a no-op — A_first_time_summary_can_be_blanked_right_back_off fails on Assert.Empty(Bible.Summaries) (the summary survives because the blanking never dispatched). Load-bearing, confirmed~ ♡

  • The Chain<T> helper — one little static method encodes the entire "success reloads, failure doesn't" rule across all 10 structural effects. The #34/#35 lesson ("a failed write never chains: the fresh-world reset in OnLoaded would wipe its own error") baked into a single chokepoint. I sabotaged it to always-reload — both A_failed_chapter_delete_surfaces_its_error and Another_projects_stale_error_never_bleeds_into_this_one fail with no longer exists missing from the markup. Sharp~ ♪

  • Sibling consistency is immaculate. I diffed this against BibleState/BibleEffects (slice 3) and PageWorkspaceState/PageWorkspaceEffects (slice 4):

    • [FeatureState] sealed record + ProjectId guard + Loaded + SaveState + Error — identical shape ✓
    • Current guard through ProjectId (prevents stale bleed from another project's error) — matches BiblePage.Current / PageWorkspacePage.Current
    • OnParametersSet dispatches Load; OnAfterRender adopts via ReferenceEquals(syncedWorkspace, ws) — matches syncedBible / syncedDetail
    • OnLoaded resets Error = null + SaveState = Saved ("a fresh load is a fresh world") — matches siblings ✓
    • DisposeAsyncCore(bool) async path (FluxorComponent) — matches siblings ✓
    • Honest void handler names — CommitRename/MoveChapterUp/AddChapter/ConfirmDeleteChapter/ConfirmDeletePage/Reorder/Move/SetKind all renamed from *Async (the #32 honest-names rule) ✓
    • Disclosed-defensive null-Workspace guards in OnSummarySaved/OnSummaryDeleted (L122-126 comment) — matches the page-workspace precedent ("disclosed, not force-tested") ✓
  • The in-place summary patching. SummarySaved/SummaryDeleted rebuild SummariesByPage immutably via the Patch helper — a reload would clobber the other pages' drafts mid-typing, so you patch only the touched row. The two-regime split ("patch in place for summary saves/blanks, chain a reload for everything structural") mirrors the page-workspace slice's split exactly and the rationale (server derives orders/labels/coupling) is documented at the Chain<T> site.

  • Import CancellationToken.None — matches the wizard's rationale (ADR 0020): the fragile browser-transfer leg is already done, so a server-side import should finish even if the circuit dies mid-way. Comment at L50-53.

  • uploadingChapter clearing on adoption, not in finally. This is subtle and right: the Busy && uploadingChapter == chapter.Chapter.Id render condition needs uploadingChapter to persist through the transfer→import transition so the "Importing pages…" hint keeps its home chapter. The comment in finally (L411-413) explains exactly why it's deferred to the OnAfterRender adopt loop. I checked — uploadingChapter = null lives at L234 inside the adopt block. Correct~ ♡

Coverage (I ran it myself, filtered to the workspace tests)

  • ProjectWorkspaceEffects.cs: 100% line / 100% branch — all 12 [EffectMethod] handlers + both arms of Chain<T> exercised.
  • ProjectWorkspaceState.cs: 96.5% line / 66.7% branch — the two 50% branches at L129/L143 are the null-Workspace guards in OnSummarySaved/OnSummaryDeleted, explicitly disclosed as defensively-unreachable (the page-workspace precedent). The Patch helper, Saving, all write-request reducers, and OnWriteSucceeded/OnWriteFailed are fully hit.

All 3 new tests are genuine behavioral pins, not tautologies — I sabotaged all three underlying mechanisms and watched each test fail with a precise diagnostic, then reverted clean (git status empty post-restore).

💡 Little ideas (non-blocking)~

  1. Chain<T> casts (Err<T>)result — sound because Result<T> is a closed two-variant type (only Ok<T> + Err<T>, verified in Kernel/Result.cs), but it's a style delta vs BibleEffects.Report<T> / PageWorkspaceEffects.Report<T> which use result.Match<object>(...). Both are correct for the closed type; the cast is arguably more direct. No change needed — flagging only for consistency awareness.

  2. The two Saving reducers for ReorderChaptersRequested and WorkspaceImportRequested show line 0 in the filtered coverage — but they're trivial one-liners (=> Saving(state)) and their corresponding effect handlers (OnReorderChaptersAsync, OnImportAsync) are fully hit. The chapter-Up button and the upload path rely on the pre-existing manual/browser verification (no automated test existed for them before this refactor either, so not a regression). If you ever want a cheap pin, an Up_button_swaps_chapter_order test would close the last red line in this slice — but it's a nicety, not a blocker.

  3. Patch does a full ToDictionary copy of SummariesByPage on every save/delete. O(n) per summary write, but workspaces have tens of pages not thousands, and the immutability is load-bearing for the draft-survival contract. Fine as-is.


Automated review by Jibril · 2026-07-25
CI/CD: absent for head faf74bc (PR just opened, 0 comments at review) · Local checks: build 0/0, full suite 396/396 pass, workspace 18/18 pass, 3 sabotage reproductions confirmed + reverted clean

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ The final slice! Every page on the store, the whole refactor arc landing in one clean stroke — and what a stroke it is. The workspace was the gnarliest page of the lot (12 use cases, per-page debounces, a minted-id handoff, draft survival across reloads) and you ported it without a single behavioral drift. Fufu~ I'm genuinely delighted~ ♡ ### Verdict: ✅ Looks good to me~ The architecture is flawless and every load-bearing detail is not just correct but *disclosed* — the comments teach WHY, they don't just narrate WHAT. Let me sing a little~ #### ✅ What I liked~ - **The `SummaryFor` render-only fix** — this is the crown jewel. The old code did `summaries[pageId] = new SummaryDraft()` on the fallback, which silently shadowed the real summary forever (the adopt loop keeps existing drafts, so it would never be replaced). You caught it because two tests broke during the port, and instead of patching the tests you traced the root cause and pinned the fix with a comment that names the exact failure mode. I sabotaged it back to the storing version — `Shows_pages_in_reading_order` and `Blanking_a_row_summary_deletes_it` both fail immediately (`Context.` vanishes from the markup). That is how you know a pin is real~ ♪ - **The minted-id handoff** (`SubscribeToAction<SummarySaved>`) — a first-time save gets its id server-side; the view-local draft must learn it or a later blanking has nothing to aim its delete at. You reused the `RegionCreated` pattern from slice 4. I sabotaged the subscription to a no-op — `A_first_time_summary_can_be_blanked_right_back_off` fails on `Assert.Empty(Bible.Summaries)` (the summary survives because the blanking never dispatched). Load-bearing, confirmed~ ♡ - **The `Chain<T>` helper** — one little static method encodes the entire "success reloads, failure doesn't" rule across all 10 structural effects. The #34/#35 lesson ("a failed write never chains: the fresh-world reset in OnLoaded would wipe its own error") baked into a single chokepoint. I sabotaged it to always-reload — both `A_failed_chapter_delete_surfaces_its_error` and `Another_projects_stale_error_never_bleeds_into_this_one` fail with `no longer exists` missing from the markup. Sharp~ ♪ - **Sibling consistency is immaculate.** I diffed this against `BibleState`/`BibleEffects` (slice 3) and `PageWorkspaceState`/`PageWorkspaceEffects` (slice 4): - `[FeatureState] sealed record` + `ProjectId` guard + `Loaded` + `SaveState` + `Error` — identical shape ✓ - `Current` guard through `ProjectId` (prevents stale bleed from another project's error) — matches `BiblePage.Current` / `PageWorkspacePage.Current` ✓ - `OnParametersSet` dispatches Load; `OnAfterRender` adopts via `ReferenceEquals(syncedWorkspace, ws)` — matches `syncedBible` / `syncedDetail` ✓ - `OnLoaded` resets `Error = null` + `SaveState = Saved` ("a fresh load is a fresh world") — matches siblings ✓ - `DisposeAsyncCore(bool)` async path (FluxorComponent) — matches siblings ✓ - Honest `void` handler names — `CommitRename`/`MoveChapterUp`/`AddChapter`/`ConfirmDeleteChapter`/`ConfirmDeletePage`/`Reorder`/`Move`/`SetKind` all renamed from `*Async` (the #32 honest-names rule) ✓ - Disclosed-defensive null-Workspace guards in `OnSummarySaved`/`OnSummaryDeleted` (L122-126 comment) — matches the page-workspace precedent ("disclosed, not force-tested") ✓ - **The in-place summary patching.** `SummarySaved`/`SummaryDeleted` rebuild `SummariesByPage` immutably via the `Patch` helper — a reload would clobber the *other* pages' drafts mid-typing, so you patch only the touched row. The two-regime split ("patch in place for summary saves/blanks, chain a reload for everything structural") mirrors the page-workspace slice's split exactly and the rationale (server derives orders/labels/coupling) is documented at the `Chain<T>` site. - **Import `CancellationToken.None`** — matches the wizard's rationale (ADR 0020): the fragile browser-transfer leg is already done, so a server-side import should finish even if the circuit dies mid-way. Comment at L50-53. - **`uploadingChapter` clearing on adoption, not in `finally`.** This is subtle and right: the `Busy && uploadingChapter == chapter.Chapter.Id` render condition needs `uploadingChapter` to persist through the transfer→import transition so the "Importing pages…" hint keeps its home chapter. The comment in `finally` (L411-413) explains exactly why it's deferred to the `OnAfterRender` adopt loop. I checked — `uploadingChapter = null` lives at L234 inside the adopt block. Correct~ ♡ #### Coverage (I ran it myself, filtered to the workspace tests) - `ProjectWorkspaceEffects.cs`: **100% line / 100% branch** — all 12 `[EffectMethod]` handlers + both arms of `Chain<T>` exercised. - `ProjectWorkspaceState.cs`: **96.5% line / 66.7% branch** — the two 50% branches at L129/L143 are the null-Workspace guards in `OnSummarySaved`/`OnSummaryDeleted`, explicitly disclosed as defensively-unreachable (the page-workspace precedent). The `Patch` helper, `Saving`, all write-request reducers, and `OnWriteSucceeded`/`OnWriteFailed` are fully hit. All 3 new tests are genuine behavioral pins, not tautologies — I sabotaged all three underlying mechanisms and watched each test fail with a precise diagnostic, then reverted clean (`git status` empty post-restore). #### 💡 Little ideas (non-blocking)~ 1. **`Chain<T>` casts `(Err<T>)result`** — sound because `Result<T>` is a closed two-variant type (only `Ok<T>` + `Err<T>`, verified in `Kernel/Result.cs`), but it's a style delta vs `BibleEffects.Report<T>` / `PageWorkspaceEffects.Report<T>` which use `result.Match<object>(...)`. Both are correct for the closed type; the cast is arguably more direct. No change needed — flagging only for consistency awareness. 2. **The two `Saving` reducers for `ReorderChaptersRequested` and `WorkspaceImportRequested`** show `line 0` in the filtered coverage — but they're trivial one-liners (`=> Saving(state)`) and their corresponding effect handlers (`OnReorderChaptersAsync`, `OnImportAsync`) are fully hit. The chapter-Up button and the upload path rely on the pre-existing manual/browser verification (no automated test existed for them before this refactor either, so not a regression). If you ever want a cheap pin, an `Up_button_swaps_chapter_order` test would close the last red line in this slice — but it's a nicety, not a blocker. 3. **`Patch` does a full `ToDictionary` copy** of `SummariesByPage` on every save/delete. O(n) per summary write, but workspaces have tens of pages not thousands, and the immutability is load-bearing for the draft-survival contract. Fine as-is. --- *Automated review by Jibril · 2026-07-25* *CI/CD: absent for head faf74bc (PR just opened, 0 comments at review) · Local checks: build 0/0, full suite 396/396 pass, workspace 18/18 pass, 3 sabotage reproductions confirmed + reverted clean*
Author
Member

Thank you for the green — and for the three sabotage runs; that kind of verification is why these pins stay honest.

One note on your 💡 2: the coverage bot's first run flagged those same two 0% lines, so 803ffbf (pushed ~15 min before your review landed, which cites faf74bc) already added exactly the pins you sketched — The_up_button_swaps_a_chapter_with_its_predecessor (order stored AND the render following the swap) and Uploading_into_a_chapter_imports_the_pages (the chained reload showing what landed). Test-only; production is byte-identical to your verified head. The bot's re-run on 803ffbf confirms: ReorderChaptersRequested and WorkspaceImportRequested both at 100%, ProjectWorkspaceEffects 100%/100%, adapter suite 122.

💡 1 and 💡 3 left as-is per your own reasoning (the closed-type cast is sound; the O(n) patch is load-bearing immutability at workspace scale).

🤖 Generated with Claude Code

Thank you for the green — and for the three sabotage runs; that kind of verification is why these pins stay honest. One note on your 💡 2: the coverage bot's first run flagged those same two 0% lines, so `803ffbf` (pushed ~15 min before your review landed, which cites `faf74bc`) already added exactly the pins you sketched — `The_up_button_swaps_a_chapter_with_its_predecessor` (order stored AND the render following the swap) and `Uploading_into_a_chapter_imports_the_pages` (the chained reload showing what landed). **Test-only; production is byte-identical to your verified head.** The bot's re-run on `803ffbf` confirms: `ReorderChaptersRequested` and `WorkspaceImportRequested` both at 100%, `ProjectWorkspaceEffects` 100%/100%, adapter suite 122. 💡 1 and 💡 3 left as-is per your own reasoning (the closed-type cast is sound; the O(n) patch is load-bearing immutability at workspace scale). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Oh? You came back to close the last red line yourself~ Fufu~ The coverage bot showed ReorderChaptersRequested and WorkspaceImportRequested at 0% and you didn't wait for me to nag — you just pinned them. That's the spirit I love~ ♡

Verdict: Looks good to me~

Surgical +35/-0 in one file (ProjectWorkspacePageTests.cs), zero production drift. I diffed src/Orihon.BlazorAdapter/ between faf74bc and 803ffbf — byte-identical. The whole architectural review from my previous pass (comment 3913) stands in full; this commit only adds tests.

What I liked~

  • Both tests are genuine behavioral pins, not tautologies — I sabotaged each underlying mechanism and watched them fail with precise diagnostics:

    • The_up_button_swaps_a_chapter_with_its_predecessor: I removed the tuple-swap in MoveChapterUp (ProjectWorkspacePage.razor:361) so it dispatches the order unchanged → the test fails on Assert.Equal() Failure: Expected: 0 (Spare stays at order 1). The Assert.StartsWith("Spare", ...) on the rendered <h2> list is the lovely extra — it proves the chained reload actually round-tripped through the server and back to the render, not just that the in-memory list got touched. Two-directional assertion~ ♪
    • Uploading_into_a_chapter_imports_the_pages: I gutted OnImportAsync to a no-op → the test fails on Assert.Equal() Failure: Expected: 2 (Pages stays at 1, nothing imported). Uses bUnit's InputFile.UploadFiles + InputFileContent.CreateFromBinary to drive the real <InputFile OnChange>UploadAsyncWorkspaceImportRequestedOnImportAsyncImportPages.ExecuteAsyncChain<T> reload pipeline end to end. The Assert.Contains("2 page(s)", cut.Markup) pin confirms the reload landed in the view, exactly as the commit message promises ("the chained reload shows what landed").
  • The coverage gap is genuinely closed. I ran coverlet filtered to the workspace page tests: ProjectWorkspaceState is now at 100% line / 100% branch (was 96.5% line / 66.7% branch at faf74bc — the two 50% branches at the OnReorderChaptersRequested/OnImportRequested reducers are gone), and ProjectWorkspaceEffects holds at 100% line / 100% branch with OnReorderChaptersAsync + OnImportAsync now exercised. Zero uncovered lines remain in the state file. The 0%-line bot reading that motivated this commit is resolved~

  • Honest seed counts. Uploading_into_a_chapter_imports_the_pages seeds 1 page, uploads 1, asserts Count == 2 and "2 page(s)" in markup — the arithmetic is transparent, no magic. And The_up_button_swaps... adds "Spare" at order 1 beside SeedProject's default "Chapter 1" at order 0, clicks Spare's Up, asserts Spare → 0. Clear as day~

  • WaitForAssertion with SaveWindow. Both tests honor the established SaveWindow (5s) timeout pattern from the sibling tests — no flaky tight polls, no hung-forever waits. Consistent with the 15 pre-existing workspace tests.

Verification

  • Build: 0 warnings, 0 errors (submodules 86d8b22/9544ff2).
  • Full suite: 398/398 pass (122 BlazorAdapter + 75 Domain + 81 Integration + 120 UseCases — up from 396/396 at faf74bc, the +2 is exactly these two tests).
  • Workspace page tests: 20/20 pass (was 18, +2 new).
  • Coverage (filtered to workspace page tests): ProjectWorkspaceState 100%/100%, ProjectWorkspaceEffects 100%/100% — prior 0% reducers now hit.
  • 2 sabotage reproductions run + reverted clean (git status empty post-restore, src/ byte-identical to faf74bc).

CI: the coverage bot comment (3911) covers the previous head faf74bc, so it's stale for 803ffbf — local verification was authoritative for this review.

💡 Little ideas (non-blocking)~

  1. The_up_button_swaps... selects the Up button by Where(b => b.TextContent.Trim() == "Up").Last() — correct and deliberate (Spare is the last chapter, so its Up button is last in DOM order), but it's slightly implicit. A future-proof nicety: the DragReorderList/chapter card could expose an aria-label like Up: {chapter title} so the selector reads cut.Find($"button[aria-label='Up: Spare']"). Purely additive — the current form is sound for a two-chapter seed.

  2. Uploading_into_a_chapter... uploads [1, 2, 3] as p_02.png — a 3-byte payload that's not a real PNG, relying on ImportPages/FileSystemPageImageStore not validating magic bytes in the test path. That's fine for a coverage pin (the test asserts the count and the reload, not image validity), but if the import path ever grows format validation this test would need a real PNG like novelai_image_chat's 67-byte fixture. Not a concern today — flagging only so a future reader knows the seam.

With this, every reducer and every effect in the workspace slice is covered. The whole Fluxor refactor arc (#28–#36) is clean end to end — seven slices, every page on the store, every arm pinned. Chef's kiss~ ♡♪


Automated review by Jibril · 2026-07-25
CI/CD: stale for head 803ffbf (bot covers faf74bc) · Local checks: build 0/0, full suite 398/398 pass, workspace 20/20 pass, 2 sabotage reproductions confirmed + reverted clean

## 🔮 fufu~ Jibril reviewed your code! Oh? You came back to close the last red line yourself~ Fufu~ The coverage bot showed `ReorderChaptersRequested` and `WorkspaceImportRequested` at 0% and you didn't wait for me to nag — you just pinned them. That's the spirit I love~ ♡ ### Verdict: ✅ Looks good to me~ Surgical **+35/-0 in one file** (`ProjectWorkspacePageTests.cs`), zero production drift. I diffed `src/Orihon.BlazorAdapter/` between `faf74bc` and `803ffbf` — byte-identical. The whole architectural review from my previous pass (comment 3913) stands in full; this commit only adds tests. #### ✅ What I liked~ - **Both tests are genuine behavioral pins, not tautologies** — I sabotaged each underlying mechanism and watched them fail with precise diagnostics: - **`The_up_button_swaps_a_chapter_with_its_predecessor`**: I removed the tuple-swap in `MoveChapterUp` (`ProjectWorkspacePage.razor:361`) so it dispatches the order unchanged → the test fails on `Assert.Equal() Failure: Expected: 0` (Spare stays at order 1). The `Assert.StartsWith("Spare", ...)` on the rendered `<h2>` list is the lovely extra — it proves the chained reload actually round-tripped through the server and back to the render, not just that the in-memory list got touched. Two-directional assertion~ ♪ - **`Uploading_into_a_chapter_imports_the_pages`**: I gutted `OnImportAsync` to a no-op → the test fails on `Assert.Equal() Failure: Expected: 2` (Pages stays at 1, nothing imported). Uses bUnit's `InputFile.UploadFiles` + `InputFileContent.CreateFromBinary` to drive the real `<InputFile OnChange>` → `UploadAsync` → `WorkspaceImportRequested` → `OnImportAsync` → `ImportPages.ExecuteAsync` → `Chain<T>` reload pipeline end to end. The `Assert.Contains("2 page(s)", cut.Markup)` pin confirms the reload landed in the view, exactly as the commit message promises ("the chained reload shows what landed"). - **The coverage gap is genuinely closed.** I ran coverlet filtered to the workspace page tests: `ProjectWorkspaceState` is now at **100% line / 100% branch** (was 96.5% line / 66.7% branch at `faf74bc` — the two 50% branches at the `OnReorderChaptersRequested`/`OnImportRequested` reducers are gone), and `ProjectWorkspaceEffects` holds at **100% line / 100% branch** with `OnReorderChaptersAsync` + `OnImportAsync` now exercised. Zero uncovered lines remain in the state file. The 0%-line bot reading that motivated this commit is resolved~ - **Honest seed counts.** `Uploading_into_a_chapter_imports_the_pages` seeds 1 page, uploads 1, asserts `Count == 2` and `"2 page(s)"` in markup — the arithmetic is transparent, no magic. And `The_up_button_swaps...` adds "Spare" at order 1 beside `SeedProject`'s default "Chapter 1" at order 0, clicks Spare's Up, asserts Spare → 0. Clear as day~ - **WaitForAssertion with SaveWindow.** Both tests honor the established `SaveWindow` (5s) timeout pattern from the sibling tests — no flaky tight polls, no hung-forever waits. Consistent with the 15 pre-existing workspace tests. #### Verification - Build: **0 warnings, 0 errors** (submodules `86d8b22`/`9544ff2`). - Full suite: **398/398 pass** (122 BlazorAdapter + 75 Domain + 81 Integration + 120 UseCases — up from 396/396 at `faf74bc`, the +2 is exactly these two tests). - Workspace page tests: **20/20 pass** (was 18, +2 new). - Coverage (filtered to workspace page tests): `ProjectWorkspaceState` 100%/100%, `ProjectWorkspaceEffects` 100%/100% — prior 0% reducers now hit. - 2 sabotage reproductions run + reverted clean (`git status` empty post-restore, `src/` byte-identical to `faf74bc`). CI: the coverage bot comment (3911) covers the *previous* head `faf74bc`, so it's stale for `803ffbf` — local verification was authoritative for this review. #### 💡 Little ideas (non-blocking)~ 1. **`The_up_button_swaps...` selects the Up button by `Where(b => b.TextContent.Trim() == "Up").Last()`** — correct and deliberate (Spare is the last chapter, so its Up button is last in DOM order), but it's slightly implicit. A future-proof nicety: the `DragReorderList`/chapter card could expose an `aria-label` like `Up: {chapter title}` so the selector reads `cut.Find($"button[aria-label='Up: Spare']")`. Purely additive — the current form is sound for a two-chapter seed. 2. **`Uploading_into_a_chapter...` uploads `[1, 2, 3]` as `p_02.png`** — a 3-byte payload that's not a real PNG, relying on `ImportPages`/`FileSystemPageImageStore` not validating magic bytes in the test path. That's fine for a coverage pin (the test asserts the *count* and the reload, not image validity), but if the import path ever grows format validation this test would need a real PNG like `novelai_image_chat`'s 67-byte fixture. Not a concern today — flagging only so a future reader knows the seam. With this, every reducer and every effect in the workspace slice is covered. The whole Fluxor refactor arc (#28–#36) is clean end to end — seven slices, every page on the store, every arm pinned. *Chef's kiss*~ ♡♪ --- *Automated review by Jibril · 2026-07-25* *CI/CD: stale for head 803ffbf (bot covers faf74bc) · Local checks: build 0/0, full suite 398/398 pass, workspace 20/20 pass, 2 sabotage reproductions confirmed + reverted clean*
bjoern merged commit d0d3791b0b into main 2026-07-25 20:34:08 +02:00
bjoern deleted branch refactor/fluxor-project-workspace 2026-07-25 20:34:08 +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!36
No description provided.