返回 DeepSeek-Reasonix
history-read-snapshot.test.tsx
根目录 / desktop / frontend / src / __tests__ / history-read-snapshot.test.tsx
1 import assert from "node:assert/strict";
2 import { JSDOM } from "jsdom";
3 import React, { act } from "react";
4 import { createRoot } from "react-dom/client";
5 import type { ReasonixDesktopHost } from "../lib/desktopHost";
6
7 const dom = new JSDOM('<div id="root"></div>', { url: "http://localhost/" });
8 Object.assign(globalThis, { window: dom.window, document: dom.window.document, localStorage: dom.window.localStorage, IS_REACT_ACT_ENVIRONMENT: true });
9 const listeners = new Map<string, (...args: unknown[]) => void>();
10 let calls = 0;
11 let expire = false;
12 let fail = false;
13 let held: (() => void) | undefined;
14 const releases: string[] = [];
15 window.reasonixDesktop = {
16 kind: "electron", contract: { commands: ["ListHistorySessions", "SearchHistoryContent", "ReleaseReadSnapshot"] },
17 native: { window: {} },
18 on: (name: string, cb: (...args: unknown[]) => void) => { listeners.set(name, cb); return () => { listeners.delete(name); }; },
19 invoke: async (name: string, args: unknown[]) => {
20 if (name === "ReleaseReadSnapshot") { releases.push(String(args[0])); return; }
21 if (name === "ListHistorySessions") return { items: [], nextCursor: "", revision: 1, partial: false, staleCursor: false, snapshotId: "sessions" };
22 calls++;
23 const req = args[0] as { cursor: string; limit: number };
24 if (held) await new Promise<void>((resolve) => { held = resolve; });
25 if (fail) throw new Error("disk failed");
26 if (expire && req.cursor) { expire = false; return { items: [], staleCursor: true }; }
27 const offset = Number(req.cursor || 0);
28 const end = Math.min(105, offset + req.limit);
29 return { items: Array.from({ length: end - offset }, (_, i) => ({ sessionPath: "/fixture", messageIndex: offset + i, partIndex: 0, snippet: String(offset + i) })), nextCursor: end < 105 ? String(end) : "", snapshotId: "search", revision: calls, partial: false, staleCursor: false, status: { indexed: 1, total: 1 } };
30 },
31 } as unknown as ReasonixDesktopHost;
32 const { useHistoryCatalog } = await import("../lib/useHistoryCatalog");
33 let state!: ReturnType<typeof useHistoryCatalog>;
34 function Harness() { state = useHistoryCatalog({ isTrash: false, suppliedSessions: [], scope: "all", status: "all", timeFilter: "all", query: "marker" }); return <div>{state.searchHits.length}:{state.error}</div>; }
35 const root = createRoot(document.getElementById("root")!);
36 await act(async () => { root.render(<Harness />); });
37 await act(async () => { await new Promise((resolve) => setTimeout(resolve, 240)); });
38 assert.equal(state.searchHits.length, 50, "body-only result remains visible");
39 expire = true;
40 await act(async () => { state.loadMore(); });
41 assert.equal(state.searchHits.length, 100, "paired expiry rebuild preserves expanded size");
42 assert.equal(new Set(state.searchHits.map((hit) => hit.messageIndex)).size, 100);
43 fail = true;
44 await act(async () => { state.loadMore(); });
45 assert.equal(state.searchHits.length, 100, "failed append retains rendered rows");
46 assert.equal(state.error, "disk failed");
47 fail = false;
48 held = () => {};
49 await act(async () => { state.loadMore(); });
50 const before = calls;
51 await act(async () => {
52 for (let i = 0; i < 20; i++) listeners.get("history-index:changed-v1")?.({ revision: i });
53 state.loadMore();
54 });
55 assert.equal(calls, before, "continuous updates and duplicate load-more do not overlap an active read");
56 await act(async () => { const finish = held; held = undefined; finish?.(); });
57 assert.equal(state.searchHits.length, 105);
58 await act(async () => { root.unmount(); });
59 assert.ok(releases.includes("search"));
60 console.log("PASS paired history recovery, retained failure state, coalesced updates and snapshot release");
61
61 lines Plain Text