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.1 KB · 166 lines typescript
Raw Blame History
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