Files
placeholder/features/steps/cli_main_shortcuts_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

168 lines
6.0 KiB
Python

"""Step definitions for CLI main shortcut coverage scenarios."""
from __future__ import annotations
import ast
from typing import Any
from unittest.mock import patch
import typer
from behave import then, when
from typer.testing import CliRunner
from cleveragents.cli.main import app, main
# Map shortcut names to their underlying implementation targets.
SHORTCUT_TARGETS = {
"tell": "cleveragents.cli.commands.plan.tell",
"build": "cleveragents.cli.commands.plan.build",
"apply": "cleveragents.cli.commands.plan.lifecycle_apply_plan",
"context-load": "cleveragents.cli.commands.context.context_add",
"context-add": "cleveragents.cli.commands.context.context_add",
}
@when('I execute the CLI shortcut "{shortcut}" with argument list {arg_literal}')
def step_execute_cli_shortcut(context: Any, shortcut: str, arg_literal: str) -> None:
"""Invoke a CLI shortcut while patching the underlying implementation."""
if shortcut not in SHORTCUT_TARGETS:
raise AssertionError(f"Unhandled CLI shortcut: {shortcut}")
try:
parsed_args = ast.literal_eval(arg_literal)
except Exception as exc: # pragma: no cover - defensive guard
raise AssertionError(
f"Argument list must be a valid Python list literal: {arg_literal}"
) from exc
if not isinstance(parsed_args, list):
raise AssertionError(f"Argument list must evaluate to a list: {arg_literal}")
runner = CliRunner()
target = SHORTCUT_TARGETS[shortcut]
with patch(target) as mock_target:
result = runner.invoke(app, [shortcut, *parsed_args])
if not mock_target.called:
raise AssertionError(f"Expected shortcut '{shortcut}' to call {target}")
# Record results for subsequent assertions.
context.shortcut_name = shortcut
context.shortcut_result = result
if (
mock_target.call_args
): # pragma: no branch - call_args always present given the assertion above
context.shortcut_call_args = list(mock_target.call_args.args)
context.shortcut_call_kwargs = dict(mock_target.call_args.kwargs)
else: # pragma: no cover - defensive guard
context.shortcut_call_args = []
context.shortcut_call_kwargs = {}
@then("the shortcut should exit with code {expected:d}")
def step_shortcut_exit_code(context: Any, expected: int) -> None:
"""Ensure the CLI shortcut returned the expected exit code."""
result = getattr(context, "shortcut_result", None)
if result is None:
raise AssertionError("Shortcut result was not captured")
assert result.exit_code == expected, (
f"Unexpected exit code for {context.shortcut_name}: "
f"expected {expected}, got {result.exit_code}"
)
@then("the shortcut should forward keyword arguments {expected_literal}")
def step_shortcut_forward_kwargs(context: Any, expected_literal: str) -> None:
"""Verify the underlying command received the correct keyword arguments."""
try:
expected_kwargs = ast.literal_eval(expected_literal)
except Exception as exc: # pragma: no cover - defensive guard
raise AssertionError(
f"Expected keyword arguments must be a valid Python dict literal: {expected_literal}"
) from exc
if not isinstance(expected_kwargs, dict):
raise AssertionError(
f"Expected keyword arguments must evaluate to a dict: {expected_literal}"
)
actual_kwargs = getattr(context, "shortcut_call_kwargs", None)
assert actual_kwargs == expected_kwargs, (
f"Forwarded kwargs mismatch for {context.shortcut_name}: "
f"expected {expected_kwargs}, got {actual_kwargs}"
)
@when('I execute main with stubbed return "{mode}" and value {value}')
def step_execute_main_with_stub(context: Any, mode: str, value: str) -> None:
"""Run main() with the Typer app patched to return specific results."""
try:
numeric_value = int(value)
except ValueError as exc:
raise AssertionError(
f"Value parameter must be an integer literal, got: {value}"
) from exc
call_tracker: dict[str, Any] = {"called": False}
def stubbed_app(args: list[str], standalone_mode: bool = False) -> Any:
call_tracker["called"] = True
call_tracker["args"] = list(args)
call_tracker["standalone_mode"] = standalone_mode
if mode == "exit":
return typer.Exit(numeric_value)
if mode == "abort":
return typer.Abort()
if mode == "integer":
return numeric_value
if mode == "none":
return None
if mode == "raise_abort":
raise typer.Abort()
raise AssertionError(f"Unhandled stubbed mode: {mode}")
with patch("cleveragents.cli.main.app", new=stubbed_app):
context.main_result = main(["--help"])
context.stub_tracker = call_tracker
if not call_tracker.get("called"):
raise AssertionError("Patched Typer app was not invoked by main()")
@then("the Typer app should have received {arg_literal} with standalone mode disabled")
def step_verify_stub_invocation(context: Any, arg_literal: str) -> None:
"""Ensure the patched Typer app was called with expected arguments."""
try:
expected_args = ast.literal_eval(arg_literal)
except Exception as exc: # pragma: no cover - defensive guard
raise AssertionError(
f"Expected argument list must be a valid Python literal: {arg_literal}"
) from exc
if not isinstance(expected_args, list):
raise AssertionError(
f"Expected argument list must evaluate to a list: {arg_literal}"
)
tracker = getattr(context, "stub_tracker", None)
if tracker is None:
raise AssertionError("No stub tracker data found on context")
actual_args = tracker.get("args")
assert actual_args == expected_args, (
f"Patched Typer app received args {actual_args}, expected {expected_args}"
)
standalone_mode = tracker.get("standalone_mode")
assert standalone_mode is False, (
"Expected standalone_mode to be False for main() invocation"
)