@@ -236,6 +236,85 @@ def test_check_audit_retry_loop_on_failure_escalates(self, recipe) -> None:
236236 assert step .on_failure == "escalate_stop"
237237
238238
239+ class TestResearchRemediationLoop :
240+ """Tests for the audit_impl → remediate → plan_phase retry cycle in research.yaml."""
241+
242+ @pytest .fixture (scope = "class" )
243+ def recipe (self ):
244+ return load_recipe (builtin_recipes_dir () / "research.yaml" )
245+
246+ def test_audit_impl_nogo_routes_to_remediate (self , recipe ) -> None :
247+ """research.yaml audit_impl default (NO GO) route must target remediate."""
248+ step = recipe .steps ["audit_impl" ]
249+ conditions = step .on_result .conditions
250+ default_cond = next ((c for c in conditions if c .when is None ), None )
251+ assert default_cond is not None , "audit_impl must have a default route"
252+ assert default_cond .route == "remediate" , (
253+ f"audit_impl default route must be 'remediate', got '{ default_cond .route } '"
254+ )
255+
256+ def test_audit_impl_error_routes_to_escalate_stop (self , recipe ) -> None :
257+ step = recipe .steps ["audit_impl" ]
258+ conditions = step .on_result .conditions
259+ error_cond = next ((c for c in conditions if c .when and "result.error" in c .when ), None )
260+ assert error_cond is not None
261+ assert error_cond .route == "escalate_stop"
262+
263+ def test_has_remediate_step (self , recipe ) -> None :
264+ assert "remediate" in recipe .steps
265+ assert recipe .steps ["remediate" ].action == "route"
266+
267+ def test_remediate_carries_remediation_path (self , recipe ) -> None :
268+ step = recipe .steps ["remediate" ]
269+ assert "context.remediation_path" in step .with_args ["remediation_path" ]
270+
271+ def test_remediate_routes_to_check_audit_retry_loop (self , recipe ) -> None :
272+ step = recipe .steps ["remediate" ]
273+ assert step .on_success == "check_audit_retry_loop"
274+
275+ def test_has_check_audit_retry_loop_step (self , recipe ) -> None :
276+ step = recipe .steps ["check_audit_retry_loop" ]
277+ assert step .tool == "run_python"
278+ assert step .with_args ["callable" ] == "autoskillit.smoke_utils.check_loop_iteration"
279+
280+ def test_check_audit_retry_loop_max_iterations (self , recipe ) -> None :
281+ step = recipe .steps ["check_audit_retry_loop" ]
282+ assert step .with_args ["max_iterations" ] == "2"
283+
284+ def test_check_audit_retry_loop_max_exceeded_routes_to_escalate_stop (self , recipe ) -> None :
285+ step = recipe .steps ["check_audit_retry_loop" ]
286+ conditions = step .on_result .conditions
287+ max_cond = next (
288+ (c for c in conditions if c .when and "max_exceeded" in c .when and "== true" in c .when ),
289+ None ,
290+ )
291+ assert max_cond is not None
292+ assert max_cond .route == "escalate_stop"
293+
294+ def test_check_audit_retry_loop_default_routes_to_pre_remediation_cleanup (
295+ self , recipe
296+ ) -> None :
297+ step = recipe .steps ["check_audit_retry_loop" ]
298+ conditions = step .on_result .conditions
299+ default_cond = next ((c for c in conditions if c .when is None ), None )
300+ assert default_cond is not None
301+ assert default_cond .route == "pre_remediation_cleanup"
302+
303+ def test_check_audit_retry_loop_on_failure_escalates (self , recipe ) -> None :
304+ step = recipe .steps ["check_audit_retry_loop" ]
305+ assert step .on_failure == "escalate_stop"
306+
307+ def test_has_pre_remediation_cleanup_step (self , recipe ) -> None :
308+ step = recipe .steps ["pre_remediation_cleanup" ]
309+ assert step .tool == "run_cmd"
310+ assert "worktree remove" in step .with_args ["cmd" ]
311+
312+ def test_pre_remediation_cleanup_routes_to_plan_phase (self , recipe ) -> None :
313+ step = recipe .steps ["pre_remediation_cleanup" ]
314+ assert step .on_success == "plan_phase"
315+ assert step .on_failure == "plan_phase"
316+
317+
239318class TestResearchCampaignAuditIngredient :
240319 """Tests for audit ingredient in research-campaign.yaml."""
241320
0 commit comments