Files
temp/features/steps/plan_commands_new_coverage_steps.py
freemo 64cfdc782a fix(resource): call bootstrap_builtin_types during initialization
Add a call to bootstrap_builtin_types() in init_command() (project.py)
immediately after initialize_project() returns.  This seeds the built-in
resource types (fs-directory, git-checkout, etc.) into the database so
that "resource add" commands succeed without "Resource type not found"
errors.

The call is idempotent — invoking it multiple times will not create
duplicate types.

Also fix the TDD robot test (resource_type_bootstrap_git.robot) to
initialize a project before running "resource add", and fix a
pre-existing parallel test failure in plan_commands_new_coverage where
unittest.mock.patch could not reliably intercept PlanApplyService under
behave-parallel fork() workers.

ISSUES CLOSED: #523, #524
2026-03-07 19:36:09 +00:00

251 lines
9.0 KiB
Python

"""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<plan_id>[^"]+)" with correction "(?P<corr>[^"]+)"'
)
def step_new_cov_diff_with_correction(
context: Context, plan_id: str, corr: str
) -> None:
"""Invoke ``plan diff <plan_id> --correction <corr>`` 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<plan_id>[^"]+)" with format "(?P<fmt>[^"]+)"')
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<plan_id>[^"]+)"')
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<plan_id>[^"]+)" with format "(?P<fmt>[^"]+)"'
)
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<plan_id>[^"]+)"')
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,
)