返回 DeepSeek-Reasonix
ready-event-meta-sync.test.tsx
根目录 / desktop / frontend / src / __tests__ / ready-event-meta-sync.test.tsx
1 // Run: tsx src/__tests__/ready-event-meta-sync.test.tsx
2
3 import { JSDOM } from "jsdom";
4 import React, { act } from "react";
5 import { createRoot } from "react-dom/client";
6 import type { AppBindings } from "../lib/bridge";
7 import { useController } from "../lib/useController";
8 import type { BalanceInfo, CheckpointMeta, ContextInfo, EffortInfo, HistoryMessage, JobView, Meta, TabMeta } from "../lib/types";
9
10 let passed = 0;
11 let failed = 0;
12
13 function ok(value: boolean, label: string) {
14 if (value) {
15 process.stdout.write(` PASS ${label}\n`);
16 passed += 1;
17 } else {
18 process.stdout.write(` FAIL ${label}\n`);
19 failed += 1;
20 }
21 }
22
23 function eq(actual: unknown, expected: unknown, label: string) {
24 if (actual === expected) {
25 ok(true, label);
26 } else {
27 ok(false, `${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
28 }
29 }
30
31 function flushPromises(): Promise<void> {
32 return new Promise((resolve) => setTimeout(resolve, 0));
33 }
34
35 function deferred<T>() {
36 let resolve!: (value: T) => void;
37 let reject!: (reason?: unknown) => void;
38 const promise = new Promise<T>((res, rej) => {
39 resolve = res;
40 reject = rej;
41 });
42 return { promise, resolve, reject };
43 }
44
45 async function waitFor(label: string, predicate: () => boolean) {
46 for (let attempt = 0; attempt < 30; attempt += 1) {
47 await act(async () => {
48 await flushPromises();
49 });
50 if (predicate()) return;
51 }
52 throw new Error(`timed out waiting for ${label}`);
53 }
54
55 function tabMeta(ready: boolean): TabMeta {
56 return {
57 id: "tab-ready",
58 scope: "project",
59 workspaceRoot: "/repo",
60 workspaceName: "repo",
61 workspacePath: "/repo",
62 gitBranch: "main",
63 topicId: "topic-ready",
64 topicTitle: "Ready race",
65 sessionPath: "/repo/sessions/ready.jsonl",
66 label: "model",
67 ready,
68 running: false,
69 mode: "normal",
70 toolApprovalMode: "ask",
71 tokenMode: "full",
72 active: true,
73 cwd: "/repo",
74 };
75 }
76
77 function meta(ready: boolean): Meta {
78 return {
79 label: "model",
80 ready,
81 eventChannel: "agent:event",
82 cwd: "/repo",
83 workspaceRoot: "/repo",
84 workspaceName: "repo",
85 workspacePath: "/repo",
86 gitBranch: "main",
87 autoApproveTools: false,
88 bypass: false,
89 collaborationMode: "normal",
90 toolApprovalMode: "ask",
91 tokenMode: "full",
92 goal: "",
93 goalStatus: "stopped",
94 };
95 }
96
97 console.log("\nready event meta sync");
98
99 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
100 pretendToBeVisual: true,
101 url: "http://localhost/",
102 });
103 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
104 globalThis.window = dom.window as unknown as Window & typeof globalThis;
105 globalThis.document = dom.window.document;
106 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
107 globalThis.Node = dom.window.Node;
108 globalThis.HTMLElement = dom.window.HTMLElement;
109 globalThis.Event = dom.window.Event;
110 globalThis.CustomEvent = dom.window.CustomEvent;
111 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
112 globalThis.MouseEvent = dom.window.MouseEvent;
113 globalThis.localStorage = dom.window.localStorage;
114 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
115 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
116
117 const context: ContextInfo = { used: 0, window: 100, sessionTokens: 0 };
118 const effort: EffortInfo = { supported: true, current: "auto", default: "auto", levels: ["auto"] };
119 const balance: BalanceInfo = { available: false, display: "" };
120 const jobs: JobView[] = [];
121 const checkpoints: CheckpointMeta[] = [];
122 const readyHandlers: Array<(tabId?: string) => void> = [];
123 const historyGate = deferred<HistoryMessage[]>();
124 let backendReady = false;
125 let listTabsCalls = 0;
126 let historyCalls = 0;
127 let metaCalls = 0;
128
129 window.runtime = {
130 EventsOn: (name: string, cb: (...data: unknown[]) => void) => {
131 if (name === "agent:ready") readyHandlers.push(cb as (tabId?: string) => void);
132 return () => {};
133 },
134 BrowserOpenURL: () => {},
135 };
136 window.go = {
137 main: {
138 App: {
139 ListTabs: async () => {
140 listTabsCalls += 1;
141 return [tabMeta(backendReady)];
142 },
143 MetaForTab: async () => {
144 metaCalls += 1;
145 return meta(backendReady);
146 },
147 ContextUsageForTab: async () => context,
148 EffortForTab: async () => effort,
149 BalanceForTab: async () => balance,
150 JobsForTab: async () => jobs,
151 CheckpointsForTab: async () => checkpoints,
152 HistoryForTab: async () => historyGate.promise,
153 HistoryPageForTab: async () => {
154 historyCalls += 1;
155 const messages = await historyGate.promise;
156 return { messages, startTurn: 0, endTurn: messages.filter((message) => message.role === "user").length, totalTurns: messages.filter((message) => message.role === "user").length, hasOlder: false };
157 },
158 HistoryCheckpointTurnsForTab: async () => [],
159 ReplayPendingPrompts: async () => {},
160 } as Partial<AppBindings> as AppBindings,
161 },
162 };
163
164 type Controller = ReturnType<typeof useController>;
165 let controller: Controller | undefined;
166
167 function Probe() {
168 controller = useController();
169 return null;
170 }
171
172 const rootEl = document.getElementById("root");
173 if (!rootEl) throw new Error("missing root");
174 const root = createRoot(rootEl);
175
176 await act(async () => {
177 root.render(<Probe />);
178 await flushPromises();
179 });
180
181 await waitFor("initial not-ready metadata", () => controller?.activeTabId === "tab-ready" && controller.state.meta?.ready === false);
182 eq(historyCalls, 1, "startup begins one history hydration");
183 eq(metaCalls, 0, "startup history remains in flight before ancillary meta");
184
185 backendReady = true;
186 await act(async () => {
187 for (const handler of readyHandlers) handler("tab-ready");
188 await flushPromises();
189 });
190 await waitFor("ready metadata refreshed before history settles", () => controller?.state.meta?.ready === true);
191
192 eq(listTabsCalls >= 2, true, "ready event refreshes active tab metadata from ListTabs");
193 eq(historyCalls, 1, "ready event joins the in-flight hydrate instead of reloading history");
194 eq(metaCalls, 0, "ready event does not wait for ancillary MetaForTab before unlocking send");
195
196 await act(async () => {
197 historyGate.resolve([{ role: "user", content: "hello" }]);
198 await historyGate.promise;
199 await flushPromises();
200 });
201 await waitFor("history finishes", () => controller?.state.hydrating === false);
202 ok(controller?.state.items.some((item) => item.kind === "user" && item.text === "hello") ?? false, "history still hydrates after the ready metadata sync");
203
204 await act(async () => {
205 root.unmount();
206 });
207 dom.window.close();
208
209 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
210 if (failed > 0) process.exit(1);
211
211 lines Plain Text