Files
temp/features/steps/action_cli_edge_cases_coverage_steps.py
T

559 lines
21 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, 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(
*,
action_id: str = "01ARZ3NDEKTSV4RRFFQ69G5FAV",
name: str = "local/test-action",
state: ActionState = ActionState.DRAFT,
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,
short_description: str | None = "Test action",
long_description: str | None = "A test action for testing",
) -> Action:
"""Create an Action instance for edge case testing."""
return Action(
action_id=action_id,
namespaced_name=NamespacedName.parse(name),
short_description=short_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."""
context.edge_action = _make_edge_action(short_description=None)
# ---------------------------------------------------------------------------
# _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}"
)
@then('the action edge case output should not contain "Description:"')
def step_edge_output_no_description_line(context: Context) -> None:
"""Verify the output does NOT contain the Description label."""
assert "Description:" not in context.print_output, (
f"Expected no 'Description:' 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",
"--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",
"--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",
"--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",
"--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(
action_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
name="local/action-a",
state=ActionState.AVAILABLE,
),
_make_edge_action(
action_id="01ARZ3NDEKTSV4RRFFQ69G5FAW",
name="local/action-b",
state=ActionState.DRAFT,
),
]
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 draft state")
def step_edge_list_called_with_draft(context: Context) -> None:
"""Verify list_actions was called with ActionState.DRAFT."""
call_kwargs = context.mock_service.list_actions.call_args[1]
assert call_kwargs["state"] == ActionState.DRAFT
@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 only findable by name")
def step_edge_show_by_name_only(context: Context) -> None:
"""Set up service so get_action raises NotFoundError but get_action_by_name works."""
context.edge_action = _make_edge_action(name="local/name-only-action")
context.mock_service.get_action.side_effect = NotFoundError(
resource_type="action", resource_id="local/name-only-action"
)
context.mock_service.get_action_by_name.return_value = context.edge_action
@given("an action edge case where both lookups fail")
def step_edge_show_both_fail(context: Context) -> None:
"""Set up service so both get_action and get_action_by_name raise NotFoundError."""
context.mock_service.get_action.side_effect = NotFoundError(
resource_type="action", resource_id="nonexistent"
)
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(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 tried get_action first")
def step_edge_service_tried_get_action(context: Context) -> None:
"""Verify get_action was called (the first lookup attempt)."""
context.mock_service.get_action.assert_called_once()
@then("the action edge case service should have fallen back to get_action_by_name")
def step_edge_service_fell_back_to_name(context: Context) -> None:
"""Verify get_action_by_name was called (the fallback)."""
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 draft action only findable by name")
def step_edge_available_by_name_only(context: Context) -> None:
"""Set up a draft action that can only be found by name."""
context.edge_action = _make_edge_action(
name="local/name-only-draft", state=ActionState.DRAFT
)
context.mock_service.get_action.side_effect = NotFoundError(
resource_type="action", resource_id="local/name-only-draft"
)
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-draft", 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.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])
@when('I run action edge case available with id "{action_id}"')
def step_edge_available_by_id(context: Context, action_id: str) -> None:
"""Run the available command with an action ID."""
context.result = context.runner.invoke(action_app, ["available", action_id])
@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 available action only findable by name")
def step_edge_archive_by_name_only(context: Context) -> None:
"""Set up an available action that can only be found by name."""
context.edge_action = _make_edge_action(
name="local/name-only-available", state=ActionState.AVAILABLE
)
context.mock_service.get_action.side_effect = NotFoundError(
resource_type="action", resource_id="local/name-only-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 both lookups fail for archive")
def step_edge_archive_both_fail(context: Context) -> None:
"""Set up service so both get_action and get_action_by_name fail."""
context.mock_service.get_action.side_effect = NotFoundError(
resource_type="action", resource_id="nonexistent"
)
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()