/** * KHAELOR * File: tests/cli/commands.test.ts * Description: Slash-command registration completeness (CLAUDE.md §14) and the real-usage /cost report. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { describe, expect, it } from "vitest"; import { CLI_SLASH_COMMANDS, buildCliCommands, costReportLines } from "../../src/cli/commands.js"; import type { CliCommandDeps } from "../../src/cli/commands.js"; import type { Engine } from "../../src/cli/engine.js"; import { loadConfig } from "../../src/config/index.js"; import { SessionEventBus } from "../../src/session/index.js"; import { TuiApp } from "../../src/tui/index.js"; import type { PaletteItem } from "../../src/tui/index.js"; import { envelope } from "../session/fixtures.js"; /** Every slash command CLAUDE.md §14 requires. */ const REQUIRED_SLASHES = [ "/model", "/config", "/permissions", "/context", "/sessions", "/resume", "/new", "/rename", "/clear", "/compact", "/cost", "/status", "/diff", "/processes", "/help", "/quit", ]; async function stubDeps(): Promise { const config = await loadConfig({ cwd: "/tmp/does-not-exist-khaelor", env: { ANTHROPIC_API_KEY: "test-key" }, userConfigDir: "/tmp/does-not-exist-khaelor-user", }); return { ui: { printBlock: () => {}, openSelector: (_items: PaletteItem[], _onSelect: (id: string) => void) => {}, setModelLabel: () => {}, }, engine: () => ({}) as unknown as Engine, config, cwd: "/tmp/does-not-exist-khaelor", sessionsDir: "/tmp/does-not-exist-khaelor-sessions", projectHash: "abc", actions: { newSession: () => {}, resumeSession: () => {}, quit: () => {} }, }; } describe("command registration completeness", () => { it("the CLI layer provides every §14 slash command except the TUI built-ins", async () => { const defs = buildCliCommands(await stubDeps()); const slashes = new Set(defs.map((d) => d.slash)); for (const required of REQUIRED_SLASHES) { if (required === "/help" || required === "/quit") continue; // TUI built-ins expect(slashes.has(required), `missing ${required}`).toBe(true); } expect([...slashes].sort()).toEqual([...CLI_SLASH_COMMANDS].sort()); }); it("registered into a real TuiApp, every §14 slash command resolves — /help and /quit included", async () => { const bus = new SessionEventBus({ sessionId: "01TESTSESSION0000000000000" }); const app = new TuiApp({ bus, model: "test-model", io: { write: () => {}, columns: () => 80, rows: () => 24 }, interactive: false, actions: { submit: () => {}, interrupt: () => {}, permission: () => {}, quit: () => {} }, }); const deps = await stubDeps(); for (const def of buildCliCommands(deps)) app.commands.register(def); for (const slash of REQUIRED_SLASHES) { expect(app.commands.bySlash(slash), `unresolved ${slash}`).not.toBeNull(); } app.stop(); }); it("every CLI command has a title and description for the palettes", async () => { for (const def of buildCliCommands(await stubDeps())) { expect(def.title.length).toBeGreaterThan(0); expect(def.description !== undefined && def.description.length > 0).toBe(true); } }); }); describe("/rename — retitles the session via next-submit capture", () => { it("publishes session.renamed with the captured title, empty input cancels", async () => { const printed: string[] = []; const published: { type: string; payload?: { title?: string } }[] = []; let capture: ((text: string) => void) | null = null; const deps = await stubDeps(); deps.ui.printBlock = (lines: string[]) => printed.push(...lines); deps.actions.captureNextSubmit = (consume) => { capture = consume; }; deps.engine = () => ({ session: { publishDurable: (event: { type: string; payload?: { title?: string } }) => { published.push(event); }, }, }) as unknown as Engine; const rename = buildCliCommands(deps).find((d) => d.slash === "/rename"); expect(rename).toBeDefined(); rename?.run(); expect(capture).not.toBeNull(); capture?.(" Fix the session store "); expect(published).toEqual([ { type: "session.renamed", payload: { title: "Fix the session store" } }, ]); expect(printed.some((l) => l.includes("session renamed"))).toBe(true); rename?.run(); capture?.(" "); expect(published).toHaveLength(1); expect(printed.some((l) => l.includes("rename cancelled"))).toBe(true); }); }); describe("/cost — real usage only (Absolute Rule #4)", () => { it("sums exclusively from ModelResponseCompleted.usage", () => { const events = [ envelope(1, { type: "user.message-created", payload: { text: "hi", mentions: [] }, }), envelope(2, { type: "model.request-started", payload: { requestId: "req-1", model: "claude-sonnet-5", purpose: "main", contextStats: { estimatedInputTokens: 999_999, sections: [] }, }, }), envelope(3, { type: "model.response-completed", payload: { requestId: "req-1", stopReason: "end_turn", usage: { inputTokens: 120, outputTokens: 45, cacheReadTokens: 300, cacheWriteTokens: 80 }, durationMs: 1200, }, }), ]; const lines = costReportLines(events).join("\n"); expect(lines).toContain("120"); expect(lines).toContain("45"); expect(lines).toContain("300"); expect(lines).toContain("80"); expect(lines).toContain("claude-sonnet-5"); // The context ESTIMATE must never leak into the cost report. expect(lines).not.toContain("999,999"); // No pricing configured → no invented dollar figure. expect(lines).not.toMatch(/\$\d/); expect(lines).toContain("n/a"); }); it("reports zero requests honestly when nothing ran", () => { const lines = costReportLines([]).join("\n"); expect(lines).toContain("no model requests yet"); }); });