Files
temp/features/steps/plan_cli_uncovered_region_coverage_steps.py
freemo 48cff5cfe0 refactor(cli): rename plan lifecycle-list and lifecycle-apply to match specification
Renames `plan lifecycle-list` to `plan list` and `plan lifecycle-apply` to `plan apply` to align with the specification's canonical command names. Removes legacy V2 plan commands that occupied those names.

- Renamed CLI command registrations from lifecycle-list/lifecycle-apply to list/apply
- Removed legacy V2 apply and list commands (~200 lines)
- Updated apply shortcut in main.py to delegate to v3 lifecycle
- Added defensive null check for plan existence in apply command
- Updated 63+ test, doc, and benchmark files for consistency

Closes #881

Co-authored-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
Co-committed-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
2026-04-02 19:09:04 +00:00

609 lines
22 KiB
Python

"""Step definitions for plan_cli_uncovered_region_coverage.feature.
Covers uncovered lines/branches in cleveragents/cli/commands/plan.py:
- Lines 1950-1954: list_plans empty result after filtering
- Lines 1956-1957: list_plans non-rich format output
- Line 2132: revert_plan CleverAgentsError handler
- Lines 2136-2143: _get_apply_service function body
- Lines 2149-2184: plan_diff command (correction branch + error paths)
- Lines 2202-2233: plan_artifacts command (success + error paths)
- Lines 2231-2273: correct_decision command (execution, dry-run, error handlers)
All step text uses the ``uncov-rgn`` prefix to avoid collisions with
other step files in the project.
"""
from __future__ import annotations
from datetime import datetime
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from behave import given, then, 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,
ResourceNotFoundError,
ValidationError,
)
from cleveragents.domain.models.core.plan import (
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
PlanTimestamps,
ProcessingState,
ProjectLink,
)
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_ULID_A = "01ARZ3NDEKTSV4RRFFQ69G5FAA"
_PATCH_GET_LIFECYCLE = "cleveragents.cli.commands.plan._get_lifecycle_service"
_PATCH_GET_APPLY = "cleveragents.cli.commands.plan._get_apply_service"
_PATCH_RESOLVE_ACTIVE = "cleveragents.cli.commands.plan._resolve_active_plan_id"
_PATCH_CONTAINER = "cleveragents.application.container.get_container"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_plan(
*,
plan_id: str = _ULID_A,
name: str = "local/uncov-test-plan",
phase: PlanPhase = PlanPhase.STRATEGIZE,
processing_state: ProcessingState = ProcessingState.QUEUED,
action_name: str = "local/test-action",
project_links: list[ProjectLink] | None = None,
) -> Plan:
"""Build a real Plan domain object for testing."""
return Plan(
identity=PlanIdentity(plan_id=plan_id),
namespaced_name=NamespacedName.parse(name),
action_name=action_name,
description="Coverage test plan",
definition_of_done=None,
strategy_actor=None,
execution_actor=None,
phase=phase,
processing_state=processing_state,
project_links=project_links or [],
timestamps=PlanTimestamps(
created_at=datetime(2025, 6, 15, 10, 0, 0),
updated_at=datetime(2025, 6, 15, 11, 0, 0),
),
reusable=True,
read_only=False,
created_by=None,
)
# ---------------------------------------------------------------------------
# Given — CLI runner and shared mocked lifecycle service
# ---------------------------------------------------------------------------
@given("an uncov-rgn CLI runner and mocked lifecycle service")
def step_uncov_rgn_cli_runner(context: Context) -> None:
"""Initialise a CliRunner, mock lifecycle service, and cleanup list."""
context.uncov_runner = CliRunner()
context.uncov_mock_svc = MagicMock()
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
p = patch(_PATCH_GET_LIFECYCLE, return_value=context.uncov_mock_svc)
p.start()
context._cleanup_handlers.append(p.stop)
# ---------------------------------------------------------------------------
# Given — list_plans scenarios
# ---------------------------------------------------------------------------
@given(
"the uncov-rgn lifecycle service returns plans that do not match an action filter"
)
def step_uncov_rgn_plans_no_match(context: Context) -> None:
"""Return plans whose action_name differs from the filter value."""
plans = [
_make_plan(action_name="local/other-action"),
]
context.uncov_mock_svc.list_plans.return_value = plans
@given("the uncov-rgn lifecycle service returns a single plan for listing")
def step_uncov_rgn_single_plan_for_list(context: Context) -> None:
"""Return one plan so the list command renders in non-rich format."""
plans = [_make_plan()]
context.uncov_mock_svc.list_plans.return_value = plans
# ---------------------------------------------------------------------------
# Given — revert_plan CleverAgentsError
# ---------------------------------------------------------------------------
@given("the uncov-rgn lifecycle service revert raises CleverAgentsError")
def step_uncov_rgn_revert_ca_error(context: Context) -> None:
"""Configure revert_plan to raise CleverAgentsError."""
context.uncov_mock_svc.revert_plan.side_effect = CleverAgentsError(
"revert service unavailable"
)
# ---------------------------------------------------------------------------
# Given — _get_apply_service
# ---------------------------------------------------------------------------
@given("an uncov-rgn mocked lifecycle service for apply service creation")
def step_uncov_rgn_mock_lifecycle_for_apply(context: Context) -> None:
"""Prepare patches for _get_apply_service test."""
context.uncov_mock_lifecycle = MagicMock()
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
# ---------------------------------------------------------------------------
# Given — plan_diff scenarios
# ---------------------------------------------------------------------------
@given('the uncov-rgn apply service diff returns formatted output "{output}"')
def step_uncov_rgn_apply_diff_ok(context: Context, output: str) -> None:
"""Set up a mock apply service whose diff() returns the given output."""
mock_apply = MagicMock()
mock_apply.diff.return_value = output
context.uncov_mock_apply = mock_apply
@given('the uncov-rgn apply service diff raises PlanError "{msg}"')
def step_uncov_rgn_apply_diff_plan_error(context: Context, msg: str) -> None:
"""Set up a mock apply service whose diff() raises PlanError."""
mock_apply = MagicMock()
mock_apply.diff.side_effect = PlanError(msg)
context.uncov_mock_apply = mock_apply
@given('the uncov-rgn apply service diff raises CleverAgentsError "{msg}"')
def step_uncov_rgn_apply_diff_ca_error(context: Context, msg: str) -> None:
"""Set up a mock apply service whose diff() raises CleverAgentsError."""
mock_apply = MagicMock()
mock_apply.diff.side_effect = CleverAgentsError(msg)
context.uncov_mock_apply = mock_apply
# ---------------------------------------------------------------------------
# Given — plan_artifacts scenarios
# ---------------------------------------------------------------------------
@given('the uncov-rgn apply service artifacts returns "{output}"')
def step_uncov_rgn_apply_artifacts_ok_dq(context: Context, output: str) -> None:
"""Set up a mock apply service whose artifacts() returns the given output."""
mock_apply = MagicMock()
mock_apply.artifacts.return_value = output
context.uncov_mock_apply = mock_apply
@given("the uncov-rgn apply service artifacts returns '{output}'")
def step_uncov_rgn_apply_artifacts_ok_sq(context: Context, output: str) -> None:
"""Set up a mock apply service with single-quoted output."""
mock_apply = MagicMock()
mock_apply.artifacts.return_value = output
context.uncov_mock_apply = mock_apply
@given('the uncov-rgn apply service artifacts raises PlanError "{msg}"')
def step_uncov_rgn_apply_artifacts_plan_error(context: Context, msg: str) -> None:
"""Set up a mock apply service whose artifacts() raises PlanError."""
mock_apply = MagicMock()
mock_apply.artifacts.side_effect = PlanError(msg)
context.uncov_mock_apply = mock_apply
@given('the uncov-rgn apply service artifacts raises CleverAgentsError "{msg}"')
def step_uncov_rgn_apply_artifacts_ca_error(context: Context, msg: str) -> None:
"""Set up a mock apply service whose artifacts() raises CleverAgentsError."""
mock_apply = MagicMock()
mock_apply.artifacts.side_effect = CleverAgentsError(msg)
context.uncov_mock_apply = mock_apply
# ---------------------------------------------------------------------------
# Given — correct_decision scenarios
# ---------------------------------------------------------------------------
def _make_mock_correction_svc(
*,
request_return: object | None = None,
request_side_effect: Exception | None = None,
impact_return: object | None = None,
execute_return: object | None = None,
) -> MagicMock:
"""Build a mock CorrectionService."""
svc = MagicMock()
if request_side_effect:
svc.request_correction.side_effect = request_side_effect
elif request_return:
svc.request_correction.return_value = request_return
else:
# Default request
svc.request_correction.return_value = SimpleNamespace(
correction_id="CORR-001",
mode=SimpleNamespace(value="revert"),
target_decision_id="DEC-001",
guidance="test guidance",
)
if impact_return:
svc.analyze_impact.return_value = impact_return
if execute_return:
svc.execute_correction.return_value = execute_return
return svc
@given(
"the uncov-rgn correction service returns a revert result with reverted decisions"
)
def step_uncov_rgn_correction_revert(context: Context) -> None:
"""Mock CorrectionService for a successful revert execution."""
request = SimpleNamespace(
correction_id="CORR-REVERT-01",
mode=SimpleNamespace(value="revert"),
target_decision_id="DEC-001",
guidance="Use Flask instead",
)
result = SimpleNamespace(
correction_id="CORR-REVERT-01",
status=SimpleNamespace(value="applied"),
new_decisions=[],
reverted_decisions=["DEC-001", "DEC-002"],
)
context.uncov_mock_correction = _make_mock_correction_svc(
request_return=request,
execute_return=result,
)
@given("the uncov-rgn correction service returns an append result with new decisions")
def step_uncov_rgn_correction_append(context: Context) -> None:
"""Mock CorrectionService for a successful append execution."""
request = SimpleNamespace(
correction_id="CORR-APPEND-01",
mode=SimpleNamespace(value="append"),
target_decision_id="DEC-003",
guidance="Add caching layer",
)
result = SimpleNamespace(
correction_id="CORR-APPEND-01",
status=SimpleNamespace(value="applied"),
new_decisions=["DEC-004", "DEC-005"],
reverted_decisions=[],
)
context.uncov_mock_correction = _make_mock_correction_svc(
request_return=request,
execute_return=result,
)
@given("the uncov-rgn correction service returns an impact analysis")
def step_uncov_rgn_correction_impact(context: Context) -> None:
"""Mock CorrectionService for dry-run impact analysis."""
request = SimpleNamespace(
correction_id="CORR-DRY-01",
mode=SimpleNamespace(value="revert"),
target_decision_id="DEC-010",
guidance="Preview changes",
)
impact = SimpleNamespace(
affected_decisions=["DEC-010", "DEC-011"],
affected_files=["main.py", "utils.py"],
estimated_cost=3.0,
risk_level="medium",
)
context.uncov_mock_correction = _make_mock_correction_svc(
request_return=request,
impact_return=impact,
)
@given("the uncov-rgn correction service request raises ResourceNotFoundError")
def step_uncov_rgn_correction_rnf(context: Context) -> None:
"""Mock CorrectionService to raise ResourceNotFoundError."""
context.uncov_mock_correction = _make_mock_correction_svc(
request_side_effect=ResourceNotFoundError("decision DEC-999 not found"),
)
@given("the uncov-rgn correction service request raises ValidationError")
def step_uncov_rgn_correction_val_error(context: Context) -> None:
"""Mock CorrectionService to raise ValidationError."""
context.uncov_mock_correction = _make_mock_correction_svc(
request_side_effect=ValidationError("plan_id must not be empty"),
)
@given("the uncov-rgn correction service request raises CleverAgentsError")
def step_uncov_rgn_correction_ca_error(context: Context) -> None:
"""Mock CorrectionService to raise CleverAgentsError."""
context.uncov_mock_correction = _make_mock_correction_svc(
request_side_effect=CleverAgentsError("correction service unavailable"),
)
# ---------------------------------------------------------------------------
# When — list_plans
# ---------------------------------------------------------------------------
@when('I uncov-rgn invoke list with action filter "{action}"')
def step_uncov_rgn_invoke_list_action(context: Context, action: str) -> None:
"""Invoke list with an --action filter."""
context.uncov_result = context.uncov_runner.invoke(
plan_app,
["list", "--action", action],
)
@when('I uncov-rgn invoke list with format "{fmt}"')
def step_uncov_rgn_invoke_list_fmt(context: Context, fmt: str) -> None:
"""Invoke list with a given --format."""
context.uncov_result = context.uncov_runner.invoke(
plan_app,
["list", "--format", fmt],
)
# ---------------------------------------------------------------------------
# When — revert_plan
# ---------------------------------------------------------------------------
@when('I uncov-rgn invoke revert for plan "{pid}"')
def step_uncov_rgn_invoke_revert(context: Context, pid: str) -> None:
"""Invoke the revert command for a given plan ID."""
context.uncov_result = context.uncov_runner.invoke(
plan_app,
["revert", pid],
)
# ---------------------------------------------------------------------------
# When — _get_apply_service
# ---------------------------------------------------------------------------
@when("I uncov-rgn call _get_apply_service directly")
def step_uncov_rgn_call_get_apply(context: Context) -> None:
"""Invoke _get_apply_service and capture the result."""
from cleveragents.cli.commands.plan import _get_apply_service
mock_pas_cls = MagicMock()
mock_pas_instance = MagicMock()
mock_pas_cls.return_value = mock_pas_instance
with (
patch(
_PATCH_GET_LIFECYCLE,
return_value=context.uncov_mock_lifecycle,
),
patch(
"cleveragents.cli.commands.plan.PlanApplyService",
mock_pas_cls,
create=True,
),
patch(
"cleveragents.application.services.plan_apply_service.PlanApplyService",
mock_pas_cls,
),
):
context.uncov_apply_result = _get_apply_service()
# ---------------------------------------------------------------------------
# When — plan_diff
# ---------------------------------------------------------------------------
@when('I uncov-rgn invoke diff for plan "{pid}" with correction "{corr}"')
def step_uncov_rgn_invoke_diff_correction(
context: Context, pid: str, corr: str
) -> None:
"""Invoke the diff command with --correction flag."""
context.uncov_result = context.uncov_runner.invoke(
plan_app,
["diff", pid, "--correction", corr],
)
@when('I uncov-rgn invoke diff for plan "{pid}" without correction')
def step_uncov_rgn_invoke_diff_no_correction(context: Context, pid: str) -> None:
"""Invoke the diff command without --correction flag."""
with patch(_PATCH_GET_APPLY, return_value=context.uncov_mock_apply):
context.uncov_result = context.uncov_runner.invoke(
plan_app,
["diff", pid],
)
# ---------------------------------------------------------------------------
# When — plan_artifacts
# ---------------------------------------------------------------------------
@when('I uncov-rgn invoke artifacts for plan "{pid}" in rich format')
def step_uncov_rgn_invoke_artifacts_rich(context: Context, pid: str) -> None:
"""Invoke the artifacts command in default rich format."""
with patch(_PATCH_GET_APPLY, return_value=context.uncov_mock_apply):
context.uncov_result = context.uncov_runner.invoke(
plan_app,
["artifacts", pid],
)
@when('I uncov-rgn invoke artifacts for plan "{pid}" with format "{fmt}"')
def step_uncov_rgn_invoke_artifacts_fmt(context: Context, pid: str, fmt: str) -> None:
"""Invoke the artifacts command with a given --format."""
with patch(_PATCH_GET_APPLY, return_value=context.uncov_mock_apply):
context.uncov_result = context.uncov_runner.invoke(
plan_app,
["artifacts", pid, "--format", fmt],
)
# ---------------------------------------------------------------------------
# When — correct_decision
# ---------------------------------------------------------------------------
def _invoke_correct(
context: Context,
*,
mode: str = "revert",
guidance: str = "test guidance",
yes: bool = True,
dry_run: bool = False,
fmt: str = "rich",
plan_id: str = "PLAN-ACTIVE-01",
decision_id: str = "DEC-001",
) -> None:
"""Helper to invoke the correct command with patches."""
args = ["correct", decision_id, "--mode", mode, "--guidance", guidance]
if yes:
args.append("--yes")
if dry_run:
args.append("--dry-run")
if fmt != "rich":
args.extend(["--format", fmt])
args.extend(["--plan", plan_id])
mock_correction = getattr(context, "uncov_mock_correction", MagicMock())
# Mock DecisionService resolved via DI container (issue #606 fix)
mock_decision_svc = MagicMock()
mock_decision_svc.list_decisions.return_value = []
mock_decision_svc.get_influence_edges.return_value = {}
mock_container = MagicMock()
mock_container.decision_service.return_value = mock_decision_svc
mock_container.correction_service.return_value = mock_correction
with patch(_PATCH_CONTAINER, return_value=mock_container):
context.uncov_result = context.uncov_runner.invoke(plan_app, args)
@when('I uncov-rgn invoke correct in revert mode with --yes and guidance "{guidance}"')
def step_uncov_rgn_correct_revert_yes(context: Context, guidance: str) -> None:
"""Invoke correct with revert mode, --yes, and given guidance."""
_invoke_correct(context, mode="revert", guidance=guidance, yes=True)
@when('I uncov-rgn invoke correct in append mode with --yes and guidance "{guidance}"')
def step_uncov_rgn_correct_append_yes(context: Context, guidance: str) -> None:
"""Invoke correct with append mode, --yes, and given guidance."""
_invoke_correct(context, mode="append", guidance=guidance, yes=True)
@when(
'I uncov-rgn invoke correct in revert mode with --yes, format "{fmt}", '
'and guidance "{guidance}"'
)
def step_uncov_rgn_correct_revert_fmt(
context: Context, fmt: str, guidance: str
) -> None:
"""Invoke correct with revert mode, --yes, custom format, and given guidance."""
_invoke_correct(context, mode="revert", guidance=guidance, yes=True, fmt=fmt)
@when(
"I uncov-rgn invoke correct in dry-run mode with rich format "
'and guidance "{guidance}"'
)
def step_uncov_rgn_correct_dryrun_rich(context: Context, guidance: str) -> None:
"""Invoke correct with dry-run flag in rich format."""
_invoke_correct(
context, mode="revert", guidance=guidance, yes=True, dry_run=True, fmt="rich"
)
@when(
'I uncov-rgn invoke correct in dry-run mode with format "{fmt}" '
'and guidance "{guidance}"'
)
def step_uncov_rgn_correct_dryrun_fmt(
context: Context, fmt: str, guidance: str
) -> None:
"""Invoke correct with dry-run flag in custom format."""
_invoke_correct(
context, mode="revert", guidance=guidance, yes=True, dry_run=True, fmt=fmt
)
@when('I uncov-rgn invoke correct with invalid mode "{mode}" and guidance "{guidance}"')
def step_uncov_rgn_correct_invalid_mode(
context: Context, mode: str, guidance: str
) -> None:
"""Invoke correct with an invalid mode."""
_invoke_correct(context, mode=mode, guidance=guidance, yes=True)
@when('I uncov-rgn invoke correct with valid mode "{mode}" and empty guidance')
def step_uncov_rgn_correct_empty_guidance(context: Context, mode: str) -> None:
"""Invoke correct with empty guidance string."""
_invoke_correct(context, mode=mode, guidance="", yes=True)
# ---------------------------------------------------------------------------
# Then — assertions
# ---------------------------------------------------------------------------
@then("the uncov-rgn command should exit normally")
def step_uncov_rgn_exit_ok(context: Context) -> None:
"""Assert the CLI exited with code 0."""
assert context.uncov_result.exit_code == 0, (
f"Expected exit_code=0, got {context.uncov_result.exit_code}. "
f"Output: {context.uncov_result.output}"
)
@then("the uncov-rgn command should abort")
def step_uncov_rgn_abort(context: Context) -> None:
"""Assert the CLI exited with a non-zero code (abort)."""
assert context.uncov_result.exit_code != 0, (
f"Expected non-zero exit_code, got {context.uncov_result.exit_code}. "
f"Output: {context.uncov_result.output}"
)
@then('the uncov-rgn output should contain "{text}"')
def step_uncov_rgn_output_contains(context: Context, text: str) -> None:
"""Assert the CLI output contains the expected text."""
output = context.uncov_result.output
assert text in output, (
f"Expected output to contain '{text}'. Actual output:\n{output}"
)
@then("the uncov-rgn apply service should be returned successfully")
def step_uncov_rgn_apply_ok(context: Context) -> None:
"""Assert _get_apply_service returned a non-None object."""
assert context.uncov_apply_result is not None, (
"Expected _get_apply_service to return a value, got None"
)