import { describe, expect, test } from "bun:test"; import { icon } from "@dihak/pix-pretty/icon-catalog"; import { compactStatus, newTpsState, rebaseTps, renderThinkingLevel, stepTps, type TpsState, } from "./footer.ts"; const theme = { fg: (_color: string, text: string) => text, getThinkingBorderColor: (level: string) => (text: string) => `<${level}>${text}`, }; describe("renderThinkingLevel", () => { test("uses the host theme's canonical thinking-level renderer", () => { expect(renderThinkingLevel(theme, "high", "high")).toBe("high"); expect(renderThinkingLevel(theme, "xhigh", "xhigh")).toBe("xhigh"); }); test("renders unknown levels with the neutral muted color", () => { const calls: string[] = []; const recordingTheme = { fg: (color: string, text: string) => { calls.push(color); return text; }, getThinkingBorderColor: theme.getThinkingBorderColor, }; expect(renderThinkingLevel(recordingTheme, "future", "future")).toBe("future"); expect(calls).toEqual(["muted"]); }); }); describe("compactStatus", () => { test("compacts current pi-lens active server lists to a count", () => { expect(compactStatus("pi-lens-lsp", "LSP Active: json, yaml, typescript", theme)).toBe( `${icon("lsp")} 3`, ); }); test("preserves active and failed counts without listing server names", () => { expect(compactStatus("pi-lens-lsp", "LSP Active: json, yaml · LSP Failed: eslint", theme)).toBe( `${icon("lsp")} 2 !1`, ); }); test("keeps compatibility with the older parenthesized count", () => { expect(compactStatus("pi-lens-lsp", "LSP Active (4)", theme)).toBe(`${icon("lsp")} 4`); }); }); describe("stepTps", () => { test("returns null on the priming tick (no prior sample)", () => { const first = stepTps(newTpsState(), 0, 0); expect(first.tps).toBeNull(); // Second tick now has a baseline to diff against. const second = stepTps(first.state, 100, 10); expect(second.tps).not.toBeNull(); }); test("converges toward a steady positive rate while tokens climb", () => { let state: TpsState = newTpsState(); let tps: number | null = null; // 100 t/s sustained: +10 tokens every 100ms tick. for (let i = 0; i <= 40; i++) { const step = stepTps(state, i * 100, i * 10); state = step.state; tps = step.tps; } expect(tps).toBe(100); }); test("first inst tick equals the raw delta rate (alpha seeds the EMA)", () => { const primed = stepTps(newTpsState(), 0, 0); // 50 tokens over 500ms → 100 t/s, EMA seeds directly to inst on first sample. const step = stepTps(primed.state, 500, 50); expect(step.tps).toBe(100); }); test("decays toward 0 when token cursor is flat (tool wait / stall)", () => { let state: TpsState = newTpsState(); // Prime + climb to a real rate. for (let i = 0; i <= 10; i++) { state = stepTps(state, i * 100, i * 10).state; } // Flat cursor: inst=0 each tick, EMA glides down. let tps: number | null = null; for (let i = 11; i <= 80; i++) { const step = stepTps(state, i * 100, 100); state = step.state; tps = step.tps; } expect(tps).toBe(0); }); test("rebase absorbs the est\u2192exact cursor snap without a spike", () => { let state: TpsState = newTpsState(); // Stream ~100 t/s off the char estimate. for (let i = 0; i <= 10; i++) { state = stepTps(state, i * 100, i * 10).state; } const steady = stepTps(state, 1100, 110).tps; expect(steady).toBe(100); // message_end: cursor jumps est(110)→exact(336) ~5ms later. Rebase, then the // next tick must NOT read a multi-thousand spike from the correction. state = rebaseTps(state, 1105, 336); const afterSnap = stepTps(state, 1205, 336).tps; expect(afterSnap).toBeLessThan(steady as number); // flat cursor → decaying, not spiking }); test("respects a custom alpha (higher = snappier)", () => { const primed = stepTps(newTpsState(), 0, 0); // alpha=1 → pure instantaneous, no smoothing. const step = stepTps(primed.state, 1000, 200, 1); expect(step.tps).toBe(200); }); });