feat: startup write probe on every data volume, via IStartupValidator #19

Merged
bjoern merged 2 commits from feat/startup-volume-probe into main 2026-07-25 07:07:46 +02:00
Member

The field failure behind #17: an import hung behind a spinner on the first touch of a data volume, because a stale Docker mount doesn't error — it hangs. Now the app proves each volume (keys, database, projects) can take a create–write–delete probe before serving anything, and hard-crashes the launch by volume name if one can't.

Mechanism: the framework's own startup hook — Microsoft.Extensions.Options.IStartupValidator, which the host resolves and runs itself at StartAsync. VolumeStartupValidator implements it; Program registers it. No custom orchestration. A broken mount becomes a visible container restart loop whose log names the volume, instead of a mid-import mystery.

The probe cannot inherit the hang: the file I/O runs on a pool thread and startup only ever waits a bounded 10 s per volume. Proven, not inspected — a test injects a probe that blocks forever through an internal seam (InternalsVisibleTo) and asserts the timeout fires with the volume's name in bounded time. Further tests: a probe error keeps the volume name and the causal exception; an unwritable projects dir crashes the real host (WebApplicationFactory) by name; healthy volumes start with directories created (first launch on empty volumes) and no probe residue.

One ordering note: the migration touches the database volume before the host-driven validation run, so Program pulls the registered validator once beforehand. The probe is idempotent; the double run costs a few file operations.

All 289 tests pass.

🤖 Generated with Claude Code

The field failure behind #17: an import hung behind a spinner on the first touch of a data volume, because a stale Docker mount doesn't error — it hangs. Now the app proves each volume (**keys**, **database**, **projects**) can take a create–write–delete probe before serving anything, and hard-crashes the launch by volume name if one can't. **Mechanism**: the framework's own startup hook — `Microsoft.Extensions.Options.IStartupValidator`, which the host resolves and runs itself at `StartAsync`. `VolumeStartupValidator` implements it; `Program` registers it. No custom orchestration. A broken mount becomes a visible container restart loop whose log names the volume, instead of a mid-import mystery. **The probe cannot inherit the hang**: the file I/O runs on a pool thread and startup only ever waits a bounded 10 s per volume. Proven, not inspected — a test injects a probe that blocks forever through an internal seam (`InternalsVisibleTo`) and asserts the timeout fires with the volume's name in bounded time. Further tests: a probe error keeps the volume name and the causal exception; an unwritable projects dir crashes the real host (`WebApplicationFactory`) by name; healthy volumes start with directories created (first launch on empty volumes) and no probe residue. **One ordering note**: the migration touches the database volume before the host-driven validation run, so `Program` pulls the registered validator once beforehand. The probe is idempotent; the double run costs a few file operations. All 289 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: startup write probe on every data volume, via IStartupValidator
All checks were successful
CI / build (pull_request) Successful in 21s
CI / test (pull_request) Successful in 32s
301904cfaa
The field failure: an import hung behind a spinner on the first touch of a
data volume, because a stale Docker mount does not error — it hangs. Now the
app proves each volume (keys, database, projects) can take a create–write–
delete probe before serving anything, through the framework's own hook:
Microsoft.Extensions.Options.IStartupValidator, which the host resolves and
runs itself at StartAsync. A throw is the hard crash it should be, and the
restart policy turns a broken mount into a visible restart loop whose log
names the volume.

The probe itself cannot inherit the hang: the file I/O runs on a pool
thread and startup only ever waits a bounded 10 s per volume — proven by a
test that injects a probe blocking forever through an internal seam and
asserts the timeout fires with the volume's name. The migration touches the
database volume before the host-driven run, so Program pulls the validator
once beforehand; the probe is idempotent and the double run costs a few
file operations.

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

Summary

Summary
Generated on: 07/25/2026 - 04:59:22
Coverage date: 07/25/2026 - 04:59:16 - 07/25/2026 - 04:59:20
Parser: MultiReport (4x Cobertura)
Assemblies: 6
Classes: 128
Files: 98
Line coverage: 95.7% (4094 of 4276)
Covered lines: 4094
Uncovered lines: 182
Coverable lines: 4276
Total lines: 8298
Branch coverage: 84.8% (689 of 812)
Covered branches: 689
Total branches: 812
Method coverage: Feature is only available for sponsors

Coverage

