Files
learnharness/tests/render-stats-bar.test.tsx
T
amosblomqvist f6626f1703 Fix wave chunk A: no Default option, composer = strong slot, catalog revalidation
Item 1 — no Default in the slot selectors (owner, fix wave): a
"default" model is not a thing in this app. Picking Default used to
delete the slot and re-arm the first-run gate — incoherent. Now the
model menus carry NO clear row at all (a slot holds a pick or reads
"—"), the thinking menus carry a "—" row (menu-none) that clears only
the thinking pin, and the hint line is the roster only — the resolved
factory pin is never named. Master's amendment confirmed: the
factory-pin hint is SUPERSEDED entirely (Platform removes the pins).

Item 2 — the composer control IS the strong slot (owner: "one and the
same"): ModelControl (model-picker.tsx) reads the raw models section
(GET /api/settings) and the federated catalog, and writes
PUT /api/settings/models via composeSlotModel — the same seam the
settings Agent section and SetupDialog use. Props retired: chatId,
settings, onSaved, fallbackModelId, noDefaultModel — the control is
self-sufficient and identical on every surface (composer, home dock,
/chat/new). The per-chat model PUT is gone from the UI; legacy
per-chat overrides keep resolving server-side. StatsBar/DockOverlay
plumbing slimmed accordingly; chat.tsx drops its per-chat settings
state and initialSettings prop.

Item 3 — the composer catalog cache no longer freezes per page load:
fetchCatalog(force) invalidates, and every model-menu open revalidates
in the background (stale-while-revalidate — cached list shows
instantly, the fresh federation swaps in). Owner's repro verified live:
a key saved mid-session widens the menu without a reload.

Item 4 — stale doc comments fixed: model-catalog.ts's "OpenRouter
catalog"/"multi-provider future" (Platform's file — mechanical,
flagged for review) and model-picker's retired "config-pinned"/
per-chat-PUT narrative.

Live-verified on 3001: unpicked slots read "—" everywhere; model
menus have no clear row; the "—" thinking row clears the pin but
keeps the model; composer pick writes strong and preserves cheap/
vision siblings; thinking pill writes strong.thinkingLevel; groq key
added mid-session appears in the open menu without reload. Caught in
live verification: the record fetch was mapping the WHOLE settings
record into the models section (picks never wrote) — fixed to read
body.settings.models.

Gates: typecheck, lint, 807/807, build.
2026-09-22 16:40:31 +03:00

86 lines
3.9 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 { 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 — everywhere when the record has no turns yet (doctrine)", () => {
const html = renderToStaticMarkup(
<StatsBar turns={[]} />,
);
assert.match(html, /data-testid="stats-bar"/);
assert.match(html, /data-testid="stats-context">—</);
assert.match(html, /data-testid="stats-cost">—</);
// 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; pi catalog limit
// 1_048_576 → "1M" (tables regenerated from pi-ai's catalog)
assert.match(html, /data-testid="stats-context">11k \/ 1M</);
// 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 also kills the context limit
assert.match(html, /data-testid="stats-context">—</);
});
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");
});
});