diff --git a/ui/src/hooks/useDurableAutosave.test.tsx b/ui/src/hooks/useDurableAutosave.test.tsx index af5860e0a85..be690a90301 100644 --- a/ui/src/hooks/useDurableAutosave.test.tsx +++ b/ui/src/hooks/useDurableAutosave.test.tsx @@ -120,6 +120,32 @@ describe("useDurableAutosave", () => { expect(store.getSnapshot().pendingWrites).toBe(0); }); + it.each(["same-target", "different-target"])("preserves newer completed saves across first identity resolution (%s)", async target => { + let server = "Original"; + const client = new QueryClient({ defaultOptions: { queries: { staleTime: Infinity, retry: false, enabled: false } } }); + const oldSave = vi.fn().mockRejectedValueOnce(outage()).mockImplementation(async value => { server = value; }); + const show = (sourceId: string, targetKey: string, save: (value: string) => Promise) => act(() => { + root.render( + + ); + }); + show("old-startup-editor", "task-a:title", oldSave); + await act(async () => { expect(await current.run("Older title")).toEqual({ kind: "waiting" }); }); + act(() => { root.unmount(); root = createRoot(container); }); + act(() => client.setQueryData(queryKeys.auth.session, { user: { id: "user-1" } })); + const newSave = vi.fn(async value => { if (target === "same-target") server = value; }); + show("new-known-editor", target === "same-target" ? "task-a:title" : "task-b:title", newSave); + await act(async () => { expect(await current.run("Newer title")).toEqual({ kind: "saved" }); }); + expect(newSave).toHaveBeenCalledExactlyOnceWith("Newer title"); + // The newer save has finished and released its registry entry before the + // detached startup job gets its first chance to adopt the account. + await advance(30_000); + expect(server).toBe(target === "same-target" ? "Newer title" : "Older title"); + expect(oldSave).toHaveBeenCalledTimes(target === "same-target" ? 1 : 2); + expect(store.getSnapshot().pendingWrites).toBe(0); + client.clear(); + }); + it("keeps a detached edit when a reopened editor resets only its local state and storage is unavailable", async () => { const storage = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { throw new Error("Storage unavailable"); }); try { diff --git a/ui/src/hooks/useDurableAutosave.ts b/ui/src/hooks/useDurableAutosave.ts index 0293975cc8b..98e062b77e3 100644 --- a/ui/src/hooks/useDurableAutosave.ts +++ b/ui/src/hooks/useDurableAutosave.ts @@ -273,6 +273,9 @@ export function useDurableAutosave(options: DurableAutosaveOptions): Durab const current = optionsRef.current; const target = current.targetKey ?? current.sourceId; const admittedOwner = ownerRef.current ?? readOwner(); + // A newer known-account edit supersedes an older startup job even if this + // save finishes before that job can adopt the account's registry key. + if (admittedOwner !== undefined) registry.delete(JSON.stringify([undefined, target])); const job: Job = { token: Symbol(), registryKey: JSON.stringify([admittedOwner, target]), resends: 0, id: ++sequence.current, target, owner: admittedOwner, account: client ? bindAccount(client) : null, draft, save: overrides.save ?? current.save, matchesServer: overrides.matchesServer ?? current.matchesServer, onRetryFailed: overrides.onRetryFailed, storageKey: current.storageKey ?? null, action: current.action ?? "save" };