/** * KHAELOR * File: tests/tui/status.test.ts * Description: Status bar width-degradation tests and agent status line tests (real timers, no fabrication). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { describe, expect, it } from "vitest"; import { renderStatusBar } from "../../src/tui/components/status-bar.js"; import type { StatusBarData } from "../../src/tui/components/status-bar.js"; import { renderStatusLine } from "../../src/tui/components/status-line.js"; import { stripAnsi } from "../../src/tui/renderer/ansi.js"; import { resolveTheme } from "../../src/tui/theme.js"; const mono = resolveTheme({ colorDepth: "mono" }); const FULL: StatusBarData = { branch: "main", dirty: { added: 4, removed: 1 }, model: "claude-sonnet-4-5", contextPct: 31, costUsd: 0.42, processCount: 2, }; describe("renderStatusBar degradation", () => { it("width ≥ 110: all segments, full model id, context gauge (TUI v2 §3)", () => { expect(renderStatusBar(FULL, 120, mono)).toBe( " main +4 −1 │ claude-sonnet-4-5 │ ▰▰▱▱▱▱▱▱ 31% │ $0.42 │ ● 2", ); }); it("width 100–109: context falls back to the worded percent", () => { expect(renderStatusBar(FULL, 105, mono)).toBe( " main +4 −1 │ claude-sonnet-4-5 │ context 31% │ $0.42 │ ● 2", ); }); it("phase ribbon leads the cockpit when a phase is active (v2 §1)", () => { const bar = renderStatusBar({ ...FULL, phase: "design" }, 130, mono); expect(bar).toContain("◑ DESIGN"); expect(bar.indexOf("◑")).toBeLessThan(bar.indexOf("main")); }); it("width 80–99: model shortens to its alias", () => { expect(renderStatusBar(FULL, 90, mono)).toBe( " main +4 −1 │ sonnet │ context 31% │ $0.42 │ ● 2", ); }); it("width 60–79: cost and processes drop whole", () => { expect(renderStatusBar(FULL, 70, mono)).toBe(" main +4 −1 │ sonnet │ 31%"); }); it("width < 60: branch and context percent only", () => { expect(renderStatusBar(FULL, 50, mono)).toBe(" main │ 31%"); }); it("segments with no data are absent, not zeroed", () => { expect(renderStatusBar({ model: "claude-sonnet-4-5" }, 120, mono)).toBe( " claude-sonnet-4-5", ); expect(renderStatusBar({}, 120, mono)).toBe(" "); }); it("processes drop when none are running", () => { const bar = renderStatusBar({ ...FULL, processCount: 0 }, 120, mono); expect(bar).not.toContain("●"); }); it("context pressure appends the /compact nudge at ≥ 80%", () => { const gauge = renderStatusBar({ ...FULL, contextPct: 85 }, 120, mono); expect(gauge).toContain("85% · /compact"); const worded = renderStatusBar({ ...FULL, contextPct: 85 }, 105, mono); expect(worded).toContain("context 85% · /compact"); }); it("queued steering shows its count", () => { const bar = renderStatusBar({ ...FULL, queuedCount: 1 }, 120, mono); expect(bar).toContain("⋯ 1 queued"); }); it("never exceeds the terminal width", () => { for (const width of [40, 60, 80, 100, 120]) { expect(stripAnsi(renderStatusBar(FULL, width, mono)).length).toBeLessThanOrEqual(width); } }); }); describe("renderStatusLine", () => { it("collapses to nothing when idle", () => { expect(renderStatusLine({ kind: "idle", startedAt: 0 }, 5000, mono, 80)).toBeNull(); }); it("shows the state, detail, and a REAL elapsed timer", () => { const line = renderStatusLine( { kind: "running", detail: "npm test", startedAt: 1000 }, 3300, mono, 80, ); expect(line).toBe(" ● Running · npm test · 2.3s"); }); it("shows extracted progress instead of the timer when present", () => { const line = renderStatusLine( { kind: "running", detail: "npm test", startedAt: 1000, progress: "41/148" }, 9000, mono, 80, ); expect(line).toBe(" ● Running · npm test · 41/148"); }); it("waits-for-permission state is spelled out", () => { const line = renderStatusLine({ kind: "waiting", startedAt: 1000 }, 1400, mono, 80); expect(line).toContain("Waiting for permission"); }); it("stopping acknowledgment uses the ◌ glyph (shape carries state)", () => { expect(renderStatusLine({ kind: "stopping", startedAt: 0 }, 100, mono, 80)).toBe( " ◌ Stopping…", ); }); it("pulses the dot between two shades in color, stable glyph in mono", () => { const dark = resolveTheme({ colorDepth: "truecolor" }); const a = renderStatusLine({ kind: "thinking", startedAt: 0 }, 0, dark, 80) as string; const b = renderStatusLine({ kind: "thinking", startedAt: 0 }, 250, dark, 80) as string; expect(a).not.toBe(b); // different pulse shade expect(stripAnsi(a).slice(0, 3)).toBe(stripAnsi(b).slice(0, 3)); // same glyph }); });