返回 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")
48 && /fork\?\.targetFor\(node\.answerKey\)/.test(message) && /forkBlockReason\(\{ target/.test(message),
49 "chat exposes only the persisted-turn fork entry and never the worktree scope");
50 ok(messageActionLabelKey("fork-worktree", false) === "rewind.forkWorktree", "isolated fork keeps its menu label after extraction");
51 ok(messageActionLabelKey("fork-worktree", true) === "rewind.confirmForkWorktree", "isolated fork keeps its confirmation label after extraction");
52 ok(/useState\(false\)/.test(mergeModal) && /autoCommitDirty/.test(mergeModal), "dirty auto-commit is opt-in by default");
53 ok(/InspectWorktreeMerge\(tabId\)[\s\S]*inspectionIdentity\(refreshed\)[\s\S]*MergeWorktreeBack\(\{/.test(mergeModal), "confirm re-inspects before sending one identity-bound merge request");
54 ok(/stateChanged/.test(mergeModal) && /setInspection\(refreshed\)/.test(mergeModal), "state drift refreshes the panel instead of continuing");
55 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");
56 ok(/WorktreeMergeModal\.css/.test(mergeModal) && mergeStyles.includes(".worktree-merge__body") && !/style=\{\{/.test(mergeModal), "lazy merge UI keeps layout rules out of inline styles");
57 ok(/worktreeStateToken/.test(mergeModal) && /expectedWorktreeStateToken/.test(mergeModal), "merge confirmation binds the exact dirty worktree content token");
58 ok(!/ModalCloseButton autoFocus/.test(mergeModal), "merge modal captures its trigger before moving focus so close restores the trigger");
59 ok(/CloseMergedWorktreeTab\(request: CloseMergedWorktreeTabRequest\)/.test(bridge), "worktree close is a request-object bridge call");
60 ok(/FinalizeWorktreeMerge\(request: WorktreeCleanupRequest\)/.test(bridge), "cleanup is a separate request-object bridge call");
61 const fencedNavigationCalls = [
62 ["const resumeSession", "app.ResumeTranscriptSessionForTab"],
63 ["const openChannelSession", "app.OpenChannelTranscriptSessionForTab"],
64 ["const pickWorkspace", "app.PickWorkspace"],
65 ["const switchWorkspace", "app.SwitchWorkspace"],
66 ["const switchTab", "app.SetActiveTab"],
67 ["const openProjectTab", "app.OpenProjectTab"],
68 ["const openGlobalTab", "app.OpenGlobalTab"],
69 ["const openTopicSession", "app.OpenTopicSession"],
70 ["const activateTopic", "app.StartTopicActivation"],
71 ["const ensureBlankTab", "app.EnsureBlankTab"],
72 ["const ensureBlankSurface", "app.EnsureBlankSurface"],
73 ["const createIsolatedWorktree", "app.CreateIsolatedWorktree"],
74 ["const closeTab", "app.CloseTabWithPolicy"],
75 ];
76 ok(fencedNavigationCalls.every(([startMarker, callMarker]) => {
77 const start = controller.indexOf(startMarker);
78 const fence = controller.indexOf("await requireRegisteredNavigationIntent", start);
79 const call = controller.indexOf(callMarker, start);
80 return start >= 0 && fence > start && call > fence;
81 }), "navigation entry points await backend intent registration before switching");
82 ok(/navigationIntentRegistrationTail\.then/.test(navigationFence) && /navigationIntentRegistrationTail = registered/.test(navigationFence), "navigation registrations preserve user-intent order across deferred bridge calls and remounts");
83
84 const { increaseMockForkTitle, makeMockForkBindings } = await import("../lib/mockForkWorktree");
85 const { settleForkConversationForTab } = await import("../lib/controllerSwitchNotices");
86 const original = { id: "source", active: true, workspaceRoot: "/project", topicTitle: "Source" } as TabMeta;
87 let mockTabs = [original];
88 const mockFork = makeMockForkBindings(() => mockTabs, tabs => { mockTabs = tabs; }, "Untitled", async () => []);
89 const isolated = await mockFork.ForkWorktreeForTab(original.id, 3);
90 ok(isolated.isolated && isolated.tab.workspaceRoot === "/project-worktree" && mockTabs[0].active === false,
91 "separate mock bindings retain isolated-worktree and activation behavior");
92 ok(isolated.tab.topicTitle === "Source (1)" && increaseMockForkTitle("计划(9)") === "计划(10)",
93 "browser mock mirrors Harness fork-title numbering");
94 const forkCalls: string[] = [];
95 const bindings = {
96 ForkForTab: async (id: string, turn: number) => { forkCalls.push(`shared:${id}:${turn}`); return isolated.tab; },
97 ForkWorktreeForTab: async (id: string, turn: number) => { forkCalls.push(`isolated:${id}:${turn}`); return { ...isolated, sourceDirty: true }; },
98 };
99 const adopt = async () => { forkCalls.push("adopt"); };
100 const sync = async () => { forkCalls.push("sync"); };
101 const notice = () => { forkCalls.push("notice"); };
102 const sharedResult = await settleForkConversationForTab(bindings, original.id, 4, false, notice, adopt, sync);
103 ok(sharedResult.ok && forkCalls.join(",") === "shared:source:4,adopt", "lazy action entry retains exact shared-fork arguments and adoption");
104 forkCalls.length = 0;
105 const dirtyResult = await settleForkConversationForTab(bindings, original.id, 5, true, notice, adopt, sync);
106 ok(!dirtyResult.ok && forkCalls.join(",") === "isolated:source:5,notice,sync", "lazy action entry preserves dirty-worktree refusal without adoption");
107
108 if (failed) process.exit(1);
109 console.log("isolated worktree tests passed");
110
110 lines TYPESCRIPT