From 3e92dfa6f335f57071aeabee37397a78b6cc82ee Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:07:07 -0700 Subject: [PATCH] refactor(web): test environment-scoped draft promotion --- apps/web/src/composerDraftStore.test.ts | 54 +++++++++---------------- apps/web/src/composerDraftStore.ts | 35 ---------------- 2 files changed, 18 insertions(+), 71 deletions(-) diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index 8e7934a21211..53d07aab21d9 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -68,10 +68,7 @@ import { clearComposerDraftsEnvironment, composerDraftHasUserContent, finalizePromotedDraftThreadByRef, - markPromotedDraftThread, markPromotedDraftThreadByRef, - markPromotedDraftThreads, - markPromotedDraftThreadsByRef, type ComposerFileAttachment, type ComposerImageAttachment, composerFileNeedsReattach, @@ -1200,7 +1197,7 @@ describe("composerDraftStore project draft thread mapping", () => { interactionMode: "plan", }); store.setPrompt(draftId, "keep this prompt"); - markPromotedDraftThread(threadId); + markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId)); store.setLogicalProjectDraftThreadId(scopedProjectKey(projectRef), projectRef, draftId, { threadId: retryThreadId, @@ -1363,12 +1360,12 @@ describe("composerDraftStore project draft thread mapping", () => { expect(draftByKey(draftId)).toBeUndefined(); }); - it("marks a promoted draft by thread id without deleting composer state", () => { + it("marks a promoted draft by scoped ref without deleting composer state", () => { const store = useComposerDraftStore.getState(); store.setProjectDraftThreadId(projectRef, draftId, { threadId }); store.setPrompt(draftId, "promote me"); - markPromotedDraftThread(threadId); + markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId)); expect(useComposerDraftStore.getState().getDraftThreadByProjectRef(projectRef)).toBeNull(); expect(useComposerDraftStore.getState().getDraftThread(draftId)?.promotedTo).toEqual( @@ -1393,20 +1390,20 @@ describe("composerDraftStore project draft thread mapping", () => { const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId); store.setPrompt(threadRef, "keep me"); - markPromotedDraftThread(threadId); + markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId)); expect(useComposerDraftStore.getState().getDraftThread(threadRef)).toBeNull(); expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt).toBe("keep me"); }); - it("marks promoted drafts from an iterable of server thread ids", () => { + it("promotes a draft without changing another thread's draft", () => { const store = useComposerDraftStore.getState(); store.setProjectDraftThreadId(projectRef, draftId, { threadId }); store.setPrompt(draftId, "promote me"); store.setProjectDraftThreadId(otherProjectRef, otherDraftId, { threadId: otherThreadId }); store.setPrompt(otherDraftId, "keep me"); - markPromotedDraftThreads([threadId]); + markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId)); expect(useComposerDraftStore.getState().getDraftThread(draftId)?.promotedTo).toEqual( scopeThreadRef(TEST_ENVIRONMENT_ID, threadId), @@ -1418,7 +1415,7 @@ describe("composerDraftStore project draft thread mapping", () => { expect(draftByKey(otherDraftId)?.prompt).toBe("keep me"); }); - it("marks every matching scoped draft when multiple environments share a thread id", () => { + it("promotes matching thread ids separately for each environment", () => { const store = useComposerDraftStore.getState(); const localThreadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId); const remoteThreadRef = scopeThreadRef(OTHER_TEST_ENVIRONMENT_ID, threadId); @@ -1428,7 +1425,16 @@ describe("composerDraftStore project draft thread mapping", () => { store.setProjectDraftThreadId(remoteProjectRef, remoteDraftId, { threadId }); store.setPrompt(remoteDraftId, "remote draft"); - markPromotedDraftThread(threadId); + markPromotedDraftThreadByRef(localThreadRef); + + expect(store.getDraftThreadByProjectRef(projectRef)).toBeNull(); + expect(store.getDraftThreadByProjectRef(remoteProjectRef)?.threadId).toBe(threadId); + expect(store.getDraftThreadByRef(localThreadRef)?.promotedTo).toEqual(localThreadRef); + expect(store.getDraftThreadByRef(remoteThreadRef)?.promotedTo).toBeNull(); + expect(draftByKey(localDraftId)?.prompt).toBe("local draft"); + expect(draftByKey(remoteDraftId)?.prompt).toBe("remote draft"); + + markPromotedDraftThreadByRef(remoteThreadRef); expect(store.getDraftThreadByProjectRef(projectRef)).toBeNull(); expect(store.getDraftThreadByProjectRef(remoteProjectRef)).toBeNull(); @@ -1451,34 +1457,10 @@ describe("composerDraftStore project draft thread mapping", () => { expect(draftByKey(draftId)?.prompt).toBe("promote me"); }); - it("only marks iterable promotion cleanup entries for the matching environment refs", () => { - const store = useComposerDraftStore.getState(); - store.setProjectDraftThreadId(projectRef, draftId, { threadId }); - store.setPrompt(draftId, "promote me"); - - markPromotedDraftThreadsByRef([scopeThreadRef(OTHER_TEST_ENVIRONMENT_ID, threadId)]); - - expect(useComposerDraftStore.getState().getDraftThreadByProjectRef(projectRef)?.threadId).toBe( - threadId, - ); - expect(draftByKey(draftId)?.prompt).toBe("promote me"); - }); - - it("keeps existing server-thread composer drafts during iterable promotion cleanup", () => { - const store = useComposerDraftStore.getState(); - const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId); - store.setPrompt(threadRef, "keep me"); - - markPromotedDraftThreads([threadId]); - - expect(useComposerDraftStore.getState().getDraftThread(threadRef)).toBeNull(); - expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt).toBe("keep me"); - }); - it("moves composer edits made during promotion to the canonical thread", () => { const store = useComposerDraftStore.getState(); store.setProjectDraftThreadId(projectRef, draftId, { threadId }); - markPromotedDraftThread(threadId); + markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId)); store.setPrompt(draftId, "typed during setup"); finalizePromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId)); diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 189ccb4fe682..3adb6e45c2ae 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -4106,29 +4106,6 @@ export function useEffectiveComposerModelState(input: { ); } -/** - * Mark a draft thread as promoting once the server has materialized the same thread id. - * - * Use the single-thread helper for live `thread.created` events and the - * iterable helper for bootstrap/recovery paths that discover multiple server - * threads at once. - */ -export function markPromotedDraftThread(threadId: ThreadId): void { - const store = useComposerDraftStore.getState(); - const draftThreadTargets: ComposerThreadTarget[] = []; - for (const [draftId, draftThread] of Object.entries(store.draftThreadsByThreadKey)) { - if (draftThread.threadId === threadId) { - draftThreadTargets.push(DraftId.make(draftId)); - } - } - if (draftThreadTargets.length === 0) { - return; - } - for (const draftThreadTarget of draftThreadTargets) { - store.markDraftThreadPromoting(draftThreadTarget); - } -} - export function markPromotedDraftThreadByRef(threadRef: ScopedThreadRef): void { const draftStore = useComposerDraftStore.getState(); for (const [draftId, draftThread] of Object.entries(draftStore.draftThreadsByThreadKey)) { @@ -4141,18 +4118,6 @@ export function markPromotedDraftThreadByRef(threadRef: ScopedThreadRef): void { } } -export function markPromotedDraftThreads(serverThreadIds: Iterable): void { - for (const threadId of serverThreadIds) { - markPromotedDraftThread(threadId); - } -} - -export function markPromotedDraftThreadsByRef(serverThreadRefs: Iterable): void { - for (const threadRef of serverThreadRefs) { - markPromotedDraftThreadByRef(threadRef); - } -} - export function finalizePromotedDraftThreadByRef(threadRef: ScopedThreadRef): void { const draftStore = useComposerDraftStore.getState(); for (const [draftId, draftThread] of Object.entries(draftStore.draftThreadsByThreadKey)) {