forked from HAL9000/cleveragents-core
434cad5d1a
- Normalize import ordering and remove unused imports - Replace non-ASCII dashes in comments with ASCII hyphens - Clean up unused assignments and use contextlib.suppress - Adjust unused variable name in plan_cli_streaming steps
577 lines
21 KiB
Python
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 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)
|