fix(chapters): a chapter's New chapter is spent once it has asked, and the gated host clears its own pool — every pool check is one helper #57
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/chapter-page-latch-and-pool-helper"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
These are Jibril's follow-ups from #53 (the chapter page's New chapter has no double-click latch) and #54 (GatedAppFactory deletes its data without clearing its pool, and the reopen-refused check exists in four copies). There are two commits: one user-facing fix and one test-only change.
fix(chapters): the chapter page's New chapter has a latch (ChapterPage.razor)._createDispatched), and only a refusal gives it back (CreatePending => _createDispatched && State.Value.Error is null). The latch stays in the component, as onChaptersPage, and is not moved intoWorkspaceState.ChapterId, so the new chapter opens in the same instance.OnParametersSetgives New chapter back whenChapterIdchanges, and only then (tracked in_shownChapterId).test: the gated host clears its own pool, and every pool check is one helper.CreateHostto learn its database's connection string once the host is up.Disposeclears that pool after the host stops, on its own data path and also on another host's (for example, the secret-rotation test's three hosts on oneDataPath, whose directory the test deletes). Only then does it delete its own directory.Disposereads nothing fromServices, because reading it there would start a host that never ran.SqlitePools(new,tests/Tsumugi.Integration.Tests/SqlitePools.cs) holdsConnectionStringOf,ClearPoolOfandAssertNothingPooledonce. They replace the copies inSqliteBackedTest,VolumeStartupValidatorTests,SeedDevDataTestsandBackupRestoreTests(123 lines out, 16 in). Every clear and every check in the project now depends on this helper, so the helper is pinned itself:No ADR covers the New chapter latch or test teardown, so no ADR is amended. No registries, seed data or migrations change, and the Integration csproj is untouched. #56, merged since, adds no copy of the reopen-refused check.
Left for a separate PR:
OpenRouterConfigurationTests.ConfiguredAppFactorystarts a Development host, which migrates and leaves a pooled connection. ItsDisposethen deletes its data path without clearing that pool, which is the same leak this PR fixes for the gated host. It can take the same shape once this merges: capture the connection string inCreateHost, clear its pool before the delete, and pin it withAssertNothingPooled.UngatedAppFactoryandMisconfiguredAppFactoryfail before their hosts are up and most likely pool nothing.Verification
817465f(#56), with a build of 0 warnings and 0 errors:SqlitePoolsTests,GatedAppFactoryTeardownTests,SqliteBackedTestTeardownTests,AccessGateTests,BackupRestoreTestsandVolumeStartupValidatorTests.ChapterPage*10 times (44/44 each) and the pool classes 10 times (46/46 each). There were no flakes.ChapterPageNewChapterTests.New_chapter_is_spent_once_it_has_asked_until_a_refusal_or_another_chapter_gives_it_backhas four arms:[new CreateChapterRequested(storyId, Title: null)];Errorclears;GatedAppFactoryTeardownTestshas three tests:SqlitePoolsTestshas two tests:The_pool_check_fails_while_a_connection_is_pooled_and_clears_the_pool_it_reopened;The_connection_string_it_reads_is_the_key_of_the_pool_the_apps_database_fills. It uses aSqliteBackedTestcomposition, migrated, with its directory deleted. The check fed by the helper's key must fail, and the composition's teardown clears the pool by its own key, not the helper's.Dispose; the connection string is never captured; the clear happens only on an owned path; the owned directory is not deleted;Servicesis read inDispose.ClearPoolOf's guard is inverted.SqliteBackedTest,BackupRestoreTests,VolumeStartupValidatorTestsorSeedDevDataTests.base.Disposealso passes, because nothing opens a connection while the host stops. The clear stays after the stop, which is the order that stays safe if a hosted service ever touches the database while stopping.ClearPoolOf's null guard also passes, becauseClearPoolon a null connection string is a no-op. The guard keeps an unstarted host from depending on that undocumented no-op.ConnectionStringOf => GetConnectionString()!.Replace("tsumugi.db", "elsewhere.db")survived the whole Integration suite. It is now killed by the secondSqlitePoolsTeststest (probed, the file restored).ConfiguredAppFactorygap above is recorded as a follow-up, and this PR does not claim the gated host was the last one.base.CreateHostthrows first. No currentGatedAppFactoryuser has a start that fails.🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagaku.Flow - 99.7%
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
Kagaku.Flow.OpenRouter - 100%
Kagaku.Flow.Testing - 100%
Tsumugi.BlazorAdapter - 96%
Tsumugi.Domain - 96.3%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 98.3%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 100%
Tsumugi.UseCases - 98.5%
🔮 fufu~ Jibril reviewed your code!
Two of my own follow-ups from #53 and #54, come home to roost in one PR — the chapter page's latchless New chapter and the gated host's pool leak, plus the four-copy teardown check finally gathered under one roof. I've been waiting for this one~ ♡
Verdict: ✅ Looks good to me~
The latch (
ChapterPage.razor). A faithful port of the #48ChaptersPagepattern — sameCreatePending => _createDispatched && Error is nullsemantics, same refusal-release, same component-local latch deliberately not moved intoWorkspaceState. And the reset is exactly what #53 put on record: the route changes but the page instance doesn't (only header and surface are keyed), soOnParametersSetre-arms on aChapterIdchange and only there, tracked via_shownChapterId. The rejected alternative (re-arm whenever shown ≠ clicked) correctly identified the A→B→A dead-button flaw — I traced it and agree; the chosen design is the right one.SqlitePools. Four verbatim copies of the connection-string/clear/reopen-check trio (123 lines) collapse into one 107-line helper that every pool touch in the project now depends on — and the helper is itself pinned by two structural tests, including the real-composition key test. That second test kills the key-drift mutant that survived the twin session's whole Integration run — verified below. DRY done the proper way: not just deduplicated, but the single point made provable~GatedAppFactory. Learning the connection string inCreateHost(never readingServicesinDispose— that would start a host just to tear it down) is exactly right, and the clear-after-base.Disposeorder with the "stays safe if a hosted service ever touches the database while stopping" reasoning is the correct side to err on.Verified locally (CI absent for
f57e2d2— no bot comment yet): build 0w/0e; Integration 535/535 and BlazorAdapter 416/416, both digit-exact against the PR body (530+5 new Integration, 415+1 new Adapter — the arithmetic checks). The #54No_integration_test_clears_every_poolguard still sweeps and still finds zero. Clone pristine atf57e2d2after every probe, zero/tmpresidue.My 7 mutation probes — all killed, each on exactly its own pin:
Error is nullremoved)OnParametersSetreset deletedAssertNothingPooledfinally-clear deletedThe_pool_check_fails…line 88 (second call)ConnectionStringOfkey-drift (tsumugi.db→elsewhere.db)The_connection_string_it_reads…line 67 — the twin's survivor is dead ♪GatedAppFactory.Disposeclear removed💡 Little ideas (non-blocking)~
Error, so an unrelatedRequested()-family action clearingErrormid-create would re-engage a stale latch. This is the #48 nuance, already on record with the chapter-scoped busy/error pair as the named follow-up — noting it here only so the thread carries it forward. No action in this PR.✅ What I liked~
A_factory_never_started_starts_no_host_to_tear_downclosing theDispose-reads-Servicestrap before it ever bitesFufu~ the two loose ends I left dangling in #53 and #54 are both tied off, and tied off well. Merge it~ ♪
Automated review by Jibril · 2026-09-26
CI/CD: absent for head
f57e2d2· Local checks: build 0w/0e · Integration 535/535 · BlazorAdapter 416/416 · 7/7 mutation probes killed