Skip to content

Commit d5c2ead

Browse files
authored
fix(console): match child-workflow inner blocks by instanceId when reconciling dropped SSE events (#4575)
* fix(console): match child-workflow inner blocks by instanceId when reconciling dropped SSE events * fix(console): drop noisy warn when reconcile finds no matching entry
1 parent 6503671 commit d5c2ead

3 files changed

Lines changed: 774 additions & 17 deletions

File tree

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
/**
2+
* @vitest-environment node
3+
*
4+
* Integration tests that exercise `reconcileFinalBlockLogs` against the real
5+
* `useTerminalConsoleStore` to validate end-to-end matching behavior. The
6+
* sibling unit-test file mocks the store and only verifies call args, which
7+
* cannot catch identity-mismatch regressions of the kind that produced the
8+
* 34.57s wall-clock symptom.
9+
*/
10+
import { beforeEach, describe, expect, it, vi } from 'vitest'
11+
12+
vi.unmock('@/stores/terminal')
13+
vi.unmock('@/stores/terminal/console/store')
14+
15+
import { reconcileFinalBlockLogs } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils'
16+
import { useExecutionStore } from '@/stores/execution'
17+
import { useTerminalConsoleStore } from '@/stores/terminal/console/store'
18+
19+
describe('reconcileFinalBlockLogs (real store)', () => {
20+
beforeEach(() => {
21+
useTerminalConsoleStore.setState({
22+
workflowEntries: {},
23+
entryIdsByBlockExecution: {},
24+
entryLocationById: {},
25+
isOpen: false,
26+
_hasHydrated: true,
27+
})
28+
vi.mocked(useExecutionStore.getState).mockReturnValue({
29+
getCurrentExecutionId: vi.fn(() => 'exec-1'),
30+
} as any)
31+
})
32+
33+
it('actually flips a child-workflow inner block from running to success', () => {
34+
const store = useTerminalConsoleStore.getState()
35+
store.addConsole({
36+
workflowId: 'wf-1',
37+
blockId: 'workflow-1',
38+
blockName: 'Workflow 1',
39+
blockType: 'workflow',
40+
executionId: 'exec-1',
41+
executionOrder: 1,
42+
isRunning: false,
43+
success: true,
44+
childWorkflowInstanceId: 'child-inst-1',
45+
})
46+
store.addConsole({
47+
workflowId: 'wf-1',
48+
blockId: 'set-projects',
49+
blockName: 'setProjects',
50+
blockType: 'variables',
51+
executionId: 'exec-1',
52+
executionOrder: 5,
53+
isRunning: true,
54+
childWorkflowBlockId: 'child-inst-1',
55+
childWorkflowName: 'Workflow 1',
56+
})
57+
58+
const startedAt = new Date().toISOString()
59+
const endedAt = new Date(Date.now() + 27).toISOString()
60+
61+
reconcileFinalBlockLogs(store.updateConsole, 'wf-1', 'exec-1', [
62+
{
63+
blockId: 'workflow-1',
64+
blockName: 'Workflow 1',
65+
blockType: 'workflow',
66+
startedAt,
67+
endedAt,
68+
durationMs: 100,
69+
success: true,
70+
executionOrder: 1,
71+
childTraceSpans: [
72+
{
73+
id: 'set-projects-span',
74+
name: 'setProjects',
75+
type: 'variables',
76+
blockId: 'set-projects',
77+
executionOrder: 5,
78+
status: 'success',
79+
duration: 27,
80+
startTime: startedAt,
81+
endTime: endedAt,
82+
output: { value: [{ id: 'p1' }] },
83+
},
84+
],
85+
} as any,
86+
])
87+
88+
const innerEntry = useTerminalConsoleStore
89+
.getState()
90+
.getWorkflowEntries('wf-1')
91+
.find((e) => e.blockId === 'set-projects')
92+
93+
expect(innerEntry).toBeDefined()
94+
expect(innerEntry?.isRunning).toBe(false)
95+
expect(innerEntry?.success).toBe(true)
96+
expect(innerEntry?.durationMs).toBe(27)
97+
expect(innerEntry?.output).toEqual({ value: [{ id: 'p1' }] })
98+
})
99+
100+
it('targets the correct invocation when the same child nodeId runs twice', () => {
101+
const store = useTerminalConsoleStore.getState()
102+
store.addConsole({
103+
workflowId: 'wf-1',
104+
blockId: 'workflow-1',
105+
blockName: 'Workflow 1',
106+
blockType: 'workflow',
107+
executionId: 'exec-1',
108+
executionOrder: 1,
109+
isRunning: false,
110+
success: true,
111+
childWorkflowInstanceId: 'inst-A',
112+
})
113+
store.addConsole({
114+
workflowId: 'wf-1',
115+
blockId: 'workflow-1',
116+
blockName: 'Workflow 1',
117+
blockType: 'workflow',
118+
executionId: 'exec-1',
119+
executionOrder: 2,
120+
isRunning: false,
121+
success: true,
122+
childWorkflowInstanceId: 'inst-B',
123+
})
124+
store.addConsole({
125+
workflowId: 'wf-1',
126+
blockId: 'fn-inner',
127+
blockName: 'Inner',
128+
blockType: 'function',
129+
executionId: 'exec-1',
130+
executionOrder: 3,
131+
isRunning: true,
132+
childWorkflowBlockId: 'inst-A',
133+
})
134+
store.addConsole({
135+
workflowId: 'wf-1',
136+
blockId: 'fn-inner',
137+
blockName: 'Inner',
138+
blockType: 'function',
139+
executionId: 'exec-1',
140+
executionOrder: 4,
141+
isRunning: true,
142+
childWorkflowBlockId: 'inst-B',
143+
})
144+
145+
const startedAt = new Date().toISOString()
146+
const endedAt = new Date(Date.now() + 5).toISOString()
147+
const baseLog = {
148+
blockName: 'Workflow 1',
149+
blockType: 'workflow',
150+
startedAt,
151+
endedAt,
152+
durationMs: 50,
153+
success: true,
154+
}
155+
156+
reconcileFinalBlockLogs(store.updateConsole, 'wf-1', 'exec-1', [
157+
{
158+
...baseLog,
159+
blockId: 'workflow-1',
160+
executionOrder: 1,
161+
childTraceSpans: [
162+
{
163+
id: 'a',
164+
name: 'Inner',
165+
type: 'function',
166+
blockId: 'fn-inner',
167+
executionOrder: 3,
168+
status: 'success',
169+
duration: 5,
170+
startTime: startedAt,
171+
endTime: endedAt,
172+
output: { result: 'A' },
173+
},
174+
],
175+
} as any,
176+
{
177+
...baseLog,
178+
blockId: 'workflow-1',
179+
executionOrder: 2,
180+
childTraceSpans: [
181+
{
182+
id: 'b',
183+
name: 'Inner',
184+
type: 'function',
185+
blockId: 'fn-inner',
186+
executionOrder: 4,
187+
status: 'success',
188+
duration: 5,
189+
startTime: startedAt,
190+
endTime: endedAt,
191+
output: { result: 'B' },
192+
},
193+
],
194+
} as any,
195+
])
196+
197+
const entries = useTerminalConsoleStore.getState().getWorkflowEntries('wf-1')
198+
const a = entries.find((e) => e.blockId === 'fn-inner' && e.childWorkflowBlockId === 'inst-A')
199+
const b = entries.find((e) => e.blockId === 'fn-inner' && e.childWorkflowBlockId === 'inst-B')
200+
201+
expect(a?.isRunning).toBe(false)
202+
expect(a?.output).toEqual({ result: 'A' })
203+
expect(b?.isRunning).toBe(false)
204+
expect(b?.output).toEqual({ result: 'B' })
205+
})
206+
207+
it('propagates error state for spans with error status', () => {
208+
const store = useTerminalConsoleStore.getState()
209+
store.addConsole({
210+
workflowId: 'wf-1',
211+
blockId: 'workflow-1',
212+
blockName: 'Workflow 1',
213+
blockType: 'workflow',
214+
executionId: 'exec-1',
215+
executionOrder: 1,
216+
isRunning: false,
217+
success: true,
218+
childWorkflowInstanceId: 'inst-1',
219+
})
220+
store.addConsole({
221+
workflowId: 'wf-1',
222+
blockId: 'http-1',
223+
blockName: 'API',
224+
blockType: 'api',
225+
executionId: 'exec-1',
226+
executionOrder: 2,
227+
isRunning: true,
228+
childWorkflowBlockId: 'inst-1',
229+
})
230+
231+
const startedAt = new Date().toISOString()
232+
const endedAt = new Date(Date.now() + 30).toISOString()
233+
234+
reconcileFinalBlockLogs(store.updateConsole, 'wf-1', 'exec-1', [
235+
{
236+
blockId: 'workflow-1',
237+
blockName: 'Workflow 1',
238+
blockType: 'workflow',
239+
startedAt,
240+
endedAt,
241+
durationMs: 100,
242+
success: true,
243+
executionOrder: 1,
244+
childTraceSpans: [
245+
{
246+
id: 'http-span',
247+
name: 'API',
248+
type: 'api',
249+
blockId: 'http-1',
250+
executionOrder: 2,
251+
status: 'error',
252+
duration: 30,
253+
startTime: startedAt,
254+
endTime: endedAt,
255+
output: { error: 'Connection refused' },
256+
},
257+
],
258+
} as any,
259+
])
260+
261+
const entry = useTerminalConsoleStore
262+
.getState()
263+
.getWorkflowEntries('wf-1')
264+
.find((e) => e.blockId === 'http-1')
265+
266+
expect(entry?.isRunning).toBe(false)
267+
expect(entry?.success).toBe(false)
268+
expect(entry?.error).toBe('Connection refused')
269+
})
270+
271+
it('matches the correct iteration row inside a child workflow loop', () => {
272+
const store = useTerminalConsoleStore.getState()
273+
store.addConsole({
274+
workflowId: 'wf-1',
275+
blockId: 'workflow-1',
276+
blockName: 'Workflow 1',
277+
blockType: 'workflow',
278+
executionId: 'exec-1',
279+
executionOrder: 1,
280+
isRunning: false,
281+
success: true,
282+
childWorkflowInstanceId: 'inst-1',
283+
})
284+
store.addConsole({
285+
workflowId: 'wf-1',
286+
blockId: 'fn-leaf',
287+
blockName: 'Leaf',
288+
blockType: 'function',
289+
executionId: 'exec-1',
290+
executionOrder: 2,
291+
isRunning: false,
292+
success: true,
293+
iterationCurrent: 0,
294+
iterationType: 'loop',
295+
iterationContainerId: 'loop-1',
296+
childWorkflowBlockId: 'inst-1',
297+
output: { i: 0 },
298+
})
299+
store.addConsole({
300+
workflowId: 'wf-1',
301+
blockId: 'fn-leaf',
302+
blockName: 'Leaf',
303+
blockType: 'function',
304+
executionId: 'exec-1',
305+
executionOrder: 3,
306+
isRunning: true,
307+
iterationCurrent: 1,
308+
iterationType: 'loop',
309+
iterationContainerId: 'loop-1',
310+
childWorkflowBlockId: 'inst-1',
311+
})
312+
313+
const startedAt = new Date().toISOString()
314+
const endedAt = new Date(Date.now() + 12).toISOString()
315+
316+
reconcileFinalBlockLogs(store.updateConsole, 'wf-1', 'exec-1', [
317+
{
318+
blockId: 'workflow-1',
319+
blockName: 'Workflow 1',
320+
blockType: 'workflow',
321+
startedAt,
322+
endedAt,
323+
durationMs: 100,
324+
success: true,
325+
executionOrder: 1,
326+
childTraceSpans: [
327+
{
328+
id: 'leaf-0',
329+
name: 'Leaf',
330+
type: 'function',
331+
blockId: 'fn-leaf',
332+
executionOrder: 2,
333+
loopId: 'loop-1',
334+
iterationIndex: 0,
335+
status: 'success',
336+
duration: 5,
337+
startTime: startedAt,
338+
endTime: endedAt,
339+
output: { i: 0 },
340+
},
341+
{
342+
id: 'leaf-1',
343+
name: 'Leaf',
344+
type: 'function',
345+
blockId: 'fn-leaf',
346+
executionOrder: 3,
347+
loopId: 'loop-1',
348+
iterationIndex: 1,
349+
status: 'success',
350+
duration: 12,
351+
startTime: startedAt,
352+
endTime: endedAt,
353+
output: { i: 1 },
354+
},
355+
],
356+
} as any,
357+
])
358+
359+
const entries = useTerminalConsoleStore.getState().getWorkflowEntries('wf-1')
360+
const iter0 = entries.find((e) => e.blockId === 'fn-leaf' && e.iterationCurrent === 0)
361+
const iter1 = entries.find((e) => e.blockId === 'fn-leaf' && e.iterationCurrent === 1)
362+
363+
expect(iter0?.isRunning).toBe(false)
364+
expect(iter0?.output).toEqual({ i: 0 })
365+
expect(iter1?.isRunning).toBe(false)
366+
expect(iter1?.output).toEqual({ i: 1 })
367+
})
368+
})

0 commit comments

Comments
 (0)