import { renderToStaticMarkup } from "react-dom/server"; import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { ActivitySection, formatElapsed, toBlobs, type ActivityFeed } from "../components/activity-dock.tsx"; import { formatTokens, formatUsd } from "../components/usage-format.ts"; // The section's pure layer: folds, formatting, and blob order — then the SSR // render (blobs, "—" doctrine, the abort affordance, the always-present // header — the sidebar's background-agents strip). const NOW = Date.parse("2026-09-09T12:00:00Z"); /** A full CallUsage (its fields are undefined-valued, not optional). */ function step(inputTokens: number, outputTokens: number) { return { inputTokens, outputTokens, cacheReadTokens: undefined, cacheWriteTokens: undefined, reasoningTokens: undefined }; } function job(over: Partial = {}): ActivityFeed["jobs"][number] { return { id: "job-1", kind: "research", chatId: "c1", toolCallId: "t1", status: "running", activity: 'web_search: "sigma.js v3"', createdAt: new Date(NOW - 45_000).toISOString(), updatedAt: new Date(NOW - 1000).toISOString(), title: "Limits lesson", contextLimit: null, ...over, }; } describe("activity dock pure layer", () => { it("merges jobs + extractions oldest-first (the stable blob order)", () => { const feed: ActivityFeed = { jobs: [job({ id: "new", createdAt: new Date(NOW - 10_000).toISOString() })], extractions: [{ chatId: "cx", title: "Old lesson", phase: "auditing", startedAt: NOW - 100_000 }], }; const blobs = toBlobs(feed); assert.equal(blobs.length, 2); const first = blobs[0]!; assert.equal(first.id, "cx"); // extraction started first assert.equal(first.process, "extraction"); assert.equal(first.phase, "auditing"); assert.equal(blobs[1]!.id, "new"); }); it("folds cost with jobCost; absent usage is null, never zero", () => { const priced = job({ usage: { model: "openrouter:z-ai/glm-5.3-flash", inputTokens: 10_000, outputTokens: 1000, cacheReadTokens: 5000, cacheWriteTokens: 0, reasoningTokens: 0, steps: 2, }, }); const blob = toBlobs({ jobs: [priced], extractions: [] })[0]!; // (10000-5000)/1e6*0.075 + 5000/1e6*0.015 + 1000/1e6*0.25 = 0.0007 // (pi-ai catalog prices) assert.ok(blob.cost !== null && blob.cost > 0.0006 && blob.cost < 0.0008); const absent = toBlobs({ jobs: [job()], extractions: [] })[0]!; assert.equal(absent.cost, null); }); it("a models-refused job reads as info — 'needs model setup', not 'failed' (BYOK, 2026-09-22)", () => { const refused = job({ status: "error", activity: "failed", error: "No model configured — set up your models first (missing: vision)", }); const blob = toBlobs({ jobs: [refused], extractions: [] })[0]!; assert.equal(blob.activity, "needs model setup"); // A real failure keeps the runner's honest wording. const crashed = job({ status: "error", activity: "failed", error: "FAILED: headless crashed" }); const real = toBlobs({ jobs: [crashed], extractions: [] })[0]!; assert.equal(real.activity, "failed"); }); it("context line = LAST step's tokens vs the model limit, never a cumulative sum", () => { const base = { model: "openrouter:z-ai/glm-5.3-flash", inputTokens: 400_000, // running total (much bigger — must NOT feed the line) outputTokens: 8_000, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0, steps: 5, }; const withLast = job({ usage: { ...base, lastStep: step(90_000, 2_000) }, // The amendment (owner 2026-09-23): the limit is the server's // live-catalog join riding the feed — the dock never resolves it. contextLimit: 131_072, }); const blob = toBlobs({ jobs: [withLast], extractions: [] })[0]!; assert.equal(blob.context?.used, 92_000); assert.equal(blob.context?.limit, 131_072); // No lastStep yet (capture pending) → "—", even though usage exists. const stripped = toBlobs({ jobs: [job({ usage: { ...base } })], extractions: [] })[0]!; assert.equal(stripped.context, null); // Model missing from the live catalog (historical session, // package update) → the feed carries null → no context line. const uncatalogued = toBlobs({ jobs: [job({ usage: { ...base, model: "nope/unknown", lastStep: step(5, 5), } })], extractions: [], })[0]!; assert.equal(uncatalogued.context, null); // The numerator is the same cache-inclusive fold the session meter // uses (context-meter audit, owner 2026-09-23): provider "input" // EXCLUDES cacheRead/cacheWrite, so input+output alone reads wildly // low on cache-heavy jobs — 10k+0.5k+120k+30k = 160.5k, not 10.5k. const cached = toBlobs({ jobs: [ job({ usage: { ...base, lastStep: { inputTokens: 10_000, outputTokens: 500, cacheReadTokens: 120_000, cacheWriteTokens: 30_000, reasoningTokens: undefined, }, }, contextLimit: 200_000, }), ], extractions: [], })[0]!; assert.equal(cached.context?.used, 160_500); assert.equal(cached.context?.limit, 200_000); }); it("extraction blobs carry no money/context (—, the doctrine)", () => { const blob = toBlobs({ jobs: [], extractions: [{ chatId: "cx", title: "T", phase: "extracting", startedAt: NOW }], })[0]!; assert.equal(blob.cost, null); assert.equal(blob.context, null); }); it("formatElapsed ticks compactly", () => { assert.equal(formatElapsed(45_000), "45s"); assert.equal(formatElapsed(184_000), "3m 04s"); assert.equal(formatElapsed(4_320_000), "1h 12m"); }); it("formatUsd keeps four decimals under $1, two above", () => { assert.equal(formatUsd(0.0014), "$0.0014"); assert.equal(formatUsd(1.5), "$1.50"); }); it("formatTokens trims (never 38.0k)", () => { assert.equal(formatTokens(38_000), "38k"); assert.equal(formatTokens(1_234_567), "1.2M"); assert.equal(formatTokens(950), "950"); }); }); describe("activity section render", () => { it("renders the ALWAYS-PRESENT header when idle — empty means empty, nothing hides", () => { const html = renderToStaticMarkup(); assert.match(html, /data-testid="activity-section"/); assert.match(html, /Background agents/); assert.doesNotMatch(html, /activity-blob-(job|extraction)/, "no rows while idle"); assert.doesNotMatch(html, /activity-total-cost/, "the cost span is population, not decoration"); }); it("renders one blob per running process with cost, elapsed, and the abort affordance", () => { const html = renderToStaticMarkup( , ); assert.match(html, /data-testid="activity-section"/); assert.match(html, /data-testid="activity-blob-job"/); assert.match(html, /data-testid="activity-blob-extraction"/); assert.match(html, /Chain-rule demo/); assert.match(html, /building sandbox…/); assert.match(html, /Extraction — merging/); // Unpriced job → "—", never $0.0000 (doctrine); both blobs show it. assert.match(html, /activity-blob-cost[^>]*>—]*>— { const usage = { model: "openrouter:z-ai/glm-5.3-flash", inputTokens: 10_000, outputTokens: 1000, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0, steps: 1, }; const html = renderToStaticMarkup( , ); // two identical priced jobs → 2 × $0.001 = $0.0020 total (pi prices) assert.match(html, /activity-total-cost[^>]*>\$0\.0020