返回 DeepSeek-Reasonix
context-panel-refresh.test.tsx
根目录 / desktop / frontend / src / __tests__ / context-panel-refresh.test.tsx
1 import assert from "node:assert/strict";
2 import { mock } from "node:test";
3 import { JSDOM } from "jsdom";
4 import React, { act } from "react";
5 import { createRoot } from "react-dom/client";
6 import { ContextPanel } from "../components/ContextPanel";
7 import { LocaleProvider } from "../lib/i18n";
8 import { installDesktopHostStub } from "./desktopHostStub";
9 import type { ContextPanelInfo } from "../lib/types";
10
11 const dom = new JSDOM('<html><body><div id="root"></div></body></html>', { url: "http://localhost", pretendToBeVisual: true });
12 Object.assign(globalThis, { window: dom.window, document: dom.window.document, Node: dom.window.Node,
13 HTMLElement: dom.window.HTMLElement, Event: dom.window.Event, IS_REACT_ACT_ENVIRONMENT: true,
14 ResizeObserver: class { observe() {} unobserve() {} disconnect() {} } });
15 mock.timers.enable({ apis: ["Date", "setTimeout"], now: 10000 });
16
17 function panel(tokens: number): ContextPanelInfo {
18 return { usedTokens: 100, windowTokens: 1000, promptTokens: 100, completionTokens: 10,
19 totalTokens: tokens, reasoningTokens: 0, cacheHitTokens: 0, cacheMissTokens: 100,
20 sessionCacheHitTokens: 0, sessionCacheMissTokens: 100, sessionCompletionTokens: 10,
21 requestCount: tokens / 1000, elapsedMs: 1000, sessionCost: tokens / 1000,
22 readFiles: [], changedFiles: [] };
23 }
24 function deferred<T>() {
25 let resolve!: (value: T) => void;
26 const promise = new Promise<T>(r => { resolve = r; });
27 return { promise, resolve };
28 }
29 let info = panel(1000);
30 let calls = 0;
31 let load = () => Promise.resolve(info);
32 installDesktopHostStub({ ContextPanel: () => { calls++; return load(); } });
33 const root = createRoot(document.getElementById("root")!);
34 let tabId = "tab-a", sessionGen = 1, refreshKey = 0, usageSeq = 0, total = 1000;
35 async function render() {
36 await act(async () => {
37 root.render(<LocaleProvider><ContextPanel tabId={tabId} sessionGen={sessionGen}
38 refreshKey={refreshKey} usageSeq={usageSeq} sessionTokens={total}
39 context={{ used: 100, window: 1000, sessionTokens: total }} /></LocaleProvider>);
40 });
41 }
42 async function tick(ms: number) { await act(async () => { mock.timers.tick(ms); }); }
43 const metrics = () => [...document.querySelectorAll(".context-panel__summary-rows .context-panel__mini-stat strong")].map(el => el.textContent);
44
45 try {
46 await render();
47 assert.equal(calls, 1, "mount takes one snapshot");
48 await tick(1000);
49 info = panel(2000); total = 2000; usageSeq++;
50 await render(); await tick(0);
51 assert.equal(metrics()[3], "2");
52 const beforeBurst = calls;
53 await tick(100);
54 info = panel(3000); total = 3000; usageSeq++;
55 await render();
56 assert.equal(metrics()[4], "3,000", "live cumulative total wins before private snapshot refresh");
57 assert.equal(calls, beforeBurst, "burst is throttled");
58 await tick(900);
59 assert.equal(calls, beforeBurst + 1, "last usage schedules a trailing refresh without another event");
60 assert.equal(metrics()[3], "3", "request count catches up on trailing refresh");
61
62 // A full refresh inside the bridge coalescing window must fetch fresh data.
63 info = panel(4000); total = 4000; refreshKey++;
64 await render();
65 assert.equal(metrics()[3], "4", "explicit refresh does not reuse the bridge's old 200ms answer");
66
67 // Same-tab session rotation and rapid A -> B -> C navigation, with the old
68 // response deliberately arriving last, must not paint the newest session.
69 const old = deferred<ContextPanelInfo>();
70 load = () => old.promise; refreshKey++;
71 await render();
72 sessionGen++; total = 0;
73 const next = deferred<ContextPanelInfo>(); load = () => next.promise;
74 await render();
75 assert.equal(metrics()[3], "-", "same-tab rotation immediately hides previous-session metrics");
76 const middle = deferred<ContextPanelInfo>(); tabId = "tab-b"; load = () => middle.promise;
77 await render();
78 tabId = "tab-c"; info = panel(7000); total = 7000; load = () => Promise.resolve(info);
79 await render();
80 await act(async () => { middle.resolve(panel(6000)); next.resolve(panel(5000)); old.resolve(panel(99000)); });
81 assert.equal(metrics()[3], "7", "late old requests cannot overwrite final tab");
82
83 // The last trailing timer must also be cancelled when the panel disappears.
84 usageSeq++; await render();
85 const beforeUnmount = calls;
86 await act(async () => root.unmount());
87 await tick(1000);
88 assert.equal(calls, beforeUnmount, "unmount cancels pending refresh");
89 console.log("context panel refresh: burst, bridge boundary, rotation, rapid switch and unmount passed");
90 } finally {
91 mock.timers.reset();
92 dom.window.close();
93 }
94
94 lines Plain Text