Files
learnharness/tests/render-stats-bar.test.tsx
T
amosblomqvist 81709afd22 Stats-bar catalog join + dock numerator consistency (context-meter close-out)
Platform's context-meter pair (04e336d, 18cc493) left two client seams
in my band; this closes the arc:

1. STATS-BAR CATALOG JOIN: contextWindow's denominator resolves ONLY
   from pi's live catalog — stats-bar now builds the provider:id ->
   contextWindow index client-side (catalogLimits in model-picker.tsx,
   the client twin of the server's catalogContextLimits join) over the
   picker's existing /api/models fetch/cache (useModelCatalog exported;
   module-level dedupe, one fetch feeds both ModelControl and the
   meter). Catalog pending or failed -> undefined index -> limit null ->
   USAGE-ONLY (Platform's interim render IS the null path; the
   honest-render precedent, never a fake window).

2. DOCK NUMERATOR CONSISTENCY: toBlobs' context fold was still
   last-step input+output only — now the same cache-inclusive footprint
   the session meter uses (all four directions; provider "input"
   EXCLUDES cacheRead/cacheWrite, so the old fold read wildly low on
   cache-heavy jobs). Same metric everywhere.

Review of Platform's interim edits in this band: verified as-shipped —
stats-bar's usage-only render (summary + popover), FeedJob.contextLimit
with the line-hides null treatment, and the render-test rewrites all
fit. Only fix needed: none beyond the above.

Tests: catalogLimits pinned pure (index shape, provider:id keying,
missing-model absence); toBlobs pinned on the cache-inclusive
numerator (10k+0.5k+120k+30k = 160.5k against a 200k feed limit).
889/889.

Live-verified on :3001: seedplan's bar reads "18.7k / 1M" (footprint
18725, catalog 1048576); a nope/unknown-model chat renders "12.8k"
usage-only with cost "-"; the popover detail matches; the dock blob's
context line renders "946 / 1M" from a server-joined feed (an
unintentionally real runner-adopted job — the full chain, live).
2026-09-24 02:15:23 +03:00

