import { describe, it, expect } from "vitest"; import { parseAgentEvents, summarizeToolCall, stripLineNumbers, } from "../../src/client/lib/agent-progress-parser"; import type { ParsedMessage } from "../../src/shared/types"; /** Build a fake ParsedMessage whose content is a JSON agent_progress payload */ function makeAgentEvent( dataOverrides: Record = {}, msgOverrides: Partial = {} ): ParsedMessage { const data = { message: { type: "assistant", message: { role: "assistant", content: [ { type: "tool_use", id: "toolu_abc123", name: "Read", input: { file_path: "/src/foo.ts" }, }, ], }, timestamp: "2026-01-30T16:22:21.000Z", }, normalizedMessages: [], type: "agent_progress", prompt: "Explore the codebase", agentId: "a6945d4", ...dataOverrides, }; return { uuid: crypto.randomUUID(), category: "hook_progress", content: JSON.stringify(data), rawIndex: 0, timestamp: "2026-01-30T16:22:21.000Z", progressSubtype: "agent", ...msgOverrides, }; } describe("agent-progress-parser", () => { describe("parseAgentEvents", () => { it("parses a user text event and extracts the prompt", () => { const event = makeAgentEvent({ message: { type: "user", message: { role: "user", content: [ { type: "text", text: "Find the auth implementation" }, ], }, timestamp: "2026-01-30T16:22:00.000Z", }, }); const result = parseAgentEvents([event]); expect(result.prompt).toBe("Explore the codebase"); expect(result.agentId).toBe("a6945d4"); expect(result.events.length).toBe(1); expect(result.events[0].kind).toBe("user_text"); if (result.events[0].kind === "user_text") { expect(result.events[0].text).toBe("Find the auth implementation"); } }); it("parses assistant turn with tool_use blocks and extracts tool info", () => { const event = makeAgentEvent(); // default has Read tool_use const result = parseAgentEvents([event]); expect(result.events.length).toBe(1); expect(result.events[0].kind).toBe("tool_call"); if (result.events[0].kind === "tool_call") { expect(result.events[0].toolName).toBe("Read"); expect(result.events[0].toolUseId).toBe("toolu_abc123"); expect(result.events[0].input).toEqual({ file_path: "/src/foo.ts" }); } }); it("parses user turn with tool_result and extracts result content", () => { const event = makeAgentEvent({ message: { type: "user", message: { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_abc123", content: "File contents here...", }, ], }, timestamp: "2026-01-30T16:22:22.000Z", }, }); const result = parseAgentEvents([event]); expect(result.events[0].kind).toBe("tool_result"); if (result.events[0].kind === "tool_result") { expect(result.events[0].toolUseId).toBe("toolu_abc123"); expect(result.events[0].content).toBe("File contents here..."); } }); it("parses assistant turn with text content as text_response", () => { const event = makeAgentEvent({ message: { type: "assistant", message: { role: "assistant", content: [ { type: "text", text: "Here is my analysis of the codebase.\nLine 2\nLine 3\nLine 4" }, ], }, timestamp: "2026-01-30T16:22:30.000Z", }, }); const result = parseAgentEvents([event]); expect(result.events[0].kind).toBe("text_response"); if (result.events[0].kind === "text_response") { expect(result.events[0].text).toContain("Here is my analysis"); expect(result.events[0].lineCount).toBe(4); } }); it("extracts agentId and prompt from events", () => { const events = [makeAgentEvent()]; const result = parseAgentEvents(events); expect(result.agentId).toBe("a6945d4"); expect(result.prompt).toBe("Explore the codebase"); }); it("calculates time range from first/last event timestamps", () => { const events = [ makeAgentEvent( { message: { type: "user", message: { role: "user", content: [{ type: "text", text: "Go" }] }, timestamp: "2026-01-30T16:22:00.000Z", }, }, { timestamp: "2026-01-30T16:22:00.000Z" } ), makeAgentEvent( { message: { type: "assistant", message: { role: "assistant", content: [ { type: "tool_use", id: "t1", name: "Read", input: { file_path: "a.ts" } }, ], }, timestamp: "2026-01-30T16:22:30.000Z", }, }, { timestamp: "2026-01-30T16:22:30.000Z" } ), ]; const result = parseAgentEvents(events); expect(result.firstTimestamp).toBe("2026-01-30T16:22:00.000Z"); expect(result.lastTimestamp).toBe("2026-01-30T16:22:30.000Z"); }); it("counts turns correctly (tool_call events = turns)", () => { const events = [ // User text makeAgentEvent({ message: { type: "user", message: { role: "user", content: [{ type: "text", text: "Go" }] }, timestamp: "2026-01-30T16:22:00.000Z", }, }), // Tool call 1 makeAgentEvent({ message: { type: "assistant", message: { role: "assistant", content: [ { type: "tool_use", id: "t1", name: "Read", input: { file_path: "a.ts" } }, ], }, timestamp: "2026-01-30T16:22:01.000Z", }, }), // Tool result 1 makeAgentEvent({ message: { type: "user", message: { role: "user", content: [{ type: "tool_result", tool_use_id: "t1", content: "..." }], }, timestamp: "2026-01-30T16:22:02.000Z", }, }), // Tool call 2 makeAgentEvent({ message: { type: "assistant", message: { role: "assistant", content: [ { type: "tool_use", id: "t2", name: "Grep", input: { pattern: "foo" } }, ], }, timestamp: "2026-01-30T16:22:03.000Z", }, }), // Tool result 2 makeAgentEvent({ message: { type: "user", message: { role: "user", content: [{ type: "tool_result", tool_use_id: "t2", content: "..." }], }, timestamp: "2026-01-30T16:22:04.000Z", }, }), // Final text makeAgentEvent({ message: { type: "assistant", message: { role: "assistant", content: [{ type: "text", text: "Done" }], }, timestamp: "2026-01-30T16:22:05.000Z", }, }), ]; const result = parseAgentEvents(events); expect(result.turnCount).toBe(2); // 2 tool_call events }); it("handles malformed JSON content gracefully", () => { const event: ParsedMessage = { uuid: "bad-json", category: "hook_progress", content: "this is not valid json {{{", rawIndex: 0, timestamp: "2026-01-30T16:22:00.000Z", progressSubtype: "agent", }; const result = parseAgentEvents([event]); expect(result.events.length).toBe(1); expect(result.events[0].kind).toBe("raw_content"); if (result.events[0].kind === "raw_content") { expect(result.events[0].content).toBe("this is not valid json {{{"); } }); it("handles empty events array", () => { const result = parseAgentEvents([]); expect(result.events).toEqual([]); expect(result.agentId).toBeUndefined(); expect(result.prompt).toBeUndefined(); expect(result.turnCount).toBe(0); }); it("handles tool_result with array content blocks", () => { const event = makeAgentEvent({ message: { type: "user", message: { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_xyz", content: [ { type: "text", text: "Part 1\n" }, { type: "text", text: "Part 2\n" }, ], }, ], }, timestamp: "2026-01-30T16:22:22.000Z", }, }); const result = parseAgentEvents([event]); expect(result.events[0].kind).toBe("tool_result"); if (result.events[0].kind === "tool_result") { expect(result.events[0].content).toBe("Part 1\nPart 2\n"); } }); it("handles multiple tool_use blocks in a single assistant message", () => { const event = makeAgentEvent({ message: { type: "assistant", message: { role: "assistant", content: [ { type: "tool_use", id: "t1", name: "Read", input: { file_path: "a.ts" } }, { type: "tool_use", id: "t2", name: "Read", input: { file_path: "b.ts" } }, ], }, timestamp: "2026-01-30T16:22:01.000Z", }, }); const result = parseAgentEvents([event]); const toolCalls = result.events.filter((e) => e.kind === "tool_call"); expect(toolCalls.length).toBe(2); }); }); describe("summarizeToolCall", () => { it("summarizes Read tool with file_path", () => { const summary = summarizeToolCall("Read", { file_path: "/src/components/App.tsx" }); expect(summary).toBe("src/components/App.tsx"); }); it("summarizes Grep tool with pattern + path", () => { const summary = summarizeToolCall("Grep", { pattern: "useChat", path: "/src/hooks/", }); expect(summary).toContain("useChat"); expect(summary).toContain("src/hooks/"); }); it("summarizes Bash tool with truncated command", () => { const summary = summarizeToolCall("Bash", { command: "npm run test -- --watch --verbose --coverage --reporter=json", }); expect(summary.length).toBeLessThanOrEqual(80); expect(summary).toContain("npm run test"); }); it("summarizes Task tool with description", () => { const summary = summarizeToolCall("Task", { description: "Find all auth handlers", subagent_type: "Explore", }); expect(summary).toContain("Find all auth handlers"); }); it("summarizes Glob tool with pattern", () => { const summary = summarizeToolCall("Glob", { pattern: "**/*.test.ts", path: "/src/", }); expect(summary).toContain("**/*.test.ts"); }); it("summarizes Edit tool with file_path", () => { const summary = summarizeToolCall("Edit", { file_path: "/src/lib/utils.ts", old_string: "foo", new_string: "bar", }); expect(summary).toContain("src/lib/utils.ts"); }); it("summarizes Write tool with file_path", () => { const summary = summarizeToolCall("Write", { file_path: "/src/new-file.ts", content: "export const x = 1;", }); expect(summary).toContain("src/new-file.ts"); }); it("summarizes unknown tool with generic label", () => { const summary = summarizeToolCall("CustomTool", { foo: "bar" }); expect(summary).toBe("CustomTool"); }); it("summarizes mcp__morph-mcp__warpgrep_codebase_search with search_string", () => { const summary = summarizeToolCall("mcp__morph-mcp__warpgrep_codebase_search", { search_string: "Find authentication middleware", repo_path: "/data/projects/app", }); expect(summary).toContain("Find authentication middleware"); }); it("summarizes mcp__morph-mcp__edit_file with path", () => { const summary = summarizeToolCall("mcp__morph-mcp__edit_file", { path: "/src/lib/utils.ts", code_edit: "// changes", instruction: "Fix bug", }); expect(summary).toContain("src/lib/utils.ts"); }); it("strips leading slash from file paths", () => { const summary = summarizeToolCall("Read", { file_path: "/src/foo.ts" }); expect(summary).toBe("src/foo.ts"); }); }); describe("stripLineNumbers", () => { it("strips cat-n style line numbers with arrow prefix", () => { const input = " 1\u2192import React from 'react';\n 2\u2192\n 3\u2192export default function App() {"; const result = stripLineNumbers(input); expect(result).toBe("import React from 'react';\n\nexport default function App() {"); }); it("returns text unchanged if no line numbers detected", () => { const input = "just plain text\nno line numbers here"; expect(stripLineNumbers(input)).toBe(input); }); it("returns text unchanged for short single-line content", () => { const input = "single line"; expect(stripLineNumbers(input)).toBe(input); }); it("handles double-digit line numbers", () => { const lines = Array.from({ length: 15 }, (_, i) => ` ${i + 1}\u2192line ${i + 1}` ).join("\n"); const result = stripLineNumbers(lines); expect(result).toContain("line 1"); expect(result).not.toMatch(/^\s*\d+\u2192/m); }); }); describe("tool_result language linking", () => { it("links tool_result to preceding Read call and sets language", () => { const events = [ makeAgentEvent({ message: { type: "assistant", message: { role: "assistant", content: [ { type: "tool_use", id: "t1", name: "Read", input: { file_path: "/src/app.tsx" } }, ], }, timestamp: "2026-01-30T16:22:01.000Z", }, }), makeAgentEvent({ message: { type: "user", message: { role: "user", content: [ { type: "tool_result", tool_use_id: "t1", content: "import React..." }, ], }, timestamp: "2026-01-30T16:22:02.000Z", }, }), ]; const result = parseAgentEvents(events); const toolResult = result.events.find((e) => e.kind === "tool_result"); expect(toolResult).toBeDefined(); if (toolResult?.kind === "tool_result") { expect(toolResult.language).toBe("tsx"); expect(toolResult.sourceTool).toBe("Read"); } }); it("sets no language for Grep results", () => { const events = [ makeAgentEvent({ message: { type: "assistant", message: { role: "assistant", content: [ { type: "tool_use", id: "t1", name: "Grep", input: { pattern: "foo" } }, ], }, timestamp: "2026-01-30T16:22:01.000Z", }, }), makeAgentEvent({ message: { type: "user", message: { role: "user", content: [ { type: "tool_result", tool_use_id: "t1", content: "src/a.ts:10:foo" }, ], }, timestamp: "2026-01-30T16:22:02.000Z", }, }), ]; const result = parseAgentEvents(events); const toolResult = result.events.find((e) => e.kind === "tool_result"); if (toolResult?.kind === "tool_result") { expect(toolResult.language).toBeUndefined(); expect(toolResult.sourceTool).toBe("Grep"); } }); }); });