"""Step definitions for plan_apply_service_coverage_boost feature. Targets uncovered lines in plan_apply_service.py: - Lines 674-688: _try_checkpoint success and exception paths - Lines 697-707: _try_rollback success, empty, and exception paths - Lines 777, 782, 787: _ApplyPhaseProxy constructor, sandbox_id, context """ from __future__ import annotations from typing import Any from unittest.mock import MagicMock from behave import given, then, when # type: ignore[import-untyped] from behave.runner import Context # type: ignore[import-untyped] from cleveragents.application.services.plan_apply_service import ( ApplyOutcome, PlanApplyService, _ApplyPhaseProxy, ) from cleveragents.core.exceptions import PlanError from cleveragents.domain.models.core.change import ( ChangeEntry, ChangeOperation, SpecChangeSet, ) from cleveragents.domain.models.core.plan import ( PlanPhase, PlanTimestamps, ProcessingState, ) __all__: list[str] = [] _PLAN_ID = "01COVERAGEBOOST0000001" _CHANGESET_ID = "01CSBOOST00000000000001" # ---------------------------------------------------------------------- # Helpers # ---------------------------------------------------------------------- class _StubPhase: """Stub for PlanPhase with a .value attribute.""" def __init__(self, phase: PlanPhase) -> None: self._phase = phase self.value = phase.value def __eq__(self, other: object) -> bool: if isinstance(other, PlanPhase): return self._phase == other if isinstance(other, _StubPhase): return self._phase == other._phase return NotImplemented class _StubState: """Stub for ProcessingState with a .value attribute.""" def __init__(self, state: ProcessingState) -> None: self._state = state self.value = state.value def __eq__(self, other: object) -> bool: if isinstance(other, ProcessingState): return self._state == other if isinstance(other, _StubState): return self._state == other._state return NotImplemented def _make_mock_plan( *, plan_id: str = _PLAN_ID, phase: PlanPhase = PlanPhase.EXECUTE, state: ProcessingState = ProcessingState.COMPLETE, changeset_id: str | None = _CHANGESET_ID, validation_summary: dict[str, Any] | None = None, is_terminal: bool = False, error_details: dict[str, str] | None = None, ) -> MagicMock: """Create a mock Plan with sensible defaults.""" plan = MagicMock() plan.identity.plan_id = plan_id plan.phase = _StubPhase(phase) plan.processing_state = _StubState(state) plan.changeset_id = changeset_id plan.validation_summary = validation_summary plan.is_terminal = is_terminal plan.error_details = error_details plan.timestamps = PlanTimestamps() plan.sandbox_refs = ["sandbox-ref-boost"] return plan def _make_lifecycle_mock(plan: MagicMock | None = None) -> MagicMock: """Create a mock PlanLifecycleService.""" lifecycle = MagicMock() if plan is not None: lifecycle.get_plan.return_value = plan lifecycle._commit_plan = MagicMock() lifecycle.complete_apply = MagicMock() lifecycle.constrain_apply = MagicMock() lifecycle.fail_apply = MagicMock() return lifecycle def _make_changeset_with_entry( plan_id: str = _PLAN_ID, changeset_id: str = _CHANGESET_ID, ) -> SpecChangeSet: """Create a SpecChangeSet with a single modify entry.""" cs = SpecChangeSet(changeset_id=changeset_id, plan_id=plan_id) entry = ChangeEntry( plan_id=plan_id, resource_id="RES001", tool_name="builtin/test-tool", operation=ChangeOperation.MODIFY, path="src/app.py", before_hash="abcdef0123456789", after_hash="123456abcdefghij", ) cs.add_change(entry) return cs # ====================================================================== # _ApplyPhaseProxy scenarios # ====================================================================== @given('pacb an ApplyPhaseProxy with plan_id "{plan_id}"') def step_create_proxy(context: Context, plan_id: str) -> None: """Create an _ApplyPhaseProxy instance.""" context.pacb_proxy = _ApplyPhaseProxy(plan_id) @then('pacb the proxy sandbox_id should be "{expected}"') def step_proxy_sandbox_id(context: Context, expected: str) -> None: """Assert the proxy sandbox_id matches.""" actual = context.pacb_proxy.sandbox_id assert actual == expected, f"Expected sandbox_id '{expected}', got '{actual}'" @then("pacb the proxy context should be None") def step_proxy_context_none(context: Context) -> None: """Assert the proxy context returns None.""" assert context.pacb_proxy.context is None, ( f"Expected context to be None, got {context.pacb_proxy.context}" ) # ====================================================================== # _try_checkpoint scenarios # ====================================================================== @given("pacb a service with a configured checkpoint manager") def step_service_with_checkpoint_manager(context: Context) -> None: """Create a service with a mock checkpoint manager.""" lifecycle = _make_lifecycle_mock() checkpoint_mgr = MagicMock() checkpoint_mgr.create_checkpoint = MagicMock() context.pacb_checkpoint_mgr = checkpoint_mgr context.pacb_service = PlanApplyService( lifecycle_service=lifecycle, changeset_store=None, checkpoint_manager=checkpoint_mgr, ) context.pacb_error = None @when('pacb I call _try_checkpoint with plan_id "{plan_id}" and phase "{phase}"') def step_call_try_checkpoint(context: Context, plan_id: str, phase: str) -> None: """Call _try_checkpoint on the service.""" context.pacb_error = None try: context.pacb_service._try_checkpoint(plan_id, phase) except Exception as exc: context.pacb_error = exc @when( 'pacb I call _try_checkpoint with plan_id "{plan_id}" and phase "{phase}" and metadata' ) def step_call_try_checkpoint_with_metadata( context: Context, plan_id: str, phase: str ) -> None: """Call _try_checkpoint on the service with metadata.""" context.pacb_error = None try: context.pacb_service._try_checkpoint( plan_id, phase, metadata={"key1": "val1", "key2": "val2"} ) except Exception as exc: context.pacb_error = exc @then("pacb the checkpoint manager create_checkpoint should have been called") def step_checkpoint_create_called(context: Context) -> None: """Assert create_checkpoint was called on the checkpoint manager.""" context.pacb_checkpoint_mgr.create_checkpoint.assert_called() @then('pacb the checkpoint sandbox should have sandbox_id starting with "apply-"') def step_checkpoint_sandbox_has_prefix(context: Context) -> None: """Assert the sandbox arg passed to create_checkpoint has correct sandbox_id.""" call_args = context.pacb_checkpoint_mgr.create_checkpoint.call_args sandbox_arg = call_args.kwargs.get("sandbox") or call_args[0][0] assert sandbox_arg.sandbox_id.startswith("apply-"), ( f"Expected sandbox_id starting with 'apply-', got '{sandbox_arg.sandbox_id}'" ) @then("pacb the checkpoint manager create_checkpoint should have received metadata") def step_checkpoint_received_metadata(context: Context) -> None: """Assert create_checkpoint was called with metadata dict.""" call_args = context.pacb_checkpoint_mgr.create_checkpoint.call_args metadata_arg = call_args.kwargs.get("metadata") or call_args[1].get( "metadata", call_args[0][3] if len(call_args[0]) > 3 else {} ) assert isinstance(metadata_arg, dict), ( f"Expected metadata dict, got {type(metadata_arg)}" ) assert len(metadata_arg) > 0, "Expected non-empty metadata" # -- _try_checkpoint exception path ---------------------------------------- @given("pacb a service with a checkpoint manager that raises on create") def step_service_checkpoint_raises_on_create(context: Context) -> None: """Create a service with a checkpoint manager that raises on create.""" lifecycle = _make_lifecycle_mock() checkpoint_mgr = MagicMock() checkpoint_mgr.create_checkpoint.side_effect = RuntimeError( "simulated checkpoint failure" ) context.pacb_checkpoint_mgr = checkpoint_mgr context.pacb_service = PlanApplyService( lifecycle_service=lifecycle, changeset_store=None, checkpoint_manager=checkpoint_mgr, ) context.pacb_error = None @then("pacb no exception should have been raised") def step_no_exception_raised(context: Context) -> None: """Assert that no exception was raised.""" assert context.pacb_error is None, ( f"Expected no exception, but got {type(context.pacb_error).__name__}: " f"{context.pacb_error}" ) # ====================================================================== # _try_rollback scenarios # ====================================================================== @given("pacb a service with a checkpoint manager that has existing checkpoints") def step_service_checkpoint_has_checkpoints(context: Context) -> None: """Create a service with a checkpoint manager that returns checkpoints.""" lifecycle = _make_lifecycle_mock() checkpoint_mgr = MagicMock() mock_checkpoint = MagicMock() mock_checkpoint.checkpoint_id = "01CKPT00000000000001" checkpoint_mgr.list_checkpoints.return_value = [mock_checkpoint] checkpoint_mgr.rollback_to = MagicMock(return_value=True) context.pacb_checkpoint_mgr = checkpoint_mgr context.pacb_mock_checkpoint = mock_checkpoint context.pacb_service = PlanApplyService( lifecycle_service=lifecycle, changeset_store=None, checkpoint_manager=checkpoint_mgr, ) context.pacb_error = None @given("pacb a service with a checkpoint manager that returns no checkpoints") def step_service_checkpoint_empty_list(context: Context) -> None: """Create a service with a checkpoint manager that returns empty list.""" lifecycle = _make_lifecycle_mock() checkpoint_mgr = MagicMock() checkpoint_mgr.list_checkpoints.return_value = [] checkpoint_mgr.rollback_to = MagicMock() context.pacb_checkpoint_mgr = checkpoint_mgr context.pacb_service = PlanApplyService( lifecycle_service=lifecycle, changeset_store=None, checkpoint_manager=checkpoint_mgr, ) context.pacb_error = None @given("pacb a service with a checkpoint manager that raises on list") def step_service_checkpoint_raises_on_list(context: Context) -> None: """Create a service with a checkpoint manager that raises on list.""" lifecycle = _make_lifecycle_mock() checkpoint_mgr = MagicMock() checkpoint_mgr.list_checkpoints.side_effect = RuntimeError("simulated list failure") context.pacb_checkpoint_mgr = checkpoint_mgr context.pacb_service = PlanApplyService( lifecycle_service=lifecycle, changeset_store=None, checkpoint_manager=checkpoint_mgr, ) context.pacb_error = None @when('pacb I call _try_rollback with plan_id "{plan_id}"') def step_call_try_rollback(context: Context, plan_id: str) -> None: """Call _try_rollback on the service.""" context.pacb_error = None try: context.pacb_service._try_rollback(plan_id) except Exception as exc: context.pacb_error = exc @then("pacb the checkpoint manager list_checkpoints should have been called") def step_list_checkpoints_called(context: Context) -> None: """Assert list_checkpoints was called.""" context.pacb_checkpoint_mgr.list_checkpoints.assert_called() @then( "pacb the checkpoint manager rollback_to should have been called with the last checkpoint" ) def step_rollback_called_with_last(context: Context) -> None: """Assert rollback_to was called with the last checkpoint.""" context.pacb_checkpoint_mgr.rollback_to.assert_called_once_with( context.pacb_mock_checkpoint ) @then("pacb the checkpoint manager rollback_to should not have been called") def step_rollback_not_called(context: Context) -> None: """Assert rollback_to was NOT called.""" context.pacb_checkpoint_mgr.rollback_to.assert_not_called() # ====================================================================== # Integration: apply_with_validation_gate + checkpoint manager # ====================================================================== @given("pacb a service with checkpoint manager and a plan with passing validations") def step_service_checkpoint_plan_passing(context: Context) -> None: """Create a service with checkpoint_manager and a plan that passes validation.""" plan = _make_mock_plan( changeset_id=_CHANGESET_ID, is_terminal=False, validation_summary={ "total": 2, "required_passed": 2, "required_failed": 0, }, ) cs = _make_changeset_with_entry() store = MagicMock() store.get.return_value = cs lifecycle = _make_lifecycle_mock(plan) checkpoint_mgr = MagicMock() checkpoint_mgr.create_checkpoint = MagicMock() context.pacb_checkpoint_mgr = checkpoint_mgr context.pacb_service = PlanApplyService( lifecycle_service=lifecycle, changeset_store=store, checkpoint_manager=checkpoint_mgr, ) context.pacb_error = None @given("pacb a service with checkpoint manager and complete_apply raises PlanError") def step_service_checkpoint_complete_raises(context: Context) -> None: """Create a service where complete_apply raises, triggering rollback.""" plan = _make_mock_plan( changeset_id=_CHANGESET_ID, is_terminal=False, validation_summary={ "total": 2, "required_passed": 2, "required_failed": 0, }, ) cs = _make_changeset_with_entry() store = MagicMock() store.get.return_value = cs lifecycle = _make_lifecycle_mock(plan) lifecycle.complete_apply.side_effect = PlanError("Not in Apply phase") checkpoint_mgr = MagicMock() checkpoint_mgr.create_checkpoint = MagicMock() checkpoint_mgr.list_checkpoints.return_value = [] context.pacb_checkpoint_mgr = checkpoint_mgr context.pacb_service = PlanApplyService( lifecycle_service=lifecycle, changeset_store=store, checkpoint_manager=checkpoint_mgr, ) context.pacb_error = None @when("pacb I call apply_with_validation_gate") def step_call_apply_gate(context: Context) -> None: """Call apply_with_validation_gate on the service.""" context.pacb_error = None try: context.pacb_apply_result = context.pacb_service.apply_with_validation_gate( plan_id=_PLAN_ID, ) except Exception as exc: context.pacb_error = exc @then('pacb the apply result outcome should be "{outcome}"') def step_apply_result_outcome(context: Context, outcome: str) -> None: """Assert the apply result outcome matches.""" expected = ApplyOutcome(outcome) assert context.pacb_apply_result.outcome == expected, ( f"Expected outcome '{outcome}', got '{context.pacb_apply_result.outcome}'" )