Confirmed — the flush layer reports success for keys it cannot write
flushDirtyKeys resolves for several dirty-key kinds that write nothing, so AutosaveQueue clears the key, the unsaved-dot goes out, and projectService.ts:89 calls setModified(false). The user is told their work is saved when nothing was persisted.
Three paths resolve without writing:
1. Unknown kinds fall through to a bare return — src/core/project/persist.ts:60-62:
2. flushProject early-returns on an id mismatch — persist.ts:68:
if (project.projectId !== id) return;
3. flushSettings writes nothing unless the key is a Q-Link key — persist.ts:231-235:
async function flushSettings(repositories: Repositories, settingsKey: string): Promise<void> {
if (settingsKey.startsWith('qlink:')) {
await repositories.settings.set(settingsKey, JSON.stringify(useHardwareStore.getState().qLinkBindings));
}
}
Any settings: key that is not settings:qlink:* is silently a no-op that reports success.
Proven: flushDirtyKeys(repos, ['settings:theme']) resolved, wrote nothing to the repositories, and drove AutosaveQueue.onIdle once with hasPending === false.
Consequence. This is the write-behind autosave path §4.4 describes ("every committed mutation marks the owning entity dirty; a write-behind queue flushes dirty entities"). A dirty key that no flushX handles is indistinguishable, from the queue's point of view, from one that flushed cleanly — so the work is dropped and the UI actively confirms it was saved. §4.4's "unobtrusive unsaved-dot until flushed" becomes a false negative.
Distinct from existing issues. #41 is about the save button's messaging on failed/no-op saves; #19 is about project switching discarding unflushed autosave. This is the flush layer itself declaring success for work it never attempted — upstream of both.
Suggested fix: make the contract explicit. flushOne should either return a written/not-written signal that the queue can act on, or throw on an unhandled kind so the retry path engages and the dirty dot stays. Silently resolving is the one behaviour that cannot be right.
Coverage context
There is no test file for persist.ts at all. The only coverage is 5 incidental cases via hydrate.test.ts:166-210, exercising track, project, sequence and events. The program, automation, song and settings flush paths are entirely untested — and the latter three are actively used in production by useSequenceStore.ts:294, useSequenceStore.ts:309 and useHardwareStore.ts:79,91.
Confirmed — the flush layer reports success for keys it cannot write
flushDirtyKeysresolves for several dirty-key kinds that write nothing, soAutosaveQueueclears the key, the unsaved-dot goes out, andprojectService.ts:89callssetModified(false). The user is told their work is saved when nothing was persisted.Three paths resolve without writing:
1. Unknown kinds fall through to a bare
return—src/core/project/persist.ts:60-62:default: return; }2.
flushProjectearly-returns on an id mismatch —persist.ts:68:3.
flushSettingswrites nothing unless the key is a Q-Link key —persist.ts:231-235:Any
settings:key that is notsettings:qlink:*is silently a no-op that reports success.Proven:
flushDirtyKeys(repos, ['settings:theme'])resolved, wrote nothing to the repositories, and droveAutosaveQueue.onIdleonce withhasPending === false.Consequence. This is the write-behind autosave path §4.4 describes ("every committed mutation marks the owning entity dirty; a write-behind queue flushes dirty entities"). A dirty key that no
flushXhandles is indistinguishable, from the queue's point of view, from one that flushed cleanly — so the work is dropped and the UI actively confirms it was saved. §4.4's "unobtrusive unsaved-dot until flushed" becomes a false negative.Distinct from existing issues. #41 is about the save button's messaging on failed/no-op saves; #19 is about project switching discarding unflushed autosave. This is the flush layer itself declaring success for work it never attempted — upstream of both.
Suggested fix: make the contract explicit.
flushOneshould either return a written/not-written signal that the queue can act on, or throw on an unhandled kind so the retry path engages and the dirty dot stays. Silently resolving is the one behaviour that cannot be right.Coverage context
There is no test file for
persist.tsat all. The only coverage is 5 incidental cases viahydrate.test.ts:166-210, exercisingtrack,project,sequenceandevents. Theprogram,automation,songandsettingsflush paths are entirely untested — and the latter three are actively used in production byuseSequenceStore.ts:294,useSequenceStore.ts:309anduseHardwareStore.ts:79,91.