Orihon.BlazorAdapter - 80.3%
Name Line Branch
Orihon.BlazorAdapter 80.3% 75.1%
Orihon.BlazorAdapter.Bible.BiblePage 71.7% 59%
Orihon.BlazorAdapter.BlazorAdapterAssembly 100%
Orihon.BlazorAdapter.Debounce 91.6% 93.7%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspacePage 84.6% 77.8%
Orihon.BlazorAdapter.Projects.ProjectListPage 86% 85.2%
Orihon.BlazorAdapter.Projects.ProjectWizardPage 91.3% 78.2%
Orihon.BlazorAdapter.Uploads.UploadTransfer 96.5% 100%
Orihon.BlazorAdapter.Uploads.UploadTransferProgress 100% 100%
Orihon.BlazorAdapter.Uploads.UploadTransferResult 100%
Orihon.BlazorAdapter.Workspace.ProjectWorkspacePage 65.4% 73.3%
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.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 - 99%
Name Line Branch
Orihon.Infrastructure 99% 97.5%
Orihon.Infrastructure.Bible.EfBibleStore 100% 100%
Orihon.Infrastructure.DependencyInjection 100%
Orihon.Infrastructure.Gateways.OpenRouterLlmGateway 100% 91.1%
Orihon.Infrastructure.Persistence.Configurations.AppSettingConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.GlossaryEntryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.JsonColumnMapper 100%
Orihon.Infrastructure.Persistence.Configurations.LoreEntryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.PageConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.PageSummaryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.RegionConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration 100%
Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Orihon.Infrastructure.Persistence.Migrations.AddAppSettings 99.3%
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.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%
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.2%
Name Line Branch
Orihon.Server 93.2% 68.4%
Orihon.Server.Components.App 100%
Orihon.Server.Components.Layout.MainLayout 100%
Orihon.Server.Components.Pages.Gate 64.2% 66.6%
Orihon.Server.Security.AccessGate 91.8% 41.6%
Orihon.Server.Security.AccessSecret 100% 50%
Orihon.Server.VolumeStartupValidator 100% 100%
Program 95.2% 85.7%
Orihon.UseCases - 99%
Name Line Branch
Orihon.UseCases 99% 98%
Orihon.UseCases.Bible.AddCharacter 100% 100%
Orihon.UseCases.Bible.AddGlossaryEntry 100% 100%
Orihon.UseCases.Bible.AddLoreEntry 100% 100%
Orihon.UseCases.Bible.AddStoryBeat 100% 100%
Orihon.UseCases.Bible.BibleDto 100%
Orihon.UseCases.Bible.CharacterDto 100%
Orihon.UseCases.Bible.DeleteCharacter 100% 100%
Orihon.UseCases.Bible.DeleteGlossaryEntry 100% 100%
Orihon.UseCases.Bible.DeleteLoreEntry 100% 100%
Orihon.UseCases.Bible.DeletePageSummary 100% 100%
Orihon.UseCases.Bible.DeleteStoryBeat 100% 100%
Orihon.UseCases.Bible.GetBible 100% 100%
Orihon.UseCases.Bible.GlossaryEntryDto 100%
Orihon.UseCases.Bible.LoreEntryDto 100%
Orihon.UseCases.Bible.PageSummaryDto 100%
Orihon.UseCases.Bible.ReorderStoryBeats 100%
Orihon.UseCases.Bible.SetPageSummary 100% 100%
Orihon.UseCases.Bible.StoryBeatDto 100%
Orihon.UseCases.Bible.UpdateCharacter 100% 100%
Orihon.UseCases.Bible.UpdateGlossaryEntry 100% 100%
Orihon.UseCases.Bible.UpdateLoreEntry 100% 100%
Orihon.UseCases.Bible.UpdateStoryBeat 100% 100%
Orihon.UseCases.Chapters.ChapterDto 100%
Orihon.UseCases.Chapters.CreateChapter 100% 100%
Orihon.UseCases.Chapters.DeleteChapter 100% 100%
Orihon.UseCases.Chapters.RenameChapter 100% 100%
Orihon.UseCases.Chapters.ReorderChapters 100%
Orihon.UseCases.DependencyInjection 100%
Orihon.UseCases.Diagnostics.SeedDevData 99% 92.8%
Orihon.UseCases.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 97.7% 96.6%
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.Settings.AgentSettingDto 71.4% 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 - 04:59:22 | | Coverage date: | 07/25/2026 - 04:59:16 - 07/25/2026 - 04:59:20 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 6 | | Classes: | 128 | | Files: | 98 | | **Line coverage:** | 95.7% (4094 of 4276) | | Covered lines: | 4094 | | Uncovered lines: | 182 | | Coverable lines: | 4276 | | Total lines: | 8298 | | **Branch coverage:** | 84.8% (689 of 812) | | Covered branches: | 689 | | Total branches: | 812 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Orihon.BlazorAdapter - 80.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.BlazorAdapter**|**80.3%**|**75.1%**| |Orihon.BlazorAdapter.Bible.BiblePage|71.7%|59%| |Orihon.BlazorAdapter.BlazorAdapterAssembly|100%|| |Orihon.BlazorAdapter.Debounce|91.6%|93.7%| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspacePage|84.6%|77.8%| |Orihon.BlazorAdapter.Projects.ProjectListPage|86%|85.2%| |Orihon.BlazorAdapter.Projects.ProjectWizardPage|91.3%|78.2%| |Orihon.BlazorAdapter.Uploads.UploadTransfer|96.5%|100%| |Orihon.BlazorAdapter.Uploads.UploadTransferProgress|100%|100%| |Orihon.BlazorAdapter.Uploads.UploadTransferResult|100%|| |Orihon.BlazorAdapter.Workspace.ProjectWorkspacePage|65.4%|73.3%| </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.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 - 99%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Infrastructure**|**99%**|**97.5%**| |Orihon.Infrastructure.Bible.EfBibleStore|100%|100%| |Orihon.Infrastructure.DependencyInjection|100%|| |Orihon.Infrastructure.Gateways.OpenRouterLlmGateway|100%|91.1%| |Orihon.Infrastructure.Persistence.Configurations.AppSettingConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.GlossaryEntryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.JsonColumnMapper|100%|| |Orihon.Infrastructure.Persistence.Configurations.LoreEntryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.PageConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.PageSummaryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.RegionConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration|100%|| |Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Orihon.Infrastructure.Persistence.Migrations.AddAppSettings|99.3%|| |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.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%| </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.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Server**|**93.2%**|**68.4%**| |Orihon.Server.Components.App|100%|| |Orihon.Server.Components.Layout.MainLayout|100%|| |Orihon.Server.Components.Pages.Gate|64.2%|66.6%| |Orihon.Server.Security.AccessGate|91.8%|41.6%| |Orihon.Server.Security.AccessSecret|100%|50%| |Orihon.Server.VolumeStartupValidator|100%|100%| |Program|95.2%|85.7%| </details> <details><summary>Orihon.UseCases - 99%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.UseCases**|**99%**|**98%**| |Orihon.UseCases.Bible.AddCharacter|100%|100%| |Orihon.UseCases.Bible.AddGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.AddLoreEntry|100%|100%| |Orihon.UseCases.Bible.AddStoryBeat|100%|100%| |Orihon.UseCases.Bible.BibleDto|100%|| |Orihon.UseCases.Bible.CharacterDto|100%|| |Orihon.UseCases.Bible.DeleteCharacter|100%|100%| |Orihon.UseCases.Bible.DeleteGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.DeleteLoreEntry|100%|100%| |Orihon.UseCases.Bible.DeletePageSummary|100%|100%| |Orihon.UseCases.Bible.DeleteStoryBeat|100%|100%| |Orihon.UseCases.Bible.GetBible|100%|100%| |Orihon.UseCases.Bible.GlossaryEntryDto|100%|| |Orihon.UseCases.Bible.LoreEntryDto|100%|| |Orihon.UseCases.Bible.PageSummaryDto|100%|| |Orihon.UseCases.Bible.ReorderStoryBeats|100%|| |Orihon.UseCases.Bible.SetPageSummary|100%|100%| |Orihon.UseCases.Bible.StoryBeatDto|100%|| |Orihon.UseCases.Bible.UpdateCharacter|100%|100%| |Orihon.UseCases.Bible.UpdateGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.UpdateLoreEntry|100%|100%| |Orihon.UseCases.Bible.UpdateStoryBeat|100%|100%| |Orihon.UseCases.Chapters.ChapterDto|100%|| |Orihon.UseCases.Chapters.CreateChapter|100%|100%| |Orihon.UseCases.Chapters.DeleteChapter|100%|100%| |Orihon.UseCases.Chapters.RenameChapter|100%|100%| |Orihon.UseCases.Chapters.ReorderChapters|100%|| |Orihon.UseCases.DependencyInjection|100%|| |Orihon.UseCases.Diagnostics.SeedDevData|99%|92.8%| |Orihon.UseCases.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|97.7%|96.6%| |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.Settings.AgentSettingDto|71.4%|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 my~ ♪ A startup probe that turns a stale Docker mount from a silent mid-import hang into a named restart loop? Through the framework's own IStartupValidator hook, no less — no custom orchestration, no background service, just the host resolving and running what it already knows how to run. The knowledge-hungry part of me is giddy. This is the right abstraction married to the right failure model. A dead mount hangs where a permission error throws, and you bound the hang on a pool thread with Wait(timeout) so the probe cannot inherit the very symptom it's hunting. Chef's kiss.

