#685 seedgo: subcommand_help standard — enforces every entry point intercepts <cmd> --help before dispatch (explicit guard or argparse parse_known_args), else <cmd> --help executes the command. 21 tests, cwd-portable (_AIPASS_ROOT anchor). Checker verified independently: 7/17 comply, 10 offenders.
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: subcommand_help_check.py
|
||||
# Description: Subcommand Help Standards Checker Handler
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-07-10
|
||||
# Modified: 2026-07-10
|
||||
# =============================================
|
||||
|
||||
"""Subcommand Help Standards Checker Handler.
|
||||
|
||||
Validates that branch entry points handle <cmd> --help by showing
|
||||
subcommand-specific help — never executing the command, never silently
|
||||
falling back to top-level help.
|
||||
|
||||
THE CONTRACT:
|
||||
Every entry point that routes subcommands must intercept --help in the
|
||||
remaining args (after command extraction) BEFORE dispatching to handlers.
|
||||
|
||||
WHAT PASSES (any one of):
|
||||
a) Explicit subcommand --help guard on remaining args
|
||||
b) argparse with parse_known_args (absorbs --help from any position)
|
||||
|
||||
Only entry point files are checked (apps/{branch}.py).
|
||||
"""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from aipass.prax import logger
|
||||
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed
|
||||
from aipass.seedgo.apps.handlers.json import json_handler
|
||||
|
||||
AUDIT_SCOPE = "entry_point"
|
||||
|
||||
_TOPLEVEL_ARG_NAMES = frozenset({"args", "argv"})
|
||||
|
||||
_HELP_STRINGS = frozenset({"--help", "-h"})
|
||||
|
||||
|
||||
_ENTRY_NAMES = {"main", "handle_command"}
|
||||
|
||||
|
||||
def _called_names(func_node: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
|
||||
"""Return names of functions called directly from func_node's body."""
|
||||
names: set[str] = set()
|
||||
for node in ast.walk(func_node):
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
||||
names.add(node.func.id)
|
||||
return names
|
||||
|
||||
|
||||
def _find_entry_functions(tree: ast.Module) -> list[ast.FunctionDef | ast.AsyncFunctionDef]:
|
||||
"""Return entry functions and their direct delegates from the module top level."""
|
||||
all_funcs: dict[str, ast.FunctionDef | ast.AsyncFunctionDef] = {}
|
||||
for node in ast.iter_child_nodes(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
all_funcs[node.name] = node
|
||||
|
||||
targets: list[ast.FunctionDef | ast.AsyncFunctionDef] = []
|
||||
for name in _ENTRY_NAMES:
|
||||
if name not in all_funcs:
|
||||
continue
|
||||
func = all_funcs[name]
|
||||
targets.append(func)
|
||||
for called in _called_names(func):
|
||||
if called in all_funcs and called.startswith("_") and called not in _ENTRY_NAMES:
|
||||
targets.append(all_funcs[called])
|
||||
return targets
|
||||
|
||||
|
||||
def _has_argparse_known_args(func_node: ast.AST) -> bool:
|
||||
"""Return True if the function uses argparse parse_known_args."""
|
||||
for node in ast.walk(func_node):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
if isinstance(node.func, ast.Attribute) and node.func.attr == "parse_known_args":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _compare_has_help_string(node: ast.Compare) -> bool:
|
||||
"""Return True if a Compare node involves a --help string constant."""
|
||||
for part in [node.left, *node.comparators]:
|
||||
if isinstance(part, ast.Constant) and part.value in _HELP_STRINGS:
|
||||
return True
|
||||
if isinstance(part, (ast.List, ast.Tuple, ast.Set)):
|
||||
for elt in part.elts:
|
||||
if isinstance(elt, ast.Constant) and elt.value in _HELP_STRINGS:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_subscript_on_name(node: ast.expr, names: frozenset[str]) -> bool:
|
||||
"""Return True if node is name[N] where name is in the given set."""
|
||||
if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name):
|
||||
return node.value.id in names
|
||||
return False
|
||||
|
||||
|
||||
def _is_toplevel_help_check(node: ast.Compare) -> bool:
|
||||
"""Return True if this is a top-level args[0] --help check."""
|
||||
return _is_subscript_on_name(node.left, _TOPLEVEL_ARG_NAMES)
|
||||
|
||||
|
||||
def _is_help_in_nonargs_var(node: ast.Compare) -> bool:
|
||||
"""Check for '"--help" in some_var' where some_var is not args/argv."""
|
||||
if not isinstance(node.left, ast.Constant) or node.left.value not in _HELP_STRINGS:
|
||||
return False
|
||||
if not any(isinstance(op, ast.In) for op in node.ops):
|
||||
return False
|
||||
return any(isinstance(c, ast.Name) and c.id not in _TOPLEVEL_ARG_NAMES for c in node.comparators)
|
||||
|
||||
|
||||
def _is_nonargs_subscript_help(node: ast.Compare) -> bool:
|
||||
"""Check for 'remaining[0] in ["--help", ...]' where remaining != args."""
|
||||
left = node.left
|
||||
if not isinstance(left, ast.Subscript) or not isinstance(left.value, ast.Name):
|
||||
return False
|
||||
return left.value.id not in _TOPLEVEL_ARG_NAMES
|
||||
|
||||
|
||||
def _has_subcommand_help_guard(func_node: ast.AST, func_name: str = "") -> bool:
|
||||
"""Return True if the function has a subcommand-level --help check.
|
||||
|
||||
Detects patterns like:
|
||||
remaining_args[0] in ["--help", "-h"]
|
||||
"--help" in remaining_args
|
||||
rest[0] == "--help"
|
||||
where the variable is NOT the raw args/argv.
|
||||
|
||||
In handle_command(), args IS the subcommand args (not full argv),
|
||||
so args[0] checks there count as subcommand guards.
|
||||
"""
|
||||
args_are_subcommand = func_name == "handle_command"
|
||||
for node in ast.walk(func_node):
|
||||
if not isinstance(node, ast.Compare):
|
||||
continue
|
||||
if not _compare_has_help_string(node):
|
||||
continue
|
||||
if _is_toplevel_help_check(node):
|
||||
if args_are_subcommand:
|
||||
return True
|
||||
continue
|
||||
if _is_subscript_on_name(node.left, _TOPLEVEL_ARG_NAMES):
|
||||
if args_are_subcommand:
|
||||
return True
|
||||
continue
|
||||
if _is_help_in_nonargs_var(node):
|
||||
return True
|
||||
if _is_nonargs_subscript_help(node):
|
||||
return True
|
||||
if isinstance(node.left, ast.Name) and node.left.id not in _TOPLEVEL_ARG_NAMES:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_module(module_path: str, bypass_rules: list | None = None) -> Dict:
|
||||
"""Check if entry point handles subcommand --help."""
|
||||
path = Path(module_path)
|
||||
|
||||
if is_bypassed(module_path, "subcommand_help", bypass_rules=bypass_rules):
|
||||
return {
|
||||
"passed": True,
|
||||
"checks": [{"name": "Bypassed", "passed": True, "message": "Standard bypassed via .seedgo/bypass.json"}],
|
||||
"score": 100,
|
||||
"standard": "SUBCOMMAND_HELP",
|
||||
}
|
||||
|
||||
if not path.exists():
|
||||
return {
|
||||
"passed": False,
|
||||
"checks": [{"name": "File exists", "passed": False, "message": f"File not found: {module_path}"}],
|
||||
"score": 0,
|
||||
"standard": "SUBCOMMAND_HELP",
|
||||
}
|
||||
|
||||
if path.parent.name != "apps":
|
||||
return {
|
||||
"passed": True,
|
||||
"checks": [{"name": "Subcommand help", "passed": True, "message": "Not an entry point (skipped)"}],
|
||||
"score": 100,
|
||||
"standard": "SUBCOMMAND_HELP",
|
||||
}
|
||||
|
||||
try:
|
||||
source = path.read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
logger.info("Cannot read %s: %s", path, e)
|
||||
return {
|
||||
"passed": False,
|
||||
"checks": [{"name": "File readable", "passed": False, "message": f"Error reading file: {e}"}],
|
||||
"score": 0,
|
||||
"standard": "SUBCOMMAND_HELP",
|
||||
}
|
||||
|
||||
try:
|
||||
tree = ast.parse(source, filename=str(path))
|
||||
except SyntaxError as e:
|
||||
logger.info("Skipped %s: SyntaxError during parse", path)
|
||||
return {
|
||||
"passed": False,
|
||||
"checks": [{"name": "File parseable", "passed": False, "message": f"Syntax error: {e}"}],
|
||||
"score": 0,
|
||||
"standard": "SUBCOMMAND_HELP",
|
||||
}
|
||||
|
||||
entry_funcs = _find_entry_functions(tree)
|
||||
if not entry_funcs:
|
||||
return {
|
||||
"passed": True,
|
||||
"checks": [
|
||||
{
|
||||
"name": "Subcommand help",
|
||||
"passed": True,
|
||||
"message": "No main/handle_command entry function found (skipped)",
|
||||
}
|
||||
],
|
||||
"score": 100,
|
||||
"standard": "SUBCOMMAND_HELP",
|
||||
}
|
||||
|
||||
for func in entry_funcs:
|
||||
if _has_argparse_known_args(func):
|
||||
json_handler.log_operation(
|
||||
"check_completed",
|
||||
{"file": str(module_path), "score": 100, "standard": "subcommand_help"},
|
||||
)
|
||||
return {
|
||||
"passed": True,
|
||||
"checks": [
|
||||
{
|
||||
"name": "Subcommand help",
|
||||
"passed": True,
|
||||
"message": f"argparse parse_known_args in {func.name}() absorbs --help",
|
||||
}
|
||||
],
|
||||
"score": 100,
|
||||
"standard": "SUBCOMMAND_HELP",
|
||||
}
|
||||
|
||||
if _has_subcommand_help_guard(func, func.name):
|
||||
json_handler.log_operation(
|
||||
"check_completed",
|
||||
{"file": str(module_path), "score": 100, "standard": "subcommand_help"},
|
||||
)
|
||||
return {
|
||||
"passed": True,
|
||||
"checks": [
|
||||
{
|
||||
"name": "Subcommand help",
|
||||
"passed": True,
|
||||
"message": f"Subcommand --help guard found in {func.name}()",
|
||||
}
|
||||
],
|
||||
"score": 100,
|
||||
"standard": "SUBCOMMAND_HELP",
|
||||
}
|
||||
|
||||
func_names = ", ".join(f.name for f in entry_funcs)
|
||||
json_handler.log_operation(
|
||||
"check_completed",
|
||||
{"file": str(module_path), "score": 0, "standard": "subcommand_help"},
|
||||
)
|
||||
return {
|
||||
"passed": False,
|
||||
"checks": [
|
||||
{
|
||||
"name": "Subcommand help",
|
||||
"passed": False,
|
||||
"message": (
|
||||
f"No subcommand --help guard in {func_names}() — "
|
||||
"<cmd> --help will execute the command or fall to top-level help"
|
||||
),
|
||||
}
|
||||
],
|
||||
"score": 0,
|
||||
"standard": "SUBCOMMAND_HELP",
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: subcommand_help_content.py
|
||||
# Description: Subcommand Help Standards Content
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-07-10
|
||||
# Modified: 2026-07-10
|
||||
# =============================================
|
||||
|
||||
"""Subcommand Help Standards Content.
|
||||
|
||||
Provides Rich-formatted reference text for the subcommand help standard.
|
||||
"""
|
||||
|
||||
from aipass.seedgo.apps.handlers.json import json_handler
|
||||
|
||||
|
||||
def get_subcommand_help_standards() -> str:
|
||||
"""Return Rich-formatted subcommand help standards text."""
|
||||
json_handler.log_operation("standard_content_queried", {"standard": "subcommand_help"})
|
||||
return """[bold white]SUBCOMMAND HELP STANDARD[/bold white]
|
||||
|
||||
[yellow]PURPOSE:[/yellow]
|
||||
Every branch entry point must handle [bold]<cmd> --help[/bold] by showing
|
||||
that subcommand's help — never execute the command, never silently
|
||||
fall back to top-level help.
|
||||
|
||||
[yellow]THE CONTRACT:[/yellow]
|
||||
|
||||
[dim]drone @branch cmd --help[/dim] → shows cmd-specific help
|
||||
[dim]drone @branch cmd --help[/dim] ✗ executes cmd (side-effect risk)
|
||||
[dim]drone @branch cmd --help[/dim] ✗ shows top-level help (unhelpful)
|
||||
|
||||
[yellow]CANONICAL PATTERN (module-discovery branches):[/yellow]
|
||||
|
||||
[dim]command = args[0][/dim]
|
||||
[dim]remaining = args[1:][/dim]
|
||||
|
||||
[bold cyan]# Subcommand --help guard (REQUIRED)[/bold cyan]
|
||||
[dim]if remaining and remaining[0] in ["--help", "-h"]:[/dim]
|
||||
[dim] for module in modules:[/dim]
|
||||
[dim] if module.handle_command(command, ["--help"]):[/dim]
|
||||
[dim] return 0[/dim]
|
||||
[dim] print_help() # fallback to top-level[/dim]
|
||||
[dim] return 0[/dim]
|
||||
|
||||
[dim]# Normal dispatch (only reached if NOT --help)[/dim]
|
||||
[dim]if route_command(command, remaining, modules):[/dim]
|
||||
[dim] return 0[/dim]
|
||||
|
||||
[yellow]ALTERNATIVE PATTERNS (also accepted):[/yellow]
|
||||
|
||||
[bold cyan]argparse with parse_known_args:[/bold cyan]
|
||||
[dim]parser.add_argument("--help", action="store_true", dest="show_help")[/dim]
|
||||
[dim]parsed, remaining = parser.parse_known_args()[/dim]
|
||||
[dim]if parsed.show_help:[/dim]
|
||||
[dim] all_args = ["--help"] + all_args # pass to handler[/dim]
|
||||
|
||||
[bold cyan]Post-dispatch fallback:[/bold cyan]
|
||||
[dim]if not route_command(command, remaining, modules):[/dim]
|
||||
[dim] if remaining and remaining[0] in ["--help", "-h"]:[/dim]
|
||||
[dim] print_module_help(command, modules)[/dim]
|
||||
|
||||
[yellow]WHAT THE CHECKER DETECTS:[/yellow]
|
||||
|
||||
[green]✓[/green] A [bold]--help[/bold] comparison on remaining/subcommand args (not args[0])
|
||||
[green]✓[/green] argparse [bold]parse_known_args()[/bold] call (absorbs --help)
|
||||
[red]✗[/red] Only top-level --help check (args[0] in ["--help", ...])
|
||||
[red]✗[/red] No --help handling at all
|
||||
|
||||
[yellow]KEY RULES:[/yellow]
|
||||
|
||||
[bold white]Intercept before dispatch[/bold white] — don't let --help reach the handler
|
||||
[bold white]Show subcommand help[/bold white] — not top-level help
|
||||
[bold white]Never execute[/bold white] — --help must never trigger side effects
|
||||
[bold white]Scope: entry points only[/bold white] — apps/{branch}.py files"""
|
||||
@@ -0,0 +1,397 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: test_subcommand_help.py
|
||||
# Description: Tests for subcommand_help_check.py
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-07-10
|
||||
# Modified: 2026-07-10
|
||||
# =============================================
|
||||
|
||||
"""Tests for subcommand_help_check — subcommand --help guard detection."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_infrastructure(monkeypatch):
|
||||
import sys
|
||||
|
||||
mock_logger = MagicMock()
|
||||
mock_json_handler = MagicMock()
|
||||
mock_json_handler.log_operation = MagicMock(return_value=True)
|
||||
|
||||
prax_mod = MagicMock()
|
||||
prax_mod.logger = mock_logger
|
||||
monkeypatch.setitem(sys.modules, "aipass.prax", prax_mod)
|
||||
|
||||
json_pkg = MagicMock()
|
||||
json_pkg.json_handler = mock_json_handler
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json", json_pkg)
|
||||
json_mod = MagicMock()
|
||||
json_mod.log_operation = mock_json_handler.log_operation
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json.json_handler", json_mod)
|
||||
|
||||
bypass_pkg = MagicMock()
|
||||
bypass_ignore = MagicMock()
|
||||
bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[])
|
||||
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed as real_is_bypassed
|
||||
|
||||
bypass_utils = MagicMock()
|
||||
bypass_utils.is_bypassed = real_is_bypassed
|
||||
bypass_pkg.utils = bypass_utils
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass", bypass_pkg)
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.ignore_handler", bypass_ignore)
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.utils", bypass_utils)
|
||||
|
||||
for mod_name in ["aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check"]:
|
||||
monkeypatch.delitem(sys.modules, mod_name, raising=False)
|
||||
|
||||
|
||||
def _entry_file(tmp_path, source):
|
||||
"""Create a file under an apps/ directory to pass entry-point check."""
|
||||
apps_dir = tmp_path / "apps"
|
||||
apps_dir.mkdir()
|
||||
f = apps_dir / "branch.py"
|
||||
f.write_text(source)
|
||||
return str(f)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Non-entry-point files — skipped
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_non_entry_point_skipped(tmp_path):
|
||||
f = tmp_path / "handler.py"
|
||||
f.write_text("def main(): pass\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
assert result["score"] == 100
|
||||
|
||||
|
||||
def test_missing_file():
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module("/nonexistent/apps/branch.py")
|
||||
assert result["passed"] is False
|
||||
assert result["score"] == 0
|
||||
|
||||
|
||||
def test_no_entry_function(tmp_path):
|
||||
src = "def helper(): pass\n"
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is True
|
||||
assert "skipped" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MUST FAIL — top-level --help only, no subcommand guard
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_toplevel_only_fails(tmp_path):
|
||||
src = """\
|
||||
import sys
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if args[0] in ["--help", "-h"]:
|
||||
print_help()
|
||||
return 0
|
||||
command = args[0]
|
||||
remaining = args[1:]
|
||||
route_command(command, remaining, modules)
|
||||
"""
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is False
|
||||
assert result["score"] == 0
|
||||
assert "No subcommand --help guard" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_no_help_at_all_fails(tmp_path):
|
||||
src = """\
|
||||
import sys
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
command = args[0]
|
||||
remaining = args[1:]
|
||||
route_command(command, remaining, modules)
|
||||
"""
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is False
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MUST PASS — explicit subcommand --help guard
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_remaining_subscript_guard_passes(tmp_path):
|
||||
src = """\
|
||||
import sys
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if args[0] in ["--help", "-h"]:
|
||||
print_help()
|
||||
return 0
|
||||
command = args[0]
|
||||
remaining = args[1:]
|
||||
if remaining and remaining[0] in ["--help", "-h"]:
|
||||
show_subcommand_help(command)
|
||||
return 0
|
||||
route_command(command, remaining, modules)
|
||||
"""
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is True
|
||||
assert result["score"] == 100
|
||||
|
||||
|
||||
def test_remaining_args_variable_passes(tmp_path):
|
||||
src = """\
|
||||
import sys
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if args[0] in ["--help", "-h"]:
|
||||
print_help()
|
||||
return 0
|
||||
command = args[0]
|
||||
remaining_args = args[1:]
|
||||
if remaining_args and remaining_args[0] in ["--help", "-h"]:
|
||||
show_subcommand_help(command)
|
||||
return 0
|
||||
route_command(command, remaining_args, modules)
|
||||
"""
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_help_in_remaining_passes(tmp_path):
|
||||
src = """\
|
||||
import sys
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
command = args[0]
|
||||
remaining = args[1:]
|
||||
if "--help" in remaining:
|
||||
show_help(command)
|
||||
return 0
|
||||
route_command(command, remaining, modules)
|
||||
"""
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_post_dispatch_fallback_passes(tmp_path):
|
||||
src = """\
|
||||
import sys
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if args[0] in ["--help", "-h"]:
|
||||
print_help()
|
||||
return 0
|
||||
command = args[0]
|
||||
remaining_args = args[1:]
|
||||
if route_command(command, remaining_args, modules):
|
||||
return 0
|
||||
if remaining_args and remaining_args[0] in ["--help", "-h"]:
|
||||
print_module_help(command, modules)
|
||||
return 0
|
||||
"""
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MUST PASS — argparse pattern
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_argparse_parse_known_args_passes(tmp_path):
|
||||
src = """\
|
||||
import argparse
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument("command", nargs="?")
|
||||
parser.add_argument("--help", "-h", action="store_true", dest="show_help")
|
||||
parsed_args, remaining = parser.parse_known_args()
|
||||
if parsed_args.show_help:
|
||||
all_args = ["--help"] + remaining
|
||||
route_command(parsed_args.command, all_args, handlers)
|
||||
"""
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is True
|
||||
assert "parse_known_args" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MUST PASS — handle_command function
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_handle_command_function_detected(tmp_path):
|
||||
src = """\
|
||||
def handle_command(command, args):
|
||||
if args and args[0] in ["--help", "-h"]:
|
||||
return False
|
||||
route_command(command, args, modules)
|
||||
"""
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MUST PASS — bypass
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_bypassed_file_passes(tmp_path):
|
||||
src = "def main(): pass\n"
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
bypass_rules = [{"file": f, "standard": "subcommand_help"}]
|
||||
result = check_module(f, bypass_rules=bypass_rules)
|
||||
assert result["passed"] is True
|
||||
assert result["score"] == 100
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Edge cases
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_syntax_error_file(tmp_path):
|
||||
src = "def main(\n"
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is False
|
||||
assert "Syntax error" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_rest_variable_passes(tmp_path):
|
||||
src = """\
|
||||
import sys
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
command = args[0]
|
||||
rest = args[1:]
|
||||
if rest and rest[0] in ["--help", "-h"]:
|
||||
show_help(command)
|
||||
return 0
|
||||
route_command(command, rest, modules)
|
||||
"""
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_cmd_args_variable_passes(tmp_path):
|
||||
src = """\
|
||||
import sys
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
command = args[0]
|
||||
cmd_args = args[1:]
|
||||
if cmd_args and cmd_args[0] in ["--help", "-h"]:
|
||||
show_help(command)
|
||||
return 0
|
||||
route_command(command, cmd_args, modules)
|
||||
"""
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_name_eq_help_passes(tmp_path):
|
||||
src = """\
|
||||
def main():
|
||||
command = args[0]
|
||||
remaining = args[1:]
|
||||
flag = remaining[0]
|
||||
if flag == "--help":
|
||||
show_help(command)
|
||||
return 0
|
||||
"""
|
||||
f = _entry_file(tmp_path, src)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(f)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Fleet fixtures — real entry points
|
||||
# ============================================================
|
||||
|
||||
_AIPASS_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_commons_entry_passes():
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(str(_AIPASS_ROOT / "commons" / "apps" / "commons.py"))
|
||||
assert result["passed"] is True, f"commons should pass: {result['checks']}"
|
||||
|
||||
|
||||
def test_prax_entry_passes():
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(str(_AIPASS_ROOT / "prax" / "apps" / "prax.py"))
|
||||
assert result["passed"] is True, f"prax should pass: {result['checks']}"
|
||||
|
||||
|
||||
def test_flow_entry_passes():
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(str(_AIPASS_ROOT / "flow" / "apps" / "flow.py"))
|
||||
assert result["passed"] is True, f"flow should pass: {result['checks']}"
|
||||
|
||||
|
||||
def test_seedgo_entry_fails():
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(str(_AIPASS_ROOT / "seedgo" / "apps" / "seedgo.py"))
|
||||
assert result["passed"] is False, f"seedgo should fail (no subcommand guard): {result['checks']}"
|
||||
|
||||
|
||||
def test_ai_mail_entry_fails():
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.subcommand_help_check import check_module
|
||||
|
||||
result = check_module(str(_AIPASS_ROOT / "ai_mail" / "apps" / "ai_mail.py"))
|
||||
assert result["passed"] is False, f"ai_mail should fail: {result['checks']}"
|
||||
Reference in New Issue
Block a user