Implements the internal AIPass navigation pattern: - `drone @module` (no args) → introspective: shows what's connected - `drone @module --help` → help: shows usage documentation Seedgo introspective lists all discovered plugins with descriptions. Modules that don't implement get_introspective() fall back to help. 442 tests pass. 5 new introspective tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
303 lines
10 KiB
Python
303 lines
10 KiB
Python
"""
|
|
Tests for the drone module routing system (aipass.drone.modules).
|
|
|
|
Covers:
|
|
- Module registry: list, lookup, register
|
|
- Module info retrieval
|
|
- Module command routing
|
|
- Module help retrieval
|
|
- CLI integration: drone systems shows modules, drone @seedgo routes correctly
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
from aipass.drone.cli import main
|
|
from aipass.drone.modules import (
|
|
ModuleInfo,
|
|
get_module_help,
|
|
get_module_info,
|
|
get_module_introspective,
|
|
is_module,
|
|
list_modules,
|
|
register_module,
|
|
route_module_command,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _run_cli(*args: str) -> tuple[int, str, str]:
|
|
"""Call main() with the given argv, capturing stdout/stderr and exit code."""
|
|
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)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Module registry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestModuleRegistry:
|
|
"""Test the internal module registry."""
|
|
|
|
def test_seedgo_is_registered(self):
|
|
"""seedgo is in the default module registry."""
|
|
assert is_module("seedgo")
|
|
|
|
def test_unknown_module_not_registered(self):
|
|
"""Unknown names return False."""
|
|
assert not is_module("nonexistent")
|
|
|
|
def test_list_modules_includes_seedgo(self):
|
|
"""list_modules includes seedgo."""
|
|
modules = list_modules()
|
|
assert "seedgo" in modules
|
|
|
|
def test_list_modules_returns_sorted(self):
|
|
"""list_modules returns sorted names."""
|
|
modules = list_modules()
|
|
assert modules == sorted(modules)
|
|
|
|
def test_register_module_adds_new(self):
|
|
"""register_module adds a new module."""
|
|
register_module("test_mod", "test.path.adapter")
|
|
assert is_module("test_mod")
|
|
# Clean up
|
|
from aipass.drone.modules import _MODULE_REGISTRY
|
|
del _MODULE_REGISTRY["test_mod"]
|
|
|
|
|
|
class TestModuleInfo:
|
|
"""Test module info retrieval."""
|
|
|
|
def test_seedgo_info_returns_module_info(self):
|
|
"""get_module_info for seedgo returns valid ModuleInfo."""
|
|
info = get_module_info("seedgo")
|
|
assert info is not None
|
|
assert isinstance(info, ModuleInfo)
|
|
assert info.name == "seedgo"
|
|
assert info.version == "1.0.0"
|
|
assert info.description != ""
|
|
|
|
def test_unknown_module_returns_none(self):
|
|
"""get_module_info for unknown module returns None."""
|
|
assert get_module_info("nonexistent") is None
|
|
|
|
def test_unimportable_module_returns_none(self):
|
|
"""get_module_info returns None if adapter can't be imported."""
|
|
register_module("broken", "nonexistent.module.path")
|
|
info = get_module_info("broken")
|
|
assert info is None
|
|
# Clean up
|
|
from aipass.drone.modules import _MODULE_REGISTRY
|
|
del _MODULE_REGISTRY["broken"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Module command routing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestModuleRouting:
|
|
"""Test routing commands to modules."""
|
|
|
|
def test_route_seedgo_list(self):
|
|
"""route_module_command to seedgo 'list' returns output."""
|
|
result = route_module_command("seedgo", "list")
|
|
assert isinstance(result, dict)
|
|
assert "stdout" in result
|
|
assert "exit_code" in result
|
|
# seedgo list should succeed and show plugins
|
|
assert result["exit_code"] == 0
|
|
assert "plugin" in result["stdout"].lower() or "PLUGIN" in result["stdout"]
|
|
|
|
def test_route_unknown_command(self):
|
|
"""Unknown seedgo command returns error."""
|
|
result = route_module_command("seedgo", "nonexistent")
|
|
assert result["exit_code"] == 1
|
|
assert "unknown command" in result["stderr"]
|
|
|
|
def test_route_unknown_module_raises(self):
|
|
"""Routing to unregistered module raises KeyError."""
|
|
import pytest
|
|
with pytest.raises(KeyError):
|
|
route_module_command("nonexistent", "list")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Module help
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestModuleHelp:
|
|
"""Test module help retrieval."""
|
|
|
|
def test_seedgo_help_returns_text(self):
|
|
"""get_module_help for seedgo returns non-empty help text."""
|
|
help_text = get_module_help("seedgo")
|
|
assert help_text != ""
|
|
assert "seedgo" in help_text
|
|
assert "check" in help_text or "audit" in help_text
|
|
|
|
def test_unknown_module_help_returns_empty(self):
|
|
"""get_module_help for unknown module returns empty string."""
|
|
assert get_module_help("nonexistent") == ""
|
|
|
|
|
|
class TestModuleIntrospective:
|
|
"""Test module introspective (discovery) retrieval."""
|
|
|
|
def test_seedgo_introspective_shows_plugins(self):
|
|
"""get_module_introspective for seedgo lists discovered plugins."""
|
|
text = get_module_introspective("seedgo")
|
|
assert text != ""
|
|
assert "SEEDGO" in text
|
|
assert "Discovered Plugins" in text
|
|
# Should show at least some known plugins
|
|
assert "drone-compliance" in text or "no-bare-except" in text
|
|
|
|
def test_introspective_shows_version(self):
|
|
"""Introspective includes the module version."""
|
|
text = get_module_introspective("seedgo")
|
|
assert "1.0.0" in text
|
|
|
|
def test_introspective_points_to_help(self):
|
|
"""Introspective tells you how to get help."""
|
|
text = get_module_introspective("seedgo")
|
|
assert "--help" in text
|
|
|
|
def test_unknown_module_introspective_returns_empty(self):
|
|
"""get_module_introspective for unknown module returns empty string."""
|
|
assert get_module_introspective("nonexistent") == ""
|
|
|
|
def test_introspective_differs_from_help(self):
|
|
"""Introspective and help return different content."""
|
|
intro = get_module_introspective("seedgo")
|
|
help_text = get_module_help("seedgo")
|
|
# Both non-empty but different
|
|
assert intro != ""
|
|
assert help_text != ""
|
|
assert intro != help_text
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI integration — drone systems shows modules
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCLISystemsModules:
|
|
"""drone systems includes modules."""
|
|
|
|
def test_systems_shows_modules(self):
|
|
"""drone systems output includes modules section."""
|
|
with patch("aipass.drone.cli.list_branches", return_value=[]):
|
|
code, out, _ = _run_cli("systems")
|
|
assert code == 0
|
|
assert "Modules" in out
|
|
assert "@seedgo" in out
|
|
|
|
def test_systems_shows_module_description(self):
|
|
"""drone systems shows module descriptions."""
|
|
with patch("aipass.drone.cli.list_branches", return_value=[]):
|
|
code, out, _ = _run_cli("systems")
|
|
assert "standards" in out.lower() or "Standards" in out
|
|
|
|
def test_systems_shows_both_modules_and_branches(self):
|
|
"""drone systems shows both modules and branches."""
|
|
with patch("aipass.drone.cli.list_branches", return_value=["@flow", "@prax"]):
|
|
code, out, _ = _run_cli("systems")
|
|
assert code == 0
|
|
assert "Modules" in out
|
|
assert "Branches" in out
|
|
assert "@seedgo" in out
|
|
assert "@flow" in out
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI integration — drone @seedgo routes to module
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCLIModuleRouting:
|
|
"""drone @seedgo commands route through module system."""
|
|
|
|
def test_seedgo_help(self):
|
|
"""drone @seedgo --help shows seedgo help text."""
|
|
code, out, _ = _run_cli("@seedgo", "--help")
|
|
assert code == 0
|
|
assert "seedgo" in out
|
|
assert "check" in out
|
|
|
|
def test_seedgo_no_args_shows_introspective(self):
|
|
"""drone @seedgo with no command shows introspective (discovery)."""
|
|
code, out, _ = _run_cli("@seedgo")
|
|
assert code == 0
|
|
assert "SEEDGO" in out
|
|
assert "Discovered Plugins" in out
|
|
# Should NOT be the help text (which has "Commands:" section)
|
|
assert "Examples:" not in out
|
|
|
|
def test_seedgo_list(self):
|
|
"""drone @seedgo list shows plugins."""
|
|
code, out, _ = _run_cli("@seedgo", "list")
|
|
assert code == 0
|
|
assert "plugin" in out.lower() or "PLUGIN" in out
|
|
|
|
def test_seedgo_unknown_command(self):
|
|
"""drone @seedgo nonexistent exits with error."""
|
|
code, _, err = _run_cli("@seedgo", "nonexistent")
|
|
assert code == 1
|
|
assert "unknown command" in err
|
|
|
|
def test_seedgo_check_specific_file(self):
|
|
"""drone @seedgo check on a file runs checks."""
|
|
code, out, _ = _run_cli("@seedgo", "check", "src/seedgo/__init__.py")
|
|
assert code == 0 or code == 1 # May pass or fail, but should run
|
|
# Should produce output from seedgo
|
|
assert out != "" or _ != ""
|
|
|
|
def test_module_takes_priority_over_branch(self):
|
|
"""Module routing takes priority over branch registry."""
|
|
# Even if a branch named 'seedgo' existed, the module should handle it
|
|
# We verify by NOT mocking route_command — if branch routing were tried,
|
|
# it would fail because no registry exists
|
|
code, out, _ = _run_cli("@seedgo", "--help")
|
|
assert code == 0
|
|
assert "seedgo" in out
|