SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
4.1 KB · 122 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: tests/tui/status.test.ts4 * Description: Status bar width-degradation tests and agent status line tests (real timers, no fabrication).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import { renderStatusBar } from "../../src/tui/components/status-bar.js";12import type { StatusBarData } from "../../src/tui/components/status-bar.js";13import { renderStatusLine } from "../../src/tui/components/status-line.js";14import { stripAnsi } from "../../src/tui/renderer/ansi.js";15import { resolveTheme } from "../../src/tui/theme.js";1617const mono = resolveTheme({ colorDepth: "mono" });1819const FULL: StatusBarData = {20  branch: "main",21  dirty: { added: 4, removed: 1 },22  model: "claude-sonnet-4-5",23  contextPct: 31,24  costUsd: 0.42,25  processCount: 2,26};2728describe("renderStatusBar degradation", () => {29  it("width ≥ 100: all segments, full model id", () => {30    expect(renderStatusBar(FULL, 120, mono)).toBe(31      " main +4 −1  │  claude-sonnet-4-5  │  context 31%  │  $0.42  │  ● 2",32    );33  });3435  it("width 80–99: model shortens to its alias", () => {36    expect(renderStatusBar(FULL, 90, mono)).toBe(37      " main +4 −1  │  sonnet  │  context 31%  │  $0.42  │  ● 2",38    );39  });4041  it("width 60–79: cost and processes drop whole", () => {42    expect(renderStatusBar(FULL, 70, mono)).toBe(" main +4 −1  │  sonnet  │  31%");43  });4445  it("width < 60: branch and context percent only", () => {46    expect(renderStatusBar(FULL, 50, mono)).toBe(" main  │  31%");47  });4849  it("segments with no data are absent, not zeroed", () => {50    expect(renderStatusBar({ model: "claude-sonnet-4-5" }, 120, mono)).toBe(51      " claude-sonnet-4-5",52    );53    expect(renderStatusBar({}, 120, mono)).toBe(" ");54  });5556  it("processes drop when none are running", () => {57    const bar = renderStatusBar({ ...FULL, processCount: 0 }, 120, mono);58    expect(bar).not.toContain("●");59  });6061  it("context pressure appends the /compact nudge at ≥ 80%", () => {62    const bar = renderStatusBar({ ...FULL, contextPct: 85 }, 120, mono);63    expect(bar).toContain("context 85% · /compact");64  });6566  it("queued steering shows its count", () => {67    const bar = renderStatusBar({ ...FULL, queuedCount: 1 }, 120, mono);68    expect(bar).toContain("⋯ 1 queued");69  });7071  it("never exceeds the terminal width", () => {72    for (const width of [40, 60, 80, 100, 120]) {73      expect(stripAnsi(renderStatusBar(FULL, width, mono)).length).toBeLessThanOrEqual(width);74    }75  });76});7778describe("renderStatusLine", () => {79  it("collapses to nothing when idle", () => {80    expect(renderStatusLine({ kind: "idle", startedAt: 0 }, 5000, mono, 80)).toBeNull();81  });8283  it("shows the state, detail, and a REAL elapsed timer", () => {84    const line = renderStatusLine(85      { kind: "running", detail: "npm test", startedAt: 1000 },86      3300,87      mono,88      80,89    );90    expect(line).toBe(" ● Running · npm test · 2.3s");91  });9293  it("shows extracted progress instead of the timer when present", () => {94    const line = renderStatusLine(95      { kind: "running", detail: "npm test", startedAt: 1000, progress: "41/148" },96      9000,97      mono,98      80,99    );100    expect(line).toBe(" ● Running · npm test · 41/148");101  });102103  it("waits-for-permission state is spelled out", () => {104    const line = renderStatusLine({ kind: "waiting", startedAt: 1000 }, 1400, mono, 80);105    expect(line).toContain("Waiting for permission");106  });107108  it("stopping acknowledgment uses the ◌ glyph (shape carries state)", () => {109    expect(renderStatusLine({ kind: "stopping", startedAt: 0 }, 100, mono, 80)).toBe(110      " ◌ Stopping…",111    );112  });113114  it("pulses the dot between two shades in color, stable glyph in mono", () => {115    const dark = resolveTheme({ colorDepth: "truecolor" });116    const a = renderStatusLine({ kind: "thinking", startedAt: 0 }, 0, dark, 80) as string;117    const b = renderStatusLine({ kind: "thinking", startedAt: 0 }, 250, dark, 80) as string;118    expect(a).not.toBe(b); // different pulse shade119    expect(stripAnsi(a).slice(0, 3)).toBe(stripAnsi(b).slice(0, 3)); // same glyph120  });121});122