返回 DeepSeek-Reasonix
topic-summary-commands.test.tsx
根目录 / desktop / frontend / src / __tests__ / topic-summary-commands.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 { useTopicSummary } from "../app-runtime/useTopicSummary";
6 import type { TabMeta } from "../lib/types";
7 import { installDesktopHostStub } from "./desktopHostStub";
8
9 const dom = new JSDOM("<div id='root'></div>");
10 Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true });
11 const root = createRoot(document.getElementById("root")!);
12
13 function deferred<T>() {
14 let resolve!: (value: T) => void;
15 let reject!: (error: unknown) => void;
16 const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no; });
17 return { promise, resolve, reject };
18 }
19
20 function tab(topicId: string, scope = "project", workspaceRoot = "/repo"): TabMeta {
21 return { id: `tab-${topicId}`, scope, workspaceRoot, topicId } as TabMeta;
22 }
23
24 const requests: { scope: string; workspaceRoot: string; topicId: string }[] = [];
25 const gates = new Map<string, ReturnType<typeof deferred<{ turns?: number }>>>();
26 let failWith: Error | null = null;
27 installDesktopHostStub(({
28 main: {
29 App: {
30 GetTopicSummary: (request: { scope: string; workspaceRoot: string; topicId: string }) => {
31 requests.push(request);
32 if (failWith) return Promise.reject(failWith);
33 const gate = gates.get(request.topicId);
34 return gate ? gate.promise : Promise.resolve({ turns: request.topicId.length });
35 },
36 },
37 },
38 }).main.App);
39
40 let states!: ReturnType<typeof useTopicSummary>;
41 function Probe({ target, revision = 0 }: { target?: TabMeta; revision?: number }) {
42 states = useTopicSummary({ activeTab: target, revision });
43 return null;
44 }
45 const paint = (props?: { target?: TabMeta; revision?: number }) => act(async () => root.render(<Probe {...props} />));
46
47 try {
48 await paint();
49 assert.equal(states.activeTopicTurns, undefined, "no active tab yields no turns");
50 assert.equal(requests.length, 0, "no active tab issues no summary request");
51
52 await paint({ target: tab("alpha") });
53 assert.equal(states.activeTopicTurns, 5, "a topic target resolves its turn count");
54 assert.deepEqual(requests, [{ scope: "project", workspaceRoot: "/repo", topicId: "alpha" }],
55 "project topics fetch with their workspace root");
56
57 await paint({ target: tab("alpha"), revision: 1 });
58 assert.equal(requests.length, 2, "a project revision refetches the same topic identity");
59 assert.equal(states.activeTopicTurns, 5, "refetch keeps the resolved turns");
60
61 gates.set("beta", deferred());
62 gates.set("gamma", deferred());
63 await paint({ target: tab("beta"), revision: 2 });
64 await paint({ target: tab("gamma"), revision: 3 });
65 await act(async () => { gates.get("beta")!.resolve({ turns: 99 }); });
66 assert.equal(states.activeTopicTurns, 5, "a superseded topic identity cannot overwrite the committed turns");
67 await act(async () => { gates.get("gamma")!.resolve({ turns: 7 }); });
68 assert.equal(states.activeTopicTurns, 7, "the newest topic identity owns the committed turns");
69 assert.deepEqual(requests.map((r) => r.topicId), ["alpha", "alpha", "beta", "gamma"], "every identity change fetches exactly once");
70
71 failWith = new Error("summary offline");
72 await paint({ target: tab("delta"), revision: 4 });
73 assert.equal(states.activeTopicTurns, undefined, "a failed fetch clears the turns");
74 failWith = null;
75
76 await paint({ target: tab("global-1", "global") });
77 assert.deepEqual(requests.at(-1), { scope: "global", workspaceRoot: "", topicId: "global-1" },
78 "global topics fetch without a workspace root");
79
80 await act(async () => root.unmount());
81 console.log("topic summary commands: identity memo, single-flight fetch, revision refetch and failure clearing passed");
82 } finally { dom.window.close(); }
83
83 lines Plain Text