@@ -277,4 +277,215 @@ describe('ParallelBlockHandler', () => {
277277 // Should not have items when no distribution
278278 expect ( context . loopItems . has ( 'parallel-1_items' ) ) . toBe ( false )
279279 } )
280+
281+ describe ( 'multiple downstream connections' , ( ) => {
282+ it ( 'should make results available to all downstream blocks' , async ( ) => {
283+ const handler = new ParallelBlockHandler ( )
284+ const parallelBlock = createMockBlock ( 'parallel-1' )
285+ parallelBlock . config . params = {
286+ parallelType : 'collection' ,
287+ count : 3 ,
288+ }
289+
290+ const parallel : SerializedParallel = {
291+ id : 'parallel-1' ,
292+ nodes : [ 'agent-1' ] ,
293+ distribution : [ 'item1' , 'item2' , 'item3' ] ,
294+ }
295+
296+ const context = createMockContext ( parallel )
297+ context . workflow ! . connections = [
298+ {
299+ source : 'parallel-1' ,
300+ target : 'agent-1' ,
301+ sourceHandle : 'parallel-start-source' ,
302+ } ,
303+ {
304+ source : 'parallel-1' ,
305+ target : 'function-1' ,
306+ sourceHandle : 'parallel-end-source' ,
307+ } ,
308+ {
309+ source : 'parallel-1' ,
310+ target : 'parallel-2' ,
311+ sourceHandle : 'parallel-end-source' ,
312+ } ,
313+ ]
314+
315+ // Initialize parallel
316+ const initResult = await handler . execute ( parallelBlock , { } , context )
317+ expect ( ( initResult as any ) . response . started ) . toBe ( true )
318+ expect ( ( initResult as any ) . response . parallelCount ) . toBe ( 3 )
319+
320+ // Simulate all virtual blocks being executed
321+ const parallelState = context . parallelExecutions ?. get ( 'parallel-1' )
322+ expect ( parallelState ) . toBeDefined ( )
323+
324+ // Mark all virtual blocks as executed and store results
325+ for ( let i = 0 ; i < 3 ; i ++ ) {
326+ const virtualBlockId = `agent-1_parallel_parallel-1_iteration_${ i } `
327+ context . executedBlocks . add ( virtualBlockId )
328+
329+ // Store iteration results
330+ parallelState ! . executionResults . set ( `iteration_${ i } ` , {
331+ 'agent-1' : {
332+ response : {
333+ content : `Result from iteration ${ i } ` ,
334+ model : 'test-model' ,
335+ } ,
336+ } ,
337+ } )
338+ }
339+
340+ // Re-execute to aggregate results
341+ const aggregatedResult = await handler . execute ( parallelBlock , { } , context )
342+
343+ // Verify results are aggregated
344+ expect ( ( aggregatedResult as any ) . response . completed ) . toBe ( true )
345+ expect ( ( aggregatedResult as any ) . response . results ) . toHaveLength ( 3 )
346+
347+ // Verify block state is stored
348+ const blockState = context . blockStates . get ( 'parallel-1' )
349+ expect ( blockState ) . toBeDefined ( )
350+ expect ( blockState ?. output . response . results ) . toHaveLength ( 3 )
351+
352+ // Verify both downstream blocks are activated
353+ expect ( context . activeExecutionPath . has ( 'function-1' ) ) . toBe ( true )
354+ expect ( context . activeExecutionPath . has ( 'parallel-2' ) ) . toBe ( true )
355+
356+ // Verify parallel is marked as completed
357+ expect ( context . completedLoops . has ( 'parallel-1' ) ) . toBe ( true )
358+
359+ // Simulate downstream blocks trying to access results
360+ // This should work without errors
361+ const storedResults = context . blockStates . get ( 'parallel-1' ) ?. output . response . results
362+ expect ( storedResults ) . toBeDefined ( )
363+ expect ( storedResults ) . toHaveLength ( 3 )
364+ } )
365+
366+ it ( 'should handle reference resolution when multiple parallel blocks exist' , async ( ) => {
367+ const handler = new ParallelBlockHandler ( )
368+
369+ // Create first parallel block
370+ const parallel1Block = createMockBlock ( 'parallel-1' )
371+ parallel1Block . config . params = {
372+ parallelType : 'collection' ,
373+ count : 2 ,
374+ }
375+
376+ // Create second parallel block (even if not connected)
377+ const parallel2Block = createMockBlock ( 'parallel-2' )
378+ parallel2Block . config . params = {
379+ parallelType : 'collection' ,
380+ collection : '<parallel.response.results>' , // This references the first parallel
381+ }
382+
383+ // Set up context with both parallels
384+ const context : ExecutionContext = {
385+ workflowId : 'test-workflow' ,
386+ blockStates : new Map ( ) ,
387+ blockLogs : [ ] ,
388+ metadata : { duration : 0 } ,
389+ environmentVariables : { } ,
390+ decisions : { router : new Map ( ) , condition : new Map ( ) } ,
391+ loopIterations : new Map ( ) ,
392+ loopItems : new Map ( ) ,
393+ completedLoops : new Set ( ) ,
394+ executedBlocks : new Set ( ) ,
395+ activeExecutionPath : new Set ( ) ,
396+ workflow : {
397+ version : '1.0' ,
398+ blocks : [
399+ parallel1Block ,
400+ parallel2Block ,
401+ {
402+ id : 'agent-1' ,
403+ position : { x : 0 , y : 0 } ,
404+ config : { tool : 'agent' , params : { } } ,
405+ inputs : { } ,
406+ outputs : { } ,
407+ metadata : { id : 'agent' , name : 'Agent 1' } ,
408+ enabled : true ,
409+ } ,
410+ {
411+ id : 'function-1' ,
412+ position : { x : 0 , y : 0 } ,
413+ config : {
414+ tool : 'function' ,
415+ params : {
416+ code : 'return <parallel.response.results>;' ,
417+ } ,
418+ } ,
419+ inputs : { } ,
420+ outputs : { } ,
421+ metadata : { id : 'function' , name : 'Function 1' } ,
422+ enabled : true ,
423+ } ,
424+ ] ,
425+ connections : [
426+ {
427+ source : 'parallel-1' ,
428+ target : 'agent-1' ,
429+ sourceHandle : 'parallel-start-source' ,
430+ } ,
431+ {
432+ source : 'parallel-1' ,
433+ target : 'function-1' ,
434+ sourceHandle : 'parallel-end-source' ,
435+ } ,
436+ {
437+ source : 'parallel-1' ,
438+ target : 'parallel-2' ,
439+ sourceHandle : 'parallel-end-source' ,
440+ } ,
441+ ] ,
442+ loops : { } ,
443+ parallels : {
444+ 'parallel-1' : {
445+ id : 'parallel-1' ,
446+ nodes : [ 'agent-1' ] ,
447+ distribution : [ 'item1' , 'item2' ] ,
448+ } ,
449+ 'parallel-2' : {
450+ id : 'parallel-2' ,
451+ nodes : [ ] ,
452+ distribution : '<parallel.response.results>' ,
453+ } ,
454+ } ,
455+ } ,
456+ }
457+
458+ // Initialize first parallel
459+ await handler . execute ( parallel1Block , { } , context )
460+
461+ // Simulate execution of agent blocks
462+ const parallelState = context . parallelExecutions ?. get ( 'parallel-1' )
463+ for ( let i = 0 ; i < 2 ; i ++ ) {
464+ context . executedBlocks . add ( `agent-1_parallel_parallel-1_iteration_${ i } ` )
465+ parallelState ! . executionResults . set ( `iteration_${ i } ` , {
466+ 'agent-1' : { response : { content : `Result ${ i } ` } } ,
467+ } )
468+ }
469+
470+ // Re-execute first parallel to aggregate results
471+ const result = await handler . execute ( parallel1Block , { } , context )
472+ expect ( ( result as any ) . response . completed ) . toBe ( true )
473+
474+ // Verify the block state is available
475+ const blockState = context . blockStates . get ( 'parallel-1' )
476+ expect ( blockState ) . toBeDefined ( )
477+ expect ( blockState ?. output . response . results ) . toHaveLength ( 2 )
478+
479+ // Now when function block tries to resolve <parallel.response.results>, it should work
480+ // even though parallel-2 exists on the canvas
481+ expect ( ( ) => {
482+ // This simulates what the resolver would do
483+ const state = context . blockStates . get ( 'parallel-1' )
484+ if ( ! state ) throw new Error ( 'No state found for block parallel-1' )
485+ const results = state . output ?. response ?. results
486+ if ( ! results ) throw new Error ( 'No results found' )
487+ return results
488+ } ) . not . toThrow ( )
489+ } )
490+ } )
280491} )
0 commit comments