authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-05 00:32:27-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 00:32:27-07:00
log82c2b7ffb4d450572f5baf9a70693f1e25cfdfed
treeb6f91610eaba7c73ac58bd37e543d7ec4885ccfe
parentb7fc81dea2b7c963c34a365b82644ce963df19a2
signature Signed by PGP key B5690EEEBB952194

refactor(web): test environment-scoped draft promotion (#10010)


2 files changed, 18 insertions(+), 71 deletions(-)

apps/web/src/composerDraftStore.test.ts+18-36
......@@ -68,10 +68,7 @@ import {
6868 clearComposerDraftsEnvironment,
6969 composerDraftHasUserContent,
7070 finalizePromotedDraftThreadByRef,
71 markPromotedDraftThread,
7271 markPromotedDraftThreadByRef,
73 markPromotedDraftThreads,
74 markPromotedDraftThreadsByRef,
7572 type ComposerFileAttachment,
7673 type ComposerImageAttachment,
7774 composerFileNeedsReattach,
......@@ -1200,7 +1197,7 @@ describe("composerDraftStore project draft thread mapping", () => {
12001197 interactionMode: "plan",
12011198 });
12021199 store.setPrompt(draftId, "keep this prompt");
1203 markPromotedDraftThread(threadId);
1200 markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));
12041201
12051202 store.setLogicalProjectDraftThreadId(scopedProjectKey(projectRef), projectRef, draftId, {
12061203 threadId: retryThreadId,
......@@ -1363,12 +1360,12 @@ describe("composerDraftStore project draft thread mapping", () => {
13631360 expect(draftByKey(draftId)).toBeUndefined();
13641361 });
13651362
1366 it("marks a promoted draft by thread id without deleting composer state", () => {
1363 it("marks a promoted draft by scoped ref without deleting composer state", () => {
13671364 const store = useComposerDraftStore.getState();
13681365 store.setProjectDraftThreadId(projectRef, draftId, { threadId });
13691366 store.setPrompt(draftId, "promote me");
13701367
1371 markPromotedDraftThread(threadId);
1368 markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));
13721369
13731370 expect(useComposerDraftStore.getState().getDraftThreadByProjectRef(projectRef)).toBeNull();
13741371 expect(useComposerDraftStore.getState().getDraftThread(draftId)?.promotedTo).toEqual(
......@@ -1393,20 +1390,20 @@ describe("composerDraftStore project draft thread mapping", () => {
13931390 const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);
13941391 store.setPrompt(threadRef, "keep me");
13951392
1396 markPromotedDraftThread(threadId);
1393 markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));
13971394
13981395 expect(useComposerDraftStore.getState().getDraftThread(threadRef)).toBeNull();
13991396 expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt).toBe("keep me");
14001397 });
14011398
1402 it("marks promoted drafts from an iterable of server thread ids", () => {
1399 it("promotes a draft without changing another thread's draft", () => {
14031400 const store = useComposerDraftStore.getState();
14041401 store.setProjectDraftThreadId(projectRef, draftId, { threadId });
14051402 store.setPrompt(draftId, "promote me");
14061403 store.setProjectDraftThreadId(otherProjectRef, otherDraftId, { threadId: otherThreadId });
14071404 store.setPrompt(otherDraftId, "keep me");
14081405
1409 markPromotedDraftThreads([threadId]);
1406 markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));
14101407
14111408 expect(useComposerDraftStore.getState().getDraftThread(draftId)?.promotedTo).toEqual(
14121409 scopeThreadRef(TEST_ENVIRONMENT_ID, threadId),
......@@ -1418,7 +1415,7 @@ describe("composerDraftStore project draft thread mapping", () => {
14181415 expect(draftByKey(otherDraftId)?.prompt).toBe("keep me");
14191416 });
14201417
1421 it("marks every matching scoped draft when multiple environments share a thread id", () => {
1418 it("promotes matching thread ids separately for each environment", () => {
14221419 const store = useComposerDraftStore.getState();
14231420 const localThreadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);
14241421 const remoteThreadRef = scopeThreadRef(OTHER_TEST_ENVIRONMENT_ID, threadId);
......@@ -1428,7 +1425,16 @@ describe("composerDraftStore project draft thread mapping", () => {
14281425 store.setProjectDraftThreadId(remoteProjectRef, remoteDraftId, { threadId });
14291426 store.setPrompt(remoteDraftId, "remote draft");
14301427
1431 markPromotedDraftThread(threadId);
1428 markPromotedDraftThreadByRef(localThreadRef);
1429
1430 expect(store.getDraftThreadByProjectRef(projectRef)).toBeNull();
1431 expect(store.getDraftThreadByProjectRef(remoteProjectRef)?.threadId).toBe(threadId);
1432 expect(store.getDraftThreadByRef(localThreadRef)?.promotedTo).toEqual(localThreadRef);
1433 expect(store.getDraftThreadByRef(remoteThreadRef)?.promotedTo).toBeNull();
1434 expect(draftByKey(localDraftId)?.prompt).toBe("local draft");
1435 expect(draftByKey(remoteDraftId)?.prompt).toBe("remote draft");
1436
1437 markPromotedDraftThreadByRef(remoteThreadRef);
14321438
14331439 expect(store.getDraftThreadByProjectRef(projectRef)).toBeNull();
14341440 expect(store.getDraftThreadByProjectRef(remoteProjectRef)).toBeNull();
......@@ -1451,34 +1457,10 @@ describe("composerDraftStore project draft thread mapping", () => {
14511457 expect(draftByKey(draftId)?.prompt).toBe("promote me");
14521458 });
14531459
1454 it("only marks iterable promotion cleanup entries for the matching environment refs", () => {
1455 const store = useComposerDraftStore.getState();
1456 store.setProjectDraftThreadId(projectRef, draftId, { threadId });
1457 store.setPrompt(draftId, "promote me");
1458
1459 markPromotedDraftThreadsByRef([scopeThreadRef(OTHER_TEST_ENVIRONMENT_ID, threadId)]);
1460
1461 expect(useComposerDraftStore.getState().getDraftThreadByProjectRef(projectRef)?.threadId).toBe(
1462 threadId,
1463 );
1464 expect(draftByKey(draftId)?.prompt).toBe("promote me");
1465 });
1466
1467 it("keeps existing server-thread composer drafts during iterable promotion cleanup", () => {
1468 const store = useComposerDraftStore.getState();
1469 const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);
1470 store.setPrompt(threadRef, "keep me");
1471
1472 markPromotedDraftThreads([threadId]);
1473
1474 expect(useComposerDraftStore.getState().getDraftThread(threadRef)).toBeNull();
1475 expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt).toBe("keep me");
1476 });
1477
14781460 it("moves composer edits made during promotion to the canonical thread", () => {
14791461 const store = useComposerDraftStore.getState();
14801462 store.setProjectDraftThreadId(projectRef, draftId, { threadId });
1481 markPromotedDraftThread(threadId);
1463 markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));
14821464 store.setPrompt(draftId, "typed during setup");
14831465
14841466 finalizePromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));
apps/web/src/composerDraftStore.ts-35
......@@ -4106,29 +4106,6 @@ export function useEffectiveComposerModelState(input: {
41064106 );
41074107}
41084108
4109/**
4110 * Mark a draft thread as promoting once the server has materialized the same thread id.
4111 *
4112 * Use the single-thread helper for live `thread.created` events and the
4113 * iterable helper for bootstrap/recovery paths that discover multiple server
4114 * threads at once.
4115 */
4116export function markPromotedDraftThread(threadId: ThreadId): void {
4117 const store = useComposerDraftStore.getState();
4118 const draftThreadTargets: ComposerThreadTarget[] = [];
4119 for (const [draftId, draftThread] of Object.entries(store.draftThreadsByThreadKey)) {
4120 if (draftThread.threadId === threadId) {
4121 draftThreadTargets.push(DraftId.make(draftId));
4122 }
4123 }
4124 if (draftThreadTargets.length === 0) {
4125 return;
4126 }
4127 for (const draftThreadTarget of draftThreadTargets) {
4128 store.markDraftThreadPromoting(draftThreadTarget);
4129 }
4130}
4131
41324109export function markPromotedDraftThreadByRef(threadRef: ScopedThreadRef): void {
41334110 const draftStore = useComposerDraftStore.getState();
41344111 for (const [draftId, draftThread] of Object.entries(draftStore.draftThreadsByThreadKey)) {
......@@ -4141,18 +4118,6 @@ export function markPromotedDraftThreadByRef(threadRef: ScopedThreadRef): void {
41414118 }
41424119}
41434120
4144export function markPromotedDraftThreads(serverThreadIds: Iterable<ThreadId>): void {
4145 for (const threadId of serverThreadIds) {
4146 markPromotedDraftThread(threadId);
4147 }
4148}
4149
4150export function markPromotedDraftThreadsByRef(serverThreadRefs: Iterable<ScopedThreadRef>): void {
4151 for (const threadRef of serverThreadRefs) {
4152 markPromotedDraftThreadByRef(threadRef);
4153 }
4154}
4155
41564121export function finalizePromotedDraftThreadByRef(threadRef: ScopedThreadRef): void {
41574122 const draftStore = useComposerDraftStore.getState();
41584123 for (const [draftId, draftThread] of Object.entries(draftStore.draftThreadsByThreadKey)) {