File size: 3,767 Bytes
e249c6d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | import fs from "node:fs";
import * as os from "node:os";
import path from "node:path";
import { Text } from "@earendil-works/pi-tui";
import { describe, expect, it } from "vitest";
import {
appendSessionToolTruncationWarning,
reuseTextComponent,
shortenPath,
trimTrailingEmptyLines,
} from "./render-utils.js";
const theme = {
fg: (color: string, text: string) => `<${color}>${text}</${color}>`,
};
describe("appendSessionToolTruncationWarning", () => {
it("leaves output unchanged when no truncation metadata is present", () => {
expect(appendSessionToolTruncationWarning("output", theme, {})).toBe("output");
});
it("combines limit, byte, and additional warnings in order", () => {
expect(
appendSessionToolTruncationWarning("output", theme, {
limit: { count: 5, noun: "matches" },
truncation: { truncated: true, maxBytes: 1024 },
additionalWarnings: ["some lines truncated"],
}),
).toBe(
"output\n<warning>[Truncated: 5 matches limit, 1.0KB limit, some lines truncated]</warning>",
);
});
});
describe("trimTrailingEmptyLines", () => {
it.each([
{
name: "removes multiple trailing empty lines",
lines: ["first", "second", "", ""],
expected: ["first", "second"],
},
{ name: "removes all-empty input", lines: ["", ""], expected: [] },
{
name: "keeps leading and interior empty lines",
lines: ["", "first", "", "second", ""],
expected: ["", "first", "", "second"],
},
{
name: "keeps whitespace-only trailing lines",
lines: ["first", " ", "\t"],
expected: ["first", " ", "\t"],
},
])("$name", ({ lines, expected }) => {
expect(trimTrailingEmptyLines(lines)).toEqual(expected);
});
it("does not mutate the caller-owned lines", () => {
const lines = ["first", "", ""];
const original = [...lines];
trimTrailingEmptyLines(lines);
expect(lines).toEqual(original);
});
});
describe("reuseTextComponent", () => {
it("creates a zero-padding Text component when no prior component exists", () => {
const component = reuseTextComponent(undefined, "hello");
expect(component).toBeInstanceOf(Text);
expect(component.render(5)).toEqual(["hello"]);
});
it("reuses the prior Text component and invalidates its rendered content", () => {
const component = new Text("before", 0, 0);
expect(component.render(6)).toEqual(["before"]);
const reused = reuseTextComponent(component, "after!");
expect(reused).toBe(component);
expect(reused.render(6)).toEqual(["after!"]);
});
});
describe("shortenPath", () => {
const home = os.homedir();
it("shortens paths inside the home directory", () => {
expect(shortenPath(`${home}/projects/app.ts`)).toBe("~/projects/app.ts");
});
it("collapses the home directory itself", () => {
expect(shortenPath(home)).toBe("~");
});
it("leaves a sibling directory that merely shares the prefix untouched", () => {
// `${home}extra` starts with `home` as a substring but is not under it,
// so it must not be rewritten to `~extra`.
expect(shortenPath(`${home}extra/app.ts`)).toBe(`${home}extra/app.ts`);
});
it("leaves unrelated paths untouched", () => {
expect(shortenPath("/var/log/syslog")).toBe("/var/log/syslog");
});
it.skipIf(process.platform !== "win32")("shortens real Windows home casing aliases", () => {
const homeAlias = home.toUpperCase();
expect(fs.statSync(homeAlias).isDirectory()).toBe(true);
expect(shortenPath(path.join(homeAlias, "projects", "app.ts"))).toBe(
`~${path.sep}projects${path.sep}app.ts`,
);
});
it("returns an empty string for non-string input", () => {
expect(shortenPath(undefined)).toBe("");
});
});
|