pi-mono/packages/coding-agent/test/assistant-message.test.ts
2026-06-30 11:55:40 +02:00

112 lines
3.9 KiB
TypeScript

import type { AssistantMessage } from "@earendil-works/pi-ai";
import { describe, expect, test } from "vitest";
import { AssistantMessageComponent } from "../src/modes/interactive/components/assistant-message.ts";
import { UserMessageComponent } from "../src/modes/interactive/components/user-message.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
import { stripAnsi } from "../src/utils/ansi.ts";
const OSC133_ZONE_START = "\x1b]133;A\x07";
const OSC133_ZONE_END = "\x1b]133;B\x07";
const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
function createAssistantMessage(
content: AssistantMessage["content"],
overrides: Partial<Pick<AssistantMessage, "stopReason">> = {},
): AssistantMessage {
return {
role: "assistant",
content,
api: "openai-responses",
provider: "openai",
model: "gpt-4o-mini",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: overrides.stopReason ?? "stop",
timestamp: Date.now(),
};
}
describe("AssistantMessageComponent", () => {
test("adds OSC 133 zone markers to assistant messages without tool calls", () => {
initTheme("dark");
const component = new AssistantMessageComponent(createAssistantMessage([{ type: "text", text: "hello" }]));
const lines = component.render(40);
expect(lines).not.toHaveLength(0);
expect(lines[0]).toContain(OSC133_ZONE_START);
expect(lines[lines.length - 1].startsWith(OSC133_ZONE_END + OSC133_ZONE_FINAL)).toBe(true);
});
test("does not add OSC 133 zone markers when assistant message contains tool calls", () => {
initTheme("dark");
const component = new AssistantMessageComponent(
createAssistantMessage([
{ type: "text", text: "calling tool" },
{ type: "toolCall", id: "tool-1", name: "read", arguments: { path: "file.txt" } },
]),
);
const rendered = component.render(60).join("\n");
expect(rendered.includes(OSC133_ZONE_START)).toBe(false);
expect(rendered.includes(OSC133_ZONE_END)).toBe(false);
expect(rendered.includes(OSC133_ZONE_FINAL)).toBe(false);
});
test("renders length stops as visible errors", () => {
initTheme("dark");
const component = new AssistantMessageComponent(
createAssistantMessage([{ type: "thinking", thinking: "private reasoning" }], { stopReason: "length" }),
true,
);
const rendered = component.render(80).join("\n");
expect(rendered).toContain("Thinking...");
expect(rendered).toContain("maximum output token limit");
expect(rendered).toContain("response may be incomplete");
});
test("uses configured output padding for text and thinking", () => {
initTheme("dark");
const component = new AssistantMessageComponent(
createAssistantMessage([
{ type: "text", text: "hello" },
{ type: "thinking", thinking: "reasoning" },
]),
false,
undefined,
"Thinking...",
1,
);
const lines = component.render(80).map((line) => stripAnsi(line));
expect(lines.some((line) => line.includes(" hello"))).toBe(true);
expect(lines.some((line) => line.includes(" reasoning"))).toBe(true);
component.setOutputPad(0);
const updatedLines = component.render(80).map((line) => stripAnsi(line));
expect(updatedLines.some((line) => line.startsWith("hello"))).toBe(true);
expect(updatedLines.some((line) => line.startsWith("reasoning"))).toBe(true);
});
test("uses configured output padding for user messages", () => {
initTheme("dark");
const paddedComponent = new UserMessageComponent("hello", undefined, 1);
const paddedLines = paddedComponent.render(40).map((line) => stripAnsi(line));
expect(paddedLines.some((line) => line.startsWith(" hello"))).toBe(true);
const unpaddedComponent = new UserMessageComponent("hello", undefined, 0);
const unpaddedLines = unpaddedComponent.render(40).map((line) => stripAnsi(line));
expect(unpaddedLines.some((line) => line.startsWith("hello"))).toBe(true);
});
});