Files
cleveragents-core/robot/helper_action_cli_spec.py
T
Jeff (CTO) d98666651f
CI / lint (pull_request) Successful in 15s
CI / typecheck (pull_request) Successful in 27s
CI / security (pull_request) Successful in 22s
CI / quality (pull_request) Successful in 15s
CI / integration_tests (pull_request) Successful in 4m24s
CI / build (pull_request) Successful in 16s
CI / unit_tests (pull_request) Successful in 9m27s
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / security (push) Waiting to run
CI / quality (push) Waiting to run
CI / unit_tests (push) Waiting to run
CI / integration_tests (push) Waiting to run
CI / coverage (push) Blocked by required conditions
CI / build (push) Waiting to run
CI / docker (push) Blocked by required conditions
CI / coverage (pull_request) Successful in 6m55s
CI / docker (pull_request) Successful in 39s
feat(cli): align action commands to spec
- Enforce config-only 'action create --config <file>' (remove legacy
  inline flags: --name, --strategy-actor, --execution-actor,
  --definition-of-done, --arg)
- Load config via ActionConfigSchema.from_yaml_file() then
  Action.from_config() with clear validation error surfacing
- Remove 'action available' subcommand (no draft state; actions are
  available by default)
- Add REGEX positional arg to 'action list' for name filtering
- Add Short Name and Definition of Done summary to all CLI outputs
- Add 'action list --namespace/-n' and '--state/-s' filters
- Update existing tests to match new config-only create flow
- Add features/action_cli_spec_alignment.feature (19 scenarios)
- Add robot/action_cli_spec.robot smoke tests
- Add benchmarks/action_cli_bench.py ASV benchmarks
- Add docs/reference/action_cli.md
2026-02-14 12:16:39 -05:00

168 lines
5.1 KiB
Python

"""Helper script for action_cli_spec.robot smoke tests.
Each subcommand is a self-contained check that prints a sentinel on success.
"""
from __future__ import annotations
import os
import sys
import tempfile
from datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock, patch
# Ensure local source tree is importable
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from typer.testing import CliRunner # noqa: E402
from cleveragents.cli.commands.action import app as action_app # noqa: E402
from cleveragents.domain.models.core.action import Action, ActionState # noqa: E402
from cleveragents.domain.models.core.plan import NamespacedName # noqa: E402
runner = CliRunner()
_VALID_YAML = """\
name: local/smoke-action
description: Smoke test action
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: All smoke tests pass
"""
def _mock_action(name: str = "local/smoke-action") -> Action:
return Action(
namespaced_name=NamespacedName.parse(name),
description="Smoke test action",
definition_of_done="All smoke tests pass",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
state=ActionState.AVAILABLE,
created_at=datetime.now(),
updated_at=datetime.now(),
)
def _write_yaml(content: str) -> str:
fd, path = tempfile.mkstemp(suffix=".yaml")
with os.fdopen(fd, "w") as fh:
fh.write(content)
return path
# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------
def create_requires_config() -> None:
"""Verify that create without --config fails."""
result = runner.invoke(action_app, ["create"])
if result.exit_code != 0:
print("action-cli-create-requires-config-ok")
else:
print("FAIL: create without --config should fail", file=sys.stderr)
sys.exit(1)
def create_rejects_name() -> None:
"""Verify that --name flag is rejected."""
result = runner.invoke(action_app, ["create", "--name", "local/test"])
if result.exit_code != 0:
print("action-cli-legacy-flag-rejected")
else:
print("FAIL: --name should be rejected", file=sys.stderr)
sys.exit(1)
def available_removed() -> None:
"""Verify that available subcommand no longer exists."""
result = runner.invoke(action_app, ["available", "local/test"])
if result.exit_code != 0:
print("action-cli-available-removed-ok")
else:
print("FAIL: available should not exist", file=sys.stderr)
sys.exit(1)
def list_filters() -> None:
"""Verify that list accepts --namespace and --state."""
mock_service = MagicMock()
mock_service.list_actions.return_value = [_mock_action()]
with patch(
"cleveragents.cli.commands.action._get_lifecycle_service",
return_value=mock_service,
):
r1 = runner.invoke(action_app, ["list", "--namespace", "local"])
r2 = runner.invoke(action_app, ["list", "--state", "available"])
if r1.exit_code == 0 and r2.exit_code == 0:
print("action-cli-list-filters-ok")
else:
print("FAIL: list filters", file=sys.stderr)
sys.exit(1)
def show_name() -> None:
"""Verify show accepts a namespaced name."""
mock_service = MagicMock()
mock_service.get_action_by_name.return_value = _mock_action()
with patch(
"cleveragents.cli.commands.action._get_lifecycle_service",
return_value=mock_service,
):
result = runner.invoke(action_app, ["show", "local/smoke-action"])
if result.exit_code == 0:
print("action-cli-show-name-ok")
else:
print(f"FAIL: show returned {result.exit_code}", file=sys.stderr)
sys.exit(1)
def archive_name() -> None:
"""Verify archive accepts a namespaced name."""
mock_service = MagicMock()
mock_service.get_action_by_name.return_value = _mock_action()
mock_service.archive_action.return_value = _mock_action()
with patch(
"cleveragents.cli.commands.action._get_lifecycle_service",
return_value=mock_service,
):
result = runner.invoke(action_app, ["archive", "local/smoke-action"])
if result.exit_code == 0:
print("action-cli-archive-name-ok")
else:
print(f"FAIL: archive returned {result.exit_code}", file=sys.stderr)
sys.exit(1)
# ---------------------------------------------------------------------------
# Main dispatcher
# ---------------------------------------------------------------------------
_COMMANDS = {
"create-requires-config": create_requires_config,
"create-rejects-name": create_rejects_name,
"available-removed": available_removed,
"list-filters": list_filters,
"show-name": show_name,
"archive-name": archive_name,
}
def main() -> None:
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr)
sys.exit(2)
_COMMANDS[sys.argv[1]]()
if __name__ == "__main__":
main()