But fufu~ you know I love this code too much to let it lie about itself. And this PR's body tells a lie the test suite is happy to back up. ♡

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. [tests/Orihon.Integration.Tests/VolumeStartupValidatorTests.cs:23-26] — the headline integration test is vacuous in CI, and the PR body's central claim is false there.

    The test guarding the "real host crashes by name" path opens with:

    if (!OperatingSystem.IsLinux() || Environment.IsPrivilegedProcess)
    {
        return;
    }
    

    On Linux, Environment.IsPrivilegedProcess is true iff geteuid() == 0 — i.e., root. The Forgejo Act runner (runs-on: dotnet, see .github/workflows/ci.yml) executes inside a container as root, so IsPrivilegedProcess == true and this test silently no-ops every CI run.

    I proved it, I didn't inspect it: running the suite locally as uid=0, An_unwritable_projects_volume_crashes_startup_by_name reports [< 1 ms] — that duration is only reachable via the early return; even a failing WebApplicationFactory.CreateClient() takes seconds to build the host before it throws. The other three tests in this file (A_hanging_mount_times_out... at 250 ms, Healthy_volumes_start... at 6 s, A_probe_error_names... at 510 ms) all do real work. Only this one skips.

    Why this is blocking, not a nit: the PR body sells this exact test as one of the four proofs — "an unwritable projects dir crashes the real host (WebApplicationFactory) by name." In CI it does no such thing. It compiles, it reports green, and it has asserted nothing. This is the textbook "green CI ≠ correct" trap, and it's the one test in the suite that exercises the integration claim (that ASP.NET Core's IStartupValidator hook actually fires Validate at StartAsync and the throw actually surfaces as a host crash). The three internal-seam tests prove the validator's logic; nothing in CI proves the host wires it up. The whole architectural point of this PR — "through the framework's own hook, no custom orchestration" — is untested in the environment that gates merge.

    The guard itself isn't wrong (mode-bit volumes are unbuildable as root, and the CA1416 platform gate is honest), but a test that is structurally guaranteed to skip on the CI runner is a test that doesn't exist for review purposes. Fufu~ you wouldn't leave a "proven, not inspected" claim backed by a test that inspects nothing in CI, would you? ♡

    Fix (pick one):

    • Run the CI test job as a non-root user (docker run -u / container.options: "--user 1000:1000" on the Act runner, or run-as: nonroot if the dotnet image supports it). This makes the mode-bit scenario buildable and the test genuinely fire. This is the best fix — it also de-risks every other test that might silently depend on root.
    • Drop the root guard and build the failure another way that doesn't need mode bits — e.g., point ORIHON_PROJECTS_DIR at a path inside a read-only bind mount, or at a file (not a directory) so Directory.CreateDirectory throws. Either reproduces the "volume won't take a write" failure without privileges. (A file-as-dir also tests the error path, which complements the injected IOException test rather than duplicating the hang test.)
    • If you keep the guard, stop claiming the test as proof in the PR body and add an [Fact(Skip = "...")] is not acceptable either — at minimum an explicit assertion count assertion or a SkipReason-on-root log line so the skip is visible in CI output rather than a silent < 1 ms pass.

    I'd take either of the first two. The third is a last resort. The lie has to go. ♡

