import { describe, expect, it } from "bun:test"; import * as os from "node:os"; import * as path from "node:path"; import { getThemeByName } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; import { dedupeParseErrors, formatDiagnostics, formatCodeFrameLine, formatParseErrors, formatScreenshot, } from "@oh-my-pi/pi-coding-agent/tools/render-utils"; describe("parse error formatting", () => { it("deduplicates parse errors while preserving order", () => { const errors = [ "foo.ts: parse error (syntax tree contains error nodes)", "foo.ts: parse error (syntax tree contains error nodes)", "bar.ts: parse error (syntax tree contains error nodes)", "foo.ts: parse error (syntax tree contains error nodes)", ]; expect(dedupeParseErrors(errors)).toEqual([ "foo.ts: parse error (syntax tree contains error nodes)", "bar.ts: parse error (syntax tree contains error nodes)", ]); }); it("formats deduplicated parse errors", () => { const formatted = formatParseErrors([ "foo.ts: parse error (syntax tree contains error nodes)", "foo.ts: parse error (syntax tree contains error nodes)", "bar.ts: parse error (syntax tree contains error nodes)", ]); expect(formatted).toEqual([ "Parse issues:", "- foo.ts: parse error (syntax tree contains error nodes)", "- bar.ts: parse error (syntax tree contains error nodes)", ]); }); }); describe("formatScreenshot", () => { function fakeResized( overrides?: Partial<{ width: number; height: number; originalWidth: number; originalHeight: number; wasResized: boolean; buffer: Uint8Array; mimeType: string; }>, ): { buffer: Uint8Array; mimeType: string; originalWidth: number; originalHeight: number; width: number; height: number; wasResized: boolean; get data(): string; } { const buf = overrides?.buffer ?? new Uint8Array(2048); return { buffer: buf, mimeType: overrides?.mimeType ?? "image/webp", originalWidth: overrides?.originalWidth ?? 800, originalHeight: overrides?.originalHeight ?? 600, width: overrides?.width ?? 800, height: overrides?.height ?? 600, wasResized: overrides?.wasResized ?? false, get data() { return Buffer.from(buf).toString("base64"); }, }; } it("formats full-res save with home-relative path", () => { const filePath = path.join(os.homedir(), "screenshots", "capture.png"); const resized = fakeResized({ mimeType: "image/webp", buffer: new Uint8Array(1024) }); expect( formatScreenshot({ saveFullRes: true, savedMimeType: "image/png", savedByteLength: 2048, dest: filePath, resized, }), ).toEqual([ "Screenshot captured", "Saved: image/png (2.00 KB) to ~/screenshots/capture.png", "Model: image/webp (1.00 KB, 800x600)", ]); }); it("formats non-home path without tilde", () => { const resized = fakeResized({ mimeType: "image/webp", buffer: new Uint8Array(1024) }); expect( formatScreenshot({ saveFullRes: true, savedMimeType: "image/png", savedByteLength: 2048, dest: "/tmp/capture.png", resized, }), ).toEqual([ "Screenshot captured", "Saved: image/png (2.00 KB) to /tmp/capture.png", "Model: image/webp (1.00 KB, 800x600)", ]); }); it("formats temp-only screenshot without save line", () => { const resized = fakeResized({ mimeType: "image/webp", buffer: new Uint8Array(3072) }); expect( formatScreenshot({ saveFullRes: false, savedMimeType: "image/webp", savedByteLength: 3072, dest: "/tmp/omp-sshots-123.png", resized, }), ).toEqual(["Screenshot captured", "Format: image/webp (3.00 KB)", "Dimensions: 800x600"]); }); it("appends dimension note when image was resized", () => { const resized = fakeResized({ wasResized: true, originalWidth: 1600, originalHeight: 1200, width: 800, height: 600, }); const lines = formatScreenshot({ saveFullRes: false, savedMimeType: "image/webp", savedByteLength: 2048, dest: "/tmp/shot.png", resized, }); expect(lines).toContain( "[Image: original 1600x1200, displayed at 800x600. Multiply coordinates by 2.00 to map to original image.]", ); }); }); describe("formatDiagnostics", () => { it("replaces tabs in rendered diagnostic text", async () => { const theme = await getThemeByName("dark"); expect(theme).toBeDefined(); const formatted = formatDiagnostics( { errored: true, summary: "1\terror(s)", messages: [ "src/example.go:183:41 [error] [compiler] too many\targuments in call (WrongArgCount)", "\tunparsed diagnostic\tmessage", ], }, true, theme!, () => "go", ); expect(formatted).not.toContain("\t"); expect(formatted.replace(/\s+/g, " ")).toContain("too many arguments in call"); expect(formatted.replace(/\s+/g, " ")).toContain("unparsed diagnostic message"); expect(formatted.replace(/\s+/g, " ")).toContain("1 error(s)"); }); }); describe("formatCodeFrameLine", () => { it("pads markers as part of the gutter", () => { expect(formatCodeFrameLine(" ", 447, "context", 3)).toBe(" 447│context"); expect(formatCodeFrameLine("*", 448, "match", 3)).toBe("*448│match"); expect(formatCodeFrameLine("+", 11, "added", 3)).toBe(" +11│added"); expect(formatCodeFrameLine("+", 235, "added", 3)).toBe("+235│added"); }); });