返回 DeepSeek-Reasonix
heartbeat-mutation-queue.test.ts
根目录 / desktop / frontend / src / __tests__ / heartbeat-mutation-queue.test.ts
1 // Run: tsx src/__tests__/heartbeat-mutation-queue.test.ts
2
3 import { JSDOM } from "jsdom";
4 import type { HeartbeatTask } from "../custom/features/heartbeat/heartbeat.types";
5
6 let passed = 0;
7 let failed = 0;
8
9 function ok(value: unknown, label: string) {
10 if (value) {
11 process.stdout.write(` PASS ${label}\n`);
12 passed += 1;
13 } else {
14 process.stdout.write(` FAIL ${label}\n`);
15 failed += 1;
16 }
17 }
18
19 function flush(): Promise<void> {
20 return new Promise((resolve) => setTimeout(resolve, 0));
21 }
22
23 const dom = new JSDOM("<!doctype html><html><body></body></html>", { url: "http://localhost/" });
24 globalThis.window = dom.window as unknown as Window & typeof globalThis;
25 globalThis.document = dom.window.document;
26
27 type ConfigUpdate = { revision: number; etag: string; tasks: HeartbeatTask[] };
28 type SaveCall = ConfigUpdate & { resolve: (view: ConfigUpdate) => void };
29
30 const initial: HeartbeatTask[] = [
31 { id: "a", title: "A", prompt: "a", interval: "1h", enabled: true },
32 { id: "b", title: "B", prompt: "b", interval: "1h", enabled: true },
33 ];
34 const saveCalls: SaveCall[] = [];
35
36 const { installDesktopHostStub } = await import("./desktopHostStub");
37 installDesktopHostStub({
38 async HeartbeatReloadConfig() {
39 return { revision: 1, etag: "etag-1", tasks: initial };
40 },
41 HeartbeatSaveConfig(update: ConfigUpdate) {
42 return new Promise<ConfigUpdate>((resolve) => saveCalls.push({ ...update, resolve }));
43 },
44 });
45
46 const { heartbeatListTasks, heartbeatMutateTasks } = await import("../custom/features/heartbeat/heartbeat.bridge");
47
48 console.log("\nheartbeat mutation queue");
49
50 await heartbeatListTasks();
51 const toggle = heartbeatMutateTasks((tasks) => tasks.map((task) => (
52 task.id === "a" ? { ...task, enabled: !task.enabled } : task
53 )));
54 const remove = heartbeatMutateTasks((tasks) => tasks.filter((task) => task.id !== "b"));
55 await flush();
56
57 ok(saveCalls.length === 1, "only one full-table mutation is in flight");
58 ok(saveCalls[0]?.revision === 1 && saveCalls[0]?.etag === "etag-1", "first mutation uses the loaded config token");
59 const firstTasks = saveCalls[0]?.tasks ?? [];
60 saveCalls[0]?.resolve({ revision: 2, etag: "etag-2", tasks: firstTasks });
61 await flush();
62
63 ok(saveCalls.length === 2, "second mutation starts after the first save settles");
64 ok(saveCalls[1]?.revision === 2 && saveCalls[1]?.etag === "etag-2", "second mutation uses the new config token");
65 ok(saveCalls[1]?.tasks.length === 1 && saveCalls[1]?.tasks[0]?.id === "a", "second mutation rebases deletion onto the saved task list");
66 ok(saveCalls[1]?.tasks[0]?.enabled === false, "rebased deletion preserves the earlier toggle");
67 const secondTasks = saveCalls[1]?.tasks ?? [];
68 saveCalls[1]?.resolve({ revision: 3, etag: "etag-3", tasks: secondTasks });
69
70 const [afterToggle, afterDelete] = await Promise.all([toggle, remove]);
71 ok(afterToggle.find((task) => task.id === "a")?.enabled === false, "first caller receives its authoritative save");
72 ok(afterDelete.length === 1 && afterDelete[0]?.id === "a", "second caller receives the combined authoritative state");
73
74 dom.window.close();
75 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
76 if (failed > 0) process.exit(1);
77
77 lines TYPESCRIPT