From 948d2ed53551bb3a7f09a2d1b0ffef98e9f11249 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Fri, 10 Jul 2026 17:58:41 -0700 Subject: [PATCH] =?UTF-8?q?#685=20seedgo:=20subcommand=5Fhelp=20standard?= =?UTF-8?q?=20=E2=80=94=20enforces=20every=20entry=20point=20intercepts=20?= =?UTF-8?q?=20--help=20before=20dispatch=20(explicit=20guard=20or=20a?= =?UTF-8?q?rgparse=20parse=5Fknown=5Fargs),=20else=20=20--help=20exec?= =?UTF-8?q?utes=20the=20command.=2021=20tests,=20cwd-portable=20(=5FAIPASS?= =?UTF-8?q?=5FROOT=20anchor).=20Checker=20verified=20independently:=207/17?= =?UTF-8?q?=20comply,=2010=20offenders.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aipass_standards/subcommand_help_check.py | 279 ++++++++++++ .../subcommand_help_content.py | 75 ++++ .../seedgo/tests/test_subcommand_help.py | 397 ++++++++++++++++++ 3 files changed, 751 insertions(+) create mode 100644 src/aipass/seedgo/apps/handlers/aipass_standards/subcommand_help_check.py create mode 100644 src/aipass/seedgo/apps/handlers/aipass_standards/subcommand_help_content.py create mode 100644 src/aipass/seedgo/tests/test_subcommand_help.py diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/subcommand_help_check.py b/src/aipass/seedgo/apps/handlers/aipass_standards/subcommand_help_check.py new file mode 100644 index 00000000..2df69707 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/subcommand_help_check.py @@ -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 --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}() — " + " --help will execute the command or fall to top-level help" + ), + } + ], + "score": 0, + "standard": "SUBCOMMAND_HELP", + } diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/subcommand_help_content.py b/src/aipass/seedgo/apps/handlers/aipass_standards/subcommand_help_content.py new file mode 100644 index 00000000..4cc1423e --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/subcommand_help_content.py @@ -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] --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""" diff --git a/src/aipass/seedgo/tests/test_subcommand_help.py b/src/aipass/seedgo/tests/test_subcommand_help.py new file mode 100644 index 00000000..765cc10a --- /dev/null +++ b/src/aipass/seedgo/tests/test_subcommand_help.py @@ -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']}"