2525 SCENARIO_1 ,
2626 SCENARIO_MULTIPLE_OK_SUBWORKFLOWS ,
2727 SCENARIO_NESTED_SUBWORKFLOWS ,
28+ SCENARIO_SUBWORKFLOW_WITH_FAILED_JOBS ,
2829)
2930
3031SLEEP = 0
@@ -42,10 +43,10 @@ def sleep(self) -> None:
4243
4344def test_polling_scenario_1 ():
4445 final_invocation_state , job_state , error_message = run_workflow_simulation (SCENARIO_1 , fail_fast = True )
45- assert final_invocation_state == "scheduled"
46- assert job_state == "failed "
46+ assert final_invocation_state == "ready" # early job error and fail fast, invocation doesn't advance to scheduled
47+ assert job_state == "error "
4748 assert error_message
48- assert "failed " in error_message
49+ assert "error " in error_message
4950
5051
5152def test_polling_scenario_three_ok_subworkflows ():
@@ -80,10 +81,10 @@ def test_polling_without_display():
8081 display ,
8182 fail_fast = True ,
8283 )
83- assert final_invocation_state == "scheduled "
84- assert job_state == "failed "
84+ assert final_invocation_state == "ready "
85+ assert job_state == "error "
8586 assert error_message
86- assert "failed " in error_message
87+ assert "error " in error_message
8788
8889
8990def test_polling_with_compact_display ():
@@ -117,19 +118,19 @@ def test_fail_fast_enabled_with_job_failure():
117118 """Test that fail_fast=True returns error when a job fails."""
118119 final_invocation_state , job_state , error_message = run_workflow_simulation (SCENARIO_1 , fail_fast = True )
119120 # Invocation should still be scheduled (workflow scheduling succeeded)
120- assert final_invocation_state == "scheduled "
121- assert job_state == "failed "
121+ assert final_invocation_state == "ready "
122+ assert job_state == "error "
122123 # fail_fast should detect the failed job and return error message
123124 assert error_message
124- assert "Failed to run workflow, at least one job is in [failed ] state." in error_message
125+ assert "Failed to run workflow, at least one job is in [error ] state." in error_message
125126
126127
127128def test_fail_fast_disabled_with_job_failure ():
128129 """Test that fail_fast=False does not report job failures as errors."""
129130 final_invocation_state , job_state , error_message = run_workflow_simulation (SCENARIO_1 , fail_fast = False )
130131 # Invocation should be scheduled (workflow scheduling succeeded)
131132 assert final_invocation_state == "scheduled"
132- assert job_state == "failed "
133+ assert job_state == "error "
133134 # Without fail_fast, job failures shouldn't cause error messages
134135 # (unless invocation itself fails, which it doesn't in this case)
135136 assert error_message is None
@@ -145,6 +146,19 @@ def test_fail_fast_enabled_with_successful_workflow():
145146 assert not error_message
146147
147148
149+ def test_fail_fast_enabled_with_subworkflow_job_failure ():
150+ """Test that fail_fast=True terminates when encountering jobs that are errored inside a subworkflow invocation."""
151+ final_invocation_state , job_state , error_message = run_workflow_simulation (
152+ SCENARIO_SUBWORKFLOW_WITH_FAILED_JOBS , fail_fast = True
153+ )
154+ # Invocation is ready to schedule more steps, yet the polling should terminate
155+ assert final_invocation_state == "ready"
156+ assert job_state == "error"
157+ # fail_fast should detect the failed job in the subworkflow and return error message
158+ assert error_message
159+ assert "Failed to run workflow, at least one job is in [error] state." in error_message
160+
161+
148162def run_workflow_simulation (
149163 yaml_str : str , display_configuration : Optional [DisplayConfiguration ] = None , fail_fast : bool = False
150164):
@@ -179,7 +193,7 @@ def show_job(self, job_id, full_details=False):
179193 """Return mock job details with exit code and stderr."""
180194 return {
181195 "id" : job_id ,
182- "state" : "failed " ,
196+ "state" : "error " ,
183197 "exit_code" : 1 ,
184198 "stderr" : f"Error: Mock job { job_id } failed with exit code 1\n Additional error details here" ,
185199 "stdout" : f"Mock job { job_id } output" ,
@@ -204,7 +218,7 @@ class MockInvocationsApi:
204218
205219 def show_invocation_step (self , invocation_id , step_id ):
206220 """Return mock invocation step details."""
207- return {"id" : step_id , "jobs" : [{"id" : f"job_{ step_id } " , "state" : "failed " }]}
221+ return {"id" : step_id , "jobs" : [{"id" : f"job_{ step_id } " , "state" : "error " }]}
208222
209223
210224class SimulatedApi (InvocationApi ):
@@ -239,7 +253,7 @@ def get_job(self, job_id: str, full_details: bool = False) -> Job:
239253 """Return mock job details."""
240254 return {
241255 "id" : job_id ,
242- "state" : "failed " ,
256+ "state" : "error " ,
243257 "exit_code" : 1 ,
244258 "stderr" : f"Error: Mock job { job_id } failed with exit code 1\n Additional error details here" ,
245259 "stdout" : f"Mock job { job_id } output" ,
0 commit comments