"""Step definitions for correction_service_coverage.feature. Provides full line-and-branch coverage for ``cleveragents.application.services.correction_service.CorrectionService``. All step-text uses a ``cov-`` prefix to avoid collisions with existing correction_flows_steps.py and correction_service_new_coverage_steps.py. """ from __future__ import annotations from behave import given, then, when from cleveragents.application.services.correction_service import CorrectionService from cleveragents.core.exceptions import ResourceNotFoundError, ValidationError from cleveragents.domain.models.core.correction import ( CorrectionMode, CorrectionStatus, ) # ------------------------------------------------------------------- # Helpers # ------------------------------------------------------------------- def _parse_adj(spec: str) -> dict[str, list[str]]: """Parse ``"A->B,C;B->D"`` into an adjacency dict.""" tree: dict[str, list[str]] = {} for seg in spec.split(";"): seg = seg.strip() if not seg: continue parent, children_str = seg.split("->") tree[parent.strip()] = [c.strip() for c in children_str.split(",")] return tree def _build_chain_cov(root: str, count: int) -> dict[str, list[str]]: """Build a linear chain of *count* total nodes starting at *root*.""" tree: dict[str, list[str]] = {} current = root for i in range(1, count): child = f"{root}_ch{i}" tree[current] = [child] current = child return tree # ------------------------------------------------------------------- # Background / Given # ------------------------------------------------------------------- @given("a coverage test correction service") def step_bg_service(context): context.cov_svc = CorrectionService() context.cov_cid = None context.cov_req = None context.cov_impact = None context.cov_revert_result = None context.cov_append_result = None context.cov_dispatch_result = None context.cov_dry_run = None context.cov_retrieved = None context.cov_list = None context.cov_attempts = None context.cov_subtree = None context.cov_error = None @given('a cov-stored revert correction for plan "{plan}" targeting "{target}"') def step_stored_revert_cov(context, plan, target): req = context.cov_svc.request_correction( plan_id=plan, target_decision_id=target, mode=CorrectionMode.REVERT, ) context.cov_cid = req.correction_id context.cov_req = req @given('a cov-stored append correction for plan "{plan}" targeting "{target}"') def step_stored_append_cov(context, plan, target): req = context.cov_svc.request_correction( plan_id=plan, target_decision_id=target, mode=CorrectionMode.APPEND, ) context.cov_cid = req.correction_id context.cov_req = req @given("the cov-stored correction has been executed via revert with empty tree") def step_pre_execute_revert(context): context.cov_svc.execute_revert(context.cov_cid, {}) @given("the cov-stored correction has been executed via append") def step_pre_execute_append(context): context.cov_svc.execute_append(context.cov_cid) @given("the cov-stored correction has been analyzed with empty tree") def step_pre_analyze(context): context.cov_svc.analyze_impact(context.cov_cid, {}) @given('the cov-stored correction status is forced to "{status}"') def step_force_status(context, status): req = context.cov_svc.get_correction(context.cov_cid) req.status = CorrectionStatus(status) # ------------------------------------------------------------------- # When steps - request_correction # ------------------------------------------------------------------- @when( 'I cov-request a correction for plan "{plan}" targeting "{target}" in revert mode' ) def step_request_revert(context, plan, target): context.cov_req = context.cov_svc.request_correction( plan_id=plan, target_decision_id=target, mode=CorrectionMode.REVERT, ) context.cov_cid = context.cov_req.correction_id @when( 'I cov-request a correction for plan "{plan}" targeting "{target}"' ' in append mode with guidance "{guidance}" and dry_run' ) def step_request_append_guidance_dryrun(context, plan, target, guidance): context.cov_req = context.cov_svc.request_correction( plan_id=plan, target_decision_id=target, mode=CorrectionMode.APPEND, guidance=guidance, dry_run=True, ) context.cov_cid = context.cov_req.correction_id @when( 'I cov-attempt to create a correction with plan_id "{plan}" and target "{target}"' ) def step_attempt_create(context, plan, target): try: context.cov_svc.request_correction( plan_id=plan, target_decision_id=target, mode=CorrectionMode.REVERT, ) context.cov_error = None except (ValidationError, Exception) as exc: context.cov_error = exc @when('I cov-attempt to create a correction with empty plan_id and target "{target}"') def step_attempt_create_empty_plan(context, target): try: context.cov_svc.request_correction( plan_id="", target_decision_id=target, mode=CorrectionMode.REVERT, ) context.cov_error = None except (ValidationError, Exception) as exc: context.cov_error = exc @when('I cov-attempt to create a correction with plan_id "{plan}" and empty target') def step_attempt_create_empty_target(context, plan): try: context.cov_svc.request_correction( plan_id=plan, target_decision_id="", mode=CorrectionMode.REVERT, ) context.cov_error = None except (ValidationError, Exception) as exc: context.cov_error = exc # ------------------------------------------------------------------- # When steps - analyze_impact # ------------------------------------------------------------------- @when("I cov-analyze impact with an empty tree") def step_analyze_empty(context): context.cov_impact = context.cov_svc.analyze_impact(context.cov_cid, {}) @when('I cov-analyze impact with adjacency "{spec}"') def step_analyze_adj(context, spec): tree = _parse_adj(spec) context.cov_impact = context.cov_svc.analyze_impact(context.cov_cid, tree) @when('I cov-attempt to analyze impact for correction id "{cid}"') def step_attempt_analyze(context, cid): try: context.cov_svc.analyze_impact(cid, {}) context.cov_error = None except ResourceNotFoundError as exc: context.cov_error = exc # ------------------------------------------------------------------- # When steps - generate_dry_run_report # ------------------------------------------------------------------- @when("I cov-generate dry-run report with an empty tree") def step_dryrun_empty(context): context.cov_dry_run = context.cov_svc.generate_dry_run_report(context.cov_cid, {}) @when('I cov-generate dry-run report with adjacency "{spec}"') def step_dryrun_adj(context, spec): tree = _parse_adj(spec) context.cov_dry_run = context.cov_svc.generate_dry_run_report(context.cov_cid, tree) @when('I cov-generate dry-run report with a chain of {n:d} nodes rooted at "{root}"') def step_dryrun_chain(context, n, root): tree = _build_chain_cov(root, n) context.cov_dry_run = context.cov_svc.generate_dry_run_report(context.cov_cid, tree) @when('I cov-attempt to generate dry-run report for correction id "{cid}"') def step_attempt_dryrun(context, cid): try: context.cov_svc.generate_dry_run_report(cid, {}) context.cov_error = None except ResourceNotFoundError as exc: context.cov_error = exc # ------------------------------------------------------------------- # When steps - execute_revert # ------------------------------------------------------------------- @when('I cov-execute revert with adjacency "{spec}"') def step_exec_revert_adj(context, spec): tree = _parse_adj(spec) context.cov_revert_result = context.cov_svc.execute_revert(context.cov_cid, tree) @when("I cov-execute revert with empty tree") def step_exec_revert_empty(context): context.cov_revert_result = context.cov_svc.execute_revert(context.cov_cid, {}) @when("I cov-attempt to execute revert on the stored correction") def step_attempt_exec_revert(context): try: context.cov_svc.execute_revert(context.cov_cid, {}) context.cov_error = None except (ValidationError, ResourceNotFoundError) as exc: context.cov_error = exc # ------------------------------------------------------------------- # When steps - execute_append # ------------------------------------------------------------------- @when("I cov-execute append for the stored correction") def step_exec_append(context): context.cov_append_result = context.cov_svc.execute_append(context.cov_cid) @when("I cov-attempt to execute append on the stored correction") def step_attempt_exec_append(context): try: context.cov_svc.execute_append(context.cov_cid) context.cov_error = None except (ValidationError, ResourceNotFoundError) as exc: context.cov_error = exc # ------------------------------------------------------------------- # When steps - execute_correction (dispatch) # ------------------------------------------------------------------- @when('I cov-dispatch execute correction with adjacency "{spec}"') def step_dispatch_adj(context, spec): tree = _parse_adj(spec) context.cov_dispatch_result = context.cov_svc.execute_correction( context.cov_cid, tree ) @when("I cov-dispatch execute correction with no tree") def step_dispatch_no_tree(context): context.cov_dispatch_result = context.cov_svc.execute_correction(context.cov_cid) # ------------------------------------------------------------------- # When steps - get_correction # ------------------------------------------------------------------- @when("I cov-get the stored correction by id") def step_get_by_id(context): context.cov_retrieved = context.cov_svc.get_correction(context.cov_cid) @when('I cov-attempt to get correction with id "{cid}"') def step_attempt_get(context, cid): try: context.cov_svc.get_correction(cid) context.cov_error = None except ResourceNotFoundError as exc: context.cov_error = exc # ------------------------------------------------------------------- # When steps - list_corrections # ------------------------------------------------------------------- @when("I cov-list all corrections without filter") def step_list_all(context): context.cov_list = context.cov_svc.list_corrections() @when('I cov-list corrections for plan "{plan}"') def step_list_by_plan(context, plan): context.cov_list = context.cov_svc.list_corrections(plan_id=plan) # ------------------------------------------------------------------- # When steps - list_attempts # ------------------------------------------------------------------- @when("I cov-list attempts for the stored correction") def step_list_attempts(context): context.cov_attempts = context.cov_svc.list_attempts(context.cov_cid) @when('I cov-attempt to list attempts for correction id "{cid}"') def step_attempt_list_attempts(context, cid): try: context.cov_svc.list_attempts(cid) context.cov_error = None except ResourceNotFoundError as exc: context.cov_error = exc # ------------------------------------------------------------------- # When steps - cancel_correction # ------------------------------------------------------------------- @when("I cov-cancel the stored correction") def step_cancel(context): context.cov_svc.cancel_correction(context.cov_cid) @when("I cov-attempt to cancel the stored correction") def step_attempt_cancel(context): try: context.cov_svc.cancel_correction(context.cov_cid) context.cov_error = None except ValidationError as exc: context.cov_error = exc # ------------------------------------------------------------------- # When steps - _compute_affected_subtree # ------------------------------------------------------------------- @when('I cov-compute subtree for "{root}" with empty tree') def step_subtree_empty(context, root): context.cov_subtree = CorrectionService._compute_affected_subtree(root, {}) @when('I cov-compute subtree for "{root}" with adjacency "{spec}"') def step_subtree_adj(context, root, spec): tree = _parse_adj(spec) context.cov_subtree = CorrectionService._compute_affected_subtree(root, tree) # =================================================================== # Then steps # =================================================================== # ------------------------------------------------------------------- # __init__ assertions # ------------------------------------------------------------------- @then("the coverage service corrections dict should be empty") def step_init_corrections(context): assert len(context.cov_svc._corrections) == 0 @then("the coverage service impacts dict should be empty") def step_init_impacts(context): assert len(context.cov_svc._impacts) == 0 @then("the coverage service attempts dict should be empty") def step_init_attempts(context): assert len(context.cov_svc._attempts) == 0 @then("the coverage service results dict should be empty") def step_init_results(context): assert len(context.cov_svc._results) == 0 # ------------------------------------------------------------------- # request_correction assertions # ------------------------------------------------------------------- @then("the cov-request should be stored in the service") def step_req_stored(context): assert context.cov_req is not None fetched = context.cov_svc.get_correction(context.cov_req.correction_id) assert fetched.correction_id == context.cov_req.correction_id @then('the cov-request plan_id should be "{val}"') def step_req_plan(context, val): assert context.cov_req.plan_id == val @then('the cov-request target_decision_id should be "{val}"') def step_req_target(context, val): assert context.cov_req.target_decision_id == val @then('the cov-request mode should be "{val}"') def step_req_mode(context, val): assert context.cov_req.mode.value == val @then('the cov-request status should be "{val}"') def step_req_status(context, val): assert context.cov_req.status.value == val @then("the cov-request dry_run should be false") def step_req_dryrun_false(context): assert context.cov_req.dry_run is False @then("the cov-request dry_run should be true") def step_req_dryrun_true(context): assert context.cov_req.dry_run is True @then('the cov-request guidance should be "{val}"') def step_req_guidance(context, val): assert context.cov_req.guidance == val @then("the cov-request guidance should be empty") def step_req_guidance_empty(context): assert context.cov_req.guidance == "" # ------------------------------------------------------------------- # Error assertions # ------------------------------------------------------------------- @then("a cov ValidationError should have been raised") def step_cov_val_error(context): assert context.cov_error is not None, "Expected ValidationError but none was raised" assert isinstance(context.cov_error, ValidationError), ( f"Expected ValidationError, got {type(context.cov_error).__name__}" ) @then("a cov ResourceNotFoundError should have been raised") def step_cov_rnf_error(context): assert context.cov_error is not None, ( "Expected ResourceNotFoundError but none was raised" ) assert isinstance(context.cov_error, ResourceNotFoundError), ( f"Expected ResourceNotFoundError, got {type(context.cov_error).__name__}" ) # ------------------------------------------------------------------- # Impact assertions # ------------------------------------------------------------------- @then('the cov-impact affected decisions should be "{expected}"') def step_impact_decisions(context, expected): expected_list = [d.strip() for d in expected.split(",")] assert context.cov_impact.affected_decisions == expected_list, ( f"Expected {expected_list}, got {context.cov_impact.affected_decisions}" ) @then('the cov-impact risk level should be "{val}"') def step_impact_risk(context, val): assert context.cov_impact.risk_level == val @then('the cov-impact rollback tier should be "{val}"') def step_impact_rollback(context, val): assert context.cov_impact.rollback_tier == val @then("the cov-impact estimated cost should be {cost:g}") def step_impact_cost(context, cost): assert context.cov_impact.estimated_cost == cost @then('the cov-impact artifacts to archive should be "{expected}"') def step_impact_artifacts(context, expected): expected_list = [a.strip() for a in expected.split(",")] assert context.cov_impact.artifacts_to_archive == expected_list @then('the cov-impact affected files should be "{expected}"') def step_impact_files(context, expected): expected_list = [f.strip() for f in expected.split(",")] assert context.cov_impact.affected_files == expected_list @then("the cov-impact affected decisions count should be {n:d}") def step_impact_count(context, n): actual = len(context.cov_impact.affected_decisions) assert actual == n, f"Expected {n}, got {actual}" @then('the cov-stored correction status should be "{val}"') def step_stored_status(context, val): req = context.cov_svc.get_correction(context.cov_cid) assert req.status.value == val, f"Expected status '{val}', got '{req.status.value}'" # ------------------------------------------------------------------- # Dry-run report assertions # ------------------------------------------------------------------- @then('the cov dry-run mode should be "{val}"') def step_dr_mode(context, val): assert context.cov_dry_run.mode.value == val @then("the cov dry-run warnings should be empty") def step_dr_warnings_empty(context): assert len(context.cov_dry_run.warnings) == 0, ( f"Expected no warnings, got {context.cov_dry_run.warnings}" ) @then('the cov dry-run warnings should contain "{fragment}"') def step_dr_warning_contains(context, fragment): joined = " | ".join(context.cov_dry_run.warnings) assert fragment in joined, ( f"'{fragment}' not found in warnings: {context.cov_dry_run.warnings}" ) @then('the cov dry-run decisions to invalidate should be "{expected}"') def step_dr_invalidate(context, expected): expected_list = [d.strip() for d in expected.split(",")] assert context.cov_dry_run.decisions_to_invalidate == expected_list @then("the cov dry-run decisions to invalidate should be empty list") def step_dr_invalidate_empty(context): assert context.cov_dry_run.decisions_to_invalidate == [] @then("the cov dry-run decisions to invalidate count should be {n:d}") def step_dr_invalidate_count(context, n): actual = len(context.cov_dry_run.decisions_to_invalidate) assert actual == n, f"Expected {n}, got {actual}" @then("the cov dry-run estimated recompute time should be {t:g}") def step_dr_recompute(context, t): assert context.cov_dry_run.estimated_recompute_time_seconds == t # ------------------------------------------------------------------- # Revert result assertions # ------------------------------------------------------------------- @then('the cov revert result status should be "{val}"') def step_revert_status(context, val): assert context.cov_revert_result.status.value == val @then('the cov revert result reverted decisions should include "{d}"') def step_revert_incl(context, d): assert d in context.cov_revert_result.reverted_decisions @then('the cov revert result archived artifacts should include "{a}"') def step_revert_artifacts(context, a): assert a in context.cov_revert_result.archived_artifacts # ------------------------------------------------------------------- # Append result assertions # ------------------------------------------------------------------- @then('the cov append result status should be "{val}"') def step_append_status(context, val): assert context.cov_append_result.status.value == val @then("the cov append result spawned_child_plan_id should not be none") def step_append_child(context): assert context.cov_append_result.spawned_child_plan_id is not None @then("the cov append result new_decisions should not be empty") def step_append_new_decisions(context): assert len(context.cov_append_result.new_decisions) > 0 @then("the cov first attempt details should contain spawned_child_plan_id") def step_append_attempt_details(context): attempts = context.cov_svc.list_attempts(context.cov_cid) assert "spawned_child_plan_id" in attempts[0].details # ------------------------------------------------------------------- # Dispatch result assertions # ------------------------------------------------------------------- @then('the cov dispatch result status should be "{val}"') def step_dispatch_status(context, val): assert context.cov_dispatch_result.status.value == val @then('the cov dispatch result reverted decisions should include "{d}"') def step_dispatch_reverted(context, d): assert d in context.cov_dispatch_result.reverted_decisions @then("the cov dispatch result spawned_child_plan_id should not be none") def step_dispatch_child(context): assert context.cov_dispatch_result.spawned_child_plan_id is not None # ------------------------------------------------------------------- # get_correction assertions # ------------------------------------------------------------------- @then('the cov-retrieved correction plan_id should be "{val}"') def step_get_plan(context, val): assert context.cov_retrieved.plan_id == val @then('the cov-retrieved correction target_decision_id should be "{val}"') def step_get_target(context, val): assert context.cov_retrieved.target_decision_id == val # ------------------------------------------------------------------- # list_corrections assertions # ------------------------------------------------------------------- @then("the cov corrections list should have at least {n:d} items") def step_list_min(context, n): actual = len(context.cov_list) assert actual >= n, f"Expected at least {n}, got {actual}" @then("the cov corrections list should have {n:d} item") def step_list_exact_one(context, n): actual = len(context.cov_list) assert actual == n, f"Expected {n}, got {actual}" @then("the cov corrections list should have {n:d} items") def step_list_exact(context, n): actual = len(context.cov_list) assert actual == n, f"Expected {n}, got {actual}" # ------------------------------------------------------------------- # list_attempts assertions # ------------------------------------------------------------------- @then("the cov attempts list should have {n:d} items") def step_attempts_count_plural(context, n): actual = len(context.cov_attempts) assert actual == n, f"Expected {n}, got {actual}" @then("the cov attempts list should have {n:d} item") def step_attempts_count_single(context, n): actual = len(context.cov_attempts) assert actual == n, f"Expected {n}, got {actual}" # ------------------------------------------------------------------- # Attempt detail assertions # ------------------------------------------------------------------- @then("the cov attempts for the stored correction should have {n:d} entry") def step_attempts_for_stored(context, n): attempts = context.cov_svc.list_attempts(context.cov_cid) assert len(attempts) == n, f"Expected {n}, got {len(attempts)}" @then("the cov first attempt should be successful") def step_first_attempt_ok(context): attempts = context.cov_svc.list_attempts(context.cov_cid) assert attempts[0].success is True @then("the cov first attempt completed_at should be set") def step_first_attempt_completed(context): attempts = context.cov_svc.list_attempts(context.cov_cid) assert attempts[0].completed_at is not None # ------------------------------------------------------------------- # _classify_risk assertions # ------------------------------------------------------------------- @then('cov classifying risk for {n:d} affected returns "{expected}"') def step_classify_risk(context, n, expected): result = CorrectionService._classify_risk(n) assert result == expected, ( f"_classify_risk({n}) = '{result}', expected '{expected}'" ) # ------------------------------------------------------------------- # _compute_affected_subtree assertions # ------------------------------------------------------------------- @then('the cov subtree should be "{expected}"') def step_subtree_result(context, expected): expected_list = [n.strip() for n in expected.split(",")] assert context.cov_subtree == expected_list, ( f"Expected {expected_list}, got {context.cov_subtree}" ) @then("the cov subtree count should be {n:d}") def step_subtree_count(context, n): actual = len(context.cov_subtree) assert actual == n, f"Expected {n}, got {actual}"