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%
4.9 KB · 125 lines typescript
Raw Blame History
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