| 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 | Object.defineProperty(globalThis, "navigator", { value: { language: "en-US" }, configurable: true }); |
| 16 | const start = 1_000_000; |
| 17 | mock.timers.enable({ apis: ["Date", "setTimeout", "setInterval"], now: start }); |
| 18 | |
| 19 | let info: ContextPanelInfo = { |
| 20 | usedTokens: 100, windowTokens: 1000, promptTokens: 100, completionTokens: 10, |
| 21 | totalTokens: 1000, reasoningTokens: 0, cacheHitTokens: 0, cacheMissTokens: 100, |
| 22 | sessionCacheHitTokens: 0, sessionCacheMissTokens: 100, sessionCompletionTokens: 10, |
| 23 | requestCount: 1, elapsedMs: 10_000, activeTurnStartedAt: start - 5_000, sessionCost: 1, |
| 24 | readFiles: [], changedFiles: [], |
| 25 | }; |
| 26 | let calls = 0; |
| 27 | installDesktopHostStub({ ContextPanel: () => { calls++; return Promise.resolve(info); } }); |
| 28 | const root = createRoot(document.getElementById("root")!); |
| 29 | let refreshKey = 0; |
| 30 | async function render() { |
| 31 | await act(async () => { |
| 32 | root.render(<LocaleProvider><ContextPanel tabId="tab-a" sessionGen={1} refreshKey={refreshKey} |
| 33 | context={{ used: 100, window: 1000, sessionTokens: 1000 }} /></LocaleProvider>); |
| 34 | }); |
| 35 | } |
| 36 | async function tick(ms: number) { await act(async () => { mock.timers.tick(ms); }); } |
| 37 | const runtime = () => document.querySelectorAll(".context-panel__summary-rows .context-panel__mini-stat strong")[2]?.textContent; |
| 38 | |
| 39 | try { |
| 40 | await render(); |
| 41 | assert.equal(runtime(), "15s", "completed turns plus the running turn so far"); |
| 42 | const fetched = calls; |
| 43 | // Tool execution: no usage event arrives, so no snapshot is refetched. |
| 44 | await tick(60_000); |
| 45 | assert.equal(calls, fetched, "the runtime advances without polling the host"); |
| 46 | assert.equal(runtime(), "1m 15s", "a running turn keeps counting between model requests"); |
| 47 | |
| 48 | info = { ...info, elapsedMs: 80_000, activeTurnStartedAt: undefined }; |
| 49 | refreshKey++; |
| 50 | await render(); |
| 51 | await tick(30_000); |
| 52 | assert.equal(runtime(), "1m 20s", "an idle session holds its accumulated runtime"); |
| 53 | console.log("context panel runtime: running turn ticks between requests, idle session holds"); |
| 54 | } finally { |
| 55 | await act(async () => root.unmount()); |
| 56 | mock.timers.reset(); |
| 57 | dom.window.close(); |
| 58 | } |
| 59 |