"""Step definitions for plan_commands_new_coverage.feature. Covers the remaining uncovered lines in plan.py: - Lines 1817, 1821, 1822: _get_apply_service() function body - Lines 1845-1855: plan_diff command body (correction branch + normal path) - Lines 1878-1888: plan_artifacts command body """ from __future__ import annotations from unittest.mock import MagicMock, patch from behave import given, then, use_step_matcher, when from behave.runner import Context from typer.testing import CliRunner from cleveragents.cli.commands.plan import app as plan_app from cleveragents.core.exceptions import CleverAgentsError, PlanError runner = CliRunner() _PATCH_GET_APPLY = "cleveragents.cli.commands.plan._get_apply_service" _PATCH_LIFECYCLE = "cleveragents.cli.commands.plan._get_lifecycle_service" _PATCH_PAS_CLASS = ( "cleveragents.application.services.plan_apply_service.PlanApplyService" ) def _build_mock_service( diff_return=None, diff_side_effect=None, artifacts_return=None, artifacts_side_effect=None, ): """Build a mock PlanApplyService with configurable diff/artifacts behaviour.""" svc = MagicMock() if diff_side_effect: svc.diff.side_effect = diff_side_effect else: svc.diff.return_value = diff_return or "" if artifacts_side_effect: svc.artifacts.side_effect = artifacts_side_effect else: svc.artifacts.return_value = artifacts_return or "" return svc # --------------------------------------------------------------------------- # GIVEN steps # --------------------------------------------------------------------------- use_step_matcher("parse") @given("new_cov no service mocks are needed") def step_new_cov_no_mocks(context: Context) -> None: """The correction branch returns early before calling any service.""" context.new_cov_mock_service = None @given('new_cov the apply service diff returns "{output}"') def step_new_cov_diff_returns_dq(context: Context, output: str) -> None: context.new_cov_mock_service = _build_mock_service(diff_return=output) @given("new_cov the apply service diff returns '{output}'") def step_new_cov_diff_returns_sq(context: Context, output: str) -> None: context.new_cov_mock_service = _build_mock_service(diff_return=output) @given('new_cov the apply service diff raises PlanError "{msg}"') def step_new_cov_diff_plan_error(context: Context, msg: str) -> None: context.new_cov_mock_service = _build_mock_service( diff_side_effect=PlanError(msg), ) @given('new_cov the apply service diff raises CleverAgentsError "{msg}"') def step_new_cov_diff_ca_error(context: Context, msg: str) -> None: context.new_cov_mock_service = _build_mock_service( diff_side_effect=CleverAgentsError(msg), ) @given('new_cov the apply service artifacts returns "{output}"') def step_new_cov_artifacts_returns_dq(context: Context, output: str) -> None: context.new_cov_mock_service = _build_mock_service(artifacts_return=output) @given("new_cov the apply service artifacts returns '{output}'") def step_new_cov_artifacts_returns_sq(context: Context, output: str) -> None: context.new_cov_mock_service = _build_mock_service(artifacts_return=output) @given('new_cov the apply service artifacts raises PlanError "{msg}"') def step_new_cov_artifacts_plan_error(context: Context, msg: str) -> None: context.new_cov_mock_service = _build_mock_service( artifacts_side_effect=PlanError(msg), ) @given('new_cov the apply service artifacts raises CleverAgentsError "{msg}"') def step_new_cov_artifacts_ca_error(context: Context, msg: str) -> None: context.new_cov_mock_service = _build_mock_service( artifacts_side_effect=CleverAgentsError(msg), ) @given("new_cov a mocked lifecycle service for apply service creation") def step_new_cov_mock_lifecycle(context: Context) -> None: context.new_cov_mock_lifecycle = MagicMock() # --------------------------------------------------------------------------- # WHEN steps # --------------------------------------------------------------------------- use_step_matcher("re") @when( r'new_cov I run plan diff for "(?P[^"]+)" with correction "(?P[^"]+)"' ) def step_new_cov_diff_with_correction( context: Context, plan_id: str, corr: str ) -> None: """Invoke ``plan diff --correction `` via the CLI runner. The correction branch returns before calling _get_apply_service, so we do NOT need to mock it. """ context.new_cov_result = runner.invoke( plan_app, ["diff", plan_id, "--correction", corr] ) @when(r'new_cov I run plan diff for "(?P[^"]+)" with format "(?P[^"]+)"') def step_new_cov_diff_with_fmt(context: Context, plan_id: str, fmt: str) -> None: with patch(_PATCH_GET_APPLY, return_value=context.new_cov_mock_service): context.new_cov_result = runner.invoke( plan_app, ["diff", plan_id, "--format", fmt] ) @when(r'new_cov I run plan diff for "(?P[^"]+)"') def step_new_cov_diff(context: Context, plan_id: str) -> None: with patch(_PATCH_GET_APPLY, return_value=context.new_cov_mock_service): context.new_cov_result = runner.invoke(plan_app, ["diff", plan_id]) @when( r'new_cov I run plan artifacts for "(?P[^"]+)" with format "(?P[^"]+)"' ) def step_new_cov_artifacts_with_fmt(context: Context, plan_id: str, fmt: str) -> None: with patch(_PATCH_GET_APPLY, return_value=context.new_cov_mock_service): context.new_cov_result = runner.invoke( plan_app, ["artifacts", plan_id, "--format", fmt] ) @when(r'new_cov I run plan artifacts for "(?P[^"]+)"') def step_new_cov_artifacts(context: Context, plan_id: str) -> None: with patch(_PATCH_GET_APPLY, return_value=context.new_cov_mock_service): context.new_cov_result = runner.invoke(plan_app, ["artifacts", plan_id]) # Switch back to parse for the remaining non-parameterized WHEN step use_step_matcher("parse") @when("new_cov I call _get_apply_service directly") def step_new_cov_call_get_apply(context: Context) -> None: """Call the real ``_get_apply_service``, mocking its dependencies. This exercises the actual function body (import, lifecycle lookup, ``PlanApplyService`` construction) to cover lines 1817, 1821, 1822. Both ``_get_lifecycle_service`` **and** ``PlanApplyService`` are patched. The class is patched at two locations — the canonical source module and the plan module (with ``create=True``) — so the lazy ``from … import PlanApplyService`` inside the function always resolves to our mock, even under ``behave-parallel``'s ``fork()``-based workers. """ from cleveragents.cli.commands.plan import _get_apply_service mock_lifecycle = MagicMock() mock_pas_cls = MagicMock() mock_pas_instance = MagicMock() mock_pas_cls.return_value = mock_pas_instance with ( patch(_PATCH_LIFECYCLE, return_value=mock_lifecycle), patch( "cleveragents.cli.commands.plan.PlanApplyService", mock_pas_cls, create=True, ), patch(_PATCH_PAS_CLASS, mock_pas_cls), ): result = _get_apply_service() context.new_cov_apply_result = result context.new_cov_mock_lifecycle_used = mock_lifecycle context.new_cov_mock_pas_cls = mock_pas_cls context.new_cov_mock_pas_instance = mock_pas_instance # --------------------------------------------------------------------------- # THEN steps # --------------------------------------------------------------------------- @then("new_cov the exit code should be {code:d}") def step_new_cov_exit_code(context: Context, code: int) -> None: assert context.new_cov_result.exit_code == code, ( f"Expected exit code {code}, got {context.new_cov_result.exit_code}. " f"Output: {context.new_cov_result.output}" ) @then("new_cov the exit code should be nonzero") def step_new_cov_exit_nonzero(context: Context) -> None: assert context.new_cov_result.exit_code != 0, ( f"Expected non-zero exit code, got {context.new_cov_result.exit_code}. " f"Output: {context.new_cov_result.output}" ) @then('new_cov the output should contain "{text}"') def step_new_cov_output_contains(context: Context, text: str) -> None: output = context.new_cov_result.output assert text in output, f"Expected '{text}' in output. Got:\n{output}" @then("new_cov the returned object should be a PlanApplyService instance") def step_new_cov_result_is_pas(context: Context) -> None: result = context.new_cov_apply_result expected = context.new_cov_mock_pas_instance assert result is expected, ( f"Expected _get_apply_service to return the mock PlanApplyService " f"instance, got {type(result).__name__}" ) @then("new_cov PlanApplyService was constructed with the lifecycle service") def step_new_cov_pas_called_with_lifecycle(context: Context) -> None: context.new_cov_mock_pas_cls.assert_called_once_with( lifecycle_service=context.new_cov_mock_lifecycle_used, )