"""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)