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.4 KB · 129 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: tests/tui/frame.test.ts4 * Description: Renderer frame-composition tests — settling, damage tracking, cursor parking, sync fallback.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import { composeFrame } from "../../src/tui/renderer/frame.js";1213const WIDTH = 80;1415describe("composeFrame", () => {16  it("first paint repaints every row and parks the cursor at the caret", () => {17    const frame = composeFrame({18      prevLines: [],19      prevCaretRow: 0,20      next: { lines: ["tail", "status", "composer", "bar"], caretRow: 2, caretCol: 4 },21      settled: [],22      width: WIDTH,23      sync: true,24    });25    expect(frame.repaintedRows).toBe(4);26    expect(frame.lines).toEqual(["tail", "status", "composer", "bar"]);27    expect(frame.caretRow).toBe(2);28    // synchronized-output bracketing29    expect(frame.data.startsWith("\x1b[?2026h")).toBe(true);30    expect(frame.data.endsWith("\x1b[?2026l")).toBe(true);31    // cursor parked: up 1 from last row (row 3 → row 2), column 5 (1-based)32    expect(frame.data).toContain("\x1b[1A");33    expect(frame.data).toContain("\x1b[5G");34  });3536  it("repaints only damaged rows on partial update", () => {37    const prev = ["tail", "status", "composer", "bar"];38    const frame = composeFrame({39      prevLines: prev,40      prevCaretRow: 2,41      next: { lines: ["tail", "status CHANGED", "composer", "bar"], caretRow: 2, caretCol: 4 },42      settled: [],43      width: WIDTH,44      sync: true,45    });46    expect(frame.repaintedRows).toBe(1);47    expect(frame.data).toContain("status CHANGED");48    // untouched rows are not re-emitted49    expect(frame.data).not.toContain("composer");50  });5152  it("flushes settled blocks above the live region and full-repaints after", () => {53    const frame = composeFrame({54      prevLines: ["old-live", "composer"],55      prevCaretRow: 1,56      next: { lines: ["composer"], caretRow: 0, caretCol: 2 },57      settled: [["▸ Read src/a.ts · 12 lines"], ["✓ src/a.ts  +3 −1"]],58      width: WIDTH,59      sync: true,60    });61    // erase live region before printing settled content62    expect(frame.data).toContain("\x1b[0J");63    const settledIdx = frame.data.indexOf("▸ Read src/a.ts");64    const secondIdx = frame.data.indexOf("✓ src/a.ts");65    const liveIdx = frame.data.indexOf("composer");66    expect(settledIdx).toBeGreaterThan(-1);67    expect(secondIdx).toBeGreaterThan(settledIdx);68    expect(liveIdx).toBeGreaterThan(secondIdx);69    // settled lines end with \r\n so they scroll into scrollback70    expect(frame.data).toContain("▸ Read src/a.ts · 12 lines\r\n");71    expect(frame.repaintedRows).toBe(1); // full repaint of the (1-row) new region72  });7374  it("erases below when the live region shrinks", () => {75    const frame = composeFrame({76      prevLines: ["a", "b", "c", "d"],77      prevCaretRow: 2,78      next: { lines: ["a", "b"], caretRow: 1, caretCol: 0 },79      settled: [],80      width: WIDTH,81      sync: true,82    });83    expect(frame.data).toContain("\x1b[0J");84    expect(frame.lines).toEqual(["a", "b"]);85  });8687  it("falls back to cursor hide/show bracketing without sync support", () => {88    const frame = composeFrame({89      prevLines: [],90      prevCaretRow: 0,91      next: { lines: ["x"], caretRow: 0, caretCol: 1 },92      settled: [],93      width: WIDTH,94      sync: false,95    });96    expect(frame.data).not.toContain("\x1b[?2026h");97    expect(frame.data.startsWith("\x1b[?25l")).toBe(true);98    expect(frame.data).toContain("\x1b[?25h");99  });100101  it("truncates every line to width − 1", () => {102    const long = "y".repeat(200);103    const frame = composeFrame({104      prevLines: [],105      prevCaretRow: 0,106      next: { lines: [long], caretRow: 0, caretCol: 0 },107      settled: [["s".repeat(200)]],108      width: 20,109      sync: true,110    });111    expect(frame.lines[0]).toBe("y".repeat(19));112    expect(frame.data).toContain("s".repeat(19) + "\r\n");113    expect(frame.data).not.toContain("s".repeat(20));114  });115116  it("moves from the previous caret row to the live-region origin", () => {117    const frame = composeFrame({118      prevLines: ["a", "b", "c"],119      prevCaretRow: 1,120      next: { lines: ["a", "b", "c"], caretRow: 1, caretCol: 0 },121      settled: [],122      width: WIDTH,123      sync: true,124    });125    expect(frame.data).toContain("\x1b[1F"); // up 1 to origin from caret row 1126    expect(frame.repaintedRows).toBe(0); // nothing changed127  });128});129