返回 DeepSeek-Reasonix
isolated-worktree.test.ts
根目录 / desktop / frontend / src / __tests__ / isolated-worktree.test.ts
1 // Run: tsx src/__tests__/isolated-worktree.test.ts
2 import { readFileSync } from "node:fs";
3 import { dirname, resolve } from "node:path";
4 import { fileURLToPath } from "node:url";
5 import { messageActionLabelKey } from "../lib/messageActions";
6 import type { TabMeta } from "../lib/types";
7
8 const dir = dirname(fileURLToPath(import.meta.url));
9 const source = (path: string) => readFileSync(resolve(dir, path), "utf8");
10 const bridge = source("../lib/bridge.ts");
11 const tree = source("../components/ProjectTree.tsx");
12 const badge = source("../components/WorktreeBadge.tsx");
13 const forkAction = source("../lib/forkWorktree.ts");
14 const message = source("../components/ChatNodes.tsx");
15 const mergeModal = source("../components/WorktreeMergeModal.tsx");
16 const mergeStyles = source("../components/WorktreeMergeModal.css");
17 const controller = source("../lib/useController.ts");
18 const navigationFence = source("../lib/useNavigationIntentFence.ts");
19
20 let failed = 0;
21 function ok(value: unknown, label: string) {
22 if (value) process.stdout.write(` PASS ${label}\n`);
23 else {
24 failed += 1;
25 process.stdout.write(` FAIL ${label}\n`);
26 }
27 }
28
29 console.log("\nisolated worktree");
30 ok(/IsolatedWorktreeAvailability\(workspaceRoot: string\)/.test(bridge), "bridge exposes non-mutating availability probe");
31 ok(/CreateIsolatedWorktree\(workspaceRoot: string\)/.test(bridge), "bridge exposes isolated workspace creation");
32 ok(/app\.IsolatedWorktreeAvailability\(projectRoot\)/.test(tree), "project menu probes Git before enabling isolation");
33 ok(/disabled: isolatingProject !== null \|\| isolationAvailability\?\.available === false/.test(tree), "menu disables unavailable or duplicate creation");
34 ok(/onCreateIsolatedWorktree\?\.\(workspaceRoot\)/.test(tree), "project menu delegates isolated workspace creation");
35 // Project commands drive the production coalescing queue under deferred work
36 // in project-topic-lifecycle.test.tsx; callback location is not a contract.
37 // desktop-navigation-lifecycle.test.tsx verifies the actual dirty-worktree notice.
38 // The top session tab strip is gone; topicbar-region.test.tsx mounts the real
39 // badge and verifies conditional identity, accessible labeling and source-bound
40 // merge actions for isolated/ordinary topics.
41 ok(/node\.isolatedWorktree && <WorktreeBadge/.test(tree), "project tree identifies isolated worktrees");
42 ok(/GitBranch/.test(badge) && /#6119/.test(badge), "shared badge preserves the credited #6119 design contribution");
43 ok(/bindings\.ForkWorktreeForTab\(sourceTabId, turn\)/.test(forkAction) && /makeMockForkBindings/.test(bridge) && !/async ForkWorktreeForTab\(tabID, turn\)/.test(bridge), "isolated conversation fork and browser mock use the extracted two-argument binding");
44 ok(!/ForkForTab\(sourceTabId, turn, isolate/.test(forkAction), "shared fork never sends an extra bridge argument");
45 ok(/result\.sourceDirty[\s\S]*forkWorktreeDirtySource/.test(forkAction), "dirty sources are refused with actionable guidance");
46 ok(/result\.fallbackToShared[\s\S]*forkWorktreeFallbackNotice/.test(forkAction), "backend fallback state reaches the user");
47 ok(!message.includes("fork-worktree") && !message.includes("actions.checkpoints") && /actions\.fork/.test(message),
48 "chat exposes only the persisted-turn fork entry and never the worktree scope");
49 ok(messageActionLabelKey("fork-worktree", false) === "rewind.forkWorktree", "isolated fork keeps its menu label after extraction");
50 ok(messageActionLabelKey("fork-worktree", true) === "rewind.confirmForkWorktree", "isolated fork keeps its confirmation label after extraction");
51 ok(/useState\(false\)/.test(mergeModal) && /autoCommitDirty/.test(mergeModal), "dirty auto-commit is opt-in by default");
52 ok(/InspectWorktreeMerge\(tabId\)[\s\S]*inspectionIdentity\(refreshed\)[\s\S]*MergeWorktreeBack\(\{/.test(mergeModal), "confirm re-inspects before sending one identity-bound merge request");
53 ok(/stateChanged/.test(mergeModal) && /setInspection\(refreshed\)/.test(mergeModal), "state drift refreshes the panel instead of continuing");
54 ok(/aria-modal="true"/.test(mergeModal) && /event\.key === "Escape"/.test(mergeModal) && /event\.key !== "Tab"/.test(mergeModal), "merge dialog exposes modal, escape, and focus-loop semantics");
55 ok(/WorktreeMergeModal\.css/.test(mergeModal) && mergeStyles.includes(".worktree-merge__body") && !/style=\{\{/.test(mergeModal), "lazy merge UI keeps layout rules out of inline styles");
56 ok(/worktreeStateToken/.test(mergeModal) && /expectedWorktreeStateToken/.test(mergeModal), "merge confirmation binds the exact dirty worktree content token");
57 ok(!/ModalCloseButton autoFocus/.test(mergeModal), "merge modal captures its trigger before moving focus so close restores the trigger");
58 ok(/CloseMergedWorktreeTab\(request: CloseMergedWorktreeTabRequest\)/.test(bridge), "worktree close is a request-object bridge call");
59 ok(/FinalizeWorktreeMerge\(request: WorktreeCleanupRequest\)/.test(bridge), "cleanup is a separate request-object bridge call");
60 const fencedNavigationCalls = [
61 ["const resumeSession", "app.ResumeTranscriptSessionForTab"],
62 ["const openChannelSession", "app.OpenChannelTranscriptSessionForTab"],
63 ["const pickWorkspace", "app.PickWorkspace"],
64 ["const switchWorkspace", "app.SwitchWorkspace"],
65 ["const switchTab", "app.SetActiveTab"],
66 ["const openProjectTab", "app.OpenProjectTab"],
67 ["const openGlobalTab", "app.OpenGlobalTab"],
68 ["const openTopicSession", "app.OpenTopicSession"],
69 ["const activateTopic", "app.StartTopicActivation"],
70 ["const ensureBlankTab", "app.EnsureBlankTab"],
71 ["const ensureBlankSurface", "app.EnsureBlankSurface"],
72 ["const createIsolatedWorktree", "app.CreateIsolatedWorktree"],
73 ["const closeTab", "app.CloseTabWithPolicy"],
74 ];
75 ok(fencedNavigationCalls.every(([startMarker, callMarker]) => {
76 const start = controller.indexOf(startMarker);
77 const fence = controller.indexOf("await requireRegisteredNavigationIntent", start);
78 const call = controller.indexOf(callMarker, start);
79 return start >= 0 && fence > start && call > fence;
80 }), "navigation entry points await backend intent registration before switching");
81 ok(/navigationIntentRegistrationTail\.then/.test(navigationFence) && /navigationIntentRegistrationTail = registered/.test(navigationFence), "navigation registrations preserve user-intent order across deferred bridge calls and remounts");
82
83 const { increaseMockForkTitle, makeMockForkBindings } = await import("../lib/mockForkWorktree");
84 const { settleForkConversationForTab } = await import("../lib/controllerSwitchNotices");
85 const original = { id: "source", active: true, workspaceRoot: "/project", topicTitle: "Source" } as TabMeta;
86 let mockTabs = [original];
87 const mockFork = makeMockForkBindings(() => mockTabs, tabs => { mockTabs = tabs; }, "Untitled", async () => []);
88 const isolated = await mockFork.ForkWorktreeForTab(original.id, 3);
89 ok(isolated.isolated && isolated.tab.workspaceRoot === "/project-worktree" && mockTabs[0].active === false,
90 "separate mock bindings retain isolated-worktree and activation behavior");
91 ok(isolated.tab.topicTitle === "Source (1)" && increaseMockForkTitle("计划(9)") === "计划(10)",
92 "browser mock mirrors Harness fork-title numbering");
93 const forkCalls: string[] = [];
94 const bindings = {
95 ForkForTab: async (id: string, turn: number) => { forkCalls.push(`shared:${id}:${turn}`); return isolated.tab; },
96 ForkWorktreeForTab: async (id: string, turn: number) => { forkCalls.push(`isolated:${id}:${turn}`); return { ...isolated, sourceDirty: true }; },
97 };
98 const adopt = async () => { forkCalls.push("adopt"); };
99 const sync = async () => { forkCalls.push("sync"); };
100 const notice = () => { forkCalls.push("notice"); };
101 const sharedResult = await settleForkConversationForTab(bindings, original.id, 4, false, notice, adopt, sync);
102 ok(sharedResult.ok && forkCalls.join(",") === "shared:source:4,adopt", "lazy action entry retains exact shared-fork arguments and adoption");
103 forkCalls.length = 0;
104 const dirtyResult = await settleForkConversationForTab(bindings, original.id, 5, true, notice, adopt, sync);
105 ok(!dirtyResult.ok && forkCalls.join(",") === "isolated:source:5,notice,sync", "lazy action entry preserves dirty-worktree refusal without adoption");
106
107 if (failed) process.exit(1);
108 console.log("isolated worktree tests passed");
109
109 lines TYPESCRIPT