返回 DeepSeek-Reasonix
remote-project-tree.test.tsx
根目录 / desktop / frontend / src / __tests__ / remote-project-tree.test.tsx
1 // Run: tsx src/__tests__/remote-project-tree.test.tsx
2 // Source-contract test: the remote project group's tree behavior — session
3 // rows, the + action, the remote context menu, the connection badge, and
4 // the local-action guards — is wired exactly once and in the remote shape.
5
6 import { readFileSync } from "node:fs";
7 import { dirname, resolve } from "node:path";
8 import { fileURLToPath } from "node:url";
9 import { activeRemoteProjectAncestorKeys, mergeRemoteSessionsIntoTree, openRemoteSessionNode, remoteSessionActionIdentity } from "../components/ProjectTreeRemoteGroups";
10 import type { RemoteTabOpenOptions } from "../lib/types";
11 import type { ProjectNode } from "../lib/types";
12
13 let passed = 0;
14 let failed = 0;
15 function ok(value: boolean, label: string) {
16 if (value) {
17 process.stdout.write(` PASS ${label}\n`);
18 passed += 1;
19 } else {
20 process.stdout.write(` FAIL ${label}\n`);
21 failed += 1;
22 }
23 }
24
25 console.log("\nRemote project tree wiring");
26 const here = dirname(fileURLToPath(import.meta.url));
27 const source = readFileSync(resolve(here, "../components/ProjectTree.tsx"), "utf8");
28 const remoteSource = readFileSync(resolve(here, "../components/ProjectTreeRemoteGroups.tsx"), "utf8");
29 const compositionSource = readFileSync(resolve(here, "../app-runtime/useAppSessionComposition.ts"), "utf8");
30 const todoSource = readFileSync(resolve(here, "../app-runtime/useTodoPanelCommands.ts"), "utf8");
31 const paletteSource = readFileSync(resolve(here, "../app-runtime/usePaletteCommands.tsx"), "utf8");
32 const exportSource = readFileSync(resolve(here, "../app-runtime/useSessionExportCommands.ts"), "utf8");
33 const exportOperationSource = readFileSync(resolve(here, "../lib/sessionExportOperation.ts"), "utf8");
34 const bridgeSource = readFileSync(resolve(here, "../lib/remoteProjectBridge.ts"), "utf8");
35 const remoteOpenSource = readFileSync(resolve(here, "../../../remote_projects.go"), "utf8");
36 const remotePendingSelectionSource = readFileSync(resolve(here, "../../../remote_tab_pending_selection.go"), "utf8");
37 const explicitEnsureSource = remoteSource.match(
38 /const ensureRemoteGroupSessions[\s\S]*?\n const openRemoteWindow/,
39 )?.[0] ?? "";
40
41 const sameWorkspaceRemoteTree: ProjectNode[] = [
42 { key: "remote-host-a", kind: "project", label: "A", remote: { hostId: "host-a", workspace: "/repo" }, children: [] },
43 { key: "remote-host-b", kind: "project", label: "B", remote: { hostId: "host-b", workspace: "/repo" }, children: [] },
44 ];
45 ok(
46 JSON.stringify(activeRemoteProjectAncestorKeys(sameWorkspaceRemoteTree, { hostId: "host-b", workspace: "/repo" }, (node) => node.key ?? "")) === JSON.stringify(["remote-host-b"]),
47 "active remote group expansion uses host-qualified identity instead of local workspace fallback",
48 );
49
50 ok(
51 /remoteSession: \{ hostId: node\.remote!\.hostId, workspace: node\.remote!\.workspace, name: row\.name, path: row\.path, sessionId: row\.sessionId, title: row\.title, current: row\.current \}/.test(remoteSource) &&
52 /openRemoteSessionNode\(remote, openRemoteProject\)/.test(source) &&
53 /sessionPath: remote\.path, sessionId: remote\.sessionId, sessionTitle: remote\.title/.test(remoteSource) &&
54 /remoteSessionIdentity\(row\)/.test(remoteSource),
55 "session rows open the matching in-app remote session",
56 );
57 // The synthetic "current" row for a canonical session carries only a
58 // sessionId: no legacy path, no basename. Opening it must hand that id to
59 // OpenRemoteProjectTab as `sessionId`, never re-encoded as a path route.
60 {
61 const opened: Array<{ hostId: string; workspace: string; opts?: RemoteTabOpenOptions }> = [];
62 const open = async (ref: { hostId: string; workspace: string }, opts?: RemoteTabOpenOptions) => { opened.push({ ...ref, opts }); };
63 const merged = mergeRemoteSessionsIntoTree(
64 [{ key: "remote-host-a", kind: "project", label: "A", remote: { hostId: "host-a", workspace: "/repo" }, children: [] }],
65 { "host-a\u0000/repo": [{ name: "", sessionId: "sess-canonical", title: "", turns: 0, current: true }] },
66 ((key: string) => key) as never,
67 );
68 const row = merged[0]?.children?.[0];
69 ok(row?.remoteSession?.sessionId === "sess-canonical" && row.remoteSession.current === true && row.sessionPath === undefined,
70 "a canonical current row keeps its sessionId and never synthesizes a path");
71 ok(openRemoteSessionNode(row?.remoteSession, open) && opened.length === 1
72 && opened[0].opts?.sessionId === "sess-canonical" && !opened[0].opts?.sessionPath && !opened[0].opts?.sessionName,
73 "opening a sessionId-only remote row forwards sessionId to OpenRemoteProjectTab without a path route");
74 ok(remoteSessionActionIdentity({ sessionId: "sess-canonical", name: "" }) === "sess-canonical"
75 && remoteSessionActionIdentity({ name: "", path: "session-id:legacy-encoded" }) === "legacy-encoded",
76 "mutations address a canonical row by sessionId and decode the legacy session-id path only as a fallback");
77 }
78 ok(
79 /rows\.map\(\(row\): ProjectNode =>/.test(remoteSource) && /useRemoteRuntimeTree\(tree, remoteSessions, t\)/.test(source) &&
80 /root: node\.remote!\.workspace/.test(remoteSource) && /sessionPath: row\.path/.test(remoteSource),
81 "remote group children render with the active workspace and session identity",
82 );
83 ok(
84 /app\.RemoteProjectSessions\(hostId, workspace\)/.test(remoteSource),
85 "sessions are fetched through the bridge",
86 );
87 ok(
88 /state !== "connected" && state !== "degraded"/.test(remoteSource),
89 "session fetch waits for a connected host",
90 );
91 ok(
92 /key: "remote-new-session"[\s\S]*?key: "remote-open-window"[\s\S]*?key: "remote-stop-server"[\s\S]*?key: "remote-unpin"/.test(remoteSource),
93 "the remote menu exposes new-session, browser, stop, and unpin actions",
94 );
95 ok(
96 /items=\{node\.remote \? remoteProjectMenuItems :/.test(source),
97 "remote groups swap out the local project menu",
98 );
99 ok(
100 /app\.ConnectRemoteHost\(ref\.hostId\)[\s\S]*?waitForRemoteConnection\(ref\.hostId\)[\s\S]*?publishNavigationIntent\("remote-workspace"\)[\s\S]*?app\.OpenRemoteWorkspace\(ref\.hostId, ref\.workspace\)/.test(remoteSource),
101 "separate remote window registers its intent before switching the external surface",
102 );
103 ok(
104 /app\.RemoveRemoteProject\(ref\.hostId, ref\.workspace\)/.test(remoteSource) && /void refresh\(\);/.test(remoteSource),
105 "unpin removes the registration and refreshes the tree",
106 );
107 ok(
108 /remoteServeBadgeState\(remoteServers\[node\.remote\.hostId\]\?\.\[node\.remote\.workspace\], remoteGroupBusy/.test(source),
109 "the group row badge reflects the workspace-specific serve state",
110 );
111 ok(
112 /sessionLoads\.current\.has\(key\)/.test(remoteSource) &&
113 /sessionLoadGenerations\.current\.set\(key, load\)/.test(remoteSource) &&
114 /eligibleSessionKeys\.current\.has\(key\)/.test(remoteSource) &&
115 /filter\(\(\[key\]\) => retained\.has\(key\)\)/.test(remoteSource) &&
116 /acceptRemoteSessionRows\(key, rows\)/.test(remoteSource) &&
117 /catch\(\(error\) => \{[\s\S]*?sessionLoadGenerations\.current\.get\(key\) === load && eligibleSessionKeys\.current\.has\(key\)[\s\S]*?recordRemoteSessionLoadError\(key, error\)/.test(remoteSource) &&
118 /recordRemoteSessionLoadError[\s\S]*?setGroupError/.test(remoteSource) &&
119 /setGroupError\(\(current\) => current\[key\] \? \{ \.\.\.current, \[key\]: "" \} : current\)/.test(remoteSource),
120 "session fetches fence stale results, retain rows on passive failures, and clear recovered group errors",
121 );
122 ok(
123 /catch \(error\) \{[\s\S]*?recordRemoteSessionLoadError\(key, error\)/.test(explicitEnsureSource) &&
124 !/removeRemoteSessionCache\(key\)/.test(explicitEnsureSource) &&
125 !/setSessions\(/.test(explicitEnsureSource),
126 "an explicit session refresh preserves the last successful rows and cache when Serve fails",
127 );
128 ok(
129 /item\.id !== "cmd-terminal" && item\.id !== "cmd-reload-runtime"/.test(paletteSource),
130 "remote command palettes hide local-only terminal and runtime reload actions",
131 );
132 ok(
133 /const remote = index\.get\(topicId\);[\s\S]*?if \(!remote\) return false;[\s\S]*?await action\(remote\);/.test(remoteSource) &&
134 /remote\.name \|\| remote\.sessionId/.test(remoteSource) &&
135 !/if \(remote\.name\) await action\(remote\)/.test(remoteSource),
136 "synthetic blank remote sessions still invoke rename, pin, and delete mutations",
137 );
138 ok(
139 /onRemoteTabUpdated\(\(meta\)/.test(remoteSource) &&
140 /RemoteProjectSessions\(meta\.remote\.hostId, meta\.remote\.workspace\)/.test(remoteSource),
141 "remote tab metadata updates refresh the affected session group",
142 );
143 ok(
144 /selector: activeTab\?\.session\?\.sessionId \? \{ ref: activeTab\.session \}/.test(compositionSource) &&
145 /runSessionExport\(\{[\s\S]*?selector: input\.selector \?\? \{\}[\s\S]*?tabId: tabId \?\? ""[\s\S]*?remote/.test(exportSource) &&
146 /app\.BeginSessionExportForTarget\(input\.selector, input\.tabId, input\.format, input\.title, observation\)/.test(exportOperationSource) &&
147 !/sessionItemsToMarkdown\(/.test(exportSource),
148 "remote exports bind the explicit remote session snapshot instead of resident transcript items",
149 );
150 ok(
151 /items: visibleRuntimeState\.items/.test(compositionSource) &&
152 /const metaTodos = input\.meta\?\.canonicalTodos/.test(todoSource) &&
153 /resolveTodoPanelTodos\(metaTodos\)/.test(todoSource) &&
154 /setDismissedTodoKeys/.test(todoSource),
155 "remote todo shelf consumes the shared runtime projection with mount-local dismissal",
156 );
157 ok(
158 /EnsureRemoteProjectSessions\(hostId: string, workspace: string\): Promise<RemoteSessionView\[\]>;/.test(bridgeSource),
159 "the bridge exposes the explicit-intent ensure listing",
160 );
161 ok(
162 /app\.EnsureRemoteProjectSessions\(hostId, workspace\)/.test(remoteSource) &&
163 /if \(groupBusyRef\.current\.has\(key\)\) return;/.test(remoteSource) &&
164 /sessionLoadGenerations\.current\.set\(key, load\);[\s\S]*?EnsureRemoteProjectSessions\(hostId, workspace\)[\s\S]*?sessionLoadGenerations\.current\.get\(key\) !== load/.test(remoteSource),
165 "a group click cold-starts the serve through the deduped, generation-fenced ensure listing",
166 );
167 ok(
168 /if \(node\.remote && willExpand\) \{\s*void ensureRemoteGroupSessions\(node\.remote\.hostId, node\.remote\.workspace\);/.test(source),
169 "an explicit expand refreshes remote rows even when an optimistic cache exists",
170 );
171 ok(
172 /activeRemoteProjectAncestorKeys\(treeWithRemoteSessions, activeRemote, projectNodeKey\)/.test(source) &&
173 /defaultExpandedProjectTreeKeys\(treeWithRemoteSessions, activeScope, activeWorkspaceRoot, activeTopicId, activeSessionPath\)/.test(source),
174 "active remote ancestors come from the merged tree and host-qualified group identity",
175 );
176 ok(
177 /markActive\(treeWithRemoteSessions\)/.test(source) &&
178 /markNodeRead, treeWithRemoteSessions\]\);/.test(source) &&
179 !/markActive\(tree\);/.test(source),
180 "active synthesized remote rows are marked read when the merged session tree arrives",
181 );
182 ok(
183 /project-tree__remote-status/.test(remoteSource) && /projectTree\.remoteConnectFailed/.test(remoteSource),
184 "remote groups render retryable connect/error rows instead of going silent",
185 );
186 ok(
187 /existing\.selectionRevision\+\+/.test(remoteOpenSource) &&
188 /a\.goRemoteTabSafe\("remoteTabResume"[\s\S]*?resumeRemoteTabSessionPathForOpenSelection\(tabID, name, sessionPath, sessionTitle, revision, selection\)/.test(remotePendingSelectionSource),
189 "session switches resume in the background behind a generation guard",
190 );
191 ok(
192 /if \(\(project\.kind !== "project" && project\.kind !== "global_folder"\) \|\| project\.remote\) return;/.test(source),
193 "remote groups never reach ListProjectTopics with their virtual root",
194 );
195
196 process.stdout.write(`\n${passed} passed, ${failed} failed\n`);
197 if (failed > 0) process.exit(1);
198
198 lines Plain Text