refactor: Fluxor 6/7 — the project list #35

Merged
bjoern merged 1 commit from refactor/fluxor-project-list into main 2026-07-25 20:33:39 +02:00
Member

Sixth slice of the Fluxor page refactor (ADR 0011; series plan in #27). The project list moves onto its own slice; the last remaining page is the project workspace (7/7, coming next as its own PR — it's 484 lines over 13 use cases and deserves its own review).

What's in

Store (ProjectListState)Loaded, Projects, Error. View-local stays view-local: deleting (which project the confirmation dialog is aimed at) — an open modal is view state.

Effects (ProjectListEffects) — sole touchpoint for ListProjects/DeleteProject. A successful delete chains LoadProjects so the list reflects the loss; a failed one dispatches only ProjectDeleteFailed — the chained reload's load-request would wipe the very error the user needs to see (the #34 lesson, same shape).

Stale-error handling — the list has no route id to guard the error render with, so it uses the wizard slice's /projects/new pattern: the LoadProjects dispatched on entry clears the slice's error via a [ReducerMethod(typeof(LoadProjects))] reducer, which is what makes the unguarded State.Value.Error render safe. Documented at the render site.

Honest namesConfirmDeleteAsync was async Task; it now only dispatches, so it's void ConfirmDelete (the #32 rule applied proactively).

Tests

+2, adapter suite 119, full suite 395/395 green. All 8 pre-existing list tests pass unchanged. New pins:

  • A_failed_delete_surfaces_its_error_and_keeps_the_list — project vanishes server-side while the modal is open; asserts the Err arm renders as an alert AND the list stays as rendered rather than half-refreshing (this is the arm a chained reload would have swallowed).
  • Reentering_the_list_clears_a_previous_visits_error — pins the clear-on-entry that justifies the unguarded error render.

Browser-verified

Driven live against a seeded world: list renders the sample project card (title, byline, relative time) from the store → card click opens the ready project's workspace → back → Delete opens the modal naming the project → Cancel keeps it → Delete forever hard-deletes, the chained reload lands on the empty state → the empty-state CTA navigates into the wizard. Console clean (one favicon 404 only).

🤖 Generated with Claude Code

Sixth slice of the Fluxor page refactor (ADR 0011; series plan in #27). The project list moves onto its own slice; the last remaining page is the project workspace (7/7, coming next as its own PR — it's 484 lines over 13 use cases and deserves its own review). ## What's in **Store (`ProjectListState`)** — `Loaded`, `Projects`, `Error`. View-local stays view-local: `deleting` (which project the confirmation dialog is aimed at) — an open modal is view state. **Effects (`ProjectListEffects`)** — sole touchpoint for `ListProjects`/`DeleteProject`. A **successful** delete chains `LoadProjects` so the list reflects the loss; a **failed** one dispatches only `ProjectDeleteFailed` — the chained reload's load-request would wipe the very error the user needs to see (the #34 lesson, same shape). **Stale-error handling** — the list has no route id to guard the error render with, so it uses the wizard slice's `/projects/new` pattern: the `LoadProjects` dispatched on entry clears the slice's error via a `[ReducerMethod(typeof(LoadProjects))]` reducer, which is what makes the unguarded `State.Value.Error` render safe. Documented at the render site. **Honest names** — `ConfirmDeleteAsync` was `async Task`; it now only dispatches, so it's `void ConfirmDelete` (the #32 rule applied proactively). ## Tests +2, adapter suite 119, full suite **395/395 green**. All 8 pre-existing list tests pass unchanged. New pins: - `A_failed_delete_surfaces_its_error_and_keeps_the_list` — project vanishes server-side while the modal is open; asserts the Err arm renders as an alert AND the list stays as rendered rather than half-refreshing (this is the arm a chained reload would have swallowed). - `Reentering_the_list_clears_a_previous_visits_error` — pins the clear-on-entry that justifies the unguarded error render. ## Browser-verified Driven live against a seeded world: list renders the sample project card (title, byline, relative time) from the store → card click opens the ready project's workspace → back → Delete opens the modal naming the project → Cancel keeps it → Delete forever hard-deletes, the chained reload lands on the empty state → the empty-state CTA navigates into the wizard. Console clean (one favicon 404 only). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
refactor: Fluxor 6/7 — the project list
All checks were successful
CI / build (pull_request) Successful in 24s
CI / test (pull_request) Successful in 37s
2f120e0ece
The list moves onto its own slice (ADR 0011). The store holds the loaded
projects and a delete's outcome; which project the confirmation dialog is
aimed at stays component-side — an open modal is view state.

Effects are the only touchpoint for ListProjects/DeleteProject. A
successful delete chains a reload so the list reflects the loss; a failed
one leaves the store untouched apart from its error — a reload's
load-request would wipe it. The list has no route id to guard a stale
error with, so the LoadProjects dispatched on entry clears it (the wizard
slice's /projects/new pattern), and ConfirmDeleteAsync drops its lying
Async suffix along the way.

Tests: +2 (119 adapter total) — a failed delete surfaces its error without
half-refreshing the list, and re-entering the list clears a previous
visit's error.

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

Summary

Summary
Generated on: 07/25/2026 - 15:29:48
Coverage date: 07/25/2026 - 15:29:38 - 07/25/2026 - 15:29:46
Parser: MultiReport (4x Cobertura)
Assemblies: 6
Classes: 251
Files: 141
Line coverage: 94.2% (6522 of 6919)
Covered lines: 6522
Uncovered lines: 397
Coverable lines: 6919
Total lines: 12486
Branch coverage: 78.9% (1363 of 1726)
Covered branches: 1363
Total branches: 1726
Method coverage: Feature is only available for sponsors

Coverage

Orihon.BlazorAdapter - 91.4%
Name Line Branch
Orihon.BlazorAdapter 91.4% 84.5%
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.DeleteProjectRequested 100%
Orihon.BlazorAdapter.Projects.FinishSetupRequested 100%
Orihon.BlazorAdapter.Projects.ImportPagesRequested 100%
Orihon.BlazorAdapter.Projects.LoadWizard 100%
Orihon.BlazorAdapter.Projects.PagesImported 100%
Orihon.BlazorAdapter.Projects.ProjectDeleteFailed 100%
Orihon.BlazorAdapter.Projects.ProjectListEffects 100% 100%
Orihon.BlazorAdapter.Projects.ProjectListPage 89.7% 91.1%
Orihon.BlazorAdapter.Projects.ProjectListReducers 100%
Orihon.BlazorAdapter.Projects.ProjectListState 100%
Orihon.BlazorAdapter.Projects.ProjectsLoaded 100%
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.ProjectWorkspacePage 73% 76.6%
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 - 15:29:48 | | Coverage date: | 07/25/2026 - 15:29:38 - 07/25/2026 - 15:29:46 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 6 | | Classes: | 251 | | Files: | 141 | | **Line coverage:** | 94.2% (6522 of 6919) | | Covered lines: | 6522 | | Uncovered lines: | 397 | | Coverable lines: | 6919 | | Total lines: | 12486 | | **Branch coverage:** | 78.9% (1363 of 1726) | | Covered branches: | 1363 | | Total branches: | 1726 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Orihon.BlazorAdapter - 91.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.BlazorAdapter**|**91.4%**|**84.5%**| |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.DeleteProjectRequested|100%|| |Orihon.BlazorAdapter.Projects.FinishSetupRequested|100%|| |Orihon.BlazorAdapter.Projects.ImportPagesRequested|100%|| |Orihon.BlazorAdapter.Projects.LoadWizard|100%|| |Orihon.BlazorAdapter.Projects.PagesImported|100%|| |Orihon.BlazorAdapter.Projects.ProjectDeleteFailed|100%|| |Orihon.BlazorAdapter.Projects.ProjectListEffects|100%|100%| |Orihon.BlazorAdapter.Projects.ProjectListPage|89.7%|91.1%| |Orihon.BlazorAdapter.Projects.ProjectListReducers|100%|| |Orihon.BlazorAdapter.Projects.ProjectListState|100%|| |Orihon.BlazorAdapter.Projects.ProjectsLoaded|100%|| |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.ProjectWorkspacePage|73%|76.6%| </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>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! The list slice, the sixth petal of the Fluxor refactor~ ♪ I've watched the whole garden bloom — Bible, PageWorkspace, Wizard, and now the list sits on the same action/effect/reducer spine as its siblings. This is wonderful craftsmanship. The shape is so consistent I could hum it from memory. ♡

Verdict: Looks good to me~

The project list now lives where it belongs — in the store, not in component fields — and the page is a thin view that dispatches and reads. Every architectural choice traces cleanly to a sibling precedent.

What I liked~

  • The failed-delete asymmetry is exactly right. A successful delete chains LoadProjects so the list reflects the loss; a failed one dispatches ONLY ProjectDeleteFailed and leaves the list untouched — because the chained reload's OnLoadRequested reducer would wipe the very error the user needs to see. That's the #34 lesson applied proactively, and the comment in ProjectListEffects.cs:7-9 names it explicitly. Fufu~ you remembered the lesson and documented it. ♡
  • The stale-error solution is elegant given the constraint. The list has no route id to guard the error render with (unlike Bible/PageWorkspace/Wizard, which key off ProjectId/PageId), so it borrows the wizard's /projects/new pattern: the LoadProjects dispatched on entry clears Error via [ReducerMethod(typeof(LoadProjects))]. The comment at the render site (ProjectListPage.razor:84-85) documents why the unguarded State.Value.Error is { } error is safe. Load-bearing comment, load-bearing reducer.
  • Honest names. ConfirmDeleteAsync was async Task; now it only dispatches, so it's void ConfirmDelete (the #32 rule). The deleting is { } doomed pattern-match is a nice touch too — no bare null deref possible.
  • Slice shape is byte-faithful to siblings. [FeatureState] sealed record ProjectListState, actions as sealed records, pure static ProjectListReducers with explicit per-action [ReducerMethod] (no base-type matching), ProjectListEffects as the sole touchpoint for ListProjects/DeleteProject. One public class per file. It matches BibleState/PageWorkspaceState/ProjectWizardState so precisely you could diff them for taste.
  • OnLoadAsync's Match-to-empty-list (result.Match<IReadOnlyList<ProjectDto>>(list => list, _ => [])) is correct — a failed load doesn't detonate the page, it renders the empty state. Pragmatic and consistent with the pre-PR behavior.

🔬 How I checked the tests (because green CI ≠ correct, fufu~)

Both new tests are genuine behavioral pins, NOT tautologies — I sabotaged the production code two ways and watched each test die precisely:

  1. A_failed_delete_surfaces_its_error_and_keeps_the_list — I replaced the error-preserving branch with a silent chained-reload-always. The test failed at the WaitForAssertion for "no longer exists", because the chained LoadProjects wiped the error before it could render. Restored, passed. Genuine pin on the asymmetry.
  2. Reentering_the_list_clears_a_previous_visits_error — I made OnLoadRequested a no-op (stopped clearing Error). The test failed at Assert.DoesNotContain("no longer exists", second.Markup) — the stale error bled through to the second visit exactly as the comment warns. Restored, passed. Genuine pin on the clear-on-entry invariant.

The failure path is real: Projects.Items.Clear() before "Delete forever" makes DeleteProject.ExecuteAsync return Err<Unit>("The project no longer exists.") (verified in DeleteProject.cs:17 + FakeProjectStore.DeleteAsyncRemoveAll > 0 = false), so the assertion string matches the real error message.

📐 Sibling consistency verified

  • ProjectListStateBibleState/PageWorkspaceState/ProjectWizardState: same [FeatureState] sealed record shape, Loaded flag, Error field, explicit per-action reducers. ✓
  • ProjectListEffectsPageWorkspaceEffects: is not Ok<T> + (Err<T>)result cast pattern identical. Result<T> is a closed two-variant type (abstract Result<T> with only Ok<T> + Err<T>), so the cast is sound. ✓
  • OnInitialized dispatches LoadProjects — matches how siblings dispatch LoadBible/LoadPageWorkspace/LoadWizard on entry. ✓

💡 Little ideas (non-blocking)~

  1. ProjectListEffects.cs:13 — the Match discards the error on a failed load. _ => [] silently swallows a ListProjects failure into an empty list, which the page then renders as the "No projects yet" empty state. This is pre-existing behavior (the old ReloadAsync did the same result.Match(list => list, _ => [])), so it's not a regression — but the store now has an Error field that didn't exist before. If you ever want a failed load to show an error instead of masquerading as an empty world, the plumbing is right there. Not blocking; the current behavior is defensible (don't block the user's front door on a transient backend hiccup).
  2. ProjectListEffects.cs:32(Err<Unit>)result cast. The is not Ok<Unit> + explicit cast pattern is identical to siblings and correct for the closed Result<T> type — but the Report<T> helper in BibleEffects/PageWorkspaceEffects (which does result.Match<object>(ok, err)) avoids the cast entirely. The list has only one fallible call so a helper would be over-engineering; just flagging the style difference for awareness. ♡

Automated review by Jibril · 2026-07-25
CI/CD: absent for head SHA 2f120e0 (PR just opened, 0 comments at review) · Local checks: build 0 warnings/0 errors (submodules 86d8b22/9544ff2), full suite 395/395 pass (119 BlazorAdapter + 81 Integration + 75 Domain + 120 UseCases — matches PR body exactly), 10/10 ProjectListPageTests, 2 sabotage reproductions run + reverted clean (git status empty post-restore)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! The list slice, the sixth petal of the Fluxor refactor~ ♪ I've watched the whole garden bloom — Bible, PageWorkspace, Wizard, and now the list sits on the same action/effect/reducer spine as its siblings. This is *wonderful* craftsmanship. The shape is so consistent I could hum it from memory. ♡ ### Verdict: ✅ Looks good to me~ The project list now lives where it belongs — in the store, not in component fields — and the page is a thin view that dispatches and reads. Every architectural choice traces cleanly to a sibling precedent. #### ✅ What I liked~ - **The failed-delete asymmetry is *exactly* right.** A successful delete chains `LoadProjects` so the list reflects the loss; a failed one dispatches ONLY `ProjectDeleteFailed` and leaves the list untouched — because the chained reload's `OnLoadRequested` reducer would wipe the very error the user needs to see. That's the #34 lesson applied proactively, and the comment in `ProjectListEffects.cs:7-9` names it explicitly. Fufu~ you remembered the lesson *and* documented it. ♡ - **The stale-error solution is elegant given the constraint.** The list has no route id to guard the error render with (unlike Bible/PageWorkspace/Wizard, which key off `ProjectId`/`PageId`), so it borrows the wizard's `/projects/new` pattern: the `LoadProjects` dispatched on entry clears `Error` via `[ReducerMethod(typeof(LoadProjects))]`. The comment at the render site (`ProjectListPage.razor:84-85`) documents *why* the unguarded `State.Value.Error is { } error` is safe. Load-bearing comment, load-bearing reducer. - **Honest names.** `ConfirmDeleteAsync` was `async Task`; now it only dispatches, so it's `void ConfirmDelete` (the #32 rule). The `deleting is { } doomed` pattern-match is a nice touch too — no bare `null` deref possible. - **Slice shape is byte-faithful to siblings.** `[FeatureState] sealed record ProjectListState`, actions as sealed records, pure static `ProjectListReducers` with explicit per-action `[ReducerMethod]` (no base-type matching), `ProjectListEffects` as the sole touchpoint for `ListProjects`/`DeleteProject`. One public class per file. It matches `BibleState`/`PageWorkspaceState`/`ProjectWizardState` so precisely you could diff them for taste. - **`OnLoadAsync`'s Match-to-empty-list** (`result.Match<IReadOnlyList<ProjectDto>>(list => list, _ => [])`) is correct — a failed load doesn't detonate the page, it renders the empty state. Pragmatic and consistent with the pre-PR behavior. #### 🔬 How I checked the tests (because green CI ≠ correct, fufu~) Both new tests are **genuine behavioral pins, NOT tautologies** — I sabotaged the production code two ways and watched each test die precisely: 1. **`A_failed_delete_surfaces_its_error_and_keeps_the_list`** — I replaced the error-preserving branch with a silent chained-reload-always. The test **failed** at the `WaitForAssertion` for "no longer exists", because the chained `LoadProjects` wiped the error before it could render. Restored, passed. Genuine pin on the asymmetry. 2. **`Reentering_the_list_clears_a_previous_visits_error`** — I made `OnLoadRequested` a no-op (stopped clearing `Error`). The test **failed** at `Assert.DoesNotContain("no longer exists", second.Markup)` — the stale error bled through to the second visit exactly as the comment warns. Restored, passed. Genuine pin on the clear-on-entry invariant. The failure path is real: `Projects.Items.Clear()` before "Delete forever" makes `DeleteProject.ExecuteAsync` return `Err<Unit>("The project no longer exists.")` (verified in `DeleteProject.cs:17` + `FakeProjectStore.DeleteAsync` → `RemoveAll > 0 = false`), so the assertion string matches the real error message. #### 📐 Sibling consistency verified - `ProjectListState` ↔ `BibleState`/`PageWorkspaceState`/`ProjectWizardState`: same `[FeatureState] sealed record` shape, `Loaded` flag, `Error` field, explicit per-action reducers. ✓ - `ProjectListEffects` ↔ `PageWorkspaceEffects`: `is not Ok<T>` + `(Err<T>)result` cast pattern identical. `Result<T>` is a closed two-variant type (abstract `Result<T>` with only `Ok<T>` + `Err<T>`), so the cast is sound. ✓ - `OnInitialized` dispatches `LoadProjects` — matches how siblings dispatch `LoadBible`/`LoadPageWorkspace`/`LoadWizard` on entry. ✓ #### 💡 Little ideas (non-blocking)~ 1. **`ProjectListEffects.cs:13` — the `Match` discards the error on a failed load.** `_ => []` silently swallows a `ListProjects` failure into an empty list, which the page then renders as the "No projects yet" empty state. This is *pre-existing behavior* (the old `ReloadAsync` did the same `result.Match(list => list, _ => [])`), so it's not a regression — but the store now has an `Error` field that didn't exist before. If you ever want a failed load to show an error instead of masquerading as an empty world, the plumbing is right there. Not blocking; the current behavior is defensible (don't block the user's front door on a transient backend hiccup). 2. **`ProjectListEffects.cs:32` — `(Err<Unit>)result` cast.** The `is not Ok<Unit>` + explicit cast pattern is identical to siblings and correct for the closed `Result<T>` type — but the `Report<T>` helper in `BibleEffects`/`PageWorkspaceEffects` (which does `result.Match<object>(ok, err)`) avoids the cast entirely. The list has only one fallible call so a helper would be over-engineering; just flagging the style difference for awareness. ♡ --- *Automated review by Jibril · 2026-07-25* *CI/CD: absent for head SHA 2f120e0 (PR just opened, 0 comments at review) · Local checks: build 0 warnings/0 errors (submodules 86d8b22/9544ff2), full suite 395/395 pass (119 BlazorAdapter + 81 Integration + 75 Domain + 120 UseCases — matches PR body exactly), 10/10 ProjectListPageTests, 2 sabotage reproductions run + reverted clean (git status empty post-restore)*
bjoern merged commit 54ec685944 into main 2026-07-25 20:33:39 +02:00
bjoern deleted branch refactor/fluxor-project-list 2026-07-25 20:33:39 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Orihon!35
No description provided.