Files
AIPass/tests/test_drone_cli.py
T
AIOSAIandClaude Opus 4.6 866b0596aa feat(drone): rename routing → drone and add CLI entry point
BREAKING: aipass.routing renamed to aipass.drone. Routing IS drone —
the command routing engine IS the CLI command center. No reason to
keep them separate.

Adds `drone` CLI entry point:
  drone systems          — list registered branches
  drone @branch command  — route command to branch
  drone @branch --help   — get branch help
  drone --version        — show version

406 tests pass (20 new CLI tests). All imports updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:14:32 -08:00

274 lines
9.7 KiB
Python

"""
Tests for the drone CLI (aipass.drone.cli).
Covers:
- drone / drone --help → shows help text
- drone --version → shows version
- drone systems → lists branches
- drone @branch command → routes correctly
- drone @branch --help → shows branch help
- unknown commands → error message + exit 1
- no args → shows help
"""
from __future__ import annotations
from unittest.mock import patch
import aipass
from aipass.drone.cli import main
from aipass.drone import CommandResult, HelpResult
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _run_cli(*args: str) -> tuple[int, str, str]:
"""
Call main() with the given argv, capturing stdout/stderr and exit code.
Returns:
(exit_code, stdout_text, stderr_text)
"""
stdout_lines: list[str] = []
stderr_lines: list[str] = []
class _Cap:
def __init__(self, buf: list[str]) -> None:
self._buf = buf
def write(self, s: str) -> int:
self._buf.append(s)
return len(s)
def flush(self) -> None:
pass
exit_code = 0
def _exit(code: int = 0) -> None:
nonlocal exit_code
exit_code = code
raise SystemExit(code)
with (
patch("sys.argv", ["drone", *args]),
patch("sys.stdout", _Cap(stdout_lines)),
patch("sys.stderr", _Cap(stderr_lines)),
patch("sys.exit", side_effect=_exit),
):
try:
main()
except SystemExit:
pass
return exit_code, "".join(stdout_lines), "".join(stderr_lines)
# ---------------------------------------------------------------------------
# Help / version
# ---------------------------------------------------------------------------
class TestHelpAndVersion:
"""drone --help and drone --version."""
def test_no_args_shows_help(self):
"""drone with no args prints help and exits 0."""
code, out, _ = _run_cli()
assert code == 0
assert "drone" in out
assert "Usage" in out or "usage" in out.lower() or "@branch" in out
def test_help_flag_shows_help(self):
"""drone --help prints help and exits 0."""
code, out, _ = _run_cli("--help")
assert code == 0
assert "drone" in out
def test_version_flag_shows_version(self):
"""drone --version prints the aipass version and exits 0."""
code, out, _ = _run_cli("--version")
assert code == 0
assert aipass.__version__ in out
# ---------------------------------------------------------------------------
# systems
# ---------------------------------------------------------------------------
class TestSystems:
"""drone systems — list registered branches."""
def test_systems_lists_branches(self):
"""drone systems calls list_branches and prints them."""
with patch("aipass.drone.cli.list_branches", return_value=["@flow", "@devpulse"]):
code, out, _ = _run_cli("systems")
assert code == 0
assert "@flow" in out
assert "@devpulse" in out
def test_systems_empty_registry(self):
"""drone systems with no branches prints a 'no branches' message."""
with patch("aipass.drone.cli.list_branches", return_value=[]):
code, out, _ = _run_cli("systems")
assert code == 0
assert "No branches" in out or "no branches" in out.lower() or out.strip() != ""
def test_systems_shows_count(self):
"""drone systems output mentions the number of branches."""
branches = ["@alpha", "@beta", "@gamma"]
with patch("aipass.drone.cli.list_branches", return_value=branches):
code, out, _ = _run_cli("systems")
assert code == 0
# Should show 3 branches somewhere in output
assert "3" in out or all(b in out for b in branches)
# ---------------------------------------------------------------------------
# @branch routing
# ---------------------------------------------------------------------------
class TestBranchRouting:
"""drone @branch command [args] — route commands."""
def _make_result(self, stdout="", stderr="", exit_code=0) -> CommandResult:
return CommandResult(stdout=stdout, stderr=stderr, exit_code=exit_code, branch="flow", command="status")
def test_routes_command_to_branch(self):
"""drone @flow status calls route_command with correct args."""
result = self._make_result(stdout="flow is running\n")
with patch("aipass.drone.cli.route_command", return_value=result) as mock_route:
code, out, _ = _run_cli("@flow", "status")
mock_route.assert_called_once_with("@flow", "status", args=None)
assert code == 0
assert "flow is running" in out
def test_routes_command_with_extra_args(self):
"""drone @flow run --verbose passes args correctly."""
result = self._make_result(stdout="verbose output\n")
with patch("aipass.drone.cli.route_command", return_value=result) as mock_route:
code, out, _ = _run_cli("@flow", "run", "--verbose")
mock_route.assert_called_once_with("@flow", "run", args=["--verbose"])
assert code == 0
def test_exits_with_command_exit_code(self):
"""drone exits with the same code as the routed command."""
result = self._make_result(exit_code=2, stderr="something failed\n")
with patch("aipass.drone.cli.route_command", return_value=result):
code, _, err = _run_cli("@flow", "status")
assert code == 2
assert "something failed" in err
def test_branch_not_found_exits_1(self):
"""BranchNotFoundError prints error and exits 1."""
from aipass.drone import BranchNotFoundError
with patch("aipass.drone.cli.route_command", side_effect=BranchNotFoundError("@ghost not found")):
code, _, err = _run_cli("@ghost", "status")
assert code == 1
assert "ghost" in err or "not found" in err.lower()
def test_command_execution_error_exits_1(self):
"""CommandExecutionError prints error and exits 1."""
from aipass.drone import CommandExecutionError
with patch("aipass.drone.cli.route_command", side_effect=CommandExecutionError("entry point missing")):
code, _, err = _run_cli("@flow", "status")
assert code == 1
assert "entry point missing" in err
def test_stdout_printed(self):
"""Command stdout is printed to stdout."""
result = self._make_result(stdout="hello world\n")
with patch("aipass.drone.cli.route_command", return_value=result):
code, out, _ = _run_cli("@flow", "status")
assert "hello world" in out
def test_stderr_printed_to_stderr(self):
"""Command stderr is printed to stderr."""
result = self._make_result(stderr="warning: something\n", exit_code=0)
with patch("aipass.drone.cli.route_command", return_value=result):
code, _, err = _run_cli("@flow", "status")
assert "warning: something" in err
# ---------------------------------------------------------------------------
# @branch --help
# ---------------------------------------------------------------------------
class TestBranchHelp:
"""drone @branch --help — show branch help."""
def test_branch_help_calls_get_help(self):
"""drone @flow --help calls get_help and prints text."""
help_result = HelpResult(branch="flow", command=None, text="flow help text\n", commands_found=["status"])
with patch("aipass.drone.cli.get_help", return_value=help_result) as mock_help:
code, out, _ = _run_cli("@flow", "--help")
mock_help.assert_called_once_with("@flow")
assert code == 0
assert "flow help text" in out
def test_branch_no_args_shows_help(self):
"""drone @flow with no command also calls get_help."""
help_result = HelpResult(branch="flow", command=None, text="flow help\n", commands_found=[])
with patch("aipass.drone.cli.get_help", return_value=help_result):
code, out, _ = _run_cli("@flow")
assert code == 0
assert "flow help" in out
def test_branch_not_found_on_help_exits_1(self):
"""BranchNotFoundError on --help exits 1."""
from aipass.drone import BranchNotFoundError
with patch("aipass.drone.cli.get_help", side_effect=BranchNotFoundError("@ghost not found")):
code, _, err = _run_cli("@ghost", "--help")
assert code == 1
def test_command_execution_error_on_help_exits_1(self):
"""CommandExecutionError on --help exits 1."""
from aipass.drone import CommandExecutionError
with patch("aipass.drone.cli.get_help", side_effect=CommandExecutionError("no entry")):
code, _, err = _run_cli("@flow", "--help")
assert code == 1
# ---------------------------------------------------------------------------
# Unknown commands
# ---------------------------------------------------------------------------
class TestUnknownCommands:
"""Unknown top-level commands show an error."""
def test_unknown_command_exits_1(self):
"""An unrecognised command exits with code 1."""
code, _, err = _run_cli("frobulate")
assert code == 1
def test_unknown_command_shows_error_message(self):
"""An unrecognised command prints an error to stderr."""
code, _, err = _run_cli("frobulate")
assert "frobulate" in err or "unknown" in err.lower()
def test_unknown_command_suggests_help(self):
"""Error message mentions --help."""
code, _, err = _run_cli("frobulate")
assert "--help" in err