返回 DeepSeek-Reasonix
controller-event-recovery.test.ts
根目录 / desktop / frontend / src / __tests__ / controller-event-recovery.test.ts
1 import assert from "node:assert/strict";
2 import { JSDOM } from "jsdom";
3 import type { Meta, TabMeta } from "../lib/types";
4 import type { ControllerRecoveryPorts } from "../lib/controllerEventRecovery";
5
6 const dom = new JSDOM("<!doctype html><html><body></body></html>");
7 globalThis.window = dom.window as unknown as Window & typeof globalThis;
8 const { installDesktopHostStub } = await import("./desktopHostStub");
9 const { startControllerEventRecovery } = await import("../lib/controllerEventRecovery");
10 const tab = { id: "task-a", sessionPath: "new", sessionGeneration: 2 } as TabMeta;
11 let reads = 0;
12 const stub = installDesktopHostStub({ ListTabs: async () => { reads++; return [tab]; } });
13 let meta = { sessionPath: "old", sessionGeneration: 1 } as Meta;
14 const hydrations: Array<{ done(): void; current(): boolean }> = [];
15 let runtimeUpdates = 0;
16 let sessionLoadSeq = 0;
17 const ports: ControllerRecoveryPorts = {
18 navigation: () => 1,
19 bindings: () => new Map([[tab.id, JSON.stringify([meta.sessionPath, meta.sessionGeneration, sessionLoadSeq])]]),
20 meta: () => meta,
21 now: () => 0,
22 flush: () => {},
23 prepare: next => { meta = { ...meta, sessionPath: next.sessionPath, sessionGeneration: next.sessionGeneration }; },
24 reset: () => {},
25 runtime: () => { runtimeUpdates++; },
26 hydrate: (_tab, current) => {
27 sessionLoadSeq++;
28 return new Promise<void>(done => hydrations.push({ done, current }));
29 },
30 };
31 const stop = startControllerEventRecovery(ports);
32 const tick = () => new Promise(resolve => setImmediate(resolve));
33 const waitFor = async (ready: () => boolean) => {
34 for (let attempt = 0; attempt < 100 && !ready(); attempt++) await new Promise(resolve => setTimeout(resolve, 5));
35 assert.ok(ready(), "lazy recovery worker loaded");
36 };
37 assert.equal(reads, 0, "mounting does not eagerly request recovery data");
38 stub.emit("desktop:resync", { generation: "g0", reason: "gap" });
39 stub.emit("desktop:resync", { generation: "g1", reason: "generation" });
40 assert.equal(reads, 0, "signals during module loading wait for the authoritative read");
41 await waitFor(() => hydrations.length === 1);
42 assert.equal(hydrations.length, 1);
43 assert.equal(reads, 1, "loading-time signals coalesce into one fresh snapshot");
44 stub.emit("desktop:resync", { generation: "g2", reason: "generation" }); await tick();
45 assert.equal(hydrations.length, 2, "new generation reissues a superseded session hydrate even after optimistic metadata moved");
46 assert.equal(hydrations[0].current(), false);
47 hydrations[0].done(); await tick();
48 assert.equal(runtimeUpdates, 0, "old-generation hydration cannot apply runtime state");
49 hydrations[1].done(); await tick();
50 assert.equal(runtimeUpdates, 1);
51 for (const [label, replaceBinding] of [
52 ["load sequence", () => { sessionLoadSeq++; }],
53 ["session path", () => { meta = { ...meta, sessionPath: "background" }; }],
54 ["session generation", () => { meta = { ...meta, sessionGeneration: 99 }; }],
55 ] as const) {
56 meta = { ...meta, sessionPath: "old", sessionGeneration: 1 };
57 stub.emit("desktop:resync", { generation: "g2", reason: "gap" }); await tick();
58 const pending: (typeof hydrations)[number] = hydrations[hydrations.length - 1]!;
59 replaceBinding();
60 assert.equal(pending.current(), true, `${label} changes without navigation or another desktop:resync`);
61 pending.done(); await tick();
62 assert.equal(runtimeUpdates, 1, `superseded ${label} cannot commit the old runtime after hydration returns`);
63 }
64 stop();
65 const readsBeforeDispose = reads;
66 const disposeDuringLoad = startControllerEventRecovery(ports);
67 stub.emit("desktop:resync", { generation: "g3", reason: "generation" });
68 disposeDuringLoad();
69 await tick(); await tick();
70 assert.equal(reads, readsBeforeDispose, "disposing during lazy loading cannot start a recovery read");
71 stub.uninstall(); dom.window.close();
72 console.log("controller event recovery: generation and post-hydration tab bindings fence stale runtime commits");
73
73 lines TYPESCRIPT