返回 DeepSeek-Reasonix
worktree-merge-lifecycle.test.ts
根目录 / desktop / frontend / src / __tests__ / worktree-merge-lifecycle.test.ts
1 // Run: tsx src/__tests__/worktree-merge-lifecycle.test.ts
2 import assert from "node:assert/strict";
3 import { runWorktreeMergeLifecycle } from "../lib/worktreeMergeLifecycle";
4 import type { TabMeta, WorktreeMergeResult } from "../lib/types";
5
6 function deferred<T>() {
7 let resolve!: (value: T) => void;
8 const promise = new Promise<T>((done) => { resolve = done; });
9 return { promise, resolve };
10 }
11
12 const sourceTab = { id: "source-tab", workspaceRoot: "/source" } as TabMeta;
13 const worktreeTab = { id: "worktree-tab", workspaceRoot: "/worktree" } as TabMeta;
14 const receipt: WorktreeMergeResult = {
15 merged: true,
16 alreadyMerged: false,
17 recoveryRequired: false,
18 sourceRoot: "/source",
19 targetBranch: "main",
20 mergedCommit: "merge-head",
21 worktreeRoot: "/worktree",
22 worktreeBranch: "reasonix/delivery-test",
23 worktreeHead: "worktree-head",
24 };
25
26 async function staleEnsureDoesNotCloseOrFinalize() {
27 const ensure = deferred<TabMeta>();
28 let current = true;
29 let closes = 0;
30 let finalizes = 0;
31 let preserved = 0;
32 const running = runWorktreeMergeLifecycle(receipt, worktreeTab.id, "nav-1", {
33 ensureSource: () => ensure.promise,
34 isNavigationCurrent: () => current,
35 seedSource: () => undefined,
36 listTabs: async () => [sourceTab, worktreeTab],
37 closeWorktree: async () => { closes++; return { closed: true, idempotent: false }; },
38 finalize: async () => { finalizes++; return { completed: true, worktreeRemoved: true, branchDeleted: true, blockers: [] }; },
39 onNavigationPreserved: () => { preserved++; },
40 onCloseBlocked: () => undefined,
41 });
42 current = false;
43 ensure.resolve(sourceTab);
44 assert.deepEqual(await running, { phase: "preserved" });
45 assert.equal(closes, 0);
46 assert.equal(finalizes, 0);
47 assert.equal(preserved, 1);
48 }
49
50 async function staleListDoesNotCloseOrFinalize() {
51 const listing = deferred<TabMeta[]>();
52 let current = true;
53 let closes = 0;
54 let finalizes = 0;
55 const running = runWorktreeMergeLifecycle(receipt, worktreeTab.id, "nav-2", {
56 ensureSource: async () => sourceTab,
57 isNavigationCurrent: () => current,
58 seedSource: () => undefined,
59 listTabs: () => listing.promise,
60 closeWorktree: async () => { closes++; return { closed: true, idempotent: false }; },
61 finalize: async () => { finalizes++; return { completed: true, worktreeRemoved: true, branchDeleted: true, blockers: [] }; },
62 onNavigationPreserved: () => undefined,
63 onCloseBlocked: () => undefined,
64 });
65 await Promise.resolve();
66 current = false;
67 listing.resolve([sourceTab, worktreeTab]);
68 assert.deepEqual(await running, { phase: "preserved" });
69 assert.equal(closes, 0);
70 assert.equal(finalizes, 0);
71 }
72
73 async function stableLifecycleClosesBeforeFinalize() {
74 const calls: string[] = [];
75 const result = await runWorktreeMergeLifecycle(receipt, worktreeTab.id, "nav-3", {
76 ensureSource: async () => { calls.push("ensure"); return sourceTab; },
77 isNavigationCurrent: () => true,
78 seedSource: () => { calls.push("seed"); },
79 listTabs: async () => { calls.push("list"); return [sourceTab, worktreeTab]; },
80 closeWorktree: async () => { calls.push("close"); return { closed: true, idempotent: false }; },
81 finalize: async () => {
82 calls.push("finalize");
83 return {
84 completed: false,
85 worktreeRemoved: false,
86 branchDeleted: false,
87 recoveryRetained: true,
88 recoveryRoot: "/recovery",
89 recoveryWorktreeRegistered: true,
90 branchRetained: true,
91 blockers: [],
92 };
93 },
94 onNavigationPreserved: () => undefined,
95 onCloseBlocked: () => undefined,
96 });
97 assert.equal(result.phase, "finalized");
98 assert.deepEqual(calls, ["ensure", "seed", "list", "close", "finalize"]);
99 if (result.phase === "finalized") {
100 assert.equal(result.cleanup.recoveryRetained, true);
101 assert.equal(result.cleanup.completed, false);
102 }
103 }
104
105 async function singleSurfaceStillUsesIdempotentCloseProof() {
106 const calls: string[] = [];
107 const result = await runWorktreeMergeLifecycle(receipt, worktreeTab.id, "nav-4", {
108 ensureSource: async () => sourceTab,
109 isNavigationCurrent: () => true,
110 seedSource: () => undefined,
111 listTabs: async () => [sourceTab],
112 closeWorktree: async () => { calls.push("close"); return { closed: true, idempotent: true }; },
113 finalize: async () => { calls.push("finalize"); return { completed: true, worktreeRemoved: true, branchDeleted: true, blockers: [] }; },
114 onNavigationPreserved: () => undefined,
115 onCloseBlocked: () => undefined,
116 });
117 assert.equal(result.phase, "finalized");
118 assert.deepEqual(calls, ["close", "finalize"]);
119 }
120
121 async function closeCarriesFenceAndNeverFinalizesAfterBackendRejectsStaleIntent() {
122 const close = deferred<{ closed: boolean; idempotent: boolean }>();
123 let finalizes = 0;
124 let closeToken = "";
125 const running = runWorktreeMergeLifecycle(receipt, worktreeTab.id, "nav-close-fence", {
126 ensureSource: async () => sourceTab,
127 isNavigationCurrent: () => true,
128 seedSource: () => undefined,
129 listTabs: async () => [sourceTab, worktreeTab],
130 closeWorktree: (request) => {
131 closeToken = request.navigationIntentToken;
132 return close.promise;
133 },
134 finalize: async () => {
135 finalizes++;
136 return { completed: true, worktreeRemoved: true, branchDeleted: true, blockers: [] };
137 },
138 onNavigationPreserved: () => undefined,
139 onCloseBlocked: () => undefined,
140 });
141 await Promise.resolve();
142 close.resolve({ closed: false, idempotent: false });
143 assert.deepEqual(await running, { phase: "close-blocked" });
144 assert.equal(closeToken, "nav-close-fence");
145 assert.equal(finalizes, 0);
146 }
147
148 await staleEnsureDoesNotCloseOrFinalize();
149 await staleListDoesNotCloseOrFinalize();
150 await stableLifecycleClosesBeforeFinalize();
151 await singleSurfaceStillUsesIdempotentCloseProof();
152 await closeCarriesFenceAndNeverFinalizesAfterBackendRejectsStaleIntent();
153 console.log("worktree merge lifecycle tests passed");
154
154 lines TYPESCRIPT