返回 DeepSeek-Reasonix
runtime-status-lifecycle.test.tsx
根目录 / desktop / frontend / src / __tests__ / runtime-status-lifecycle.test.tsx
1 import assert from "node:assert/strict";
2 import React, { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import { JSDOM } from "jsdom";
5 import { createPollingOwner, type PollClock } from "../app-runtime/pollingOwner";
6 import { useRuntimeStatus } from "../app-runtime/useRuntimeStatus";
7 import type { BackgroundRuntimeView, WorkspaceConflictView } from "../lib/types";
8 import { installDesktopHostStub } from "./desktopHostStub";
9
10 function deferred<T>() { let resolve!: (value: T) => void; let reject!: (error: unknown) => void;
11 const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; }
12 let nextTimer = 0;
13 const timers = new Map<number, () => void>();
14 const clock: PollClock = { setTimeout: callback => { const id = ++nextTimer; timers.set(id, callback); return id; },
15 clearTimeout: handle => { timers.delete(handle as number); } };
16 const fire = () => { const queued = [...timers.values()]; timers.clear(); for (const callback of queued) callback(); };
17 let gate = deferred<number>(); let reads = 0; let operations = 0;
18 const results: number[] = []; const errors: unknown[] = [];
19 const owner = createPollingOwner({ target: { kind: "application" }, periodMs: 1000, clock,
20 read: () => { reads++; return gate.promise; }, publish: value => results.push(value), failed: error => errors.push(error),
21 }, delta => { operations += delta; });
22 const first = owner.refresh();
23 assert.equal(owner.refresh(), first, "manual refresh and timer share one in-flight request");
24 fire(); assert.equal(reads, 1); assert.equal(operations, 1);
25 gate.resolve(1); await first;
26 assert.deepEqual(results, [1]); assert.equal(operations, 0); assert.equal(timers.size, 1);
27 gate = deferred<number>(); fire(); assert.equal(reads, 2);
28 const second = owner.refresh(); gate.reject("fixture failure"); await second;
29 assert.deepEqual(errors, ["fixture failure"]); assert.equal(operations, 0);
30 const staleTimer = [...timers.values()][0];
31 owner.dispose(); owner.dispose(); staleTimer(); await owner.refresh();
32 assert.equal(reads, 2); assert.equal(timers.size, 0);
33
34 const dom = new JSDOM("<div id='root'></div>");
35 Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true });
36 const background = deferred<BackgroundRuntimeView[]>();
37 const requests: { tab: string; value: ReturnType<typeof deferred<WorkspaceConflictView>> }[] = [];
38 let backgroundReads = 0;
39 installDesktopHostStub({
40 BackgroundRuntimes: () => { backgroundReads++; return background.promise; },
41 WorkspaceConflictForTab: (tab: string) => { const value = deferred<WorkspaceConflictView>(); requests.push({ tab, value }); return value.promise; },
42 });
43 const root = createRoot(document.getElementById("root")!);
44 let current!: ReturnType<typeof useRuntimeStatus>;
45 function Probe({ tab, generation = 1, running = true }: { tab: string; generation?: number; running?: boolean }) {
46 current = useRuntimeStatus({ tabId: tab, sessionKey: `${tab}:${generation}`, running }, clock);
47 return <div>{current.workspaceConflict?.ownerTitle ?? "clear"}</div>;
48 }
49 const paint = (tab: string, generation = 1, running = true) => act(async () => root.render(<Probe tab={tab} generation={generation} running={running} />));
50 const conflict = (ownerTitle: string) => ({ state: "local", ownerTitle } as WorkspaceConflictView);
51 try {
52 await paint("A");
53 const refresh = current.refreshBackgroundRuntimes;
54 const manual = refresh(); fire();
55 assert.equal(backgroundReads, 1);
56 await paint("B"); await paint("A", 2);
57 assert.deepEqual(requests.map(request => request.tab), ["A", "B", "A"]);
58 await act(async () => {
59 requests[0].value.resolve(conflict("old A")); requests[1].value.resolve(conflict("B"));
60 requests[2].value.resolve(conflict("new A")); background.resolve([]); await manual;
61 });
62 assert.equal(document.body.textContent, "new A", "only the current resource generation publishes a conflict");
63 await paint("A", 2, false);
64 assert.equal(document.body.textContent, "clear");
65 await act(async () => current.setWorkspaceConflict(conflict("fixture decision")));
66 assert.equal(document.body.textContent, "fixture decision", "explicit decision fixtures remain reachable without a running turn");
67 await act(async () => current.setWorkspaceConflict(null));
68 const queuedTimers = [...timers.values()];
69 await act(async () => root.unmount());
70 assert.equal(timers.size, 0);
71 queuedTimers.forEach(callback => callback()); await refresh();
72 assert.equal(backgroundReads, 1, "queued timer and retained manual refresh are inert after unmount");
73 assert.equal(requests.length, 3);
74 console.log("runtime polling: single flight, deterministic timers, terminal counts, source replacement and synchronous disposal passed");
75 } finally { dom.window.close(); }
76
76 lines Plain Text