@@ -5,14 +5,144 @@ import { resetTerminalConsoleMock, terminalConsoleMockFns } from '@sim/testing'
55import { beforeEach , describe , expect , it , vi } from 'vitest'
66import {
77 addExecutionErrorConsoleEntry ,
8+ createBlockEventHandlers ,
89 handleExecutionErrorConsole ,
910 reconcileFinalBlockLogs ,
1011} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils'
1112import type { BlockLog } from '@/executor/types'
13+ import { useExecutionStore } from '@/stores/execution'
1214
1315describe ( 'workflow-execution-utils' , ( ) => {
1416 beforeEach ( ( ) => {
1517 resetTerminalConsoleMock ( )
18+ vi . mocked ( useExecutionStore . getState ) . mockReturnValue ( {
19+ getCurrentExecutionId : vi . fn ( ( ) => 'exec-1' ) ,
20+ } as any )
21+ } )
22+
23+ describe ( 'createBlockEventHandlers' , ( ) => {
24+ it ( 'skips duplicate block start rows during reconnect replay' , ( ) => {
25+ terminalConsoleMockFns . mockAddConsole ( {
26+ workflowId : 'wf-1' ,
27+ blockId : 'fn-1' ,
28+ blockName : 'Function 1' ,
29+ blockType : 'function' ,
30+ executionId : 'exec-1' ,
31+ executionOrder : 7 ,
32+ isRunning : false ,
33+ success : true ,
34+ iterationCurrent : 0 ,
35+ iterationTotal : 2 ,
36+ iterationType : 'loop' ,
37+ iterationContainerId : 'loop-1' ,
38+ childWorkflowBlockId : 'child-inst-1' ,
39+ childWorkflowName : 'Child Workflow' ,
40+ parentIterations : [
41+ {
42+ iterationCurrent : 1 ,
43+ iterationTotal : 3 ,
44+ iterationType : 'parallel' ,
45+ iterationContainerId : 'parallel-1' ,
46+ } ,
47+ ] ,
48+ } )
49+
50+ const addConsole = vi . fn ( )
51+ const handlers = createBlockEventHandlers (
52+ {
53+ workflowId : 'wf-1' ,
54+ executionIdRef : { current : 'exec-1' } ,
55+ workflowEdges : [ ] ,
56+ activeBlocksSet : new Set < string > ( ) ,
57+ activeBlockRefCounts : new Map < string , number > ( ) ,
58+ accumulatedBlockLogs : [ ] ,
59+ accumulatedBlockStates : new Map ( ) ,
60+ executedBlockIds : new Set < string > ( ) ,
61+ includeStartConsoleEntry : true ,
62+ } ,
63+ {
64+ addConsole,
65+ updateConsole : vi . fn ( ) ,
66+ setActiveBlocks : vi . fn ( ) ,
67+ setBlockRunStatus : vi . fn ( ) ,
68+ setEdgeRunStatus : vi . fn ( ) ,
69+ }
70+ )
71+
72+ handlers . onBlockStarted ( {
73+ blockId : 'fn-1' ,
74+ blockName : 'Function 1' ,
75+ blockType : 'function' ,
76+ executionOrder : 7 ,
77+ iterationCurrent : 0 ,
78+ iterationTotal : 2 ,
79+ iterationType : 'loop' ,
80+ iterationContainerId : 'loop-1' ,
81+ childWorkflowBlockId : 'child-inst-1' ,
82+ childWorkflowName : 'Child Workflow' ,
83+ parentIterations : [
84+ {
85+ iterationCurrent : 1 ,
86+ iterationTotal : 3 ,
87+ iterationType : 'parallel' ,
88+ iterationContainerId : 'parallel-1' ,
89+ } ,
90+ ] ,
91+ } )
92+
93+ expect ( addConsole ) . not . toHaveBeenCalled ( )
94+ } )
95+
96+ it ( 'keeps distinct start rows when replay identity differs' , ( ) => {
97+ terminalConsoleMockFns . mockAddConsole ( {
98+ workflowId : 'wf-1' ,
99+ blockId : 'fn-1' ,
100+ blockName : 'Function 1' ,
101+ blockType : 'function' ,
102+ executionId : 'exec-1' ,
103+ executionOrder : 7 ,
104+ isRunning : true ,
105+ iterationCurrent : 0 ,
106+ iterationTotal : 2 ,
107+ iterationType : 'loop' ,
108+ iterationContainerId : 'loop-1' ,
109+ } )
110+
111+ const addConsole = vi . fn ( )
112+ const handlers = createBlockEventHandlers (
113+ {
114+ workflowId : 'wf-1' ,
115+ executionIdRef : { current : 'exec-1' } ,
116+ workflowEdges : [ ] ,
117+ activeBlocksSet : new Set < string > ( ) ,
118+ activeBlockRefCounts : new Map < string , number > ( ) ,
119+ accumulatedBlockLogs : [ ] ,
120+ accumulatedBlockStates : new Map ( ) ,
121+ executedBlockIds : new Set < string > ( ) ,
122+ includeStartConsoleEntry : true ,
123+ } ,
124+ {
125+ addConsole,
126+ updateConsole : vi . fn ( ) ,
127+ setActiveBlocks : vi . fn ( ) ,
128+ setBlockRunStatus : vi . fn ( ) ,
129+ setEdgeRunStatus : vi . fn ( ) ,
130+ }
131+ )
132+
133+ handlers . onBlockStarted ( {
134+ blockId : 'fn-1' ,
135+ blockName : 'Function 1' ,
136+ blockType : 'function' ,
137+ executionOrder : 7 ,
138+ iterationCurrent : 1 ,
139+ iterationTotal : 2 ,
140+ iterationType : 'loop' ,
141+ iterationContainerId : 'loop-1' ,
142+ } )
143+
144+ expect ( addConsole ) . toHaveBeenCalledTimes ( 1 )
145+ } )
16146 } )
17147
18148 describe ( 'addExecutionErrorConsoleEntry' , ( ) => {
@@ -225,6 +355,103 @@ describe('workflow-execution-utils', () => {
225355 expect ( updateConsole ) . not . toHaveBeenCalled ( )
226356 } )
227357
358+ it ( 'reconciles child workflow spans before running entries are swept to canceled' , ( ) => {
359+ terminalConsoleMockFns . mockAddConsole ( {
360+ workflowId : 'wf-1' ,
361+ blockId : 'workflow-1' ,
362+ blockName : 'Workflow 1' ,
363+ blockType : 'workflow' ,
364+ executionId : 'exec-1' ,
365+ executionOrder : 2 ,
366+ isRunning : false ,
367+ success : true ,
368+ childWorkflowInstanceId : 'child-inst-1' ,
369+ } )
370+ terminalConsoleMockFns . mockAddConsole ( {
371+ workflowId : 'wf-1' ,
372+ blockId : 'starter' ,
373+ blockName : 'Start' ,
374+ blockType : 'starter' ,
375+ executionId : 'exec-1' ,
376+ executionOrder : 3 ,
377+ isRunning : true ,
378+ childWorkflowBlockId : 'child-inst-1' ,
379+ childWorkflowName : 'Workflow 1' ,
380+ } )
381+ terminalConsoleMockFns . mockAddConsole ( {
382+ workflowId : 'wf-1' ,
383+ blockId : 'api-1' ,
384+ blockName : 'API 1' ,
385+ blockType : 'api' ,
386+ executionId : 'exec-1' ,
387+ executionOrder : 4 ,
388+ isRunning : true ,
389+ childWorkflowBlockId : 'child-inst-1' ,
390+ childWorkflowName : 'Workflow 1' ,
391+ } )
392+
393+ const startedAt = new Date ( ) . toISOString ( )
394+ const endedAt = new Date ( Date . now ( ) + 20 ) . toISOString ( )
395+ const updateConsole = vi . fn ( )
396+ reconcileFinalBlockLogs ( updateConsole , 'wf-1' , 'exec-1' , [
397+ makeLog ( {
398+ blockId : 'workflow-1' ,
399+ blockName : 'Workflow 1' ,
400+ blockType : 'workflow' ,
401+ executionOrder : 2 ,
402+ success : true ,
403+ childTraceSpans : [
404+ {
405+ id : 'starter-span' ,
406+ name : 'Start' ,
407+ type : 'starter' ,
408+ blockId : 'starter' ,
409+ status : 'success' ,
410+ duration : 5 ,
411+ startTime : startedAt ,
412+ endTime : endedAt ,
413+ output : { } ,
414+ } ,
415+ {
416+ id : 'api-span' ,
417+ name : 'API 1' ,
418+ type : 'api' ,
419+ blockId : 'api-1' ,
420+ status : 'error' ,
421+ errorHandled : true ,
422+ duration : 20 ,
423+ startTime : startedAt ,
424+ endTime : endedAt ,
425+ output : { error : 'Request failed' } ,
426+ } ,
427+ ] ,
428+ } ) ,
429+ ] )
430+
431+ expect ( updateConsole ) . toHaveBeenCalledTimes ( 2 )
432+ expect ( updateConsole . mock . calls [ 0 ] ) . toEqual ( [
433+ 'starter' ,
434+ expect . objectContaining ( {
435+ success : true ,
436+ isRunning : false ,
437+ isCanceled : false ,
438+ childWorkflowBlockId : 'child-inst-1' ,
439+ } ) ,
440+ 'exec-1' ,
441+ ] )
442+ expect ( updateConsole . mock . calls [ 1 ] ) . toEqual ( [
443+ 'api-1' ,
444+ expect . objectContaining ( {
445+ success : false ,
446+ error : 'Request failed' ,
447+ isRunning : false ,
448+ isCanceled : false ,
449+ childWorkflowBlockId : 'child-inst-1' ,
450+ } ) ,
451+ 'exec-1' ,
452+ ] )
453+ } )
454+
228455 it ( 'is a no-op when finalBlockLogs is empty or executionId is missing' , ( ) => {
229456 const updateConsole = vi . fn ( )
230457 reconcileFinalBlockLogs ( updateConsole , 'wf-1' , 'exec-1' , [ ] )
@@ -256,6 +483,7 @@ describe('workflow-execution-utils', () => {
256483
257484 expect ( calls [ 0 ] ) . toBe ( 'cancel' )
258485 expect ( calls ) . toContain ( 'add' )
486+ expect ( cancelRunningEntries ) . toHaveBeenCalledWith ( 'wf-1' , 'exec-1' )
259487 } )
260488
261489 it ( 'reconciles finalBlockLogs before sweeping running entries (Fix C)' , ( ) => {
0 commit comments