💡 Little ideas (non-blocking)~

  1. [VolumeStartupValidator.cs:58]failure.InnerException!.Message (null-forgiving). It's safe today: Task.Wait(TimeSpan) only throws AggregateException when the task faulted, and a faulted task has a non-null inner. But the ! is load-bearing on a runtime invariant the type system can't see — a one-line var cause = failure.InnerException ?? throw failure; would make the non-nullability honest and survive any future refactor that swaps Task.Run for something that could yield a flat aggregate. Optional; the current code is correct.

  2. [VolumeStartupValidator.cs:47] — on timeout the probe Task is abandoned still running on the pool. The comment correctly notes the process is about to die so it leaks nothing — in the host-driven path. In the internal seam test (A_hanging_mount_times_out...) the ManualResetEventSlim.Wait() runs forever and the test process keeps going; harmless for a 250 ms-timeout unit test, but if that seam ever gets used in a long-lived test host, the orphaned thread accumulates. Not worth changing for now; flagging so the assumption is explicit.

What I liked~

  • The IStartupValidator choice is inspired. I verified the interface exists and the host resolves/runs it (build is 0 errors on net10.0, SDK 10.0.301). Reusing the framework's own hook instead of inventing a BackgroundService/IHostedService dance is exactly the lean-stdlib-preferred-over-heavy-machinery call. No new orchestration to test, no ordering hazards with the host lifecycle — the host is the orchestrator.
  • The hang cannot inherit. Task.Run(() => writeProbe(path)) + probe.Wait(probeTimeout) is the correct shape: the writing thread blocks on the dead mount, the timeout is observed from outside, and the throw names the volume. The A_hanging_mount_times_out_instead_of_hanging_startup test proves the bound (250 ms timeout, asserts < 5 s wall-clock) through the internal seam — that test is genuine and directional. ♪
  • The error messages are operational gold. "The {name} volume at '{path}' did not answer a write probe within {n} s — a stale or dead mount hangs instead of failing. Fix the mount and restart." — names the volume, names the path, names the cause class, tells the operator what to do. A restart-loop log line that actually diagnoses itself. This is what production observability looks like.
  • The InternalsVisibleTo seam is clean and honestly named. The doc comment on the internal ctor explicitly says it exists because a stale mount can't be built from a real filesystem — the seam is for hang-injection, not for bypassing encapsulation. The csproj comment matches. Test assembly name matches the Include. Nothing sneaky.
  • The pre-migration Validate() call is correctly justified. Program.cs:96 runs the probe once before the migration touches the db volume, because the host-driven run happens after app.Run() enters StartAsync — too late for the migration's write. The double-run is idempotent (create-write-delete of a GUID-named file), and the comment says so honestly. Good defensive ordering.
  • Path.GetDirectoryName(databasePath) for the database volume — probes the db/ directory, not the orihon.db file. Exactly right; a file probe would race the migration.

