fix: every Stories, Settings, manuscript and History request settles when its use case throws, then lets the bug through — no Create left spinning, no save left on Saving…, New chapter comes back or opens what it made, History's buttons come back #62
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/effects-settle-everywhere"
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
#55 fixed this for documents. The other effects had the same bug with no
tryorcatchanywhere: a use case that threw instead of refusing (a bug, or a database busy past its timeout) left the page where the request had put it. Kagaku's containment keeps the circuit alive, so after Continue Create kept spinning, the indicator stayed on Saving…, New chapter stayed spent, History's buttons stayed disabled, and Back up now and Remove key stayed busy. Four commits:EffectCalls.CallAsync. #55's helper moved out ofDocumentsEffectsunchanged into one static class at the adapter root. Every effect imports it withusing static. On a throw it dispatches the handler's own refusal action, thenthrow;, so the bug still reaches the containment's modal. The documents tests pass untouched.StoryCreateFailed("Something went wrong; the story may not have been created."), so the form comes back with the reason. The row actions (archive, trash, restore, purge) hold nothing, so they are not wrapped, and a test pins that they claim no refusal.BackupFailed.BackupCompleted/BackupFailedare now dispatched before the re-read, so a re-read that throws cannot leave the button spinning.LlmDefaultsSaveFailed.ModelCatalogLoadFailed, which offers Try again.Readid.OpenRouterKeySaveFailed.OpenRouterKeyRemoveFailed, which frees only Remove key.AutoSaveFailed(storyId, "Something went wrong; the change may not have been saved."), the same sentence the documents' indicator uses.ChapterSavedno longer carries the undo status. It is dispatched first, andUndoStatusChangedfollows from its own read, the wayDocumentSavedworks. Before, a throwing undo read after a save that landed also left Saving… stuck.WorkspaceActionFailed(…"the chapter may not have been created."), which gives New chapter back. A create that lands whose reload throws still opens the chapter it made, because the navigation sits in afinally.WorkspaceActionFailedand their own words.HistoryReadFailed(storyId). It clearsHistoryLoadingandHistoryBusy, keepsError, and claims no refusal. This frees the buttons when the reload after a version or revert that landed throws.Every settle copies its handler's own
Errarm, as in #55. Where it is not known whether a write that threw landed, the words hedge. No ADR covers what a request does when its use case throws (#55 noted the same), and none mentionsChapterSaved, so no ADR is amended. Everything touched is in the adapter's effects, their reducers, the records they need, and the adapter tests. Test seams follow #55'sFailingStoryReadsidiom:Failswitches onFakeChapterStore,FakeSnapshotStore,FakeStoryStore,FakeBackupStore,FakeAppSettingsStoreandFakeModelProvider, and new decoratorsFailingUndoStoreandFailingSnapshotReads.FakeUndoStore.csandFakeChangeJournal.csare untouched.Verification
e19247c(#61, #59):*StoreFailureTests,ManuscriptEffectsTests,HistoryEffectsTests,WorkspaceReducersTests,EffectCallsTests,BackupsSettingsTests; 111 tests): 7 more runs, no flakes.ManuscriptEffects, 9HistoryEffects, 6 reducers). Each was restored by a trap, and every one fails at least one test.ManuscriptEffectsTestsexpectedUndoStatus(false, false, null, null), which is also the page's default because the fakes do not journal. Dispatching that constant, orstate.Value.Undo, passed all 516 tests. Fixed: the new theoryA_saved_title_or_summary_is_followed_by_what_Ctrl_Z_would_do_nowjournals an edit first and asserts exactly[ChapterSaved, UndoStatusChanged(storyId, UndoStatus(true, false, "User — wrote the opening", null))]. Both mutations are now killed (rename and summary rows).OnChapterSavedleavingUndoalone was not pinned (WR2). A reducer that resetUndopassed. Fixed in two places. The reducer factA_saved_chapter_leaves_what_Ctrl_Z_would_do_to_the_status_that_follows_itassertsAssert.Same. The page testA_title_that_lands_whose_undo_read_throws_says_Savednow journals an edit and checks that Undo is still enabled after Continue. Both kill it.OpenRouterKeyRemoveFailedclaimed the removal "wrote nothing".EfAppSettingsStoredoes not guarantee that. ItsSaveChangesAsyncruns on the circuit's context without clearing the tracker, so a Deleted entity left by a failed save is committed by the circuit's next write. Fixed within this PR's paths: the record's doc, the effect's comment and the reducer's comment now hedge ("whether the key went is not known; the tab keeps the view it last read"). The store is a follow-up below.ManuscriptSurface.razorreads the undo status beforeChapterWritten. This is the same shape as theChapterSavedfix above. It belongs to the editor track and is a follow-up below.EfAppSettingsStore.RemoveAsync/UpsertAsyncshould clear the tracker when a write fails, either with atry/finallyaroundChangeTracker.Clear()or by running throughTsumugiDbContext.WriteAsync. Until then, a failed key removal or save can be committed by the circuit's next unrelated write, andProviderSection's "The key you entered was not saved." can be untrue.ManuscriptSurface.razor'sWriteAsyncshould dispatchChapterWrittenwithoutUndofirst andUndoStatusChangedafter it. Otherwise a throwing undo read costs a write that landed its ack and its patch.HistoryReadFailed, but the panel shows the line whileHistoryis null), and the Stories list, the trash, Backups, Defaults and Provider on first mount. Fixing these is a razor change.DocumentsEffects.SettleAsync: when its re-query throws,UndoStatusChangedis skipped, so Undo lags until the next reload. This was optional in the brief and was not taken.🤖 Generated with Claude Code
OnCreateAsync called CreateStory with nothing around it. CreateStoryRequested sets Creating, and only StoryCreated or StoryCreateFailed clears it; the form is spent while Creating is set or while it has asked with no reason to show. So a store that failed outright left Create spinning under the modal's Continue, and the form could not ask again until it was closed and opened afresh. - The create goes through EffectCalls.CallAsync: a throw lands what a refusal does, StoryCreateFailed("Something went wrong; the story may not have been created.") — Creating off, the reason under the title, the form the writer's again — and nothing navigates; then the exception goes on to the circuit's error containment. Whether the story was made is not known; one that was rings the doorbell, and the list shows it. - The row actions (archive, trash, restore, purge) need nothing: their requests only clear the last refusal, no button waits on them, and a write that landed rings the doorbell. Said in the effects' summary and pinned as it stands. The load needs no settle either — Loading is read by no page; its first load that throws is left for a follow-up (the page shows no read failure in place of the loading line). - FakeStoryStore.Fail: every write throws it before anything changes, the reads still answer — as FakeDocumentStore.Fail does. - Pinned in the effect (the same exception, exactly StoryCreateFailed with its words, no story, no navigation) and on the page through the real Fluxor pipeline: the containment's modal names the failure; after Continue the form is still open with the reason under the title, Creating off, Create neither busy nor disabled, nothing navigated; the next submit asks again — a spent form would not — and makes the story and opens it. A row action the store fails outright shows the modal, claims no refusal, leaves the list and the trash as the store holds them, and the next Archive from the row menu lands. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>None of the three Settings effects had anything around its use cases, and each request sets a flag only its outcome clears. Checked per handler whether the use case can throw past a refusal at all: - Back up now: the store turns I/O and SQLite failures into refusals but lets anything else through (FileSystemBackupStore.RunAsync rethrows), and the re-read after a run lists the backups directory unguarded. Either left Running set — the button spinning, and its click guard refusing every later click. The run now goes through EffectCalls.CallAsync with BackupFailed("Something went wrong; the backup may not have been made."); and refused or not, the button stops first and the view is read after, so a re-read that throws leaves nothing spinning and claims no refusal of a run that landed. BackupCompleted carries no view any more; the BackupSettingsLoaded that follows it does. - The Defaults save: every setting is an EF write. A throw left Save = Saving — the indicator on Saving… for good. It lands LlmDefaultsSaveFailed("Something went wrong; the change may not have been saved.") now. - The model catalog: the gateway throws once disposed. A throw left CatalogLoading set — the picker loading and the effort disabled, with no Try again. It lands ModelCatalogLoadFailed("The model catalog could not be loaded.") now, and Try again is offered. - A Provider load or Check again: the view reads the database and the check asks the gateway. A throw in either left Checking set, so Busy: Check again spinning, Remove key disabled. Both now end as a refused check of that same load does — ProviderKeyChecked(null, "The key in effect could not be checked.", its read id) — so an older load's failure is still dropped. - A key save: the check asks the gateway, the write protects and stores. A throw left Saving set. It lands OpenRouterKeySaveFailed("Something went wrong.") now; the tab adds that the key was not saved. - A removal: an EF write, and it has no refusal to mirror. A throw left Removing set. It lands OpenRouterKeyRemoveFailed, which frees only Removing: the store wrote nothing, so the view shown stands and nothing is read again. - The loads of the Backups view and of the Defaults record hold no flag: they need nothing. A first load of either — like the Provider's view and the story list — that throws leaves the tab's loading line standing until it mounts again; saying so in its place is a page change, left for a follow-up. The fakes can fail: FakeBackupStore.Fail (a run) and FailReads (a listing); FakeAppSettingsStore.Fail (every write) and FailReads (every read); FakeModelProvider.Fail (every question OpenRouter would answer — a gateway already disposed; which key is in effect still answers). Pinned in the effects — the same exception, exactly the settle with its words, nothing after it: no re-read, no "saved", no read of the view — for every case above, the key save for a throwing check and a throwing write, and the Back up now re-read for a run that landed and one refused; the removal's reducer frees Removing and nothing else. On each tab through the real Fluxor pipeline: the containment's modal names the failure; after Continue Back up now is neither busy nor refusing clicks (the next one runs, and the danger alert says why or — for a landed run whose re-read threw — nothing), the indicator shows the reason and the next pick saves the whole record, the catalog's warning and Try again appear and the effort is enabled, Check again after a throwing read or check ends with the warning, no figures and nothing busy, a throwing save says the key was not saved and the next save stores it, and a throwing removal gives Remove key back with the key still stored and the next removal lands. Every settle was probed with a compiling mutation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Neither ManuscriptEffects nor HistoryEffects had anything around its use cases. Checked per handler what a throw would leave held: - A chapter's title or summary, and the story's details: the request sets Save = Saving, and only a saved or refused answer clears it — the shell's indicator stayed on Saving… for good. Each goes through EffectCalls.CallAsync and lands AutoSaveFailed("Something went wrong; the change may not have been saved.") now. A title or summary that landed dispatched ChapterSaved only after reading the undo state, so a read that threw kept the save in flight too: ChapterSaved no longer carries the undo state, and UndoStatusChanged follows it, as DocumentSaved does. - New chapter: the Chapters page and a chapter's toolbar spend it once it has asked, until a refusal names why or the new chapter opens. A create that threw lands WorkspaceActionFailed("Something went wrong; the chapter may not have been created.") now — the shell says why, and the button is the writer's again. A create that landed opened its chapter only after the reload; a reload that threw left the button spent on a page that never moved. The chapter exists, so it opens now even then. - Save a version, Revert to here and History's Redo hold HistoryBusy until they answer. A throw lands WorkspaceActionFailed with words of its own ("…the version may not have been saved.", "…the story may not have been reverted.", "…the revert may not have been redone."). A landed one frees the buttons through History's reload; a read of History that threw left them disabled. It lands the new HistoryReadFailed now, which ends HistoryLoading and HistoryBusy and claims no refusal. A version asks for that reload before it reads the undo state, so that read throwing holds nothing. - Need nothing: a move, a merge, a delete, an undo and a redo only clear the previous refusal and hold nothing; the doorbell's re-reads of the undo state feed no flag; the manuscript's load sets Loading, which no page reads. A first load that throws — the story's or History's — leaves its loading line standing, which only the shell or the panel could replace: left for a follow-up, beside the other first loads this arc leaves. The fakes can fail: FakeChapterStore.Fail (every write) and FakeSnapshotStore.Fail (a new snapshot); FailingUndoStore and FailingSnapshotReads stand in front of the journal fakes, so an undo, a revert, the undo state's read or History's read can fail alone. Pinned in the effects — the same exception, exactly the settle with its words, nothing after it — for each save, the create, the create whose reload threw (it opens the chapter and claims nothing), the title and summary whose undo read threw (ChapterSaved alone), a version, a revert, a redo, History's read, the version and the doorbell whose undo read threw (LoadHistory alone), and every request that holds nothing (nothing dispatched, nothing moved); the reducer frees HistoryBusy and HistoryLoading and nothing else, and drops another story's read. On the pages through the real Fluxor pipeline: the containment's modal names the failure; after Continue the indicator shows the reason and the next save lands, a title typed is kept as typed and the next pause saves it, a title that landed says Saved, New chapter says why and makes the chapter on the next click or opens the one it made, a row action or an undo or redo claims nothing and the next one lands, and History's buttons are the writer's again with the reason in the shell — or with no word at all after a version whose reload threw — and the next request lands. Every settle was probed with a compiling mutation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>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 - 95.8%
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.8%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Four commits, +1792/−93, thirty-one files — and every single one of them an effect learning the same lesson #55 taught the documents: a page must never be left holding a request its use case abandoned. This is my favorite kind of PR, fufu~ — one idea, applied everywhere, with the symmetry actually thought through instead of assumed. I read every changed source file in full, traced every settle against its handler's own
Errarm, and then I got to prove things. ♪Verdict: ✅ Looks good to me~
The architecture, verified branch by branch:
EffectCalls.CallAsync— the lift fromDocumentsEffectsis byte-faithful to #55's helper:catch→ dispatch the request's own refusal →throw;so the original exception (and its stack) still reaches the circuit's containment. Barecatch, notcatch (Exception)— correct under the repo's conventions, and the class doc ties it to ADR 0011 properly. Both throwing arms are pinned, including the sneaky throws-before-returning-a-task one.Errarm. Manuscript saves →AutoSaveFailed; create →WorkspaceActionFailed; History's three requests → hedged words that honestly don't know whether the write landed; Settings per-tab refusals; the Provider load's refused-check carrying that load'sReadid. I checked each one against its sibling refusal — no orphan records, no wrong-tab words. TheOpenRouterKeyRemoveFaileddoc hedging (whether the key went is not known) is exactly the right honesty givenEfAppSettingsStore's tracker behavior — nicely caught by your reviewers and correctly scoped as a store-level follow-up.ChapterSavedno longer carries the undo status — dispatched first,UndoStatusChangedfrom its own read, exactly theDocumentSavedshape. And the reducer leavesUndoalone (Assert.Same pin). The save-that-lands-whose-undo-read-throws window is properly closed: indicator says Saved, Undo at worst a step behind. ♡finallyaround the navigate inOnCreateChapterAsync— a create that lands whose reload throws still opens the chapter it made. That's the correct reading of "the chapter exists," and it has its own dedicated pin.HistoryReadFailedhonors the staleness rule (StoryIdguard), keepsHistoryandError, claims no refusal — and the reducer fact pins both the flag-clearing and the stale-story drop.Mechanical verification (CI absent for
c7eb653, PR minutes old at fetch): clone pristine at head, submodules pinned. Release build 0 warnings / 0 errors; BlazorAdapter 528/528 (digit-exact vs your body, three-run claim honored by my re-runs); Integration 556/556; the StoreFailure/EffectCalls filter 82/82. Tree pristine at head after everything (git diff --quiet).My mutation probes — 8 compiled mutants, every one killed on exactly its own pins, each restore verified:
return default!)EffectCallsTestscatchnarrowed toIOExceptionfinally…whose_reload_throws_still_opens…HistoryReadFailedkeepsHistoryBusyOnChapterSavedresetsUndo(WR2)…re_read_throws…pinsTwo harness traps caught mid-run, both mine: a src-only rebuild +
--no-buildsilently tested the stale pristine DLL (false green), and one probe failed to compile and "passed" the same way. Every probe above was re-run with a test-project rebuild and an md5 check that the DLL the tests loaded actually carried the mutant. Never trust a green you didn't watch turn red, fufu~The MS15/WR2 fixes from your independent reviewers are real, not cosmetic: the journaled-edit theory (
A_saved_title_or_summary_is_followed_by_what_Ctrl_Z_would_do_now) kills the dispatch-the-constant mutant on both rows, and my reducer-level WR2 mutant died on exactly the two new pins. Your 80-of-82 probe record plus my 8-of-8 gives me everything I need.💡 Little ideas (non-blocking, both already on your follow-up list — agreed they're out of this arc's paths)~
ManuscriptSurface.razorreads the undo status beforeChapterWritten— same shape as theChapterSavedfix; it belongs to the editor track as you say. When it lands, it gets this same treatment, yes? ♡✅ What I liked~
HistoryReadFailedclaiming nothing rather than borrowing a refusal's words. That's careful.Failswitches on the fakes where the store would fail outright, decorators (FailingUndoStore,FailingSnapshotReads) only where a subset of operations must throw —FakeUndoStore.csandFakeChangeJournal.csuntouched, as promised.BackupCompletedlosing its payload so the re-read can't be smuggled through the completion action — smaller surface, honest sequencing, and the reducer comment says exactly why.No ADR amended, and I agree: none mentions what a request does when its use case throws, and #55 already noted the gap. The follow-ups are real, named, and correctly outside these paths.
Ready to merge. Come back with the ManuscriptSurface one soon — I'll be waiting~ fufu ♡
Automated review by Jibril · 2026-09-26
CI/CD: absent for head SHA
c7eb653· Local checks: Release 0w/0e · BlazorAdapter 528/528 · Integration 556/556 · 8/8 mutation probes killed