repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/ephemlets/__tests__/tool-renderer.test.ts
Rawimport {
type AgentToolResult,
initTheme,
type Theme,
} from "@earendil-works/pi-coding-agent";
import type { Component } from "@earendil-works/pi-tui";
import { beforeAll, describe, expect, it } from "vitest";
import { ephemletRenderers } from "../src/tool-renderer.ts";
beforeAll(() => initTheme("dark", false));
const theme = {
fg: (_color: string, text: string) => text,
bold: (text: string) => text,
} as Theme;
const source = "private-source-body";
const state = { selected: "private-state-body" };
const record = {
id: "stable-id",
title: "Counter",
kind: "interactive",
status: "ready",
revision: 4,
source,
state,
jobId: null,
error: null,
};
function context(
expanded: boolean,
lastComponent?: Component,
): Parameters<NonNullable<typeof ephemletRenderers.renderCall>>[2] {
return {
args: {},
toolCallId: "row",
invalidate() {},
lastComponent,
state: undefined,
cwd: "/fixture",
executionStarted: true,
argsComplete: true,
isPartial: false,
expanded,
showImages: false,
isError: false,
durationMs: undefined,
outputPad: 0,
};
}
function output(component: Component): string {
return component.render(120).join("\n");
}
function result(details: unknown): AgentToolResult<unknown> {
return {
content: [{ type: "text", text: "private-fallback-body" }],
details,
};
}
describe("native ephemlet tool rows", () => {
it.each(["put", "build", "get", "state", "list", "delete", "show", "hide"])(
"keeps %s calls compact, expands args, and reuses the native component",
(action) => {
const args = {
action,
id: "stable-id",
source,
state,
brief: "private-brief-body",
};
const collapsed = ephemletRenderers.renderCall!(
args,
theme,
context(false),
);
expect(output(collapsed)).toContain(`ephemlet ${action}`);
expect(output(collapsed)).toContain("to expand");
for (const hidden of [source, state.selected, args.brief])
expect(output(collapsed)).not.toContain(hidden);
const expanded = ephemletRenderers.renderCall!(
args,
theme,
context(true, collapsed),
);
expect(expanded).toBe(collapsed);
for (const visible of [source, state.selected, args.brief])
expect(output(expanded)).toContain(visible);
const folded = ephemletRenderers.renderCall!(
args,
theme,
context(false, expanded),
);
expect(output(folded)).not.toContain(source);
},
);
it.each([
[record, "ready"],
[{ ...record, status: "building", jobId: "job-id" }, "building"],
[{ ...record, status: "failed", error: "private-error-body" }, "failed"],
[{ ephemlets: [record], guidance: "/private-guidance-path" }, "1 ephemlet"],
[{ deleted: "stable-id" }, "deleted stable-id"],
[{ visible: false, selected: "stable-id" }, "hidden"],
[{ visible: true, selected: "stable-id" }, "shown"],
] as const)("keeps result bodies behind native expansion", (data, status) => {
const collapsed = ephemletRenderers.renderResult!(
result(data),
{ expanded: false, isPartial: false },
theme,
context(false),
);
expect(output(collapsed)).toContain(status);
expect(output(collapsed)).toContain("to expand");
for (const hidden of [
source,
state.selected,
"private-error-body",
"/private-guidance-path",
])
expect(output(collapsed)).not.toContain(hidden);
const expanded = ephemletRenderers.renderResult!(
result(data),
{ expanded: true, isPartial: false },
theme,
context(true, collapsed),
);
expect(expanded).toBe(collapsed);
expect(output(expanded)).toContain("stable-id");
if ("source" in data || "ephemlets" in data) {
expect(output(expanded)).toContain(source);
expect(output(expanded)).toContain(state.selected);
}
});
it.each([
undefined,
null,
42,
"private-malformed-body",
[source],
{ unknown: source },
])("honors expansion for malformed details %s", (data) => {
const collapsed = ephemletRenderers.renderResult!(
result(data),
{ expanded: false, isPartial: false },
theme,
context(false),
);
expect(output(collapsed)).toContain("finished");
expect(output(collapsed)).toContain("to expand");
expect(output(collapsed)).not.toContain("private-");
const expanded = ephemletRenderers.renderResult!(
result(data),
{ expanded: true, isPartial: false },
theme,
context(true),
);
const expected =
data === undefined || data === null
? "private-fallback-body"
: typeof data === "string" || typeof data === "number"
? String(data)
: source;
expect(output(expanded)).toContain(expected);
});
it("honors expansion for progress and errors, including empty error details", () => {
const progress = ephemletRenderers.renderResult!(
result(record),
{ expanded: false, isPartial: true },
theme,
context(false),
);
expect(output(progress)).toContain("working");
expect(output(progress)).not.toContain(source);
const expandedProgress = ephemletRenderers.renderResult!(
result(record),
{ expanded: true, isPartial: true },
theme,
context(true),
);
expect(output(expandedProgress)).toContain(source);
const collapsedErrorContext = context(false);
collapsedErrorContext.isError = true;
const error = ephemletRenderers.renderResult!(
result({}),
{ expanded: false, isPartial: false },
theme,
collapsedErrorContext,
);
expect(output(error)).toContain("failed");
expect(output(error)).not.toContain("private-fallback-body");
const expandedErrorContext = context(true, error);
expandedErrorContext.isError = true;
const expandedError = ephemletRenderers.renderResult!(
result({}),
{ expanded: true, isPartial: false },
theme,
expandedErrorContext,
);
expect(output(expandedError)).toContain("private-fallback-body");
});
it("bounds expanded previews and escapes controls without invoking custom inspectors", () => {
let invoked = false;
const data = {
source: "x".repeat(128 * 1024),
state,
title: "Counter\x1b[2J\n",
[Symbol.for("nodejs.util.inspect.custom")]() {
invoked = true;
return "untrusted";
},
};
const expanded = ephemletRenderers.renderResult!(
result(data),
{ expanded: true, isPartial: false },
theme,
context(true),
);
const text = output(expanded);
expect(text.length).toBeLessThan(18000);
expect(text).toContain("truncated");
expect(text).not.toContain("\x1b");
expect(invoked).toBe(false);
const cyclic: Record<string, unknown> = {};
cyclic["self"] = cyclic;
expect(() =>
ephemletRenderers.renderResult!(
result(cyclic),
{ expanded: true, isPartial: false },
theme,
context(true),
),
).not.toThrow();
});
});