Files
temp/features/steps/action_cli_edge_cases_coverage_steps.py

529 lines
19 KiB
Python

"""Step definitions for action CLI edge cases coverage."""
from __future__ import annotations
from datetime import datetime
from io import StringIO
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from rich.console import Console
from typer.testing import CliRunner
from cleveragents.cli.commands.action import _print_action
from cleveragents.cli.commands.action import app as action_app
from cleveragents.core.exceptions import (
BusinessRuleViolation,
NotFoundError,
)
from cleveragents.domain.models.core.action import (
Action,
ActionArgument,
ActionState,
ArgumentRequirement,
ArgumentType,
)
from cleveragents.domain.models.core.plan import NamespacedName
runner = CliRunner()
def _make_edge_action(
*,
name: str = "local/test-action",
state: ActionState = ActionState.AVAILABLE,
definition_of_done: str = "Test passes",
strategy_actor: str = "openai/gpt-4",
execution_actor: str = "openai/gpt-4",
arguments: list[ActionArgument] | None = None,
reusable: bool = True,
read_only: bool = False,
created_by: str | None = None,
description: str = "Test action",
long_description: str | None = "A test action for testing",
) -> Action:
"""Create an Action instance for edge case testing."""
return Action(
namespaced_name=NamespacedName.parse(name),
description=description,
long_description=long_description,
definition_of_done=definition_of_done,
strategy_actor=strategy_actor,
execution_actor=execution_actor,
arguments=arguments or [],
reusable=reusable,
read_only=read_only,
created_by=created_by,
state=state,
created_at=datetime.now(),
updated_at=datetime.now(),
)
# ---------------------------------------------------------------------------
# Background steps
# ---------------------------------------------------------------------------
@given("an action edge case CLI runner")
def step_action_edge_case_cli_runner(context: Context) -> None:
"""Set up the CLI runner for edge case testing."""
context.runner = CliRunner()
@given("an action edge case mocked lifecycle service")
def step_action_edge_case_mocked_service(context: Context) -> None:
"""Set up a mock PlanLifecycleService for edge cases."""
context.mock_service = MagicMock()
context.service_patcher = patch(
"cleveragents.cli.commands.action._get_lifecycle_service",
return_value=context.mock_service,
)
context.service_patcher.start()
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(context.service_patcher.stop)
# ---------------------------------------------------------------------------
# _print_action: Given steps
# ---------------------------------------------------------------------------
@given("an action edge case action with no arguments")
def step_edge_action_no_arguments(context: Context) -> None:
"""Create an action with an empty arguments list."""
context.edge_action = _make_edge_action(arguments=[])
@given("an action edge case action with multiple arguments")
def step_edge_action_multiple_arguments(context: Context) -> None:
"""Create an action with multiple arguments, some with descriptions."""
args = [
ActionArgument(
name="target_coverage",
arg_type=ArgumentType.INTEGER,
requirement=ArgumentRequirement.REQUIRED,
description="Target coverage percentage",
),
ActionArgument(
name="test_framework",
arg_type=ArgumentType.STRING,
requirement=ArgumentRequirement.OPTIONAL,
description="",
),
]
context.edge_action = _make_edge_action(arguments=args)
@given("an action edge case action without short description")
def step_edge_action_no_short_description(context: Context) -> None:
"""Create an action without a short_description (use minimal description)."""
context.edge_action = _make_edge_action(description="Minimal")
# ---------------------------------------------------------------------------
# _print_action: When steps
# ---------------------------------------------------------------------------
@when("I call action edge case print action")
def step_call_edge_print_action(context: Context) -> None:
"""Call _print_action and capture the console output."""
buf = StringIO()
test_console = Console(file=buf, force_terminal=False, width=200)
# Temporarily replace the module-level console so _print_action writes to buf
with patch("cleveragents.cli.commands.action.console", test_console):
_print_action(context.edge_action)
context.print_output = buf.getvalue()
# ---------------------------------------------------------------------------
# _print_action: Then steps
# ---------------------------------------------------------------------------
@then('the action edge case output should contain "(none)"')
def step_edge_output_contains_none(context: Context) -> None:
"""Verify output contains the (none) indicator for empty arguments."""
assert "(none)" in context.print_output, (
f"Expected '(none)' in output but got:\n{context.print_output}"
)
@then('the action edge case output should contain argument "{arg_name}"')
def step_edge_output_contains_argument(context: Context, arg_name: str) -> None:
"""Verify the output contains the named argument."""
assert arg_name in context.print_output, (
f"Expected '{arg_name}' in output but got:\n{context.print_output}"
)
@then('the action edge case output should contain "{text}"')
def step_edge_output_contains_text(context: Context, text: str) -> None:
"""Verify the output contains the given text."""
assert text in context.print_output, (
f"Expected '{text}' in output but got:\n{context.print_output}"
)
# ---------------------------------------------------------------------------
# Create command: When steps
# ---------------------------------------------------------------------------
@when("I run action edge case create with multiple args")
def step_edge_create_multiple_args(context: Context) -> None:
"""Run create with two --arg flags."""
created = _make_edge_action(
name="local/multi-arg",
arguments=[
ActionArgument(
name="target",
arg_type=ArgumentType.INTEGER,
requirement=ArgumentRequirement.REQUIRED,
description="Target value",
),
ActionArgument(
name="mode",
arg_type=ArgumentType.STRING,
requirement=ArgumentRequirement.OPTIONAL,
description="Execution mode",
),
],
)
context.mock_service.create_action.return_value = created
context.result = context.runner.invoke(
action_app,
[
"create",
"local/multi-arg",
"--strategy-actor",
"openai/gpt-4",
"--execution-actor",
"openai/gpt-4",
"--definition-of-done",
"Done",
"--description",
"Multi arg action",
"--arg",
"target:int:required:Target value",
"--arg",
"mode:str:optional:Execution mode",
],
)
@when("I run action edge case create with read-only flag")
def step_edge_create_read_only(context: Context) -> None:
"""Run create with --read-only flag."""
created = _make_edge_action(name="local/readonly", read_only=True)
context.mock_service.create_action.return_value = created
context.result = context.runner.invoke(
action_app,
[
"create",
"local/readonly",
"--strategy-actor",
"openai/gpt-4",
"--execution-actor",
"openai/gpt-4",
"--definition-of-done",
"Done",
"--description",
"Read-only action",
"--read-only",
],
)
@when("I run action edge case create with multiple tags")
def step_edge_create_tags(context: Context) -> None:
"""Run create with multiple --tag flags."""
created = _make_edge_action(name="local/tagged")
context.mock_service.create_action.return_value = created
context.result = context.runner.invoke(
action_app,
[
"create",
"local/tagged",
"--strategy-actor",
"openai/gpt-4",
"--execution-actor",
"openai/gpt-4",
"--definition-of-done",
"Done",
"--description",
"Tagged action",
"--tag",
"ci",
"--tag",
"coverage",
],
)
@when("I run action edge case create with long description")
def step_edge_create_long_description(context: Context) -> None:
"""Run create with --long-description."""
created = _make_edge_action(
name="local/longdesc",
long_description="Detailed documentation text",
)
context.mock_service.create_action.return_value = created
context.result = context.runner.invoke(
action_app,
[
"create",
"local/longdesc",
"--strategy-actor",
"openai/gpt-4",
"--execution-actor",
"openai/gpt-4",
"--definition-of-done",
"Done",
"--description",
"Long desc action",
"--long-description",
"Detailed documentation text",
],
)
# ---------------------------------------------------------------------------
# Create command: Then steps
# ---------------------------------------------------------------------------
@then("the action edge case create should succeed")
def step_edge_create_succeeds(context: Context) -> None:
"""Verify the create command succeeded."""
assert context.result.exit_code == 0, (
f"CLI failed with exit code {context.result.exit_code}: {context.result.output}"
)
context.mock_service.create_action.assert_called_once()
@then("the action edge case service should receive 2 arguments")
def step_edge_service_receives_two_args(context: Context) -> None:
"""Verify the service was called with exactly 2 arguments."""
call_kwargs = context.mock_service.create_action.call_args[1]
assert len(call_kwargs["arguments"]) == 2, (
f"Expected 2 arguments, got {len(call_kwargs['arguments'])}"
)
@then("the action edge case service should receive read_only true")
def step_edge_service_receives_read_only(context: Context) -> None:
"""Verify the service was called with read_only=True."""
call_kwargs = context.mock_service.create_action.call_args[1]
assert call_kwargs["read_only"] is True
@then('the action edge case service should receive tags "ci" and "coverage"')
def step_edge_service_receives_tags(context: Context) -> None:
"""Verify the service was called with both tags."""
call_kwargs = context.mock_service.create_action.call_args[1]
assert "ci" in call_kwargs["tags"]
assert "coverage" in call_kwargs["tags"]
@then('the action edge case service should receive long description "{desc}"')
def step_edge_service_receives_long_desc(context: Context, desc: str) -> None:
"""Verify the service was called with the given long_description."""
call_kwargs = context.mock_service.create_action.call_args[1]
assert call_kwargs["long_description"] == desc
# ---------------------------------------------------------------------------
# List command: Given / When / Then
# ---------------------------------------------------------------------------
@given("there are action edge case existing actions")
def step_edge_existing_actions(context: Context) -> None:
"""Set up actions for list testing."""
context.edge_actions = [
_make_edge_action(
name="local/action-a",
state=ActionState.AVAILABLE,
),
_make_edge_action(
name="local/action-b",
state=ActionState.AVAILABLE,
),
]
context.mock_service.list_actions.return_value = context.edge_actions
@when('I run action edge case list with state "{state_value}"')
def step_edge_list_with_state(context: Context, state_value: str) -> None:
"""Run the list command with a --state filter."""
context.result = context.runner.invoke(action_app, ["list", "--state", state_value])
context.edge_state_value = state_value
@then("the action edge case list should succeed")
def step_edge_list_succeeds(context: Context) -> None:
"""Verify list command succeeded."""
assert context.result.exit_code == 0, f"CLI failed: {context.result.output}"
@then("the action edge case service list should be called with available state")
def step_edge_list_called_with_available(context: Context) -> None:
"""Verify list_actions was called with ActionState.AVAILABLE."""
call_kwargs = context.mock_service.list_actions.call_args[1]
assert call_kwargs["state"] == ActionState.AVAILABLE
@then("the action edge case service list should be called with archived state")
def step_edge_list_called_with_archived(context: Context) -> None:
"""Verify list_actions was called with ActionState.ARCHIVED."""
call_kwargs = context.mock_service.list_actions.call_args[1]
assert call_kwargs["state"] == ActionState.ARCHIVED
# ---------------------------------------------------------------------------
# Show command: Given / When / Then
# ---------------------------------------------------------------------------
@given("an action edge case that is findable by name")
def step_edge_show_by_name(context: Context) -> None:
"""Set up service so get_action_by_name returns the action."""
context.edge_action = _make_edge_action(name="local/name-only-action")
context.mock_service.get_action_by_name.return_value = context.edge_action
@given("an action edge case where name lookup fails")
def step_edge_show_name_fails(context: Context) -> None:
"""Set up service so get_action_by_name raises NotFoundError."""
context.mock_service.get_action_by_name.side_effect = NotFoundError(
resource_type="action", resource_id="nonexistent"
)
@when('I run action edge case show with name "{name}"')
def step_edge_show_by_name_cmd(context: Context, name: str) -> None:
"""Run the show command with the given name."""
context.result = context.runner.invoke(action_app, ["show", name])
@then("the action edge case show should succeed")
def step_edge_show_succeeds(context: Context) -> None:
"""Verify show command succeeded."""
assert context.result.exit_code == 0, f"CLI failed: {context.result.output}"
@then("the action edge case service should have used get_action_by_name")
def step_edge_service_used_get_action_by_name(context: Context) -> None:
"""Verify get_action_by_name was called."""
context.mock_service.get_action_by_name.assert_called_once()
@then("the action edge case show should abort with not found")
def step_edge_show_aborts_not_found(context: Context) -> None:
"""Verify show aborted with a not found message."""
assert context.result.exit_code != 0
assert "not found" in context.result.output.lower()
# ---------------------------------------------------------------------------
# Available command: Given / When / Then
# ---------------------------------------------------------------------------
@given("an action edge case action findable by name for available")
def step_edge_available_by_name_setup(context: Context) -> None:
"""Set up an action that can be found by name for the available command."""
context.edge_action = _make_edge_action(
name="local/name-only-action", state=ActionState.AVAILABLE
)
context.mock_service.get_action_by_name.return_value = context.edge_action
context.mock_service.make_action_available.return_value = _make_edge_action(
name="local/name-only-action", state=ActionState.AVAILABLE
)
@given("an action edge case available action that violates business rule")
def step_edge_available_business_rule(context: Context) -> None:
"""Set up an action whose make_available raises BusinessRuleViolation."""
context.edge_action = _make_edge_action(state=ActionState.AVAILABLE)
context.mock_service.get_action_by_name.return_value = context.edge_action
context.mock_service.make_action_available.side_effect = BusinessRuleViolation(
"Action is already available"
)
@when('I run action edge case available with name "{name}"')
def step_edge_available_by_name(context: Context, name: str) -> None:
"""Run the available command with a namespaced name."""
context.result = context.runner.invoke(action_app, ["available", name])
@then("the action edge case available should succeed")
def step_edge_available_succeeds(context: Context) -> None:
"""Verify available command succeeded."""
assert context.result.exit_code == 0, f"CLI failed: {context.result.output}"
context.mock_service.make_action_available.assert_called_once()
@then("the action edge case available should abort with business rule message")
def step_edge_available_business_rule_abort(context: Context) -> None:
"""Verify available aborted with a business rule violation message."""
assert context.result.exit_code != 0
assert "Cannot make available" in context.result.output
# ---------------------------------------------------------------------------
# Archive command: Given / When / Then
# ---------------------------------------------------------------------------
@given("an action edge case action findable by name for archive")
def step_edge_archive_by_name_setup(context: Context) -> None:
"""Set up an available action that can be found by name for archive."""
context.edge_action = _make_edge_action(
name="local/name-only-available", state=ActionState.AVAILABLE
)
context.mock_service.get_action_by_name.return_value = context.edge_action
context.mock_service.archive_action.return_value = _make_edge_action(
name="local/name-only-available", state=ActionState.ARCHIVED
)
@given("an action edge case where name lookup fails for archive")
def step_edge_archive_name_fails(context: Context) -> None:
"""Set up service so get_action_by_name fails for archive."""
context.mock_service.get_action_by_name.side_effect = NotFoundError(
resource_type="action", resource_id="nonexistent"
)
@when('I run action edge case archive with name "{name}"')
def step_edge_archive_by_name(context: Context, name: str) -> None:
"""Run the archive command with a namespaced name."""
context.result = context.runner.invoke(action_app, ["archive", name])
@then("the action edge case archive should succeed")
def step_edge_archive_succeeds(context: Context) -> None:
"""Verify archive command succeeded."""
assert context.result.exit_code == 0, f"CLI failed: {context.result.output}"
context.mock_service.archive_action.assert_called_once()
@then("the action edge case archive should abort with not found")
def step_edge_archive_aborts_not_found(context: Context) -> None:
"""Verify archive aborted with a not found message."""
assert context.result.exit_code != 0
assert "not found" in context.result.output.lower()