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%
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