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%
6.0 KB · 178 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: tests/cli/commands.test.ts4 * Description: Slash-command registration completeness (CLAUDE.md §14) and the real-usage /cost report.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import { CLI_SLASH_COMMANDS, buildCliCommands, costReportLines } from "../../src/cli/commands.js";12import type { CliCommandDeps } from "../../src/cli/commands.js";13import type { Engine } from "../../src/cli/engine.js";14import { loadConfig } from "../../src/config/index.js";15import { SessionEventBus } from "../../src/session/index.js";16import { TuiApp } from "../../src/tui/index.js";17import type { PaletteItem } from "../../src/tui/index.js";18import { envelope } from "../session/fixtures.js";1920/** Every slash command CLAUDE.md §14 requires. */21const REQUIRED_SLASHES = [22  "/model",23  "/config",24  "/permissions",25  "/context",26  "/sessions",27  "/resume",28  "/new",29  "/rename",30  "/clear",31  "/compact",32  "/cost",33  "/status",34  "/diff",35  "/processes",36  "/help",37  "/quit",38];3940async function stubDeps(): Promise<CliCommandDeps> {41  const config = await loadConfig({42    cwd: "/tmp/does-not-exist-khaelor",43    env: { ANTHROPIC_API_KEY: "test-key" },44    userConfigDir: "/tmp/does-not-exist-khaelor-user",45  });46  return {47    ui: {48      printBlock: () => {},49      openSelector: (_items: PaletteItem[], _onSelect: (id: string) => void) => {},50      setModelLabel: () => {},51    },52    engine: () => ({}) as unknown as Engine,53    config,54    cwd: "/tmp/does-not-exist-khaelor",55    sessionsDir: "/tmp/does-not-exist-khaelor-sessions",56    projectHash: "abc",57    actions: { newSession: () => {}, resumeSession: () => {}, quit: () => {} },58  };59}6061describe("command registration completeness", () => {62  it("the CLI layer provides every §14 slash command except the TUI built-ins", async () => {63    const defs = buildCliCommands(await stubDeps());64    const slashes = new Set(defs.map((d) => d.slash));65    for (const required of REQUIRED_SLASHES) {66      if (required === "/help" || required === "/quit") continue; // TUI built-ins67      expect(slashes.has(required), `missing ${required}`).toBe(true);68    }69    expect([...slashes].sort()).toEqual([...CLI_SLASH_COMMANDS].sort());70  });7172  it("registered into a real TuiApp, every §14 slash command resolves — /help and /quit included", async () => {73    const bus = new SessionEventBus({ sessionId: "01TESTSESSION0000000000000" });74    const app = new TuiApp({75      bus,76      model: "test-model",77      io: { write: () => {}, columns: () => 80, rows: () => 24 },78      interactive: false,79      actions: { submit: () => {}, interrupt: () => {}, permission: () => {}, quit: () => {} },80    });81    const deps = await stubDeps();82    for (const def of buildCliCommands(deps)) app.commands.register(def);83    for (const slash of REQUIRED_SLASHES) {84      expect(app.commands.bySlash(slash), `unresolved ${slash}`).not.toBeNull();85    }86    app.stop();87  });8889  it("every CLI command has a title and description for the palettes", async () => {90    for (const def of buildCliCommands(await stubDeps())) {91      expect(def.title.length).toBeGreaterThan(0);92      expect(def.description !== undefined && def.description.length > 0).toBe(true);93    }94  });95});9697describe("/rename — retitles the session via next-submit capture", () => {98  it("publishes session.renamed with the captured title, empty input cancels", async () => {99    const printed: string[] = [];100    const published: { type: string; payload?: { title?: string } }[] = [];101    let capture: ((text: string) => void) | null = null;102    const deps = await stubDeps();103    deps.ui.printBlock = (lines: string[]) => printed.push(...lines);104    deps.actions.captureNextSubmit = (consume) => {105      capture = consume;106    };107    deps.engine = () =>108      ({109        session: {110          publishDurable: (event: { type: string; payload?: { title?: string } }) => {111            published.push(event);112          },113        },114      }) as unknown as Engine;115116    const rename = buildCliCommands(deps).find((d) => d.slash === "/rename");117    expect(rename).toBeDefined();118119    rename?.run();120    expect(capture).not.toBeNull();121    capture?.("  Fix the session store  ");122    expect(published).toEqual([123      { type: "session.renamed", payload: { title: "Fix the session store" } },124    ]);125    expect(printed.some((l) => l.includes("session renamed"))).toBe(true);126127    rename?.run();128    capture?.("   ");129    expect(published).toHaveLength(1);130    expect(printed.some((l) => l.includes("rename cancelled"))).toBe(true);131  });132});133134describe("/cost — real usage only (Absolute Rule #4)", () => {135  it("sums exclusively from ModelResponseCompleted.usage", () => {136    const events = [137      envelope(1, {138        type: "user.message-created",139        payload: { text: "hi", mentions: [] },140      }),141      envelope(2, {142        type: "model.request-started",143        payload: {144          requestId: "req-1",145          model: "claude-sonnet-5",146          purpose: "main",147          contextStats: { estimatedInputTokens: 999_999, sections: [] },148        },149      }),150      envelope(3, {151        type: "model.response-completed",152        payload: {153          requestId: "req-1",154          stopReason: "end_turn",155          usage: { inputTokens: 120, outputTokens: 45, cacheReadTokens: 300, cacheWriteTokens: 80 },156          durationMs: 1200,157        },158      }),159    ];160    const lines = costReportLines(events).join("\n");161    expect(lines).toContain("120");162    expect(lines).toContain("45");163    expect(lines).toContain("300");164    expect(lines).toContain("80");165    expect(lines).toContain("claude-sonnet-5");166    // The context ESTIMATE must never leak into the cost report.167    expect(lines).not.toContain("999,999");168    // No pricing configured → no invented dollar figure.169    expect(lines).not.toMatch(/\$\d/);170    expect(lines).toContain("n/a");171  });172173  it("reports zero requests honestly when nothing ran", () => {174    const lines = costReportLines([]).join("\n");175    expect(lines).toContain("no model requests yet");176  });177});178