import { describe, it, expect } from "vitest"; import { createTurnProgressState, reduceTurnProgress, formatProgressLine, formatTokens, type TurnProgressSnapshot, } from "../src/turnProgress"; // Minimal event factories — structurally match the @cline/sdk AgentRuntimeEvent // members the reducer reads. `as any` because we only supply the fields used. const ev = (e: any) => e as any; const textDelta = (text: string, accumulatedText: string) => ev({ type: "assistant-text-delta", text, accumulatedText }); const toolStarted = (toolName: string) => ev({ type: "tool-started", toolCall: { toolName } }); const toolFinished = (toolName: string) => ev({ type: "tool-finished", toolCall: { toolName } }); const usage = (inputTokens: number, outputTokens: number) => ev({ type: "usage-updated", usage: { inputTokens, outputTokens, cacheReadTokens: 0, cacheWriteTokens: 0 } }); // Drive a sequence of events, returning every emitted snapshot. function run(events: any[]): TurnProgressSnapshot[] { let state = createTurnProgressState(); const out: TurnProgressSnapshot[] = []; for (const e of events) { const r = reduceTurnProgress(state, e); state = r.state; if (r.snapshot) out.push(r.snapshot); } return out; } describe("reduceTurnProgress", () => { it("starts in thinking", () => { const s = run([ev({ type: "run-started" })]); expect(s.at(-1)!.phase).toBe("thinking"); expect(s.at(-1)!.chars).toBe(0); expect(s.at(-1)!.tokens).toBeUndefined(); }); it("text deltas move to writing and accumulate chars + carry the delta", () => { const s = run([textDelta("Hel", "Hel"), textDelta("lo", "Hello")]); expect(s.map((x) => x.phase)).toEqual(["writing", "writing"]); expect(s.at(-1)!.chars).toBe(5); expect(s.map((x) => x.textDelta)).toEqual(["Hel", "lo"]); }); it("usage sets a running token total (input+output)", () => { const s = run([textDelta("Hi", "Hi"), usage(1000, 234)]); expect(s.at(-1)!.tokens).toBe(1234); }); it("tool start shows the tool name; tool finish reverts to writing once text was seen", () => { const s = run([textDelta("x", "x"), toolStarted("read_file"), toolFinished("read_file")]); expect(s[1].phase).toBe("tool"); expect(s[1].tool).toBe("read_file"); expect(s.at(-1)!.phase).toBe("writing"); }); it("tool finish reverts to thinking when no text was seen", () => { const s = run([toolStarted("grep"), toolFinished("grep")]); expect(s.at(-1)!.phase).toBe("thinking"); }); it("overlapping tools resolve in order", () => { const s = run([toolStarted("a"), toolStarted("b"), toolFinished("b"), toolFinished("a")]); expect(s.map((x) => x.phase)).toEqual(["tool", "tool", "tool", "thinking"]); expect(s[1].tool).toBe("b"); expect(s[2].tool).toBe("a"); }); it("reasoning deltas stay thinking and surface no text", () => { const s = run([ev({ type: "assistant-reasoning-delta", text: "secret", accumulatedText: "secret" })]); expect(s.at(-1)!.phase).toBe("thinking"); expect(s.at(-1)!.textDelta).toBeUndefined(); }); it("ignores lifecycle/finish events (no snapshot)", () => { const s = run([ev({ type: "turn-finished" }), ev({ type: "run-finished" })]); expect(s).toEqual([]); }); }); describe("formatProgressLine / formatTokens", () => { it("thinking", () => { expect(formatProgressLine({ phase: "thinking", chars: 0 })).toBe("thinking…"); }); it("writing with chars", () => { expect(formatProgressLine({ phase: "writing", chars: 412 })).toBe("writing… (412 chars)"); }); it("writing with chars + tokens", () => { expect(formatProgressLine({ phase: "writing", chars: 412, tokens: 1234 })).toBe( "writing… (412 chars) · 1.2k tokens", ); }); it("tool with name", () => { expect(formatProgressLine({ phase: "tool", tool: "read_file", chars: 0 })).toBe("running read_file…"); }); it("formats token magnitudes", () => { expect(formatTokens(950)).toBe("950"); expect(formatTokens(1234)).toBe("1.2k"); }); });