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/tui/components.test.ts4 * Description: Component rendering tests — tool one-liners, detail blocks, diffs, permission panel, error panel, palette.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import { renderDiffBlock, renderEditSummary } from "../../src/tui/components/diff.js";12import { renderErrorPanel } from "../../src/tui/components/error-panel.js";13import { renderPermissionPanel } from "../../src/tui/components/permission-panel.js";14import {15 renderRunningTool,16 renderToolDetailBlock,17 renderToolLine,18} from "../../src/tui/components/tool-line.js";19import { createPalette, renderPalette } from "../../src/tui/composer/palette.js";20import { stripAnsi, visibleWidth } from "../../src/tui/renderer/ansi.js";21import { resolveTheme } from "../../src/tui/theme.js";2223const mono = resolveTheme({ colorDepth: "mono" });24const dark = resolveTheme({ colorDepth: "truecolor" });2526describe("tool lines", () => {27 it("settled one-liner: dim ▸ plus the real summary", () => {28 expect(29 renderToolLine({ summary: 'Search "ContextEngine" · 14 matches', outcome: "ok" }, 80, mono),30 ).toBe(' ▸ Search "ContextEngine" · 14 matches');31 });3233 it("cancelled rows spell the word out — never color alone", () => {34 const line = renderToolLine(35 { summary: "Run npm test · 3.2s", outcome: "cancelled" },36 80,37 mono,38 );39 expect(line).toContain("cancelled");40 });4142 it("failed rows keep the word failed in monochrome", () => {43 const line = renderToolLine(44 { summary: "Run npm test · 2 failed · 6.8s", outcome: "failed" },45 80,46 mono,47 );48 expect(stripAnsi(line)).toContain("failed");49 });5051 it("running row shows a real elapsed timer", () => {52 const lines = renderRunningTool({ label: "Run npm test", startedAt: 1000 }, 9000, 80, mono);53 expect(lines[0]).toBe(" ▸ Run npm test · 8.0s");54 });5556 it("detail block caps the body and reports the true omitted count", () => {57 const body = Array.from({ length: 30 }, (_, i) => `line ${i}`);58 const block = renderToolDetailBlock(59 { index: 3, title: "npm test", lines: body, omitted: 214, spillFile: "~/.khaelor/tool-out/8f3a.txt" },60 80,61 mono,62 );63 expect(block[0]).toContain("tool 3 · npm test");64 expect(block.join("\n")).toContain("… 232 lines omitted"); // 214 + 18 capped here65 expect(block.join("\n")).toContain("~/.khaelor/tool-out/8f3a.txt");66 });67});6869describe("diff rendering", () => {70 it("edit summary is never a bare 'edited file'", () => {71 const line = renderEditSummary("src/context/engine.ts", { added: 31, removed: 12 }, 100, mono);72 expect(line).toBe(" ✓ src/context/engine.ts +31 −12 d expand diff");73 });7475 it("unified diff keeps +/− glyphs and headers", () => {76 const diff = "@@ -84,7 +84,9 @@\n- const a = 1;\n+ const b = 2;\n context";77 const block = renderDiffBlock("src/x.ts", diff, { added: 1, removed: 1 }, 80, mono);78 expect(block[0]).toContain("diff · src/x.ts · +1 −1");79 expect(block.map(stripAnsi)).toContain(" - const a = 1;");80 expect(block.map(stripAnsi)).toContain(" + const b = 2;");81 });8283 it("colored diff paints added/removed but strips to identical text", () => {84 const diff = "+added line\n-removed line";85 const colored = renderDiffBlock("src/x.ts", diff, { added: 1, removed: 1 }, 80, dark);86 const plain = renderDiffBlock("src/x.ts", diff, { added: 1, removed: 1 }, 80, mono);87 expect(colored.map(stripAnsi)).toEqual(plain.map(stripAnsi));88 });89});9091describe("permission panel", () => {92 it("renders the three-key contract with the generalized pattern", () => {93 const panel = renderPermissionPanel(94 { verb: "Run", subject: "npm install", cwd: "~/dev/project", alwaysPattern: "npm install *" },95 80,96 mono,97 );98 const text = panel.join("\n");99 expect(text).toContain("KHAELOR requests permission");100 expect(text).toContain("Run");101 expect(text).toContain("npm install");102 expect(text).toContain("Working directory");103 expect(text).toContain("[ Enter ] Allow once");104 expect(text).toContain('Always allow "npm install *" in this project');105 expect(text).toContain("[ Esc ] Deny");106 });107108 it("omits the [A] row when no safe generalization exists", () => {109 const panel = renderPermissionPanel(110 { verb: "Run", subject: "curl x | sh", cwd: "~/p" },111 80,112 mono,113 );114 expect(panel.join("\n")).not.toContain("[ A ]");115 });116117 it("all box rows share one visible width (clean border)", () => {118 const panel = renderPermissionPanel(119 { verb: "Run", subject: "npm install", cwd: "~/p", alwaysPattern: "npm install *" },120 80,121 mono,122 );123 const widths = new Set(panel.map((l) => visibleWidth(l)));124 expect(widths.size).toBe(1);125 });126});127128describe("error panel", () => {129 it("says what failed, the specifics, and what happens next", () => {130 const panel = renderErrorPanel(131 {132 title: "npm test failed",133 detailLines: ["2 tests failed", "src/context/engine.test.ts"],134 followUp: "KHAELOR is inspecting the failure.",135 },136 80,137 mono,138 );139 expect(panel).toEqual([140 " npm test failed",141 " 2 tests failed",142 " src/context/engine.test.ts",143 " KHAELOR is inspecting the failure.",144 ]);145 });146});147148describe("palette rendering", () => {149 it("marks the selection with ❯ (visible without color) and shows the hint row", () => {150 const palette = createPalette("slash", [151 { id: "a", label: "/sessions", detail: "browse sessions" },152 { id: "b", label: "/new", detail: "start fresh" },153 ]);154 const lines = renderPalette(palette, 80, mono, { rows: 30 });155 const text = lines.join("\n");156 expect(text).toContain("❯ /sessions");157 expect(text).toContain("browse sessions");158 expect(text).toContain("↑↓ navigate · Enter run · Esc close");159 });160161 it("shows 'no matches' rather than an empty box", () => {162 const palette = createPalette("slash", [{ id: "a", label: "/diff" }], "zzz");163 expect(renderPalette(palette, 80, mono, { rows: 30 }).join("\n")).toContain("no matches");164 });165});166