import { describe, it, expect } from "vitest"; import { groupProgress } from "../../src/server/services/progress-grouper.js"; import type { ParsedMessage } from "../../src/shared/types.js"; function makeMsg( overrides: Partial & { uuid: string; rawIndex: number } ): ParsedMessage { return { category: "assistant_text", content: "test", ...overrides, }; } describe("progress-grouper", () => { it("partitions parented progress into toolProgress map", () => { const messages: ParsedMessage[] = [ makeMsg({ uuid: "tc-1", rawIndex: 0, category: "tool_call", toolUseId: "toolu_1" }), makeMsg({ uuid: "hp-1", rawIndex: 1, category: "hook_progress", parentToolUseId: "toolu_1", progressSubtype: "hook" }), makeMsg({ uuid: "hp-2", rawIndex: 2, category: "hook_progress", parentToolUseId: "toolu_1", progressSubtype: "bash" }), makeMsg({ uuid: "txt-1", rawIndex: 3, category: "assistant_text" }), ]; const result = groupProgress(messages); expect(result.toolProgress["toolu_1"]).toHaveLength(2); expect(result.toolProgress["toolu_1"][0].uuid).toBe("hp-1"); expect(result.toolProgress["toolu_1"][1].uuid).toBe("hp-2"); }); it("removes parented progress from messages array", () => { const messages: ParsedMessage[] = [ makeMsg({ uuid: "tc-1", rawIndex: 0, category: "tool_call", toolUseId: "toolu_1" }), makeMsg({ uuid: "hp-1", rawIndex: 1, category: "hook_progress", parentToolUseId: "toolu_1" }), makeMsg({ uuid: "txt-1", rawIndex: 2, category: "assistant_text" }), ]; const result = groupProgress(messages); expect(result.messages).toHaveLength(2); expect(result.messages.map((m) => m.uuid)).toEqual(["tc-1", "txt-1"]); }); it("keeps orphaned progress (no parentToolUseId) in messages", () => { const messages: ParsedMessage[] = [ makeMsg({ uuid: "hp-orphan", rawIndex: 0, category: "hook_progress", progressSubtype: "hook" }), makeMsg({ uuid: "txt-1", rawIndex: 1, category: "assistant_text" }), ]; const result = groupProgress(messages); expect(result.messages).toHaveLength(2); expect(result.messages[0].uuid).toBe("hp-orphan"); expect(Object.keys(result.toolProgress)).toHaveLength(0); }); it("keeps progress with invalid parentToolUseId (no matching tool_call) in messages", () => { const messages: ParsedMessage[] = [ makeMsg({ uuid: "hp-invalid", rawIndex: 0, category: "hook_progress", parentToolUseId: "toolu_nonexistent" }), makeMsg({ uuid: "txt-1", rawIndex: 1, category: "assistant_text" }), ]; const result = groupProgress(messages); expect(result.messages).toHaveLength(2); expect(result.messages[0].uuid).toBe("hp-invalid"); }); it("returns empty results for empty input", () => { const result = groupProgress([]); expect(result.messages).toEqual([]); expect(result.toolProgress).toEqual({}); }); it("sorts each toolProgress group by rawIndex", () => { const messages: ParsedMessage[] = [ makeMsg({ uuid: "tc-1", rawIndex: 0, category: "tool_call", toolUseId: "toolu_1" }), makeMsg({ uuid: "hp-3", rawIndex: 5, category: "hook_progress", parentToolUseId: "toolu_1" }), makeMsg({ uuid: "hp-1", rawIndex: 1, category: "hook_progress", parentToolUseId: "toolu_1" }), makeMsg({ uuid: "hp-2", rawIndex: 3, category: "hook_progress", parentToolUseId: "toolu_1" }), ]; const result = groupProgress(messages); const group = result.toolProgress["toolu_1"]; expect(group.map((m) => m.rawIndex)).toEqual([1, 3, 5]); }); it("handles multiple tool_call parents independently", () => { const messages: ParsedMessage[] = [ makeMsg({ uuid: "tc-1", rawIndex: 0, category: "tool_call", toolUseId: "toolu_1" }), makeMsg({ uuid: "hp-1a", rawIndex: 1, category: "hook_progress", parentToolUseId: "toolu_1" }), makeMsg({ uuid: "tc-2", rawIndex: 2, category: "tool_call", toolUseId: "toolu_2" }), makeMsg({ uuid: "hp-2a", rawIndex: 3, category: "hook_progress", parentToolUseId: "toolu_2" }), makeMsg({ uuid: "hp-2b", rawIndex: 4, category: "hook_progress", parentToolUseId: "toolu_2" }), ]; const result = groupProgress(messages); expect(result.toolProgress["toolu_1"]).toHaveLength(1); expect(result.toolProgress["toolu_2"]).toHaveLength(2); expect(result.messages).toHaveLength(2); // only the 2 tool_calls }); });