Files
pi/packages/coding-agent/test/codemode-renderer.test.ts
Armin Ronacher 9a100c7cc4 feat(coding-agent): report codemode classifier and nested tool usage in session cost
Codemode sums the usage of models.classify() calls into its tool result and shows each call's cost. Usage of tools called through ctx.executeTool() is added to the calling tool's result usage instead of being dropped.
2026-09-29 16:21:39 +02:00

85 lines
2.7 KiB
TypeScript

import { stripVTControlCharacters } from "node:util";
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
import type { Text } 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): string {
const context = {
args: { code: "" },
toolCallId: "call",
invalidate: () => {},
lastComponent: undefined,
state: {},
cwd: "/",
executionStarted: true,
argsComplete: true,
isPartial: false,
expanded: true,
showImages: false,
isError,
} satisfies ToolRenderContext;
const component = codemodeRenderers.renderResult?.(
result,
{ expanded: true, isPartial: false },
theme,
context,
) as Text;
return stripVTControlCharacters(component.render(200).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");
});
});