1import { describe, expect, it } from "vite-plus/test";
2
3import { mergeClaudeWorkflowProgress } from "./claudeWorkflowProgress.ts";
4
5const snapshotFrame = (
6 entries: ReadonlyArray<Record<string, unknown>>,
7 usage?: Record<string, number>,
8) => ({
9 type: "system",
10 subtype: "task_progress",
11 task_id: "w1",
12 description: "Alpha: alpha:one",
13 workflow_progress: entries,
14 ...(usage === undefined ? {} : { usage }),
15});
16
17const phase = (index: number, title: string) => ({ type: "workflow_phase", index, title });
18
19const agent = (overrides: Record<string, unknown>) => ({
20 type: "workflow_agent",
21 index: 1,
22 label: "alpha:one",
23 state: "start",
24 phaseIndex: 1,
25 phaseTitle: "Alpha",
26 ...overrides,
27});
28
29describe("mergeClaudeWorkflowProgress", () => {
30 it("maps provider agent states and per-member metrics", () => {
31 const workflow = mergeClaudeWorkflowProgress({
32 previous: undefined,
33 message: snapshotFrame([
34 phase(1, "Alpha"),
35 agent({
36 state: "done",
37 agentId: "a1",
38 model: "claude-opus-5[1m]",
39 attempt: 2,
40 tokens: 71_141,
41 toolCalls: 3,
42 durationMs: 1277,
43 startedAt: 1_700_000_000_000,
44 lastProgressAt: 1_700_000_004_432,
45 queuedAt: 1_699_999_999_000,
46 promptPreview: "Reply with exactly: A1",
47 resultPreview: "A1",
48 }),
49 agent({ index: 2, label: "alpha:two", state: "queued", lastProgressAt: 1000 }),
50 agent({ index: 3, state: "progress", lastProgressAt: 2000 }),
51 ]),
52 });
53 expect(workflow?.phases).toEqual([{ index: 1, title: "Alpha" }]);
54 expect(workflow?.agents[0]).toEqual({
55 index: 1,
56 label: "alpha:one",
57 agentId: "a1",
58 state: "completed",
59 phaseIndex: 1,
60 phaseTitle: "Alpha",
61 model: "claude-opus-5[1m]",
62 attempt: 2,
63 totalTokens: 71_141,
64 toolCalls: 3,
65 durationMs: 1277,
66 queuedAt: 1_699_999_999_000,
67 startedAt: 1_700_000_000_000,
68 completedAt: 1_700_000_004_432,
69 prompt: "Reply with exactly: A1",
70 result: "A1",
71 });
72 expect(workflow?.agents[1]?.state).toBe("queued");
73 expect(workflow?.agents[1]).not.toHaveProperty("completedAt");
74 expect(workflow?.agents[2]).not.toHaveProperty("completedAt");
75 });
76
77 it("keeps a started member queued until the provider reports its startedAt", () => {
78 const scheduled = mergeClaudeWorkflowProgress({
79 previous: undefined,
80 message: snapshotFrame([agent({ state: "start", queuedAt: 1000 })]),
81 });
82 expect(scheduled?.agents[0]?.state).toBe("queued");
83 const started = mergeClaudeWorkflowProgress({
84 previous: scheduled,
85 message: snapshotFrame([agent({ state: "start", queuedAt: 1000, startedAt: 21_000 })]),
86 });
87 expect(started?.agents[0]).toMatchObject({ state: "running", startedAt: 21_000 });
88 });
89
90 it.each(["something-new", "toString", "constructor", "__proto__"])(
91 "treats unrecognised state %s as still running rather than settled",
92 (state) => {
93 const workflow = mergeClaudeWorkflowProgress({
94 previous: undefined,
95 message: snapshotFrame([agent({ state })]),
96 });
97 expect(workflow?.agents[0]?.state).toBe("running");
98 },
99 );
100
101 it("keeps the roster through a usage-only frame", () => {
102 const first = mergeClaudeWorkflowProgress({
103 previous: undefined,
104 name: "probe-wf",
105 message: snapshotFrame([phase(1, "Alpha"), agent({})], {
106 total_tokens: 100,
107 tool_uses: 1,
108 duration_ms: 40,
109 }),
110 });
111 const second = mergeClaudeWorkflowProgress({
112 previous: first,
113 message: snapshotFrame([], { total_tokens: 250, tool_uses: 2, duration_ms: 900 }),
114 });
115 expect(second?.agents).toHaveLength(1);
116 expect(second?.phases).toHaveLength(1);
117 expect(second?.name).toBe("probe-wf");
118 expect(second?.totalTokens).toBe(250);
119 });
120
121 it("keeps other members and phase links when a sparse frame starts a new attempt", () => {
122 const first = mergeClaudeWorkflowProgress({
123 previous: undefined,
124 message: snapshotFrame([
125 phase(1, "Alpha"),
126 agent({
127 state: "error",
128 attempt: 1,
129 promptPreview: "First prompt",
130 resultPreview: "First result",
131 tokens: 100,
132 toolCalls: 2,
133 durationMs: 50,
134 queuedAt: 1000,
135 startedAt: 1010,
136 lastProgressAt: 2010,
137 }),
138 agent({ index: 2, state: "done" }),
139 ]),
140 });
141 expect(first?.agents[0]?.completedAt).toBe(2010);
142 expect(first?.agents[1]).not.toHaveProperty("completedAt");
143 const second = mergeClaudeWorkflowProgress({
144 previous: first && {
145 ...first,
146 agents: first.agents.map((member) => ({ ...member, effort: "max", speed: "standard" })),
147 },
148 message: snapshotFrame([
149 {
150 type: "workflow_agent",
151 index: 1,
152 label: "alpha:one",
153 state: "start",
154 attempt: 2,
155 startedAt: 3000,
156 },
157 ]),
158 });
159 expect(second?.agents).toEqual([
160 {
161 index: 1,
162 label: "alpha:one",
163 state: "running",
164 attempt: 2,
165 startedAt: 3000,
166 phaseIndex: 1,
167 phaseTitle: "Alpha",
168 },
169 { ...first?.agents[1], effort: "max", speed: "standard" },
170 ]);
171 expect(second?.phases).toEqual(first?.phases);
172 });
173
174 it("ignores an older attempt after a retry completes", () => {
175 const first = mergeClaudeWorkflowProgress({
176 previous: undefined,
177 message: snapshotFrame([agent({ state: "done", attempt: 2, resultPreview: "New result" })]),
178 });
179 const second = mergeClaudeWorkflowProgress({
180 previous: first,
181 message: snapshotFrame([agent({ state: "error", attempt: 1, resultPreview: "Old result" })]),
182 });
183 expect(second?.agents).toEqual(first?.agents);
184 });
185
186 it.each([
187 ["done", "start"],
188 ["done", "queued"],
189 ["error", "start"],
190 ["error", "queued"],
191 ["start", "queued"],
192 ])("keeps member state when the same attempt regresses from %s to %s", (state, staleState) => {
193 const first = mergeClaudeWorkflowProgress({
194 previous: undefined,
195 message: snapshotFrame([
196 agent({ state, attempt: 2, startedAt: 1010, resultPreview: "Current result" }),
197 ]),
198 });
199 const second = mergeClaudeWorkflowProgress({
200 previous: first,
201 message: snapshotFrame([
202 agent({ state: staleState, attempt: 2, startedAt: 1010, resultPreview: "Old result" }),
203 ]),
204 });
205 expect(second?.agents).toEqual(first?.agents);
206 });
207
208 it("retains member fields omitted by a later frame in the same attempt", () => {
209 const first = mergeClaudeWorkflowProgress({
210 previous: undefined,
211 message: snapshotFrame([
212 agent({
213 attempt: 2,
214 model: "claude-opus-5[1m]",
215 promptPreview: "Current prompt",
216 tokens: 100,
217 }),
218 ]),
219 });
220 const second = mergeClaudeWorkflowProgress({
221 previous: first && {
222 ...first,
223 agents: first.agents.map((member) => ({ ...member, effort: "xhigh", speed: "standard" })),
224 },
225 message: snapshotFrame([agent({ state: "done", resultPreview: "Current result" })]),
226 });
227 expect(second?.agents[0]).toMatchObject({
228 state: "completed",
229 attempt: 2,
230 prompt: "Current prompt",
231 result: "Current result",
232 totalTokens: 100,
233 model: "claude-opus-5[1m]",
234 effort: "xhigh",
235 speed: "standard",
236 });
237 });
238
239 it("drops malformed entries instead of failing the frame", () => {
240 const workflow = mergeClaudeWorkflowProgress({
241 previous: undefined,
242 message: snapshotFrame([
243 { type: "workflow_phase", title: "No index" },
244 { type: "workflow_agent", index: 4 },
245 { type: "something_else", index: 1, title: "Ignored" },
246 phase(1, "Alpha"),
247 ]),
248 });
249 expect(workflow).toEqual({ phases: [{ index: 1, title: "Alpha" }], agents: [] });
250 });
251});