✨ feat(drone): add internal module routing system for drone commands

- introduce drone module registry with metadata and command routing
- add seedgo as a registered internal drone module with adapter
- update drone CLI to route commands to modules or branches by @target
- enhance `drone systems` to list registered modules with descriptions
- provide module help integration alongside branch help

✨ feat(seedgo): create drone adapter for seedgo module commands
- implement handle_command forwarding to seedgo CLI subprocess
- provide get_help function with command-specific help text
- support commands: check, audit, list, init with options and examples

✨ feat(seedgo): add __main__.py to support `python -m seedgo` execution

✨ feat(seedgo plugin): add drone-compliance plugin to verify drone adapter compliance
- checks existence of drone_adapter.py and required interface elements
- validates presence of DRONE_MODULE dict, handle_command(), and get_help()
- reports compliance results with pass/fail and score with detailed checks
- skips non-target packages and handles syntax errors gracefully

✅ test(drone modules): add comprehensive tests for drone module registry and routing
- test listing, info retrieval, command routing, and help retrieval
- test CLI integration for `drone systems` and `drone @seedgo` commands
- verify module priority over branch routing and error handling

✅ test(seedgo drone compliance): add tests for drone-compliance plugin
- test passing cases with proper adapter and interface
- test failures for missing adapter, metadata, or functions
- test warnings for missing get_help function
- test skipping non-target packages and file-not-found cases
- test handling of syntax errors in adapter files
This commit is contained in:
AIOSAI
2026-03-02 22:44:41 -08:00
parent ea39879faf
commit 2435a70c79
8 changed files with 933 additions and 14 deletions
BIN
View File
Binary file not shown.
+76 -14
View File
@@ -7,9 +7,10 @@ Usage:
drone Show available commands
drone --help Show help
drone --version Show version
drone systems List registered branches
drone systems List registered branches and modules
drone @branch command [args] Route command to branch
drone @branch --help Show help for branch
drone @module command [args] Route command to internal module
drone @branch --help Show help for branch or module
Zero external dependencies — stdlib only (sys).
"""
@@ -26,22 +27,30 @@ from aipass.drone import (
list_branches,
route_command,
)
from aipass.drone.modules import (
is_module,
list_modules,
get_module_info,
route_module_command,
get_module_help,
)
_HELP_TEXT = """\
drone — AIPass branch command router
drone — AIPass command router
Usage:
drone Show this help
drone --help Show this help
drone --version Show version
drone systems List registered branches
drone @branch command [args] Route command to branch
drone @branch --help Show help for branch
drone systems List registered branches and modules
drone @target command [args] Route command to branch or module
drone @target --help Show help for branch or module
Examples:
drone systems
drone @seedgo audit src/
drone @seedgo check src/myfile.py
drone @flow status
drone @flow run --verbose
drone @flow --help
"""
@@ -82,23 +91,49 @@ def main() -> None:
def _cmd_systems() -> None:
"""Handle `drone systems` — list registered branches."""
"""Handle `drone systems` — list registered branches and modules."""
branches = list_branches()
if not branches:
print("No branches registered.")
modules = list_modules()
if not branches and not modules:
print("No branches or modules registered.")
sys.exit(0)
print(f"Registered branches ({len(branches)}):")
for name in sorted(branches):
print(f" {name}")
if modules:
print(f"Modules ({len(modules)}):")
for name in modules:
info = get_module_info(name)
if info:
print(f" @{name} {info.description}")
else:
print(f" @{name} (not available)")
if branches:
print()
if branches:
print(f"Branches ({len(branches)}):")
for name in sorted(branches):
print(f" {name}")
sys.exit(0)
def _cmd_branch(args: list[str]) -> None:
"""Handle `drone @branch command [args]` or `drone @branch --help`."""
"""Handle `drone @target command [args]` or `drone @target --help`.
Checks internal modules first (e.g. @seedgo), then falls back
to branch registry for external branches.
"""
target = args[0]
rest = args[1:]
module_name = target.lstrip("@")
# Check if this is a registered internal module
if is_module(module_name):
_cmd_module(module_name, rest)
return
# Fall through to branch routing
# drone @branch --help
if not rest or rest == ["--help"]:
try:
@@ -133,3 +168,30 @@ def _cmd_branch(args: list[str]) -> None:
if result.stderr:
print(result.stderr, end="", file=sys.stderr)
sys.exit(result.exit_code)
def _cmd_module(name: str, args: list[str]) -> None:
"""Handle routing to an internal module (e.g. @seedgo)."""
# drone @module --help (or no args)
if not args or args == ["--help"]:
help_text = get_module_help(name)
if help_text:
print(help_text, end="")
else:
print(f"No help available for @{name}.")
sys.exit(0)
command = args[0]
cmd_args = args[1:] if len(args) > 1 else None
try:
result = route_module_command(name, command, cmd_args)
except (ImportError, AttributeError) as exc:
print(f"drone: module @{name} is registered but not available: {exc}", file=sys.stderr)
sys.exit(1)
if result.get("stdout"):
print(result["stdout"], end="")
if result.get("stderr"):
print(result["stderr"], end="", file=sys.stderr)
sys.exit(result.get("exit_code", 0))
+100
View File
@@ -0,0 +1,100 @@
"""Internal module registry for drone.
Routes @module commands to Python packages installed alongside drone,
as opposed to external branches in BRANCH_REGISTRY.json.
Modules register by providing a drone_adapter module with:
- DRONE_MODULE dict (name, version, description)
- handle_command(command, args) -> dict with stdout/stderr/exit_code
- get_help(command=None) -> str
"""
from __future__ import annotations
import importlib
from dataclasses import dataclass
# Maps module name -> import path for its drone_adapter
_MODULE_REGISTRY: dict[str, str] = {
"seedgo": "seedgo.drone_adapter",
# Future modules register here:
# "prax": "aipass.prax.drone_adapter",
# "cortex": "aipass.cortex.drone_adapter",
}
@dataclass
class ModuleInfo:
"""Metadata about a registered module."""
name: str
version: str
description: str
adapter_path: str
def list_modules() -> list[str]:
"""Return sorted list of registered module names."""
return sorted(_MODULE_REGISTRY.keys())
def get_module_info(name: str) -> ModuleInfo | None:
"""Get module metadata without executing anything.
Returns None if the module is not registered or not importable.
"""
adapter_path = _MODULE_REGISTRY.get(name)
if adapter_path is None:
return None
try:
mod = importlib.import_module(adapter_path)
meta = getattr(mod, "DRONE_MODULE", {})
return ModuleInfo(
name=meta.get("name", name),
version=meta.get("version", "unknown"),
description=meta.get("description", ""),
adapter_path=adapter_path,
)
except ImportError:
return None
def is_module(name: str) -> bool:
"""Check if name is a registered module (doesn't verify importability)."""
return name in _MODULE_REGISTRY
def route_module_command(name: str, command: str, args: list[str] | None = None) -> dict:
"""Route a command to a module's drone adapter.
Returns dict with keys: stdout, stderr, exit_code.
Raises: KeyError if module not registered, ImportError if adapter missing,
AttributeError if adapter lacks handle_command.
"""
adapter_path = _MODULE_REGISTRY[name]
mod = importlib.import_module(adapter_path)
handler = getattr(mod, "handle_command")
return handler(command, args)
def get_module_help(name: str, command: str | None = None) -> str:
"""Get help text from a module's drone adapter.
Returns help string, or empty string if unavailable.
"""
adapter_path = _MODULE_REGISTRY.get(name)
if adapter_path is None:
return ""
try:
mod = importlib.import_module(adapter_path)
help_fn = getattr(mod, "get_help", None)
if help_fn is None:
return ""
return help_fn(command)
except (ImportError, AttributeError):
return ""
def register_module(name: str, adapter_path: str) -> None:
"""Register a new module. Used for dynamic registration (e.g., plugins)."""
_MODULE_REGISTRY[name] = adapter_path
+6
View File
@@ -0,0 +1,6 @@
"""Allow running seedgo as a module: python -m seedgo."""
from seedgo.cli import main
if __name__ == "__main__":
main()
+101
View File
@@ -0,0 +1,101 @@
"""Drone adapter for seedgo — code standards framework.
Provides the drone module interface so `drone @seedgo` commands work.
Delegates to the seedgo CLI via subprocess for clean stdout/stderr capture.
"""
from __future__ import annotations
import subprocess
import sys
DRONE_MODULE = {
"name": "seedgo",
"version": "1.0.0",
"description": "Code standards framework — check and audit code against configurable standards",
}
_HELP_TEXT = """\
seedgo — code standards framework
Commands:
check [FILE ...] Run checks on files (or project if no files given)
audit [PATH ...] Alias for check (run audit on paths)
list Show all discovered plugins
init Initialize .seedgo/ config in current directory
Options (for check/audit):
--format FORMAT Output format: human, json, github (default: human)
--threshold N Pass threshold 0-100 (default: 75)
--plugin NAME Run only this plugin
Examples:
drone @seedgo check src/myfile.py
drone @seedgo audit src/
drone @seedgo list
drone @seedgo init --profile strict
"""
def handle_command(command: str, args: list[str] | None = None) -> dict:
"""Handle a command routed by drone.
Returns dict with stdout, stderr, exit_code.
"""
cmd_args = args or []
# Map commands to seedgo CLI arguments
if command == "audit":
# audit is an alias for check
cli_args = ["check"] + cmd_args
elif command in ("check", "list", "init"):
cli_args = [command] + cmd_args
else:
return {
"stdout": "",
"stderr": f"seedgo: unknown command '{command}'\nRun 'drone @seedgo --help' for usage.\n",
"exit_code": 1,
}
try:
result = subprocess.run(
[sys.executable, "-m", "seedgo"] + cli_args,
capture_output=True,
text=True,
timeout=120,
)
return {
"stdout": result.stdout,
"stderr": result.stderr,
"exit_code": result.returncode,
}
except subprocess.TimeoutExpired:
return {
"stdout": "",
"stderr": "seedgo: command timed out after 120 seconds\n",
"exit_code": 1,
}
except FileNotFoundError:
return {
"stdout": "",
"stderr": "seedgo: Python executable not found\n",
"exit_code": 1,
}
def get_help(command: str | None = None) -> str:
"""Return help text for drone @seedgo."""
if command is None:
return _HELP_TEXT
# For specific command help, delegate to seedgo
try:
result = subprocess.run(
[sys.executable, "-m", "seedgo", command, "--help"],
capture_output=True,
text=True,
timeout=10,
)
return result.stdout if result.stdout else _HELP_TEXT
except (subprocess.TimeoutExpired, FileNotFoundError):
return _HELP_TEXT
+212
View File
@@ -0,0 +1,212 @@
"""Drone compliance check — verifies modules have proper drone integration.
Checks that Python packages in the aipass ecosystem provide:
1. A drone_adapter module
2. DRONE_MODULE metadata dict (name, version, description)
3. handle_command() function
4. get_help() function
This check runs on __init__.py files to identify packages, then looks
for their drone_adapter.py sibling.
"""
from __future__ import annotations
import ast
from pathlib import Path
from seedgo.models import CheckItem, CheckResult, Severity
PLUGIN_NAME = "drone-compliance"
PLUGIN_DESCRIPTION = "Verify modules provide drone adapter interface"
PLUGIN_VERSION = "1.0.0"
FILE_TYPES = ["__init__.py"]
def check(file_path: str, config: dict | None = None) -> CheckResult:
"""Check if the package containing this __init__.py is drone-compliant.
Args:
file_path: Absolute path to an __init__.py file.
config: Optional plugin config dict. Supports:
target_packages (list[str]): Package names to check.
Defaults to ["aipass", "seedgo"].
Returns:
CheckResult with compliance check items.
"""
path = Path(file_path)
if not path.exists():
return CheckResult(
plugin=PLUGIN_NAME,
passed=True,
checks=[],
file_path=file_path,
metadata={"skipped": True, "reason": "file_not_found"},
)
package_dir = path.parent
package_name = package_dir.name
# Determine if this package should be checked
cfg = config or {}
target_packages = cfg.get("target_packages", ["aipass", "seedgo"])
should_check = package_name in target_packages
if not should_check:
return CheckResult(
plugin=PLUGIN_NAME,
passed=True,
checks=[
CheckItem(
name="scope",
passed=True,
message=f"Package '{package_name}' is not a drone-target module, skipped.",
severity=Severity.INFO,
)
],
score=100,
file_path=file_path,
metadata={"skipped": True},
)
checks: list[CheckItem] = []
adapter_path = package_dir / "drone_adapter.py"
# Check 1: drone_adapter.py exists
has_adapter = adapter_path.exists()
checks.append(
CheckItem(
name="adapter-exists",
passed=has_adapter,
message=(
f"drone_adapter.py found in {package_name}/"
if has_adapter
else f"Missing drone_adapter.py in {package_name}/ — module is not drone-routable"
),
severity=Severity.ERROR if not has_adapter else Severity.INFO,
fix_hint=(
"Create drone_adapter.py with DRONE_MODULE, handle_command(), and get_help()"
if not has_adapter
else None
),
)
)
if not has_adapter:
return CheckResult(
plugin=PLUGIN_NAME,
passed=False,
checks=checks,
score=0,
file_path=file_path,
metadata={},
)
# Parse the adapter file with AST
try:
source = adapter_path.read_text()
tree = ast.parse(source)
except (SyntaxError, OSError) as exc:
checks.append(
CheckItem(
name="adapter-parseable",
passed=False,
message=f"drone_adapter.py has syntax error: {exc}",
severity=Severity.ERROR,
)
)
return CheckResult(
plugin=PLUGIN_NAME,
passed=False,
checks=checks,
score=0,
file_path=file_path,
metadata={},
)
# Check 2: DRONE_MODULE dict exists
has_meta = any(
isinstance(node, ast.Assign)
and any(isinstance(t, ast.Name) and t.id == "DRONE_MODULE" for t in node.targets)
for node in ast.walk(tree)
)
checks.append(
CheckItem(
name="drone-module-meta",
passed=has_meta,
message=(
"DRONE_MODULE metadata dict found"
if has_meta
else "Missing DRONE_MODULE dict — drone can't read module metadata"
),
severity=Severity.ERROR if not has_meta else Severity.INFO,
fix_hint=(
'Add: DRONE_MODULE = {"name": "...", "version": "...", "description": "..."}'
if not has_meta
else None
),
)
)
# Check 3: handle_command() function exists
functions = [
node.name for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
]
has_handle = "handle_command" in functions
checks.append(
CheckItem(
name="handle-command",
passed=has_handle,
message=(
"handle_command() function found"
if has_handle
else "Missing handle_command() — drone can't route commands to this module"
),
severity=Severity.ERROR if not has_handle else Severity.INFO,
fix_hint=(
"Add: def handle_command(command: str, args: list[str] | None = None) -> dict:"
if not has_handle
else None
),
)
)
# Check 4: get_help() function exists
has_help = "get_help" in functions
checks.append(
CheckItem(
name="get-help",
passed=has_help,
message=(
"get_help() function found"
if has_help
else "Missing get_help() — drone can't show help for this module"
),
severity=Severity.WARNING if not has_help else Severity.INFO,
fix_hint="Add: def get_help(command: str | None = None) -> str:" if not has_help else None,
)
)
# Score: errors block pass, warnings degrade
error_checks = [c for c in checks if not c.passed and c.severity == Severity.ERROR]
all_passed = len(error_checks) == 0
# Simple scoring: deduct for failures
failed_errors = len(error_checks)
failed_warnings = len([c for c in checks if not c.passed and c.severity == Severity.WARNING])
total_weight = len(checks)
deductions = failed_errors * 1.0 + failed_warnings * 0.5
score = int(((total_weight - deductions) / max(total_weight, 1)) * 100)
return CheckResult(
plugin=PLUGIN_NAME,
passed=all_passed,
checks=checks,
score=score,
file_path=file_path,
metadata={"adapter_path": str(adapter_path)},
)
+176
View File
@@ -0,0 +1,176 @@
"""
Tests for the drone-compliance seedgo plugin.
Covers:
- Passes on packages with proper drone_adapter.py
- Fails on packages missing drone_adapter.py
- Fails on adapter missing DRONE_MODULE / handle_command / get_help
- Skips non-target packages
- Handles syntax errors in adapter files
"""
from __future__ import annotations
import textwrap
from pathlib import Path
from seedgo.plugins.drone_compliance import PLUGIN_NAME, check
class TestDroneCompliancePass:
"""Packages with proper drone adapters should pass."""
def test_seedgo_adapter_passes(self, tmp_path: Path):
"""seedgo's own drone_adapter.py passes all checks."""
# Create a fake "seedgo" package with a proper adapter
pkg = tmp_path / "seedgo"
pkg.mkdir()
(pkg / "__init__.py").write_text('"""Seedgo package."""\n')
(pkg / "drone_adapter.py").write_text(
textwrap.dedent("""\
DRONE_MODULE = {"name": "seedgo", "version": "1.0.0", "description": "test"}
def handle_command(command, args=None):
return {"stdout": "", "stderr": "", "exit_code": 0}
def get_help(command=None):
return "help text"
""")
)
result = check(str(pkg / "__init__.py"), config={"target_packages": ["seedgo"]})
assert result.plugin == PLUGIN_NAME
assert result.passed is True
assert result.score == 100
def test_all_four_checks_present(self, tmp_path: Path):
"""All 4 checks pass: adapter exists, DRONE_MODULE, handle_command, get_help."""
pkg = tmp_path / "mymod"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
(pkg / "drone_adapter.py").write_text(
textwrap.dedent("""\
DRONE_MODULE = {"name": "mymod", "version": "0.1.0", "description": "test"}
def handle_command(command, args=None):
return {}
def get_help(command=None):
return ""
""")
)
result = check(str(pkg / "__init__.py"), config={"target_packages": ["mymod"]})
assert result.passed is True
class TestDroneComplianceFail:
"""Packages missing drone adapter components should fail."""
def test_missing_adapter_fails(self, tmp_path: Path):
"""Package without drone_adapter.py fails."""
pkg = tmp_path / "seedgo"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
result = check(str(pkg / "__init__.py"), config={"target_packages": ["seedgo"]})
assert result.passed is False
assert result.score == 0
error_names = [c.name for c in result.checks if not c.passed]
assert "adapter-exists" in error_names
def test_missing_drone_module_dict(self, tmp_path: Path):
"""Adapter without DRONE_MODULE fails."""
pkg = tmp_path / "mymod"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
(pkg / "drone_adapter.py").write_text(
textwrap.dedent("""\
def handle_command(command, args=None):
return {}
def get_help(command=None):
return ""
""")
)
result = check(str(pkg / "__init__.py"), config={"target_packages": ["mymod"]})
assert result.passed is False
failed = [c.name for c in result.checks if not c.passed]
assert "drone-module-meta" in failed
def test_missing_handle_command(self, tmp_path: Path):
"""Adapter without handle_command fails."""
pkg = tmp_path / "mymod"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
(pkg / "drone_adapter.py").write_text(
textwrap.dedent("""\
DRONE_MODULE = {"name": "mymod", "version": "1.0.0", "description": "test"}
def get_help(command=None):
return ""
""")
)
result = check(str(pkg / "__init__.py"), config={"target_packages": ["mymod"]})
assert result.passed is False
failed = [c.name for c in result.checks if not c.passed]
assert "handle-command" in failed
def test_missing_get_help_is_warning(self, tmp_path: Path):
"""Adapter without get_help is a warning, not an error — still passes."""
pkg = tmp_path / "mymod"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
(pkg / "drone_adapter.py").write_text(
textwrap.dedent("""\
DRONE_MODULE = {"name": "mymod", "version": "1.0.0", "description": "test"}
def handle_command(command, args=None):
return {}
""")
)
result = check(str(pkg / "__init__.py"), config={"target_packages": ["mymod"]})
# Missing get_help is WARNING severity, not ERROR — should still pass
assert result.passed is True
warnings = [c for c in result.checks if not c.passed]
assert any(c.name == "get-help" for c in warnings)
class TestDroneComplianceSkip:
"""Non-target packages should be skipped."""
def test_non_target_package_skipped(self, tmp_path: Path):
"""Packages not in target_packages are skipped with pass."""
pkg = tmp_path / "unrelated"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
result = check(str(pkg / "__init__.py"), config={"target_packages": ["seedgo"]})
assert result.passed is True
assert result.metadata.get("skipped") is True
def test_file_not_found_passes(self, tmp_path: Path):
"""Non-existent file returns pass (skipped)."""
result = check(str(tmp_path / "nonexistent" / "__init__.py"))
assert result.passed is True
class TestDroneComplianceEdgeCases:
"""Edge cases for the compliance check."""
def test_syntax_error_in_adapter(self, tmp_path: Path):
"""Adapter with syntax error fails gracefully."""
pkg = tmp_path / "mymod"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
(pkg / "drone_adapter.py").write_text("def broken(:\n")
result = check(str(pkg / "__init__.py"), config={"target_packages": ["mymod"]})
assert result.passed is False
failed = [c.name for c in result.checks if not c.passed]
assert "adapter-parseable" in failed
+262
View File
@@ -0,0 +1,262 @@
"""
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,
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") == ""
# ---------------------------------------------------------------------------
# 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_help(self):
"""drone @seedgo with no command shows help."""
code, out, _ = _run_cli("@seedgo")
assert code == 0
assert "seedgo" 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