forked from HAL9000/cleveragents-core
48cff5cfe0
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>
168 lines
6.0 KiB
Python
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"
|
|
)
|