返回 DeepSeek-Reasonix
worktree-merge-commands.test.tsx
根目录 / desktop / frontend / src / __tests__ / worktree-merge-commands.test.tsx
1 import assert from "node:assert/strict";
2 import React, { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import { JSDOM } from "jsdom";
5 import { useWorktreeMergeCommands } from "../app-runtime/useWorktreeMergeCommands";
6 import type { TabMeta, WorktreeMergeResult } from "../lib/types";
7 import type { Translator } from "../lib/i18n";
8
9 const dom = new JSDOM("<div id='root'></div>");
10 Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true });
11 const root = createRoot(document.getElementById("root")!);
12
13 const t = ((key: string) => key) as Translator;
14 const sourceTab = { id: "source-tab", workspaceRoot: "/source" } as TabMeta;
15 const worktreeTab = { id: "worktree-tab", workspaceRoot: "/worktree" } as TabMeta;
16 const receipt: WorktreeMergeResult = {
17 merged: true,
18 alreadyMerged: false,
19 recoveryRequired: false,
20 sourceRoot: "/source",
21 targetBranch: "main",
22 mergedCommit: "merge-head",
23 worktreeRoot: "/worktree",
24 worktreeBranch: "reasonix/delivery-test",
25 worktreeHead: "worktree-head",
26 };
27
28 const toasts: string[] = [];
29 const cleanups: unknown[] = [];
30 const lifecycleCalls: string[] = [];
31 let navigationToken: string | null = "nav-token";
32 let navigationCurrent = true;
33 let staleAfterEnsure = false;
34
35 let states!: ReturnType<typeof useWorktreeMergeCommands>;
36 function Probe() {
37 states = useWorktreeMergeCommands({
38 noteNavigationIntent: () => 42,
39 registeredNavigationIntent: async () => navigationToken,
40 isNavigationIntentCurrent: () => navigationCurrent,
41 ensureBlankSurface: async () => {
42 lifecycleCalls.push("ensure");
43 if (staleAfterEnsure) navigationCurrent = false;
44 return sourceTab;
45 },
46 seedSource: () => { lifecycleCalls.push("seed"); },
47 listTabs: async () => { lifecycleCalls.push("list"); return [sourceTab, worktreeTab]; },
48 closeWorktree: async () => { lifecycleCalls.push("close"); return { closed: true, idempotent: false }; },
49 finalize: async () => {
50 lifecycleCalls.push("finalize");
51 return { completed: true, worktreeRemoved: true, branchDeleted: true, blockers: [] };
52 },
53 showToast: (message) => { toasts.push(message); },
54 t,
55 showCleanup: (cleanup) => { cleanups.push(cleanup); },
56 });
57 return null;
58 }
59
60 try {
61 await act(async () => root.render(<Probe />));
62 assert.equal(states.worktreeMergeTabId, null, "the merge overlay starts closed");
63
64 await act(async () => { states.openWorktreeMerge("worktree-tab"); });
65 assert.equal(states.worktreeMergeTabId, "worktree-tab", "the topicbar merge action opens the overlay for its tab");
66 await act(async () => { states.closeWorktreeMerge(); });
67 assert.equal(states.worktreeMergeTabId, null, "the overlay close command clears the tab");
68
69 await assert.rejects(
70 () => states.handleWorktreeMerged({ ...receipt, mergedCommit: "" }),
71 /worktree\.mergeReceiptInvalid/,
72 "an invalid receipt rejects without touching navigation",
73 );
74 assert.equal(toasts.length, 0, "an invalid receipt surfaces through the throw, not a toast");
75
76 navigationToken = null;
77 await act(async () => { states.openWorktreeMerge("worktree-tab"); });
78 await act(async () => { await states.handleWorktreeMerged(receipt); });
79 assert.deepEqual(toasts, ["worktree.navigationChangedPreserved"], "a superseded navigation intent preserves the worktree with a toast");
80 assert.deepEqual(lifecycleCalls, [], "a superseded intent runs no close or finalize");
81 navigationToken = "nav-token";
82
83 toasts.length = 0;
84 await act(async () => { await states.handleWorktreeMerged(receipt); });
85 assert.deepEqual(lifecycleCalls, ["ensure", "seed", "list", "close", "finalize"], "a stable intent runs the full close/finalize chain in order");
86 assert.equal(cleanups.length, 1, "a finalized merge hands the cleanup receipt to the notice");
87
88 staleAfterEnsure = true;
89 toasts.length = 0;
90 lifecycleCalls.length = 0;
91 await act(async () => { await states.handleWorktreeMerged(receipt); });
92 assert.deepEqual(lifecycleCalls, ["ensure"], "a mid-flight navigation change stops the lifecycle before closing anything");
93 assert.deepEqual(toasts, ["worktree.navigationChangedPreserved"], "the preserved path reports through the lifecycle toast");
94 staleAfterEnsure = false;
95 navigationCurrent = true;
96
97 await act(async () => root.unmount());
98 console.log("worktree merge commands: overlay state, receipt gate, intent fences and close/finalize chain passed");
99 } finally { dom.window.close(); }
100
100 lines Plain Text