/** * @file replay.test.ts * @description Unit tests for the Replay tab's pure derivations: playback * order, lifecycle markers and the session state at a cursor. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect } from "vitest"; import { chronological, lifecycleMarkers, replayState } from "../replay"; import type { DashboardEvent } from "../types"; let nextId = 1; function ev(event_type: string, sec: number, tool_name: string | null = null, agent = "main") { return { id: nextId++, session_id: "s", agent_id: agent, event_type, tool_name, summary: null, data: null, created_at: new Date(Date.UTC(2026, 9, 1, 10, 0, sec)).toISOString(), } as unknown as DashboardEvent; } const events = [ ev("SessionStart", 0), ev("UserPromptSubmit", 1), ev("PreToolUse", 2, "Read"), ev("PostToolUse", 3, "Read"), ev("PreToolUse", 4, "Bash"), ev("PreToolUse", 5, "Read", "sub"), ev("APIError", 6), ev("PostToolUse", 7, "Bash"), ev("Stop", 8), ]; describe("chronological", () => { it("orders by time, then id", () => { const tie = [ev("Stop", 9), ev("Stop", 9)]; const out = chronological([tie[1]!, events[2]!, tie[0]!, events[0]!]); expect(out.map((e) => e.id)).toEqual([events[0]!.id, events[2]!.id, tie[0]!.id, tie[1]!.id]); }); }); describe("lifecycleMarkers", () => { it("marks lifecycle events at their sequence position", () => { const m = lifecycleMarkers(events); expect(m.map((x) => x.type)).toEqual(["SessionStart", "UserPromptSubmit", "APIError", "Stop"]); expect(m[0]!.pct).toBe(0); expect(m[3]!.pct).toBe(100); expect(m[2]!.index).toBe(6); }); it("handles a single event", () => { expect(lifecycleMarkers([events[0]!])).toEqual([{ index: 0, type: "SessionStart", pct: 0 }]); }); }); describe("replayState", () => { it("is empty for no events", () => { const s = replayState([], 0); expect(s.status).toBeNull(); expect(s.currentTool).toBeNull(); expect(s.toolCounts).toEqual([]); }); it("tracks the tool in flight and counts up to the cursor", () => { const at4 = replayState(events, 4); expect(at4.currentTool).toBe("Bash"); expect(at4.status).toBe("working"); expect(at4.prompts).toBe(1); expect(at4.toolCounts).toEqual([ { tool: "Bash", count: 1 }, { tool: "Read", count: 1 }, ]); expect(at4.elapsedMs).toBe(4000); const at6 = replayState(events, 6); expect(at6.currentTool).toBe("Read"); // the subagent's, newest still open expect(at6.errors).toBe(1); expect(at6.status).toBe("error"); expect(at6.toolCounts[0]).toEqual({ tool: "Read", count: 2 }); }); it("closes a finished tool and an ended turn", () => { expect(replayState(events, 3).currentTool).toBeNull(); // Stop on main leaves only the subagent's Read open. expect(replayState(events, 8).currentTool).toBe("Read"); expect(replayState(events, 8).status).toBe("waiting"); }); it("clamps a cursor past the end", () => { expect(replayState(events, 99).elapsedMs).toBe(8000); }); });