@@ -322,7 +322,8 @@ describe('ConditionBlockHandler', () => {
322322
323323 await handler . execute ( mockContext , mockBlock , inputs )
324324
325- expect ( mockCollectBlockData ) . toHaveBeenCalledWith ( mockContext )
325+ // collectBlockData is now called with the current node ID for parallel branch context
326+ expect ( mockCollectBlockData ) . toHaveBeenCalledWith ( mockContext , mockBlock . id )
326327 } )
327328
328329 it ( 'should handle function_execute tool failure' , async ( ) => {
@@ -620,4 +621,248 @@ describe('ConditionBlockHandler', () => {
620621 expect ( mockContext . decisions . condition . has ( mockBlock . id ) ) . toBe ( false )
621622 } )
622623 } )
624+
625+ describe ( 'Parallel branch handling' , ( ) => {
626+ it ( 'should resolve connections and block data correctly when inside a parallel branch' , async ( ) => {
627+ // Simulate a condition block inside a parallel branch
628+ // Virtual block ID uses subscript notation: blockId₍branchIndex₎
629+ const parallelConditionBlock : SerializedBlock = {
630+ id : 'cond-block-1₍0₎' , // Virtual ID for branch 0
631+ metadata : { id : 'condition' , name : 'Condition' } ,
632+ position : { x : 0 , y : 0 } ,
633+ config : { } ,
634+ }
635+
636+ // Source block also has a virtual ID in the same branch
637+ const sourceBlockVirtualId = 'agent-block-1₍0₎'
638+
639+ // Set up workflow with connections using BASE block IDs (as they are in the workflow definition)
640+ const parallelWorkflow : SerializedWorkflow = {
641+ blocks : [
642+ {
643+ id : 'agent-block-1' ,
644+ metadata : { id : 'agent' , name : 'Agent' } ,
645+ position : { x : 0 , y : 0 } ,
646+ config : { } ,
647+ } ,
648+ {
649+ id : 'cond-block-1' ,
650+ metadata : { id : 'condition' , name : 'Condition' } ,
651+ position : { x : 100 , y : 0 } ,
652+ config : { } ,
653+ } ,
654+ {
655+ id : 'target-block-1' ,
656+ metadata : { id : 'api' , name : 'Target' } ,
657+ position : { x : 200 , y : 0 } ,
658+ config : { } ,
659+ } ,
660+ ] ,
661+ connections : [
662+ // Connections use base IDs, not virtual IDs
663+ { source : 'agent-block-1' , target : 'cond-block-1' } ,
664+ { source : 'cond-block-1' , target : 'target-block-1' , sourceHandle : 'condition-cond1' } ,
665+ ] ,
666+ loops : [ ] ,
667+ parallels : [ ] ,
668+ }
669+
670+ // Block states use virtual IDs (as outputs are stored per-branch)
671+ const parallelBlockStates = new Map < string , BlockState > ( [
672+ [
673+ sourceBlockVirtualId ,
674+ { output : { response : 'hello from branch 0' , success : true } , executed : true } ,
675+ ] ,
676+ ] )
677+
678+ const parallelContext : ExecutionContext = {
679+ workflowId : 'test-workflow-id' ,
680+ workspaceId : 'test-workspace-id' ,
681+ workflow : parallelWorkflow ,
682+ blockStates : parallelBlockStates ,
683+ blockLogs : [ ] ,
684+ completedBlocks : new Set ( ) ,
685+ decisions : {
686+ router : new Map ( ) ,
687+ condition : new Map ( ) ,
688+ } ,
689+ environmentVariables : { } ,
690+ workflowVariables : { } ,
691+ }
692+
693+ const conditions = [
694+ { id : 'cond1' , title : 'if' , value : 'context.response === "hello from branch 0"' } ,
695+ { id : 'else1' , title : 'else' , value : '' } ,
696+ ]
697+ const inputs = { conditions : JSON . stringify ( conditions ) }
698+
699+ const result = await handler . execute ( parallelContext , parallelConditionBlock , inputs )
700+
701+ // The condition should evaluate to true because:
702+ // 1. Connection lookup uses base ID 'cond-block-1' (extracted from 'cond-block-1₍0₎')
703+ // 2. Source block output is found at virtual ID 'agent-block-1₍0₎' (same branch)
704+ // 3. The evaluation context contains { response: 'hello from branch 0' }
705+ expect ( ( result as any ) . conditionResult ) . toBe ( true )
706+ expect ( ( result as any ) . selectedOption ) . toBe ( 'cond1' )
707+ expect ( ( result as any ) . selectedPath ) . toEqual ( {
708+ blockId : 'target-block-1' ,
709+ blockType : 'api' ,
710+ blockTitle : 'Target' ,
711+ } )
712+ } )
713+
714+ it ( 'should find correct source block output in parallel branch context' , async ( ) => {
715+ // Test that when multiple branches exist, the correct branch output is used
716+ const parallelConditionBlock : SerializedBlock = {
717+ id : 'cond-block-1₍1₎' , // Virtual ID for branch 1
718+ metadata : { id : 'condition' , name : 'Condition' } ,
719+ position : { x : 0 , y : 0 } ,
720+ config : { } ,
721+ }
722+
723+ const parallelWorkflow : SerializedWorkflow = {
724+ blocks : [
725+ {
726+ id : 'agent-block-1' ,
727+ metadata : { id : 'agent' , name : 'Agent' } ,
728+ position : { x : 0 , y : 0 } ,
729+ config : { } ,
730+ } ,
731+ {
732+ id : 'cond-block-1' ,
733+ metadata : { id : 'condition' , name : 'Condition' } ,
734+ position : { x : 100 , y : 0 } ,
735+ config : { } ,
736+ } ,
737+ {
738+ id : 'target-block-1' ,
739+ metadata : { id : 'api' , name : 'Target' } ,
740+ position : { x : 200 , y : 0 } ,
741+ config : { } ,
742+ } ,
743+ ] ,
744+ connections : [
745+ { source : 'agent-block-1' , target : 'cond-block-1' } ,
746+ { source : 'cond-block-1' , target : 'target-block-1' , sourceHandle : 'condition-cond1' } ,
747+ ] ,
748+ loops : [ ] ,
749+ parallels : [ ] ,
750+ }
751+
752+ // Multiple branches have executed - each has different output
753+ const parallelBlockStates = new Map < string , BlockState > ( [
754+ [ 'agent-block-1₍0₎' , { output : { value : 10 } , executed : true } ] ,
755+ [ 'agent-block-1₍1₎' , { output : { value : 25 } , executed : true } ] , // Branch 1 has value 25
756+ [ 'agent-block-1₍2₎' , { output : { value : 5 } , executed : true } ] ,
757+ ] )
758+
759+ const parallelContext : ExecutionContext = {
760+ workflowId : 'test-workflow-id' ,
761+ workspaceId : 'test-workspace-id' ,
762+ workflow : parallelWorkflow ,
763+ blockStates : parallelBlockStates ,
764+ blockLogs : [ ] ,
765+ completedBlocks : new Set ( ) ,
766+ decisions : {
767+ router : new Map ( ) ,
768+ condition : new Map ( ) ,
769+ } ,
770+ environmentVariables : { } ,
771+ workflowVariables : { } ,
772+ }
773+
774+ // Condition checks if value > 20 - should be true for branch 1 (value=25)
775+ const conditions = [
776+ { id : 'cond1' , title : 'if' , value : 'context.value > 20' } ,
777+ { id : 'else1' , title : 'else' , value : '' } ,
778+ ]
779+ const inputs = { conditions : JSON . stringify ( conditions ) }
780+
781+ const result = await handler . execute ( parallelContext , parallelConditionBlock , inputs )
782+
783+ // Should evaluate using branch 1's data (value=25), not branch 0 (value=10) or branch 2 (value=5)
784+ expect ( ( result as any ) . conditionResult ) . toBe ( true )
785+ expect ( ( result as any ) . selectedOption ) . toBe ( 'cond1' )
786+ } )
787+
788+ it ( 'should fall back to else when condition is false in parallel branch' , async ( ) => {
789+ const parallelConditionBlock : SerializedBlock = {
790+ id : 'cond-block-1₍2₎' , // Virtual ID for branch 2
791+ metadata : { id : 'condition' , name : 'Condition' } ,
792+ position : { x : 0 , y : 0 } ,
793+ config : { } ,
794+ }
795+
796+ const parallelWorkflow : SerializedWorkflow = {
797+ blocks : [
798+ {
799+ id : 'agent-block-1' ,
800+ metadata : { id : 'agent' , name : 'Agent' } ,
801+ position : { x : 0 , y : 0 } ,
802+ config : { } ,
803+ } ,
804+ {
805+ id : 'cond-block-1' ,
806+ metadata : { id : 'condition' , name : 'Condition' } ,
807+ position : { x : 100 , y : 0 } ,
808+ config : { } ,
809+ } ,
810+ {
811+ id : 'target-true' ,
812+ metadata : { id : 'api' , name : 'True Path' } ,
813+ position : { x : 200 , y : 0 } ,
814+ config : { } ,
815+ } ,
816+ {
817+ id : 'target-false' ,
818+ metadata : { id : 'api' , name : 'False Path' } ,
819+ position : { x : 200 , y : 100 } ,
820+ config : { } ,
821+ } ,
822+ ] ,
823+ connections : [
824+ { source : 'agent-block-1' , target : 'cond-block-1' } ,
825+ { source : 'cond-block-1' , target : 'target-true' , sourceHandle : 'condition-cond1' } ,
826+ { source : 'cond-block-1' , target : 'target-false' , sourceHandle : 'condition-else1' } ,
827+ ] ,
828+ loops : [ ] ,
829+ parallels : [ ] ,
830+ }
831+
832+ const parallelBlockStates = new Map < string , BlockState > ( [
833+ [ 'agent-block-1₍0₎' , { output : { value : 100 } , executed : true } ] ,
834+ [ 'agent-block-1₍1₎' , { output : { value : 50 } , executed : true } ] ,
835+ [ 'agent-block-1₍2₎' , { output : { value : 5 } , executed : true } ] , // Branch 2 has value 5
836+ ] )
837+
838+ const parallelContext : ExecutionContext = {
839+ workflowId : 'test-workflow-id' ,
840+ workspaceId : 'test-workspace-id' ,
841+ workflow : parallelWorkflow ,
842+ blockStates : parallelBlockStates ,
843+ blockLogs : [ ] ,
844+ completedBlocks : new Set ( ) ,
845+ decisions : {
846+ router : new Map ( ) ,
847+ condition : new Map ( ) ,
848+ } ,
849+ environmentVariables : { } ,
850+ workflowVariables : { } ,
851+ }
852+
853+ // Condition checks if value > 20 - should be false for branch 2 (value=5)
854+ const conditions = [
855+ { id : 'cond1' , title : 'if' , value : 'context.value > 20' } ,
856+ { id : 'else1' , title : 'else' , value : '' } ,
857+ ]
858+ const inputs = { conditions : JSON . stringify ( conditions ) }
859+
860+ const result = await handler . execute ( parallelContext , parallelConditionBlock , inputs )
861+
862+ // Should fall back to else path because branch 2's value (5) is not > 20
863+ expect ( ( result as any ) . conditionResult ) . toBe ( true )
864+ expect ( ( result as any ) . selectedOption ) . toBe ( 'else1' )
865+ expect ( ( result as any ) . selectedPath . blockId ) . toBe ( 'target-false' )
866+ } )
867+ } )
623868} )
0 commit comments