Files
temp/features/steps/plan_cli_streaming_coverage_steps.py
T

577 lines
21 KiB
Python

"""Step definitions for plan CLI streaming and helper function coverage tests.
These steps cover _print_lifecycle_plan, _get_current_project, and the
programmatic wrapper functions in cleveragents.cli.commands.plan.
"""
from __future__ import annotations
import re
from io import StringIO
from unittest.mock import MagicMock, patch
import typer
from behave import given, then, when
from rich.console import Console
from cleveragents.core.exceptions import CleverAgentsError
def _make_mock_container(project=None):
"""Create a mock container with plan and project services."""
mock_container = MagicMock()
mock_plan_service = MagicMock()
mock_project_service = MagicMock()
mock_project_service.get_current_project.return_value = project
mock_container.plan_service.return_value = mock_plan_service
mock_container.project_service.return_value = mock_project_service
return mock_container, mock_plan_service, mock_project_service
def _strip_markup(text: str) -> str:
"""Strip Rich markup tags from text."""
return re.sub(r"\[[^\]]*\]", "", text)
def _make_lifecycle_plan(
description: str = "Test plan description",
error_message: str | None = None,
project_links: list | None = None,
):
"""Create a real LifecyclePlan (v3 Plan) instance for testing."""
from cleveragents.domain.models.core.plan import (
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
PlanTimestamps,
ProcessingState,
ProjectLink,
)
links = (
project_links
if project_links is not None
else [
ProjectLink(project_name="test-project"),
]
)
return Plan(
identity=PlanIdentity(plan_id="01HXYZ01HXYZ01HXYZ01HXYZ01"),
namespaced_name=NamespacedName(namespace="local", name="test-plan"),
action_name="local/test-action",
description=description,
phase=PlanPhase.STRATEGIZE,
state=ProcessingState.QUEUED,
project_links=links,
error_message=error_message,
timestamps=PlanTimestamps(),
)
# ---------------------------------------------------------------------------
# _print_lifecycle_plan scenarios
# ---------------------------------------------------------------------------
@when("I call print_lifecycle_plan with a non-lifecycle plan object")
def step_call_print_lifecycle_plan_non_lifecycle(context):
"""Call _print_lifecycle_plan with a plain object (not a LifecyclePlan)."""
from cleveragents.cli.commands.plan import _print_lifecycle_plan
output = StringIO()
test_console = Console(file=output, force_terminal=False, width=120)
fake_plan = MagicMock()
fake_plan.__str__ = lambda self: "FakePlanRepr"
with patch("cleveragents.cli.commands.plan.console", test_console):
_print_lifecycle_plan(fake_plan, title="Fallback Test")
context.captured_output = output.getvalue()
@then("the fallback display path should be used for non-lifecycle plan")
def step_assert_fallback_display(context):
"""Assert the fallback path was used, printing a simple panel."""
output = _strip_markup(context.captured_output)
assert "Plan:" in output or "FakePlanRepr" in output, (
f"Expected fallback display but got: {output}"
)
@when("I call print_lifecycle_plan with a lifecycle plan that has an error_message")
def step_call_print_lifecycle_plan_with_error(context):
"""Call _print_lifecycle_plan with a plan that has error_message set."""
from cleveragents.cli.commands.plan import _print_lifecycle_plan
output = StringIO()
test_console = Console(file=output, force_terminal=False, width=120)
plan = _make_lifecycle_plan(
description="Plan with error",
error_message="Something went terribly wrong",
)
with patch("cleveragents.cli.commands.plan.console", test_console):
_print_lifecycle_plan(plan, title="Error Plan")
context.captured_output = output.getvalue()
@then("the lifecycle plan error_message should appear in the output")
def step_assert_error_message_displayed(context):
"""Assert the error_message text is rendered."""
output = _strip_markup(context.captured_output)
assert "Something went terribly wrong" in output, (
f"Expected error message in output but got: {output}"
)
@when("I call print_lifecycle_plan with a description longer than 200 characters")
def step_call_print_lifecycle_plan_long_desc(context):
"""Call _print_lifecycle_plan with a >200 char description."""
from cleveragents.cli.commands.plan import _print_lifecycle_plan
output = StringIO()
test_console = Console(file=output, force_terminal=False, width=120)
long_description = "A" * 250
plan = _make_lifecycle_plan(description=long_description)
with patch("cleveragents.cli.commands.plan.console", test_console):
_print_lifecycle_plan(plan, title="Long Desc Plan")
context.captured_output = output.getvalue()
@then("the lifecycle plan description should be truncated with ellipsis")
def step_assert_description_truncated(context):
"""Assert the description is truncated and ends with '...'."""
output = _strip_markup(context.captured_output)
# The full 250-char string should NOT appear; the truncated version (200 chars + ...) should
assert "A" * 250 not in output, "Full description should be truncated"
assert "..." in output, f"Expected ellipsis for truncation but got: {output}"
@when("I call print_lifecycle_plan with a lifecycle plan with no project links")
def step_call_print_lifecycle_plan_no_projects(context):
"""Call _print_lifecycle_plan with empty project_links."""
from cleveragents.cli.commands.plan import _print_lifecycle_plan
output = StringIO()
test_console = Console(file=output, force_terminal=False, width=120)
plan = _make_lifecycle_plan(project_links=[])
with patch("cleveragents.cli.commands.plan.console", test_console):
_print_lifecycle_plan(plan, title="No Projects Plan")
context.captured_output = output.getvalue()
@then("the lifecycle plan output should show projects as none")
def step_assert_projects_none(context):
"""Assert the output shows (none) for projects."""
output = _strip_markup(context.captured_output)
assert "(none)" in output, f"Expected '(none)' for empty projects but got: {output}"
# ---------------------------------------------------------------------------
# _get_current_project scenarios
# ---------------------------------------------------------------------------
@when("I call the get_current_project helper with a valid project")
def step_call_get_current_project_valid(context):
"""Call _get_current_project when a project is available."""
from cleveragents.cli.commands.plan import _get_current_project
mock_project = MagicMock()
mock_project.name = "valid-project"
container, _, project_service = _make_mock_container(project=mock_project)
with patch(
"cleveragents.application.container.get_container", return_value=container
):
context.returned_project = _get_current_project()
context.expected_project = mock_project
@then("the get_current_project helper should return the mock project")
def step_assert_get_current_project_returns(context):
"""Assert _get_current_project returned the mock project."""
assert context.returned_project is context.expected_project
@when("I call the get_current_project helper with no project available")
def step_call_get_current_project_no_project(context):
"""Call _get_current_project when no project exists."""
from cleveragents.cli.commands.plan import _get_current_project
container, _, _ = _make_mock_container(project=None)
context.call_exception = None
with patch(
"cleveragents.application.container.get_container", return_value=container
):
# Also patch the console to avoid side effects
output = StringIO()
test_console = Console(file=output, force_terminal=False, width=120)
with patch("cleveragents.cli.commands.plan.console", test_console):
try:
_get_current_project()
except Exception as exc:
context.call_exception = exc
@then("the get_current_project helper should raise typer Abort")
def step_assert_get_current_project_aborts(context):
"""Assert _get_current_project raised typer.Abort."""
assert isinstance(context.call_exception, typer.Abort), (
f"Expected typer.Abort but got: {type(context.call_exception)}"
)
# ---------------------------------------------------------------------------
# tell_command programmatic wrapper
# ---------------------------------------------------------------------------
@when(
"I invoke the plan tell_command programmatic wrapper with valid project and prompt"
)
def step_invoke_tell_command_wrapper(context):
"""Call tell_command with a valid project."""
from cleveragents.cli.commands.plan import tell_command
mock_project = MagicMock()
mock_project.name = "wrapper-project"
container, plan_service, _ = _make_mock_container(project=mock_project)
with patch(
"cleveragents.application.container.get_container", return_value=container
):
tell_command(prompt="Build a REST API", name="api-plan")
context.plan_service_mock = plan_service
context.mock_project = mock_project
@then("the plan tell_command programmatic wrapper should call create_plan")
def step_assert_tell_command_calls_create(context):
"""Assert tell_command called create_plan with correct args."""
context.plan_service_mock.create_plan.assert_called_once_with(
project=context.mock_project, prompt="Build a REST API", name="api-plan"
)
@when("I invoke the plan tell_command programmatic wrapper with no project")
def step_invoke_tell_command_wrapper_no_project(context):
"""Call tell_command when no project is available."""
from cleveragents.cli.commands.plan import tell_command
container, _, _ = _make_mock_container(project=None)
context.call_exception = None
with patch(
"cleveragents.application.container.get_container", return_value=container
):
try:
tell_command(prompt="Something")
except Exception as exc:
context.call_exception = exc
@then("the plan tell_command programmatic wrapper should raise CleverAgentsError")
def step_assert_tell_command_raises(context):
"""Assert tell_command raised CleverAgentsError."""
assert isinstance(context.call_exception, CleverAgentsError), (
f"Expected CleverAgentsError but got: {type(context.call_exception)}"
)
assert "No project found" in str(context.call_exception)
# ---------------------------------------------------------------------------
# build_command programmatic wrapper
# ---------------------------------------------------------------------------
@when("I invoke the plan build_command programmatic wrapper with valid project")
def step_invoke_build_command_wrapper(context):
"""Call build_command with a valid project."""
from cleveragents.cli.commands.plan import build_command
mock_project = MagicMock()
mock_change = MagicMock()
mock_change.file_path = "file.py"
mock_change.operation = "create"
container, plan_service, _ = _make_mock_container(project=mock_project)
plan_service.build_plan.return_value = [mock_change]
with patch(
"cleveragents.application.container.get_container", return_value=container
):
context.build_result = build_command()
context.plan_service_mock = plan_service
context.mock_project = mock_project
@then("the plan build_command programmatic wrapper should return the changes list")
def step_assert_build_command_returns_changes(context):
"""Assert build_command returned the changes list."""
assert len(context.build_result) == 1
context.plan_service_mock.build_plan.assert_called_once()
@when("I invoke the plan build_command programmatic wrapper with no project")
def step_invoke_build_command_wrapper_no_project(context):
"""Call build_command when no project is available."""
from cleveragents.cli.commands.plan import build_command
container, _, _ = _make_mock_container(project=None)
context.call_exception = None
with patch(
"cleveragents.application.container.get_container", return_value=container
):
try:
build_command()
except Exception as exc:
context.call_exception = exc
@then("the plan build_command programmatic wrapper should raise CleverAgentsError")
def step_assert_build_command_raises(context):
"""Assert build_command raised CleverAgentsError."""
assert isinstance(context.call_exception, CleverAgentsError)
assert "No project found" in str(context.call_exception)
# ---------------------------------------------------------------------------
# apply_command programmatic wrapper
# ---------------------------------------------------------------------------
@when("I invoke the plan apply_command programmatic wrapper with valid project")
def step_invoke_apply_command_wrapper(context):
"""Call apply_command with a valid project."""
from cleveragents.cli.commands.plan import apply_command
mock_project = MagicMock()
container, plan_service, _ = _make_mock_container(project=mock_project)
plan_service.apply_changes.return_value = 5
with patch(
"cleveragents.application.container.get_container", return_value=container
):
context.apply_result = apply_command()
context.plan_service_mock = plan_service
context.mock_project = mock_project
@then("the plan apply_command programmatic wrapper should return applied count")
def step_assert_apply_command_returns_count(context):
"""Assert apply_command returned the applied count."""
assert context.apply_result == 5
context.plan_service_mock.apply_changes.assert_called_once_with(
project=context.mock_project
)
# ---------------------------------------------------------------------------
# new_command programmatic wrapper
# ---------------------------------------------------------------------------
@when("I invoke the plan new_command programmatic wrapper with valid project")
def step_invoke_new_command_wrapper(context):
"""Call new_command with a valid project."""
from cleveragents.cli.commands.plan import new_command
mock_project = MagicMock()
container, plan_service, _ = _make_mock_container(project=mock_project)
with patch(
"cleveragents.application.container.get_container", return_value=container
):
new_command(name="my-new-plan")
context.plan_service_mock = plan_service
context.mock_project = mock_project
@then("the plan new_command programmatic wrapper should call new_plan")
def step_assert_new_command_calls_new_plan(context):
"""Assert new_command called new_plan with correct args."""
context.plan_service_mock.new_plan.assert_called_once_with(
project=context.mock_project, name="my-new-plan"
)
# ---------------------------------------------------------------------------
# current_command programmatic wrapper
# ---------------------------------------------------------------------------
@when("I invoke the plan current_command programmatic wrapper with valid project")
def step_invoke_current_command_wrapper(context):
"""Call current_command with a valid project."""
from cleveragents.cli.commands.plan import current_command
mock_project = MagicMock()
mock_plan = MagicMock()
mock_plan.name = "current-plan"
container, plan_service, _ = _make_mock_container(project=mock_project)
plan_service.get_current_plan.return_value = mock_plan
with patch(
"cleveragents.application.container.get_container", return_value=container
):
context.current_result = current_command()
context.plan_service_mock = plan_service
context.mock_project = mock_project
context.expected_plan = mock_plan
@then("the plan current_command programmatic wrapper should return the plan")
def step_assert_current_command_returns_plan(context):
"""Assert current_command returned the current plan."""
assert context.current_result is context.expected_plan
context.plan_service_mock.get_current_plan.assert_called_once_with(
project=context.mock_project
)
# ---------------------------------------------------------------------------
# list_command programmatic wrapper
# ---------------------------------------------------------------------------
@when("I invoke the plan list_command programmatic wrapper with valid project")
def step_invoke_list_command_wrapper(context):
"""Call list_command with a valid project."""
from cleveragents.cli.commands.plan import list_command
mock_project = MagicMock()
mock_plan_a = MagicMock()
mock_plan_a.name = "plan-a"
mock_plan_b = MagicMock()
mock_plan_b.name = "plan-b"
container, plan_service, _ = _make_mock_container(project=mock_project)
plan_service.list_plans.return_value = [mock_plan_a, mock_plan_b]
with patch(
"cleveragents.application.container.get_container", return_value=container
):
context.list_result = list_command()
context.plan_service_mock = plan_service
@then("the plan list_command programmatic wrapper should return the plans list")
def step_assert_list_command_returns_plans(context):
"""Assert list_command returned the plans list."""
assert len(context.list_result) == 2
context.plan_service_mock.list_plans.assert_called_once()
# ---------------------------------------------------------------------------
# cd_command programmatic wrapper
# ---------------------------------------------------------------------------
@when("I invoke the plan cd_command programmatic wrapper with valid project")
def step_invoke_cd_command_wrapper(context):
"""Call cd_command with a valid project."""
from cleveragents.cli.commands.plan import cd_command
mock_project = MagicMock()
container, plan_service, _ = _make_mock_container(project=mock_project)
with patch(
"cleveragents.application.container.get_container", return_value=container
):
cd_command(name="target-plan")
context.plan_service_mock = plan_service
context.mock_project = mock_project
@then("the plan cd_command programmatic wrapper should call switch_to_plan")
def step_assert_cd_command_calls_switch(context):
"""Assert cd_command called switch_to_plan with correct args."""
context.plan_service_mock.switch_to_plan.assert_called_once_with(
project=context.mock_project, name="target-plan"
)
# ---------------------------------------------------------------------------
# continue_command programmatic wrapper
# ---------------------------------------------------------------------------
@when(
"I invoke the plan continue_command programmatic wrapper with prompt and valid project"
)
def step_invoke_continue_command_wrapper_with_prompt(context):
"""Call continue_command with prompt and valid project."""
from cleveragents.cli.commands.plan import continue_command
mock_project = MagicMock()
container, plan_service, _ = _make_mock_container(project=mock_project)
with patch(
"cleveragents.application.container.get_container", return_value=container
):
continue_command(prompt="Add authentication")
context.plan_service_mock = plan_service
context.mock_project = mock_project
@then("the plan continue_command programmatic wrapper should call continue_plan")
def step_assert_continue_command_calls_continue(context):
"""Assert continue_command called continue_plan with the prompt."""
context.plan_service_mock.continue_plan.assert_called_once_with(
project=context.mock_project, prompt="Add authentication"
)
@when(
"I invoke the plan continue_command programmatic wrapper without prompt and no current plan"
)
def step_invoke_continue_command_wrapper_no_prompt_no_plan(context):
"""Call continue_command without prompt when no plan exists."""
from cleveragents.cli.commands.plan import continue_command
mock_project = MagicMock()
container, plan_service, _ = _make_mock_container(project=mock_project)
plan_service.get_current_plan.return_value = None
context.call_exception = None
with patch(
"cleveragents.application.container.get_container", return_value=container
):
try:
continue_command(prompt=None)
except Exception as exc:
context.call_exception = exc
@then(
"the plan continue_command programmatic wrapper should raise CleverAgentsError for no plan"
)
def step_assert_continue_command_raises_no_plan(context):
"""Assert continue_command raised CleverAgentsError for missing plan."""
assert isinstance(context.call_exception, CleverAgentsError), (
f"Expected CleverAgentsError but got: {type(context.call_exception)}"
)
assert "No current plan to continue" in str(context.call_exception)