141 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { renderToStaticMarkup } from "react-dom/server";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { StatsBar } from "../components/stats-bar.tsx";
import { catalogLimits } from "../components/model-picker.tsx";
import type { ModelCatalogEntry, ModelCatalogResponse } from "../src/core/contracts.ts";
import { prettyModel } from "../components/usage-format.ts";
import type { ChatSettings, TurnUsage } from "../src/core/contracts.ts";
// The stats bar: folds only (no cost math in the component), the "—"
// doctrine for absent data, honest cost (never a fake $0), and the model +
// thinking control (components/model-picker.tsx — pi-ai catalog backed).
function step(inputTokens: number, outputTokens: number, over: Record<string, number | undefined> = {}) {
return { inputTokens, outputTokens, cacheReadTokens: undefined, cacheWriteTokens: undefined, reasoningTokens: undefined, ...over };
}
function turn(over: Partial<TurnUsage> = {}): TurnUsage {
return {
id: "t1",
messageId: "m1",
at: "2026-09-09T12:00:00Z",
model: "openrouter:z-ai/glm-5.3-flash",
steps: [step(10_000, 1_000)],
...over,
};
}
describe("stats bar", () => {
it("renders NOTHING on the left when there are no turns yet (owner, wave 7 sweep)", () => {
const html = renderToStaticMarkup(
<StatsBar turns={[]} />,
);
assert.match(html, /data-testid="stats-bar"/);
// pre-chat: no summary button, no "—" theater (the knowledge-context
// slider died with the injection — the left side is simply empty)
assert.ok(!html.includes('data-testid="stats-summary"'), "no summary button pre-chat");
assert.ok(!html.includes('data-testid="stats-context"'), "no context figure pre-chat");
assert.ok(!html.includes('data-testid="stats-cost"'), "no cost figure pre-chat");
// no turns, no settings, catalog not loaded → both control labels
// "—" (the thinking button shows just the level, like the model
// button shows just the model name — no prefix, no dot separator)
assert.match(html, /data-testid="stats-model"[^>]*>\s*<span[^>]*>—<\/span>/);
assert.match(html, /data-testid="stats-thinking"[^>]*>\s*<span[^>]*>—<\/span>/);
assert.ok(!html.includes("aria-hidden>·"), "no dot between the controls");
// the control reads as a button: pill border + dropdown chevron
assert.match(html, /data-testid="stats-model"[^>]*rounded-full[^>]*>/);
assert.match(html, /▾/);
});
it("folds context + cost from the turns log (core/prices, no local math)", () => {
const html = renderToStaticMarkup(
<StatsBar turns={[turn(), turn()]} />,
);
// last turn's last call: 10k in + 1k out = 11k. The LIMIT is the
// live catalog's — and the bar doesn't hold the index yet (the
// catalog join lands with the UI chunk), so the meter renders
// USAGE-ONLY (owner amendment: never a fallback window).
assert.match(html, /data-testid="stats-context">11k</);
// pi catalog price 0.075/0.25 per 1M: each turn 10k×0.075 + 1k×0.25
// = $0.001 → two = $0.0020
assert.match(html, /data-testid="stats-cost">\$0\.0020</);
// model button label: SSR (record fetch not run yet) reads "—" —
// the strong slot arrives client-side via GET /api/settings; the
// turn log below still folds the ACTUAL models billed
assert.match(html, /data-testid="stats-model"[^>]*>\s*<span[^>]*>—<\/span>/);
});
it("shows — for cost when every turn is unpriceable, never a fake $0", () => {
const html = renderToStaticMarkup(
<StatsBar turns={[turn({ model: "nope/unknown" })]} />,
);
assert.match(html, /data-testid="stats-cost">—</);
// unknown model: no catalog limit exists, but the footprint is
// still an honest reading — usage-only (never "—" theater over
// real data, never a fake window)
assert.match(html, /data-testid="stats-context">11k</);
});
it("prettyModel strips vendor + date suffix, capitalizes", () => {
assert.equal(prettyModel("z-ai/glm-5.3-flash"), "Glm 5.3 Flash");
assert.equal(prettyModel("qwen/qwen3.8-max-0902"), "Qwen3.8 Max");
});
it("provider-qualified stamps (usage contract, model-slots wave)", () => {
// qualified: the provider rides as a quiet suffix — federation makes
// the same model via two providers different bookkeeping
assert.equal(prettyModel("openrouter:z-ai/glm-5.3-flash", true), "Glm 5.3 Flash · openrouter");
assert.equal(prettyModel("openai:gpt-5.2", true), "Gpt 5.2 · openai");
// unqualified on a qualified stamp: provider silently dropped (old shape)
assert.equal(prettyModel("openrouter:z-ai/glm-5.3-flash"), "Glm 5.3 Flash");
// qualified on a bare id: no suffix invented
assert.equal(prettyModel("z-ai/glm-5.3-flash", true), "Glm 5.3 Flash");
});
});
describe("catalogLimits (the stats-bar live-limits join, pure)", () => {
// The client-side twin of the server's catalogContextLimits join:
// provider:id → contextWindow straight from the cached /api/models
// payload. A static render holds no catalog (the fetch is a live
// concern — Playwright verifies the wiring), so the pure index shape
// is pinned here: one entry per model, the exact key contextWindow()
// looks up, missing models absent (→ limit null → usage-only).
const entry = (over: Pick<ModelCatalogEntry, "id" | "provider" | "contextWindow">): ModelCatalogEntry => ({
name: over.id,
reasoning: false,
input: ["text"],
supportedLevels: [],
cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
maxTokens: 32_768,
...over,
});
const catalog: Pick<ModelCatalogResponse, "models"> = {
models: [
entry({ id: "z-ai/glm-5.3-flash", provider: "openrouter", contextWindow: 1_048_576 }),
entry({ id: "gpt-x", provider: "openai", contextWindow: 400_000 }),
],
};
it("builds provider:id → contextWindow over every catalog model", () => {
const limits = catalogLimits(catalog);
assert.equal(limits.get("openrouter:z-ai/glm-5.3-flash"), 1_048_576);
assert.equal(limits.get("openai:gpt-x"), 400_000);
assert.equal(limits.size, 2);
});
it("a model missing from the index is absent — limit null, usage-only (never a fallback)", () => {
const limits = catalogLimits(catalog);
assert.equal(limits.has("nope/unknown"), false);
});
it("same provider+model on different providers keys apart", () => {
const twin: Pick<ModelCatalogResponse, "models"> = {
models: [...catalog.models, entry({ id: "z-ai/glm-5.3-flash", provider: "zai", contextWindow: 200_000 })],
};
const limits = catalogLimits(twin);
assert.equal(limits.get("openrouter:z-ai/glm-5.3-flash"), 1_048_576);
assert.equal(limits.get("zai:z-ai/glm-5.3-flash"), 200_000);
});
});