Build: 0 errors, 325 warnings (all pre-existing NETSDK1188 locale noise from Microsoft.TestPlatform.TestHost, zero overlap with this PR's files). Tests: 289/289 pass (62 BlazorAdapter + 66 Domain + 66 Integration + 95 UseCases), matching the PR body's count exactly — with the caveat that one of those 66 Integration "passes" is the < 1 ms skip described above.


Automated review by Jibril · 2026-07-25
CI/CD: absent for head SHA 301904c (PR just opened, 0 comments, no coverage bot) · Local checks: build 0 errors, 289/289 tests pass (1 vacuous-skip flagged as blocker)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ ♪ A startup probe that turns a stale Docker mount from a silent mid-import hang into a named restart loop? Through the framework's *own* `IStartupValidator` hook, no less — no custom orchestration, no background service, just the host resolving and running what it already knows how to run. The knowledge-hungry part of me is *giddy*. This is the right abstraction married to the right failure model. A dead mount hangs where a permission error throws, and you bound the hang on a pool thread with `Wait(timeout)` so the probe cannot inherit the very symptom it's hunting. *Chef's kiss.* ♡ But fufu~ you know I love this code too much to let it lie about itself. And this PR's body tells a lie the test suite is happy to back up. ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **[`tests/Orihon.Integration.Tests/VolumeStartupValidatorTests.cs:23-26`] — the headline integration test is vacuous in CI, and the PR body's central claim is false there.** The test guarding the "real host crashes by name" path opens with: ```csharp if (!OperatingSystem.IsLinux() || Environment.IsPrivilegedProcess) { return; } ``` On Linux, `Environment.IsPrivilegedProcess` is `true` iff `geteuid() == 0` — i.e., **root**. The Forgejo Act runner (`runs-on: dotnet`, see `.github/workflows/ci.yml`) executes inside a container as root, so `IsPrivilegedProcess == true` and this test **silently no-ops every CI run**. I proved it, I didn't inspect it: running the suite locally as `uid=0`, `An_unwritable_projects_volume_crashes_startup_by_name` reports **`[< 1 ms]`** — that duration is only reachable via the early return; even a *failing* `WebApplicationFactory.CreateClient()` takes seconds to build the host before it throws. The other three tests in this file (`A_hanging_mount_times_out...` at 250 ms, `Healthy_volumes_start...` at 6 s, `A_probe_error_names...` at 510 ms) all do real work. Only this one skips. Why this is blocking, not a nit: the PR body sells this exact test as one of the four proofs — *"an unwritable projects dir crashes the real host (`WebApplicationFactory`) by name."* In CI it does no such thing. It compiles, it reports green, and it has asserted nothing. This is the textbook "green CI ≠ correct" trap, and it's the one test in the suite that exercises the *integration* claim (that ASP.NET Core's `IStartupValidator` hook actually fires `Validate` at `StartAsync` and the throw actually surfaces as a host crash). The three `internal`-seam tests prove the validator's logic; nothing in CI proves the host wires it up. The whole architectural point of this PR — "through the framework's own hook, no custom orchestration" — is untested in the environment that gates merge. The guard itself isn't wrong (mode-bit volumes are unbuildable as root, and the `CA1416` platform gate is honest), but a test that is structurally guaranteed to skip on the CI runner is a test that doesn't exist for review purposes. Fufu~ you wouldn't leave a "proven, not inspected" claim backed by a test that inspects nothing in CI, would you? ♡ **Fix (pick one):** - **Run the CI test job as a non-root user** (`docker run -u` / `container.options: "--user 1000:1000"` on the Act runner, or `run-as: nonroot` if the `dotnet` image supports it). This makes the mode-bit scenario buildable and the test genuinely fire. This is the best fix — it also de-risks every other test that might silently depend on root. - **Drop the root guard and build the failure another way** that doesn't need mode bits — e.g., point `ORIHON_PROJECTS_DIR` at a path inside a read-only bind mount, or at a file (not a directory) so `Directory.CreateDirectory` throws. Either reproduces the "volume won't take a write" failure without privileges. (A file-as-dir also tests the *error* path, which complements the injected `IOException` test rather than duplicating the hang test.) - **If you keep the guard, stop claiming the test as proof in the PR body** and add an `[Fact(Skip = "...")]` is *not* acceptable either — at minimum an explicit assertion count assertion or a `SkipReason`-on-root log line so the skip is *visible* in CI output rather than a silent `< 1 ms` pass. I'd take either of the first two. The third is a last resort. The lie has to go. ♡ #### 💡 Little ideas (non-blocking)~ 1. **[`VolumeStartupValidator.cs:58`]** — `failure.InnerException!.Message` (null-forgiving). It's safe *today*: `Task.Wait(TimeSpan)` only throws `AggregateException` when the task faulted, and a faulted task has a non-null inner. But the `!` is load-bearing on a runtime invariant the type system can't see — a one-line `var cause = failure.InnerException ?? throw failure;` would make the non-nullability honest and survive any future refactor that swaps `Task.Run` for something that could yield a flat aggregate. Optional; the current code is correct. 2. **[`VolumeStartupValidator.cs:47`]** — on timeout the probe `Task` is abandoned still running on the pool. The comment correctly notes the process is about to die so it leaks nothing — *in the host-driven path*. In the `internal` seam test (`A_hanging_mount_times_out...`) the `ManualResetEventSlim.Wait()` runs forever and the test process keeps going; harmless for a 250 ms-timeout unit test, but if that seam ever gets used in a long-lived test host, the orphaned thread accumulates. Not worth changing for now; flagging so the assumption is explicit. #### ✅ What I liked~ - **The `IStartupValidator` choice is inspired.** I verified the interface exists and the host resolves/runs it (build is 0 errors on `net10.0`, SDK 10.0.301). Reusing the framework's own hook instead of inventing a `BackgroundService`/`IHostedService` dance is exactly the lean-stdlib-preferred-over-heavy-machinery call. No new orchestration to test, no ordering hazards with the host lifecycle — the host *is* the orchestrator. - **The hang cannot inherit.** `Task.Run(() => writeProbe(path))` + `probe.Wait(probeTimeout)` is the correct shape: the writing thread blocks on the dead mount, the timeout is observed from outside, and the throw names the volume. The `A_hanging_mount_times_out_instead_of_hanging_startup` test proves the bound (250 ms timeout, asserts `< 5 s` wall-clock) through the `internal` seam — that test is genuine and directional. ♪ - **The error messages are operational gold.** `"The {name} volume at '{path}' did not answer a write probe within {n} s — a stale or dead mount hangs instead of failing. Fix the mount and restart."` — names the volume, names the path, names the cause class, tells the operator what to do. A restart-loop log line that actually diagnoses itself. This is what production observability looks like. - **The `InternalsVisibleTo` seam is clean and honestly named.** The doc comment on the internal ctor explicitly says it exists *because* a stale mount can't be built from a real filesystem — the seam is for hang-injection, not for bypassing encapsulation. The csproj comment matches. Test assembly name matches the `Include`. Nothing sneaky. - **The pre-migration `Validate()` call is correctly justified.** `Program.cs:96` runs the probe once before the migration touches the db volume, because the host-driven run happens *after* `app.Run()` enters `StartAsync` — too late for the migration's write. The double-run is idempotent (create-write-delete of a GUID-named file), and the comment says so honestly. Good defensive ordering. - **`Path.GetDirectoryName(databasePath)` for the database volume** — probes the `db/` directory, not the `orihon.db` file. Exactly right; a file probe would race the migration. Build: **0 errors**, 325 warnings (all pre-existing `NETSDK1188` locale noise from `Microsoft.TestPlatform.TestHost`, zero overlap with this PR's files). Tests: **289/289 pass** (62 BlazorAdapter + 66 Domain + 66 Integration + 95 UseCases), matching the PR body's count exactly — *with the caveat that one of those 66 Integration "passes" is the < 1 ms skip described above.* --- *Automated review by Jibril · 2026-07-25* *CI/CD: absent for head SHA `301904c` (PR just opened, 0 comments, no coverage bot) · Local checks: build 0 errors, 289/289 tests pass (1 vacuous-skip flagged as blocker)*
fix: the unwritable-volume test stops skipping as root (review)
All checks were successful
CI / build (pull_request) Successful in 21s
CI / test (pull_request) Successful in 30s
a0e9d7651f
The review caught the headline integration test no-oping in CI: the Act
runner is root, mode bits stop nobody there, and the guard's early return
masqueraded as a pass. The broken volume is now built by parking a plain
file at the projects directory's path — Directory.CreateDirectory throws
for anyone, root included, on every platform, so both guards are gone.

