返回 DeepSeek-Reasonix
terminal-store.test.ts
根目录 / desktop / frontend / src / __tests__ / terminal-store.test.ts
1 import { JSDOM } from "jsdom";
2
3 const dom = new JSDOM("<!doctype html><html><body></body></html>");
4 const previousWindow = globalThis.window;
5 globalThis.window = dom.window as unknown as Window & typeof globalThis;
6
7 type Session = {
8 id: string;
9 title: string;
10 shell: string;
11 cwd: string;
12 createdAt: number;
13 running: boolean;
14 };
15 type Workspace = { available: boolean; readOnly: boolean; sessions: Session[]; shells: { id: string; label: string }[] };
16 const pending = new Map<string, (value: Workspace) => void>();
17 const pendingReject = new Map<string, (error: Error) => void>();
18 const createPending: Array<(value: Session) => void> = [];
19 const createReject: Array<(error: Error) => void> = [];
20 let calls = 0;
21 let closeError: Error | null = null;
22 let writeError: Error | null = null;
23 const fakeApp = {
24 TerminalWorkspaceForTab(tabId: string) {
25 calls += 1;
26 return new Promise<Workspace>((resolve, reject) => {
27 pending.set(tabId, resolve);
28 pendingReject.set(tabId, reject);
29 });
30 },
31 CreateTerminalForTab() {
32 return new Promise<Session>((resolve, reject) => {
33 createPending.push(resolve);
34 createReject.push(reject);
35 });
36 },
37 async CloseTerminalForTab() {
38 if (closeError) throw closeError;
39 },
40 async WriteTerminalForTab() {
41 if (writeError) throw writeError;
42 },
43 };
44 (globalThis.window as unknown as { go: unknown }).go = { main: { App: fakeApp } };
45
46 const { resetTerminalStoreForTests, useTerminalStore } = await import("../store/terminal");
47 resetTerminalStoreForTests();
48
49 const oldRequest = useTerminalStore.getState().syncWorkspace("old-tab");
50 const newRequest = useTerminalStore.getState().syncWorkspace("new-tab");
51 pending.get("new-tab")?.({ available: true, readOnly: false, sessions: [], shells: [] });
52 await newRequest;
53 pending.get("old-tab")?.({ available: true, readOnly: false, sessions: [], shells: [] });
54 await oldRequest;
55 if (useTerminalStore.getState().tabId !== "new-tab") throw new Error("stale workspace response replaced the selected tab");
56 if (calls !== 2) throw new Error(`expected two workspace calls, got ${calls}`);
57
58 resetTerminalStoreForTests();
59 calls = 0;
60 const shared = useTerminalStore.getState().ensureReady("same-tab");
61 const sharedAgain = useTerminalStore.getState().ensureReady("same-tab");
62 pending.get("same-tab")?.({ available: true, readOnly: false, sessions: [], shells: [] });
63 await Promise.all([shared, sharedAgain]);
64 if (calls !== 1) throw new Error(`ensureReady duplicated the first request: ${calls}`);
65 process.stdout.write("PASS stale responses and first-ready deduplication\n");
66
67 resetTerminalStoreForTests();
68 calls = 0;
69 createPending.length = 0;
70 const firstOpenCreate = useTerminalStore.getState().createSession("first-open");
71 await Promise.resolve();
72 const panelReady = useTerminalStore.getState().syncWorkspace("first-open");
73 await Promise.resolve();
74 if (calls !== 1) throw new Error(`panel readiness duplicated the first-open workspace request: ${calls}`);
75 pending.get("first-open")?.({
76 available: true,
77 readOnly: false,
78 sessions: [],
79 shells: [{ id: "default", label: "Default shell" }],
80 });
81 await panelReady;
82 await Promise.resolve();
83 if (createPending.length !== 1) throw new Error(`first-open create did not reach the backend: ${createPending.length}`);
84 const firstOpenSession: Session = {
85 id: "first-open-session",
86 title: "default",
87 shell: "default",
88 cwd: ".",
89 createdAt: 1,
90 running: true,
91 };
92 createPending[0]?.(firstOpenSession);
93 const createdOnFirstOpen = await firstOpenCreate;
94 if (createdOnFirstOpen?.id !== firstOpenSession.id) throw new Error("first-open create was discarded");
95 process.stdout.write("PASS first-open panel readiness shares the pending workspace request\n");
96
97 resetTerminalStoreForTests();
98 calls = 0;
99 const staleCapability = useTerminalStore.getState().syncWorkspace("capability-refresh");
100 await Promise.resolve();
101 const resolveStaleCapability = pending.get("capability-refresh");
102 const latestCapability = useTerminalStore.getState().syncWorkspace("capability-refresh", true);
103 await Promise.resolve();
104 if (calls !== 2) throw new Error(`forced capability refresh did not start a new request: ${calls}`);
105 pending.get("capability-refresh")?.({
106 available: true,
107 readOnly: false,
108 sessions: [],
109 shells: [{ id: "default", label: "Default shell" }],
110 });
111 await latestCapability;
112 resolveStaleCapability?.({
113 available: true,
114 readOnly: true,
115 sessions: [],
116 shells: [{ id: "default", label: "Default shell" }],
117 });
118 await staleCapability;
119 if (useTerminalStore.getState().workspace?.readOnly) {
120 throw new Error("stale read-only capability replaced the forced writable refresh");
121 }
122 process.stdout.write("PASS forced capability refresh supersedes an in-flight same-tab request\n");
123
124 resetTerminalStoreForTests();
125 createPending.length = 0;
126 const readyWorkspace: Workspace = {
127 available: true,
128 readOnly: false,
129 sessions: [],
130 shells: [{ id: "default", label: "Default shell" }],
131 };
132 useTerminalStore.setState({
133 tabId: "create-tab",
134 workspace: readyWorkspace,
135 loading: false,
136 activeSessionId: null,
137 });
138 const firstCreate = useTerminalStore.getState().createSession("create-tab");
139 const secondCreate = useTerminalStore.getState().createSession("create-tab");
140 await Promise.resolve();
141 await Promise.resolve();
142 if (createPending.length !== 2) throw new Error(`expected two create calls, got ${createPending.length}`);
143 const firstSession: Session = {
144 id: "first",
145 title: "first",
146 shell: "default",
147 cwd: ".",
148 createdAt: 1,
149 running: true,
150 };
151 const secondSession: Session = {
152 id: "second",
153 title: "second",
154 shell: "default",
155 cwd: ".",
156 createdAt: 2,
157 running: true,
158 };
159 createPending[1]?.(secondSession);
160 await secondCreate;
161 createPending[0]?.(firstSession);
162 await firstCreate;
163 const sessionIDs = useTerminalStore.getState().workspace?.sessions.map((session) => session.id).sort();
164 if (JSON.stringify(sessionIDs) !== JSON.stringify(["first", "second"])) {
165 throw new Error(`concurrent creates lost a session: ${JSON.stringify(sessionIDs)}`);
166 }
167 process.stdout.write("PASS concurrent terminal creates merge into the latest workspace state\n");
168
169 resetTerminalStoreForTests();
170 const failedSync = useTerminalStore.getState().syncWorkspace("sync-error");
171 pendingReject.get("sync-error")?.(new Error("workspace unavailable"));
172 await failedSync.catch(() => {});
173 if (useTerminalStore.getState().error !== "workspace unavailable") {
174 throw new Error(`workspace sync error was not exposed: ${useTerminalStore.getState().error}`);
175 }
176 if (useTerminalStore.getState().loading || useTerminalStore.getState().workspace !== null) {
177 throw new Error("failed workspace sync left a loading or partial workspace state");
178 }
179 process.stdout.write("PASS workspace sync failures remain visible and retryable\n");
180
181 resetTerminalStoreForTests();
182 createPending.length = 0;
183 createReject.length = 0;
184 useTerminalStore.setState({
185 tabId: "create-error",
186 workspace: readyWorkspace,
187 loading: false,
188 activeSessionId: null,
189 });
190 const failedCreate = useTerminalStore.getState().createSession("create-error");
191 await Promise.resolve();
192 createReject[0]?.(new Error("shell failed to start"));
193 await failedCreate.catch(() => {});
194 if (useTerminalStore.getState().error !== "shell failed to start") {
195 throw new Error(`terminal create error was not exposed: ${useTerminalStore.getState().error}`);
196 }
197 if (useTerminalStore.getState().workspace?.sessions.length !== 0) {
198 throw new Error("failed terminal create inserted a phantom session");
199 }
200 process.stdout.write("PASS terminal create failures do not insert phantom sessions\n");
201
202 resetTerminalStoreForTests();
203 closeError = new Error("terminal did not close");
204 useTerminalStore.setState({
205 tabId: "close-error",
206 workspace: { ...readyWorkspace, sessions: [firstSession] },
207 loading: false,
208 activeSessionId: firstSession.id,
209 });
210 await useTerminalStore.getState().closeSession("close-error", firstSession.id).catch(() => {});
211 if (useTerminalStore.getState().error !== "terminal did not close") {
212 throw new Error(`terminal close error was not exposed: ${useTerminalStore.getState().error}`);
213 }
214 if (useTerminalStore.getState().workspace?.sessions[0]?.id !== firstSession.id) {
215 throw new Error("failed terminal close removed the live session from the workspace");
216 }
217 useTerminalStore.getState().clearError();
218 if (useTerminalStore.getState().error !== null) throw new Error("terminal error dismissal did not clear the error");
219 closeError = null;
220 process.stdout.write("PASS terminal close failures preserve the session and can be dismissed\n");
221
222 writeError = new Error("terminal input failed");
223 await useTerminalStore.getState().write("close-error", firstSession.id, "echo test").catch(() => {});
224 if (useTerminalStore.getState().error !== "terminal input failed") {
225 throw new Error(`terminal write error was not exposed: ${useTerminalStore.getState().error}`);
226 }
227 writeError = null;
228 process.stdout.write("PASS terminal write failures remain visible to the user\n");
229
230 if (previousWindow) globalThis.window = previousWindow;
231 else delete (globalThis as { window?: unknown }).window;
232
232 lines TYPESCRIPT