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/markdown.test.ts4 * Description: Markdown streaming tests — settled-block scanning, fenced-code integrity, block rendering.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import { MarkdownStreamScanner } from "../../src/tui/markdown/scanner.js";12import { renderMarkdownBlock, renderTailLine } from "../../src/tui/markdown/render.js";13import { stripAnsi } from "../../src/tui/renderer/ansi.js";14import { resolveTheme } from "../../src/tui/theme.js";1516const mono = resolveTheme({ colorDepth: "mono" });17const dark = resolveTheme({ colorDepth: "truecolor" });1819describe("MarkdownStreamScanner", () => {20 it("settles a paragraph at a blank line, keeps the tail live", () => {21 const s = new MarkdownStreamScanner();22 expect(s.append("first paragraph line\n")).toEqual([]);23 expect(s.tail()).toContain("first paragraph line");24 const settled = s.append("\nsecond para");25 expect(settled).toEqual(["first paragraph line"]);26 expect(s.tail()).toBe("second para");27 });2829 it("does not settle across an open fence even at blank lines", () => {30 const s = new MarkdownStreamScanner();31 const settled = s.append("```ts\nconst a = 1;\n\nconst b = 2;\n");32 expect(settled).toEqual([]);33 const closed = s.append("```\n");34 expect(closed).toEqual(["```ts\nconst a = 1;\n\nconst b = 2;\n```"]);35 });3637 it("preserves fence content byte-exactly across fragmented deltas", () => {38 const source = "```python\ndef f(x):\n return x * 2\n```";39 const s = new MarkdownStreamScanner();40 const settled: string[] = [];41 for (const ch of source + "\n\n") settled.push(...s.append(ch));42 expect(settled).toEqual([source]);43 });4445 it("finish settles the remainder including a partial last line", () => {46 const s = new MarkdownStreamScanner();47 s.append("done paragraph\n\nlive line\nno newline yet");48 const settled = s.finish();49 expect(settled).toEqual(["live line\nno newline yet"]);50 expect(s.tail()).toBe("");51 });5253 it("settleHead flushes early at safe boundaries but never through a fence", () => {54 const s = new MarkdownStreamScanner();55 s.append("a\nb\nc\nd\n");56 expect(s.settleHead(2)).toBe("a\nb");57 expect(s.tail()).toContain("c");5859 const fenced = new MarkdownStreamScanner();60 fenced.append("```\nx\ny\n");61 expect(fenced.insideFence()).toBe(true);62 expect(fenced.settleHead(2)).toBeNull(); // fence integrity is load-bearing63 });6465 it("settles multiple blocks from one append", () => {66 const s = new MarkdownStreamScanner();67 const settled = s.append("one\n\ntwo\n\nthree is live\n");68 expect(settled).toEqual(["one", "two"]);69 });70});7172describe("renderMarkdownBlock", () => {73 it("renders headings, lists, and quotes legibly in monochrome", () => {74 const lines = renderMarkdownBlock("## Plan\n1. first step\n- bullet\n> quoted words", 80, mono);75 const text = lines.join("\n");76 expect(text).toContain("Plan");77 expect(text).toContain("1. first step");78 expect(text).toContain("• bullet");79 expect(text).toContain("│ quoted words");80 expect(text).not.toContain("\x1b["); // mono emits zero escapes81 });8283 it("renders fenced code with a gutter and keeps content copy-clean", () => {84 const lines = renderMarkdownBlock("```ts\nconst n = 42;\n```", 80, dark);85 expect(lines).toHaveLength(1);86 const plain = stripAnsi(lines[0] as string);87 expect(plain).toBe("│ const n = 42;");88 });8990 it("hard-wraps long code lines with the ↪ continuation marker", () => {91 const long = "x".repeat(120);92 const lines = renderMarkdownBlock("```\n" + long + "\n```", 60, mono);93 expect(lines.length).toBeGreaterThan(1);94 expect(lines[1]).toContain("↪");95 expect(lines.map((l) => stripAnsi(l).replace(/^│ (↪ )?/, "")).join("")).toBe(long);96 });9798 it("renders aligned tables when they fit", () => {99 const md = "| step | risk |\n| --- | --- |\n| retry | low |";100 const lines = renderMarkdownBlock(md, 80, mono).map(stripAnsi);101 expect(lines[0]).toContain("step");102 expect(lines[0]).toContain("│");103 expect(lines[2]).toContain("retry");104 });105106 it("styles inline code and bold via the theme", () => {107 const out = renderMarkdownBlock("call `fn()` **now**", 80, dark).join("\n");108 expect(stripAnsi(out)).toBe("call fn() now");109 expect(out).toContain("\x1b[1m"); // bold110 });111112 it("renders links as text (url)", () => {113 const out = renderMarkdownBlock("see [docs](https://example.com)", 80, mono).join("\n");114 expect(out).toBe("see docs (https://example.com)");115 });116});117118describe("renderTailLine", () => {119 it("applies only cheap inline styling, structure untouched", () => {120 const styled = renderTailLine("use `retry` and **fsync**", dark);121 expect(stripAnsi(styled)).toBe("use retry and fsync");122 expect(renderTailLine("## not a heading yet", mono)).toBe("## not a heading yet");123 });124});125