@@ -719,3 +719,90 @@ func TestStepsWhenSkipNoRequeue(t *testing.T) {
719719 require .NotNil (t , nodeB )
720720 assert .Equal (t , wfv1 .NodeSkipped , nodeB .Phase )
721721}
722+
723+ var stepsWhenExprWithParamFilter = `
724+ apiVersion: argoproj.io/v1alpha1
725+ kind: Workflow
726+ metadata:
727+ generateName: steps-when-expr-filter-
728+ spec:
729+ entrypoint: main
730+ templates:
731+ - name: main
732+ inputs:
733+ parameters:
734+ - name: test
735+ value: 'true'
736+ - name: list
737+ value: "{{= concat(['always'], inputs.parameters.test == 'true' ? ['test'] : []) | toJSON() }}"
738+ steps:
739+ - - name: fst
740+ template: run
741+ when: |
742+ "{{= get(item, 'type') ?? 'always' }}"
743+ in
744+ ("{{= inputs.parameters.list | fromJSON() | join('","') }}","")
745+ withParam: |
746+ [
747+ { "name": "first", "type": "" },
748+ { "name": "second", "type": "always" },
749+ { "name": "third", "type": "test" },
750+ { "name": "fourth" }
751+ ]
752+ arguments:
753+ parameters:
754+ - name: name
755+ value: "{{ item.name }}{{ inputs.parameters.list }}"
756+ - name: run
757+ inputs:
758+ parameters:
759+ - name: name
760+ container:
761+ image: alpine:3.23
762+ command: [echo]
763+ args: ["{{inputs.parameters.name}}"]
764+ `
765+
766+ // TestStepsWhenExprWithParamFilter verifies that expression templates work correctly
767+ // in a steps workflow with withParam expansion and a when clause that filters items
768+ // using expression functions (concat, get, ??, toJSON, fromJSON, join).
769+ // This mirrors a real-world pattern where a dynamic list parameter controls which
770+ // withParam items execute.
771+ func TestStepsWhenExprWithParamFilter (t * testing.T ) {
772+ ctx := logging .TestContext (t .Context ())
773+ cancel , controller := newController (ctx )
774+ defer cancel ()
775+ wfcset := controller .wfclientset .ArgoprojV1alpha1 ().Workflows ("" )
776+
777+ wf := wfv1 .MustUnmarshalWorkflow (stepsWhenExprWithParamFilter )
778+ wf , err := wfcset .Create (ctx , wf , metav1.CreateOptions {})
779+ require .NoError (t , err )
780+ woc := newWorkflowOperationCtx (ctx , wf , controller )
781+
782+ woc .operate (ctx )
783+
784+ // Workflow is Running because pods haven't completed, but we can verify:
785+ // 1. No error occurred during expression evaluation
786+ // 2. All 4 items were expanded and scheduled (none were incorrectly skipped/errored)
787+ assert .Equal (t , wfv1 .WorkflowRunning , woc .wf .Status .Phase )
788+
789+ // "first" has type "" which matches empty string in the filter list
790+ node0 := woc .wf .Status .Nodes .FindByDisplayName ("fst(0:name:first,type:)" )
791+ require .NotNil (t , node0 )
792+ assert .Equal (t , wfv1 .NodePending , node0 .Phase )
793+
794+ // "second" has type "always" which is in the filter list
795+ node1 := woc .wf .Status .Nodes .FindByDisplayName ("fst(1:name:second,type:always)" )
796+ require .NotNil (t , node1 )
797+ assert .Equal (t , wfv1 .NodePending , node1 .Phase )
798+
799+ // "third" has type "test" which is in the filter list (test=true)
800+ node2 := woc .wf .Status .Nodes .FindByDisplayName ("fst(2:name:third,type:test)" )
801+ require .NotNil (t , node2 )
802+ assert .Equal (t , wfv1 .NodePending , node2 .Phase )
803+
804+ // "fourth" has no type, defaults to "always" via ?? operator
805+ node3 := woc .wf .Status .Nodes .FindByDisplayName ("fst(3:name:fourth)" )
806+ require .NotNil (t , node3 )
807+ assert .Equal (t , wfv1 .NodePending , node3 .Phase )
808+ }
0 commit comments