forked from HAL9000/cleveragents-core
559 lines
21 KiB
Python
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()
|