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/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