Files
pi/packages/coding-agent/test/codemode-renderer.test.ts
Armin Ronacher 0582d9c11d fix(coding-agent): limit codemode and MCP result previews to wrapped lines
Collapsed previews counted logical lines, so one long JSON line filled the
screen. Share a VisualLinePreview component with the bash renderer.
2026-09-30 17:25:07 +02:00

110 lines
3.3 KiB
TypeScript

import { stripVTControlCharacters } from "node:util";
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
import type { Component } from "@earendil-works/pi-tui";
import { beforeAll, describe, expect, it } from "vitest";
import type { ToolRenderContext } from "../src/core/extensions/types.ts";
import { codemodeRenderers } from "../src/extensions/codemode/renderer.ts";
import type { CodemodeToolDetails } from "../src/extensions/codemode/tool.ts";
import { initTheme, theme } from "../src/modes/interactive/theme/theme.ts";
function render(
result: AgentToolResult<CodemodeToolDetails | undefined>,
isError = false,
expanded = true,
width = 200,
): string {
const context = {
args: { code: "" },
toolCallId: "call",
invalidate: () => {},
lastComponent: undefined,
state: {},
cwd: "/",
executionStarted: true,
argsComplete: true,
isPartial: false,
expanded,
showImages: false,
isError,
} satisfies ToolRenderContext;
const component = codemodeRenderers.renderResult?.(
result,
{ expanded, isPartial: false },
theme,
context,
) as Component;
return stripVTControlCharacters(component.render(width).join("\n"))
.split("\n")
.map((line) => line.trimEnd())
.join("\n")
.trim();
}
describe("codemode renderer", () => {
beforeAll(() => initTheme("dark"));
it("hides the script header and shows the output", () => {
const text = render({
content: [
{ type: "text", text: "Script completed\nWall time 0.1 seconds\nOutput:\n" },
{ type: "text", text: "hello" },
],
details: { calls: [{ id: "call/1", name: "read", args: '{"path":"a"}', status: "ok", durationMs: 5 }] },
});
expect(text).toBe('✓ read {"path":"a"} 5ms\n\nhello');
});
it("shows the cost of model calls and their total", () => {
const call = { name: "models.classify", args: "scorer/judge", status: "ok" as const, durationMs: 5 };
const text = render({
content: [{ type: "text", text: "Script completed\nWall time 0.1 seconds\nOutput:\n" }],
details: {
calls: [
{ ...call, id: "call/models.classify/1", cost: 0.000012936 },
{ ...call, id: "call/models.classify/2", cost: 0.02 },
{ ...call, id: "call/models.classify/3" },
],
},
});
expect(text).toBe(
[
"✓ models.classify scorer/judge 5ms $0.000013",
"✓ models.classify scorer/judge 5ms $0.02",
"✓ models.classify scorer/judge 5ms",
"Model calls: $0.02",
].join("\n"),
);
});
it("shows results without a header, such as rejected options", () => {
const text = render(
{
content: [{ type: "text", text: "The @options line must be followed by JavaScript source" }],
details: undefined,
},
true,
);
expect(text).toBe("The @options line must be followed by JavaScript source");
});
it("limits collapsed output to wrapped lines, not logical lines", () => {
const text = render(
{
content: [
{ type: "text", text: "Script completed\nWall time 0.1 seconds\nOutput:\n" },
{ type: "text", text: "x".repeat(1000) },
],
details: { calls: [], fullOutputPath: "/tmp/out.txt" },
},
false,
false,
50,
);
const lines = text.split("\n");
expect(lines).toHaveLength(7);
expect(lines.slice(0, 5)).toEqual(Array(5).fill("x".repeat(50)));
expect(lines[5]).toMatch(/^\.\.\. \(15 more lines,/);
expect(lines[6]).toBe("Full output: /tmp/out.txt");
});
});