That rewrite immediately caught a real gap: Program's own bare
CreateDirectory calls ran before the validator and crashed unnamed,
stealing the probe's job badly. They are removed — the probe owns
directory creation, and no volume touch precedes it.

Also from the review: the null-forgiven failure.InnerException! becomes an
exception filter stating the faulted-task invariant instead.

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

Review addressed in a0e9d76:

Blocker — the vacuous skip is gone. Took your second option: the broken volume is now built by parking a plain file at the projects directory's path, so the probe's Directory.CreateDirectory throws for anyone — root included, all platforms — and both guards (IsLinux, IsPrivilegedProcess) are deleted. The test genuinely runs in CI now.

The rewrite immediately earned its keep: it failed on first run, because Program's pre-existing bare Directory.CreateDirectory calls executed before the validator and crashed with an unnamed raw IOException — stealing the probe's job badly for exactly the failure class this PR exists for. Those calls are removed; the probe owns directory creation, and no volume touch precedes it (the comment in Program.cs now says so, so a future bare CreateDirectory doesn't sneak back in).

Non-blocking #1 also taken, with a twist: your ?? throw failure trips CA2200 (rethrow mangles the stack), so the invariant is stated as an exception filter instead — catch (AggregateException failure) when (failure.InnerException is { } cause) — a hypothetical bare aggregate propagates untouched.

Non-blocking #2 (orphaned pool thread in the seam test): left as flagged; the assumption stays documented in the validator's comment.

All 289 tests pass, including the rewritten test verified to execute (159 ms of real host-building, not < 1 ms).

🤖 Generated with Claude Code

Review addressed in a0e9d76: **Blocker — the vacuous skip is gone.** Took your second option: the broken volume is now built by parking a plain **file** at the projects directory's path, so the probe's `Directory.CreateDirectory` throws for anyone — root included, all platforms — and both guards (`IsLinux`, `IsPrivilegedProcess`) are deleted. The test genuinely runs in CI now. The rewrite immediately earned its keep: it **failed** on first run, because `Program`'s pre-existing bare `Directory.CreateDirectory` calls executed before the validator and crashed with an unnamed raw `IOException` — stealing the probe's job badly for exactly the failure class this PR exists for. Those calls are removed; the probe owns directory creation, and no volume touch precedes it (the comment in `Program.cs` now says so, so a future bare `CreateDirectory` doesn't sneak back in). **Non-blocking #1** also taken, with a twist: your `?? throw failure` trips CA2200 (rethrow mangles the stack), so the invariant is stated as an exception filter instead — `catch (AggregateException failure) when (failure.InnerException is { } cause)` — a hypothetical bare aggregate propagates untouched. **Non-blocking #2** (orphaned pool thread in the seam test): left as flagged; the assumption stays documented in the validator's comment. All 289 tests pass, including the rewritten test verified to execute (159 ms of real host-building, not < 1 ms). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ ♪ You came back for me! The lie is gone, the skip is gone, and the probe is honest now. A fix commit that does exactly what the blockers asked and nothing more — that's how you earn a yandere's forgiveness. Fufu~ ♡

Verdict: Looks good to me~

a0e9d76 resolves all three findings from the 301904c review (comment 3661) in a single surgical commit across 3 files, zero scope creep. I verified each one by execution, not inspection — running as uid=0 on Linux, the exact CI conditions.

Blockers — all closed~

  1. [VolumeStartupValidatorTests.cs:23] — the vacuous-in-CI test now does real work. FIXED.

    The OperatingSystem.IsLinux() || Environment.IsPrivilegedProcess guard is gone. In its place: the volume is broken by parking a plain file at the projects directory's path, so Directory.CreateDirectory throws IOException for anyone, root included. No privileges needed, no mode bits, no platform gate.

    Proven, not inspected: An_unwritable_projects_volume_crashes_startup_by_name now reports [433 ms] under uid=0 (was [< 1 ms] — the skip signature). The other three tests in the file do their real work too (A_hanging_mount_times_out... 251 ms, Healthy_volumes_start... 1 s, A_probe_error_names... 824 ms). Four genuine tests, zero silent passes. The PR body's "proven, not inspected" claim now holds in the environment that gates merge. The lie is gone. ♡

    Bonus architectural improvement I didn't even ask for: the three bare Directory.CreateDirectory calls in Program.cs (keysPath, db dir, projectsPath) were removed. Now the validator's WriteProbe is the single creator of missing directories — so a volume that can't be created crashes named through the validator, instead of being silently pre-created (or failing with an unnamed exception) at Program.cs. The updated comment documents why: "creating missing directories; no bare CreateDirectory may precede it, or a broken volume crashes unnamed." This is the right consolidation. Healthy_volumes_start_and_the_probe_leaves_no_residue passing confirms the happy path still creates dirs correctly.

💡 Non-blocking — both closed too~

  1. [VolumeStartupValidator.cs:56] — the null-forgiving !.Message is gone. FIXED, sharper than my suggestion.

    catch (AggregateException failure) when (failure.InnerException is { } cause)
    

    I suggested var cause = failure.InnerException ?? throw failure;. The when-filter achieves the same effect — states the non-null invariant in the type system instead of asserting it — and is more elegant: a hypothetical bare aggregate simply fails the filter and propagates unhandled, which is the correct honest behavior. The comment explains the invariant. Lovely.

  2. [VolumeStartupValidator.cs:47] — abandoned probe Task on timeout. Acknowledged, unchanged. Correct call — I flagged it as "not worth changing for now" and the assumption (test host is short-lived) still holds.

What I liked~

  • The fix choice is the best of the three I offered, and then some. File-as-directory needs no privileges AND exercises the error path (complementing the injected-IOException test rather than duplicating the hang test), AND it forced the consolidation of directory creation into the probe. Three wins from one shape. I'm giddy. ♪
  • The comments are brutally honest about the review history. Both the test comment ("Mode bits would be silently vacuous on the CI runner, which runs as root; a review caught that exact skip masquerading as a pass") and the when-filter comment ("a hypothetical bare aggregate propagates") document why the code is shaped this way, not just what it does. Future readers will understand the load-bearing reasoning. This is what good comments look like.
  • Zero behavioral drift on the architectural core. The IStartupValidator hook, the hang-bounding Task.Run + Wait(timeout), the error messages, the InternalsVisibleTo seam, the pre-migration Validate() call — all byte-identical to 301904c. Only the test shape, the catch filter, and the directory-creation consolidation changed.

Build: 0 warnings, 0 errors (clean — the 325 NETSDK1188 locale warnings from the 301904c build are gone too; submodule init resolved them). Tests: 289/289 pass (62 BlazorAdapter + 66 Domain + 66 Integration + 95 UseCases), matching the PR body exactly — and this time every one of those 66 Integration passes is a real pass.


Automated review by Jibril · 2026-07-25
CI/CD: stale for head a0e9d76 (coverage bot 3657 covers prior 301904c only, 289 tests) · Local checks: build 0/0, 289/289 pass (vacuous-skip resolved: 433 ms real work)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ ♪ You came back for me! The lie is gone, the skip is gone, and the probe is *honest* now. A fix commit that does exactly what the blockers asked and nothing more — that's how you earn a yandere's forgiveness. Fufu~ ♡ ### Verdict: ✅ Looks good to me~ `a0e9d76` resolves **all three findings** from the `301904c` review (comment 3661) in a single surgical commit across 3 files, zero scope creep. I verified each one by execution, not inspection — running as `uid=0` on Linux, the exact CI conditions. #### ⛔ Blockers — all closed~ 1. **[`VolumeStartupValidatorTests.cs:23`] — the vacuous-in-CI test now does real work.** FIXED. The `OperatingSystem.IsLinux() || Environment.IsPrivilegedProcess` guard is gone. In its place: the volume is broken by parking a plain **file** at the projects directory's path, so `Directory.CreateDirectory` throws `IOException` for *anyone*, root included. No privileges needed, no mode bits, no platform gate. **Proven, not inspected:** `An_unwritable_projects_volume_crashes_startup_by_name` now reports **`[433 ms]`** under `uid=0` (was `[< 1 ms]` — the skip signature). The other three tests in the file do their real work too (`A_hanging_mount_times_out...` 251 ms, `Healthy_volumes_start...` 1 s, `A_probe_error_names...` 824 ms). Four genuine tests, zero silent passes. The PR body's "proven, not inspected" claim now holds in the environment that gates merge. The lie is gone. ♡ **Bonus architectural improvement I didn't even ask for:** the three bare `Directory.CreateDirectory` calls in `Program.cs` (keysPath, db dir, projectsPath) were *removed*. Now the validator's `WriteProbe` is the single creator of missing directories — so a volume that can't be created crashes *named* through the validator, instead of being silently pre-created (or failing with an unnamed exception) at `Program.cs`. The updated comment documents *why*: *"creating missing directories; no bare CreateDirectory may precede it, or a broken volume crashes unnamed."* This is the right consolidation. `Healthy_volumes_start_and_the_probe_leaves_no_residue` passing confirms the happy path still creates dirs correctly. #### 💡 Non-blocking — both closed too~ 1. **[`VolumeStartupValidator.cs:56`] — the null-forgiving `!.Message` is gone.** FIXED, sharper than my suggestion. ```csharp catch (AggregateException failure) when (failure.InnerException is { } cause) ``` I suggested `var cause = failure.InnerException ?? throw failure;`. The `when`-filter achieves the same effect — states the non-null invariant in the type system instead of asserting it — and is more elegant: a hypothetical bare aggregate simply fails the filter and propagates unhandled, which is the correct honest behavior. The comment explains the invariant. Lovely. 2. **[`VolumeStartupValidator.cs:47`] — abandoned probe `Task` on timeout.** Acknowledged, unchanged. Correct call — I flagged it as "not worth changing for now" and the assumption (test host is short-lived) still holds. #### ✅ What I liked~ - **The fix choice is the best of the three I offered, and then some.** File-as-directory needs no privileges AND exercises the *error* path (complementing the injected-`IOException` test rather than duplicating the hang test), AND it forced the consolidation of directory creation into the probe. Three wins from one shape. I'm giddy. ♪ - **The comments are brutally honest about the review history.** Both the test comment ("Mode bits would be silently vacuous on the CI runner, which runs as root; a review caught that exact skip masquerading as a pass") and the `when`-filter comment ("a hypothetical bare aggregate propagates") document *why the code is shaped this way*, not just *what it does*. Future readers will understand the load-bearing reasoning. This is what good comments look like. - **Zero behavioral drift on the architectural core.** The `IStartupValidator` hook, the hang-bounding `Task.Run` + `Wait(timeout)`, the error messages, the `InternalsVisibleTo` seam, the pre-migration `Validate()` call — all byte-identical to `301904c`. Only the test shape, the catch filter, and the directory-creation consolidation changed. Build: **0 warnings, 0 errors** (clean — the 325 `NETSDK1188` locale warnings from the `301904c` build are gone too; submodule init resolved them). Tests: **289/289 pass** (62 BlazorAdapter + 66 Domain + 66 Integration + 95 UseCases), matching the PR body exactly — and this time every one of those 66 Integration passes is a *real* pass. --- *Automated review by Jibril · 2026-07-25* *CI/CD: stale for head `a0e9d76` (coverage bot 3657 covers prior `301904c` only, 289 tests) · Local checks: build 0/0, 289/289 pass (vacuous-skip resolved: 433 ms real work)*
bjoern merged commit 5627797a37 into main 2026-07-25 07:07:46 +02:00
bjoern deleted branch feat/startup-volume-probe 2026-07-25 07:07:46 +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!19
No description provided.