Files
Codewalkers/apps/web/src/lib/parse-agent-output.test.ts
Lukas May ee6b0da976 feat: Add tool correlation, toolInput metadata, and unknown-type fallbacks to parser
- Extend ParsedMessage.meta with toolInput to expose raw tool arguments
- Add toolUseRegistry to correlate tool_result blocks back to originating tool_use
- Set toolInput on tool_call messages and populate meta.toolName/toolInput on tool_result
- Fix tool_result with is_error:true now correctly produces type "error"
- Add catch-all for unknown top-level event types (emits system message)
- Add catch-all for unknown assistant content block types (emits system message)
- Add unit tests covering all 8 scenarios including regression cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 16:02:50 +01:00

265 lines
7.8 KiB
TypeScript

import { parseAgentOutput } from "./parse-agent-output";
function chunk(events: object[]): string {
return events.map((e) => JSON.stringify(e)).join("\n");
}
describe("parseAgentOutput", () => {
// 1. toolInput is set on tool_call messages
it("sets meta.toolInput on tool_call messages", () => {
const input = chunk([
{
type: "assistant",
message: {
content: [
{
type: "tool_use",
id: "tu1",
name: "Read",
input: { file_path: "/foo.ts" },
},
],
},
},
]);
const messages = parseAgentOutput(input);
const toolCall = messages.find((m) => m.type === "tool_call");
expect(toolCall).toBeDefined();
expect(toolCall!.meta?.toolInput).toEqual({ file_path: "/foo.ts" });
});
// 2. tool_result with tool_use_id gets meta.toolName and meta.toolInput from registry
it("correlates tool_result to its tool_use via registry", () => {
const input = chunk([
{
type: "assistant",
message: {
content: [
{
type: "tool_use",
id: "tu1",
name: "Read",
input: { file_path: "/foo.ts" },
},
],
},
},
{
type: "user",
message: {
content: [
{
type: "tool_result",
tool_use_id: "tu1",
content: "file contents",
},
],
},
},
]);
const messages = parseAgentOutput(input);
const toolResult = messages.find((m) => m.type === "tool_result");
expect(toolResult).toBeDefined();
expect(toolResult!.meta?.toolName).toBe("Read");
expect(toolResult!.meta?.toolInput).toEqual({ file_path: "/foo.ts" });
});
// 3. tool_result with no matching registry entry has no meta.toolName
it("tool_result with unknown tool_use_id has no meta.toolName", () => {
const input = chunk([
{
type: "user",
message: {
content: [
{
type: "tool_result",
tool_use_id: "unknown-id",
content: "output",
},
],
},
},
]);
const messages = parseAgentOutput(input);
const toolResult = messages.find((m) => m.type === "tool_result");
expect(toolResult).toBeDefined();
expect(toolResult!.meta?.toolName).toBeUndefined();
});
// 4. tool_result with is_error: true produces type: "error" and meta.isError: true
it("tool_result with is_error: true produces error message", () => {
const input = chunk([
{
type: "user",
message: {
content: [
{
type: "tool_result",
tool_use_id: "tu1",
is_error: true,
content: "something went wrong",
},
],
},
},
]);
const messages = parseAgentOutput(input);
const errorMsg = messages.find((m) => m.content === "something went wrong");
expect(errorMsg).toBeDefined();
expect(errorMsg!.type).toBe("error");
expect(errorMsg!.meta?.isError).toBe(true);
});
// 5. tool_result from a Task tool_use gets correct meta.toolName and meta.toolInput
it("tool_result from Task tool_use has correct meta", () => {
const taskInput = {
subagent_type: "Explore",
description: "find files",
prompt: "search for *.ts",
};
const input = chunk([
{
type: "assistant",
message: {
content: [
{
type: "tool_use",
id: "tu2",
name: "Task",
input: taskInput,
},
],
},
},
{
type: "user",
message: {
content: [
{
type: "tool_result",
tool_use_id: "tu2",
content: "found 10 files",
},
],
},
},
]);
const messages = parseAgentOutput(input);
const toolResult = messages.find((m) => m.type === "tool_result");
expect(toolResult).toBeDefined();
expect(toolResult!.meta?.toolName).toBe("Task");
expect(toolResult!.meta?.toolInput).toEqual(taskInput);
});
// 6. Unknown top-level event type produces a system message
it("unknown top-level event type produces system message", () => {
const input = chunk([{ type: "future_event_type", data: {} }]);
const messages = parseAgentOutput(input);
expect(messages).toHaveLength(1);
expect(messages[0].type).toBe("system");
expect(messages[0].content).toBe("[unknown event: future_event_type]");
});
// 7. Unknown assistant content block type produces a system message
it("unknown assistant content block type produces system message", () => {
const input = chunk([
{
type: "assistant",
message: {
content: [{ type: "image", data: "base64..." }],
},
},
]);
const messages = parseAgentOutput(input);
expect(messages).toHaveLength(1);
expect(messages[0].type).toBe("system");
expect(messages[0].content).toBe("[unsupported content block: image]");
});
// 8. Previously passing behavior unchanged
describe("previously passing behavior", () => {
it("system event with session_id produces system message", () => {
const input = chunk([{ type: "system", session_id: "sess-123" }]);
const messages = parseAgentOutput(input);
expect(messages).toHaveLength(1);
expect(messages[0].type).toBe("system");
expect(messages[0].content).toBe("Session started: sess-123");
});
it("assistant text block produces text message", () => {
const input = chunk([
{
type: "assistant",
message: {
content: [{ type: "text", text: "Hello, world!" }],
},
},
]);
const messages = parseAgentOutput(input);
expect(messages).toHaveLength(1);
expect(messages[0].type).toBe("text");
expect(messages[0].content).toBe("Hello, world!");
});
it("assistant tool_use block produces tool_call message with meta.toolName", () => {
const input = chunk([
{
type: "assistant",
message: {
content: [
{
type: "tool_use",
id: "tu1",
name: "Bash",
input: { command: "ls -la", description: "list files" },
},
],
},
},
]);
const messages = parseAgentOutput(input);
const toolCall = messages.find((m) => m.type === "tool_call");
expect(toolCall).toBeDefined();
expect(toolCall!.meta?.toolName).toBe("Bash");
});
it("result event with is_error: false produces session_end", () => {
const input = chunk([
{
type: "result",
is_error: false,
total_cost_usd: 0.01,
duration_ms: 5000,
},
]);
const messages = parseAgentOutput(input);
expect(messages).toHaveLength(1);
expect(messages[0].type).toBe("session_end");
expect(messages[0].content).toBe("Session completed");
});
it("result event with is_error: true produces session_end with meta.isError", () => {
const input = chunk([
{
type: "result",
is_error: true,
total_cost_usd: 0.01,
duration_ms: 5000,
},
]);
const messages = parseAgentOutput(input);
expect(messages).toHaveLength(1);
expect(messages[0].type).toBe("session_end");
expect(messages[0].meta?.isError).toBe(true);
});
it("non-JSON line produces error message with raw line as content", () => {
const rawLine = "This is not JSON at all";
const messages = parseAgentOutput(rawLine);
expect(messages).toHaveLength(1);
expect(messages[0].type).toBe("error");
expect(messages[0].content).toBe(rawLine);
});
});
});