diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/handler_import.md b/src/aipass/seedgo/apps/handlers/aipass_standards/handler_import.md new file mode 100644 index 00000000..535543d5 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/handler_import.md @@ -0,0 +1,119 @@ +# Handler Import Standard +**Status:** Active v1 +**Date:** 2026-04-26 + +--- + +## Core Rule: Every apps/__init__.py Must Import Handlers + +Every AIPass branch with an `apps/` package must include `from . import handlers` in its `apps/__init__.py`. Without this explicit import, Python 3.10's `mock.patch` cannot resolve handler subpackages through the attribute chain. + +--- + +## Why This Matters + +**The problem:** `mock.patch("aipass.branch.apps.handlers.some_module")` traverses the dotted path using `getattr`. In Python 3.10, if `handlers` was never explicitly imported in `apps/__init__.py`, the `getattr(apps, "handlers")` call raises `AttributeError` -- even though the `handlers/` directory exists on disk with its own `__init__.py`. + +**What goes wrong without the import:** +1. **Tests silently break** -- `mock.patch` targeting handler submodules raises `AttributeError` instead of patching. +2. **CI false greens** -- if the test catches the exception or skips, violations slip through undetected. +3. **3.10 vs 3.11 divergence** -- Python 3.11+ is more lenient about lazy subpackage resolution. Code that works on 3.11 breaks on 3.10, which AIPass CI uses. + +**What the import does:** +```python +from . import handlers +``` +This single line forces Python to import and register the `handlers` subpackage on the `apps` namespace at import time. After that, `getattr(apps, "handlers")` succeeds and `mock.patch` can walk the full dotted path. + +--- + +## What the Checker Validates + +The handler_import standard checks **one thing**: does `apps/__init__.py` contain the string `from . import handlers`? + +- **Scope:** `AUDIT_SCOPE = "branch_level"` -- one check per branch +- **Pass:** The string `from . import handlers` is found in the file content -- score 100 +- **Fail:** The string is missing or the file does not exist -- score 0 + +--- + +## Examples + +**Good -- explicit handler import present:** +```python +# apps/__init__.py +from . import handlers +from . import modules +``` + +**Good -- import with trailing comment:** +```python +# apps/__init__.py +from . import handlers # required for mock.patch resolution +``` + +**Bad -- empty init:** +```python +# apps/__init__.py +``` + +**Bad -- modules imported but not handlers:** +```python +# apps/__init__.py +from . import modules +``` + +--- + +## Scoring + +| Condition | Score | +|-----------|-------| +| `from . import handlers` found in apps/__init__.py | 100 | +| Import missing from apps/__init__.py | 0 | +| apps/__init__.py not found | 0 | +| apps/__init__.py not readable | 0 | +| Bypassed via `.seedgo/bypass.json` | 100 | + +Binary pass/fail -- there is no partial credit. Either the import is there or it is not. + +--- + +## How to Fix + +Add the import line to `apps/__init__.py`. That is it. + +**Before:** +```python +# apps/__init__.py +from . import modules +``` + +**After:** +```python +# apps/__init__.py +from . import handlers +from . import modules +``` + +--- + +## Bypass + +If a branch genuinely does not have a `handlers/` subpackage (rare edge case), add a bypass rule in `.seedgo/bypass.json`: + +```json +{ + "standard": "handler_import", + "file": "apps/__init__.py" +} +``` + +--- + +## Summary + +1. **Every apps/__init__.py needs `from . import handlers`** -- required for mock.patch resolution on Python 3.10 +2. **One check per branch** -- scope is `branch_level` +3. **Binary scoring** -- 100 (import present) or 0 (import missing) +4. **Fix is one line** -- add `from . import handlers` to apps/__init__.py diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/handler_import_check.py b/src/aipass/seedgo/apps/handlers/aipass_standards/handler_import_check.py new file mode 100644 index 00000000..554af876 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/handler_import_check.py @@ -0,0 +1,183 @@ +# =================== AIPass ==================== +# Name: handler_import_check.py +# Description: Handler Import Standards Checker +# Version: 1.0.0 +# Created: 2026-04-26 +# Modified: 2026-04-26 +# ============================================= + +""" +Handler Import Standards Checker + +Validates that every branch's apps/__init__.py contains +``from . import handlers`` to ensure Python 3.10 mock.patch +can resolve handler subpackages correctly. + +Score: 100 (import present) or 0 (missing / file absent). +""" + +from pathlib import Path + +from aipass.prax import logger +from aipass.seedgo.apps.handlers.json import json_handler + +AUDIT_SCOPE = "branch_level" + + +# ============================================= +# BYPASS HELPER +# ============================================= + + +def is_bypassed( + file_path: str, + standard: str, + line: int | None = None, + bypass_rules: list | None = None, +) -> bool: + """Check if a violation should be bypassed.""" + if not bypass_rules: + return False + for rule in bypass_rules: + if rule.get("standard") and rule.get("standard") != standard: + continue + rule_file = rule.get("file", "") + if rule_file and rule_file not in file_path: + continue + rule_lines = rule.get("lines", []) + if rule_lines and line is not None and line not in rule_lines: + continue + return True + return False + + +# ============================================= +# BRANCH-LEVEL CHECK (audit pipeline entry) +# ============================================= + + +def check_branch(branch_path: str, bypass_rules: list | None = None) -> dict: + """ + Check that a branch's apps/__init__.py contains ``from . import handlers``. + + Args: + branch_path: Path to branch root (e.g., src/aipass/seedgo) + bypass_rules: Optional list of bypass rules to skip certain checks + + Returns: + dict: { + 'passed': bool, + 'score': int, + 'checks': [{'name': str, 'passed': bool, 'message': str}], + 'standard': 'HANDLER_IMPORT' + } + """ + bp = Path(branch_path) + + # Check if entire standard is bypassed + if is_bypassed(branch_path, "handler_import", bypass_rules=bypass_rules): + result = { + "passed": True, + "checks": [ + { + "name": "Bypassed", + "passed": True, + "message": "Standard bypassed via .seedgo/bypass.json", + } + ], + "score": 100, + "standard": "HANDLER_IMPORT", + } + json_handler.log_operation( + "check_completed", + {"branch": branch_path, "score": 100, "standard": "handler_import"}, + ) + return result + + init_file = bp / "apps" / "__init__.py" + + if not init_file.exists(): + message = "apps/__init__.py not found" + logger.info("handler_import check: %s in %s", message, branch_path) + result = { + "passed": False, + "checks": [ + { + "name": "Handler import present", + "passed": False, + "message": message, + } + ], + "score": 0, + "standard": "HANDLER_IMPORT", + } + json_handler.log_operation( + "check_completed", + {"branch": branch_path, "score": 0, "standard": "handler_import"}, + ) + return result + + # Read the file and check for the import line + try: + content = init_file.read_text(encoding="utf-8") + except OSError as exc: + logger.info("handler_import check: cannot read %s: %s", init_file, exc) + result = { + "passed": False, + "checks": [ + { + "name": "Handler import present", + "passed": False, + "message": f"Cannot read apps/__init__.py: {exc}", + } + ], + "score": 0, + "standard": "HANDLER_IMPORT", + } + json_handler.log_operation( + "check_completed", + {"branch": branch_path, "score": 0, "standard": "handler_import"}, + ) + return result + + if "from . import handlers" in content: + result = { + "passed": True, + "checks": [ + { + "name": "Handler import present", + "passed": True, + "message": ("apps/__init__.py contains 'from . import handlers'"), + } + ], + "score": 100, + "standard": "HANDLER_IMPORT", + } + else: + result = { + "passed": False, + "checks": [ + { + "name": "Handler import present", + "passed": False, + "message": ( + "apps/__init__.py is missing 'from . import handlers'" + " -- Python 3.10 mock.patch cannot resolve handler" + " subpackages without an explicit import in the" + " parent __init__.py" + ), + } + ], + "score": 0, + "standard": "HANDLER_IMPORT", + } + + json_handler.log_operation( + "check_completed", + { + "branch": branch_path, + "score": result["score"], + "standard": "handler_import", + }, + ) + return result diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/handler_import_content.py b/src/aipass/seedgo/apps/handlers/aipass_standards/handler_import_content.py new file mode 100644 index 00000000..0efb0e43 --- /dev/null +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/handler_import_content.py @@ -0,0 +1,90 @@ +# =================== AIPass ==================== +# Name: handler_import_content.py +# Description: Handler Import Standards Content Handler +# Version: 1.0.0 +# Created: 2026-04-26 +# Modified: 2026-04-26 +# ============================================= + +""" +Handler Import Standards Content Handler + +Provides formatted handler import standards content. +Module orchestrates, handler implements. +""" + +from aipass.seedgo.apps.handlers.json import json_handler + + +def get_handler_import_standards() -> str: + """Return formatted handler import standards content with Rich markup. + + Returns: + str: Formatted standards text with Rich styling + """ + lines = [ + "[bold red]HANDLER IMPORT STANDARD[/bold red]", + "", + "[bold cyan]CORE RULE:[/bold cyan] Every apps/__init__.py must contain 'from . import handlers'", + "", + "[yellow]RULE:[/yellow] Python 3.10 mock.patch resolution requires explicit subpackage imports in __init__.py.", + " Without 'from . import handlers', mock.patch() targeting handler" + " submodules fails with AttributeError at test time.", + "", + "─" * 70, + "", + "[bold cyan]WHY THIS MATTERS:[/bold cyan]", + "", + " [red]✗[/red] mock.patch('aipass.branch.apps.handlers.some_handler') fails silently", + " [red]✗[/red] Python 3.10 getattr resolution needs the subpackage pre-imported", + " [red]✗[/red] Tests pass on 3.11+ but break on 3.10 without the explicit import", + " [red]✗[/red] CI uses 3.10 -- broken patches mean false green tests", + "", + "─" * 70, + "", + "[bold cyan]WHAT IS CHECKED:[/bold cyan]", + "", + " [yellow]Scope:[/yellow] branch_level -- one check per branch", + " [yellow]Check:[/yellow] Does apps/__init__.py contain 'from . import handlers'?", + " [yellow]Pass:[/yellow] Import found → [green]score 100[/green]", + " [yellow]Fail:[/yellow] Import missing → [red]score 0[/red]", + "", + "─" * 70, + "", + "[bold cyan]EXAMPLES:[/bold cyan]", + "", + "[bold]Good (explicit handler import):[/bold]", + " [green]✓[/green] [dim]from . import handlers[/dim]", + " [green]✓[/green] [dim]from . import modules[/dim]", + " [green]✓[/green] [dim]from . import handlers # noqa[/dim]", + "", + "[bold]Bad (missing handler import):[/bold]", + " [red]✗[/red] [dim]# empty __init__.py[/dim]", + " [red]✗[/red] [dim]from . import modules # but no handlers[/dim]", + "", + "─" * 70, + "", + "[bold cyan]HOW TO FIX:[/bold cyan]", + "", + " Add this line to apps/__init__.py:", + "", + " [bold]from . import handlers[/bold]", + "", + " That's it. One line. The import ensures Python registers the", + " handlers subpackage on the apps namespace so mock.patch can", + " resolve it via getattr chain.", + "", + "─" * 70, + "", + "[bold cyan]SCORING:[/bold cyan]", + " [green]100[/green] - 'from . import handlers' found in apps/__init__.py", + " [red] 0[/red] - Import missing from apps/__init__.py", + " [red] 0[/red] - apps/__init__.py not found", + " [green]100[/green] - Bypassed via .seedgo/bypass.json", + "", + "[bold cyan]REFERENCE:[/bold cyan]", + " [dim]See: seedgo standards pack (handler_import)[/dim]", + ] + + json_handler.log_operation("standard_content_queried", {"standard": "handler_import"}) + return "\n".join(lines) diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/ruff_check.py b/src/aipass/seedgo/apps/handlers/aipass_standards/ruff_check.py index cdcc45c6..b127be53 100644 --- a/src/aipass/seedgo/apps/handlers/aipass_standards/ruff_check.py +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/ruff_check.py @@ -1,13 +1,15 @@ # =================== AIPass ==================== # Name: ruff_check.py -# Description: Ruff Linter Standards Checker Handler -# Version: 1.1.0 +# Description: Ruff Linter & Formatter Standards Checker Handler +# Version: 1.2.0 # Created: 2026-04-16 -# Modified: 2026-04-20 +# Modified: 2026-04-26 # ============================================= """ -Ruff Linter Standards Checker Handler +Ruff Linter & Formatter Standards Checker Handler + +Runs both ``ruff check`` (lint) and ``ruff format --check`` (formatting). Two modes: - check_branch(): runs ruff across entire apps/ tree (used by audit pipeline, @@ -186,32 +188,60 @@ def check_module(module_path: str, bypass_rules: list | None = None) -> Dict: active = [v for v in violations if not _is_ruff_bypassed(v, ruff_bypass)] count = len(active) - if count == 0: - json_handler.log_operation( - "check_completed", - {"file": module_path, "score": 100, "standard": "ruff_check"}, - ) - return { - "passed": True, - "checks": [{"name": "Ruff check", "passed": True, "message": "No ruff violations found"}], - "score": 100, - "standard": "RUFF_CHECK", - } + checks: list[dict] = [] + passed = True - top = active[:5] - msgs = [f"{v.get('code', '?')} L{v.get('location', {}).get('row', '?')}: {v.get('message', '?')[:80]}" for v in top] - suffix = f" (and {count - 5} more)" if count > 5 else "" - detail = f"{count} violation(s) — " + "; ".join(msgs) + suffix + if count == 0: + checks.append({"name": "Ruff lint", "passed": True, "message": "No ruff violations found"}) + else: + top = active[:5] + msgs = [ + f"{v.get('code', '?')} L{v.get('location', {}).get('row', '?')}: {v.get('message', '?')[:80]}" for v in top + ] + suffix = f" (and {count - 5} more)" if count > 5 else "" + detail = f"{count} violation(s) — " + "; ".join(msgs) + suffix + checks.append({"name": "Ruff lint", "passed": False, "message": detail}) + passed = False + + # --- ruff format --check --- + needs_format = False + try: + fmt_proc = subprocess.run( + ["ruff", "format", "--check", str(fp)], + capture_output=True, + text=True, + timeout=15, + ) + if fmt_proc.returncode != 0: + needs_format = True + except subprocess.TimeoutExpired: + logger.warning("ruff format --check timed out on %s", module_path) + except Exception as exc: + logger.warning("ruff format --check failed on %s: %s", module_path, exc) + + if needs_format: + checks.append({"name": "Ruff format", "passed": False, "message": f"{fp.name} needs ruff format"}) + passed = False + else: + checks.append({"name": "Ruff format", "passed": True, "message": "File is formatted"}) + + score = 100 if passed else 0 json_handler.log_operation( "check_completed", - {"file": module_path, "score": 0, "standard": "ruff_check", "violations": count}, + { + "file": module_path, + "score": score, + "standard": "ruff_check", + "violations": count, + "needs_format": needs_format, + }, ) return { - "passed": False, - "checks": [{"name": "Ruff check", "passed": False, "message": detail}], - "score": 0, + "passed": passed, + "checks": checks, + "score": score, "standard": "RUFF_CHECK", } @@ -320,9 +350,10 @@ def check_branch(branch_path: str, bypass_rules: list | None = None) -> Dict: count = len(active) score = _score_from_count(count) + checks: list[dict] = [] + if count == 0: - message = "No ruff violations found" - check_passed = True + checks.append({"name": "Ruff lint", "passed": True, "message": "No ruff violations found"}) else: top = active[:5] codes = ", ".join( @@ -331,16 +362,55 @@ def check_branch(branch_path: str, bypass_rules: list | None = None) -> Dict: ) suffix = f" (and {count - 5} more)" if count > 5 else "" message = f"{count} violation(s) — {codes}{suffix}" - check_passed = False + checks.append({"name": "Ruff lint", "passed": False, "message": message}) + + # --- ruff format --check (advisory) --- + fmt_files: list[str] = [] + try: + fmt_proc = subprocess.run( + ["ruff", "format", "--check", str(scan_target)], + capture_output=True, + text=True, + timeout=60, + ) + if fmt_proc.returncode != 0 and fmt_proc.stdout.strip(): + fmt_files = [line.strip() for line in fmt_proc.stdout.strip().splitlines() if line.strip()] + except subprocess.TimeoutExpired: + logger.warning("ruff format --check timed out on branch %s", branch_path) + except Exception as exc: + logger.warning("ruff format --check failed on branch %s: %s", branch_path, exc) + + fmt_count = len(fmt_files) + if fmt_count == 0: + checks.append({"name": "Ruff format", "passed": True, "message": "All files formatted"}) + else: + names = ", ".join(Path(f).name for f in fmt_files[:5]) + fmt_suffix = f" (and {fmt_count - 5} more)" if fmt_count > 5 else "" + checks.append( + { + "name": "Ruff format", + "passed": False, + "message": f"{fmt_count} file(s) need formatting — {names}{fmt_suffix}", + } + ) + # Penalise score: subtract 2 points per unformatted file, floor at 25 + score = max(25, score - fmt_count * 2) json_handler.log_operation( "check_completed", - {"branch": branch_path, "score": score, "standard": "ruff_check", "violations": count, "advisory": True}, + { + "branch": branch_path, + "score": score, + "standard": "ruff_check", + "violations": count, + "format_violations": fmt_count, + "advisory": True, + }, ) return { "passed": True, # Advisory: never blocks the audit - "checks": [{"name": "Ruff check", "passed": check_passed, "message": message}], + "checks": checks, "score": score, "standard": "RUFF_CHECK", "advisory": True, diff --git a/src/aipass/seedgo/tests/test_checkers_batch5.py b/src/aipass/seedgo/tests/test_checkers_batch5.py index 581401a5..d880f75a 100644 --- a/src/aipass/seedgo/tests/test_checkers_batch5.py +++ b/src/aipass/seedgo/tests/test_checkers_batch5.py @@ -45,6 +45,19 @@ def _ruff_proc(violations: list, returncode: int = 1) -> MagicMock: return proc +def _fmt_proc(unformatted_files: list[str] | None = None) -> MagicMock: + """Build a fake subprocess.CompletedProcess for ruff format --check.""" + proc = MagicMock() + if unformatted_files: + proc.stdout = "\n".join(unformatted_files) + "\n" + proc.returncode = 1 + else: + proc.stdout = "" + proc.returncode = 0 + proc.stderr = "" + return proc + + def _make_violation(filename: str, code: str = "F401", row: int = 1) -> dict: return { "filename": filename, @@ -139,11 +152,14 @@ class TestCheckBranch: """Branch with zero ruff violations scores 100.""" branch = _make_branch(tmp_path) clean_proc = _ruff_proc([], returncode=0) + fmt_clean = _fmt_proc() with patch( - "aipass.seedgo.apps.handlers.aipass_standards.ruff_check.shutil.which", return_value="/usr/bin/ruff" + "aipass.seedgo.apps.handlers.aipass_standards.ruff_check.shutil.which", + return_value="/usr/bin/ruff", ): with patch( - "aipass.seedgo.apps.handlers.aipass_standards.ruff_check.subprocess.run", return_value=clean_proc + "aipass.seedgo.apps.handlers.aipass_standards.ruff_check.subprocess.run", + side_effect=[clean_proc, fmt_clean], ): result = check_branch(str(branch)) assert result["score"] == 100 @@ -156,10 +172,15 @@ class TestCheckBranch: branch = _make_branch(tmp_path) violations = [_make_violation(str(branch / "apps" / "modules" / "x.py"), "F401", i) for i in range(10)] proc = _ruff_proc(violations, returncode=1) + fmt_clean = _fmt_proc() with patch( - "aipass.seedgo.apps.handlers.aipass_standards.ruff_check.shutil.which", return_value="/usr/bin/ruff" + "aipass.seedgo.apps.handlers.aipass_standards.ruff_check.shutil.which", + return_value="/usr/bin/ruff", ): - with patch("aipass.seedgo.apps.handlers.aipass_standards.ruff_check.subprocess.run", return_value=proc): + with patch( + "aipass.seedgo.apps.handlers.aipass_standards.ruff_check.subprocess.run", + side_effect=[proc, fmt_clean], + ): result = check_branch(str(branch)) assert result["score"] == 85 # 10 violations → 6–20 band assert result["passed"] is True # advisory: always True @@ -192,14 +213,19 @@ class TestCheckBranch: _make_violation(v_path, "E501", 10), ] proc = _ruff_proc(violations, returncode=1) + fmt_clean = _fmt_proc() # Bypass the F401 bypass_file = branch / ".seedgo" / "ruff_bypass.json" bypass_file.parent.mkdir(parents=True, exist_ok=True) bypass_file.write_text(json.dumps([{"file": "thing.py", "code": "F401"}])) with patch( - "aipass.seedgo.apps.handlers.aipass_standards.ruff_check.shutil.which", return_value="/usr/bin/ruff" + "aipass.seedgo.apps.handlers.aipass_standards.ruff_check.shutil.which", + return_value="/usr/bin/ruff", ): - with patch("aipass.seedgo.apps.handlers.aipass_standards.ruff_check.subprocess.run", return_value=proc): + with patch( + "aipass.seedgo.apps.handlers.aipass_standards.ruff_check.subprocess.run", + side_effect=[proc, fmt_clean], + ): result = check_branch(str(branch)) # Only 1 active violation (E501), score should be 95 assert result["score"] == 95 diff --git a/src/aipass/seedgo/tests/test_handler_import.py b/src/aipass/seedgo/tests/test_handler_import.py new file mode 100644 index 00000000..d9040a36 --- /dev/null +++ b/src/aipass/seedgo/tests/test_handler_import.py @@ -0,0 +1,169 @@ +"""Tests for handler_import_check.""" + +# =================== META ==================== +# Name: test_handler_import.py +# Description: Unit tests for handler_import_check checker handler +# Version: 1.0.0 +# Created: 2026-04-26 +# Modified: 2026-04-26 +# ============================================= + +import sys +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _write_file(path: Path, content: str) -> None: + """Write content to a file, creating parent dirs as needed.""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def _mock_infrastructure(monkeypatch): + """Mock heavy infrastructure imports for the handler_import checker.""" + mock_logger = MagicMock() + mock_json_handler = MagicMock() + mock_json_handler.log_operation = MagicMock(return_value=True) + + # -- prax --------------------------------------------------------------- + prax_mod = MagicMock() + prax_mod.logger = mock_logger + monkeypatch.setitem(sys.modules, "aipass.prax", prax_mod) + + # -- seedgo json handler ------------------------------------------------ + 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 handler ----------------------------------------------------- + bypass_pkg = MagicMock() + bypass_ignore = MagicMock() + bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[]) + bypass_pkg.ignore_handler = bypass_ignore + monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass", bypass_pkg) + monkeypatch.setitem( + sys.modules, + "aipass.seedgo.apps.handlers.bypass.ignore_handler", + bypass_ignore, + ) + + # Force re-import so checker picks up fresh mocks + monkeypatch.delitem( + sys.modules, + "aipass.seedgo.apps.handlers.aipass_standards.handler_import_check", + raising=False, + ) + monkeypatch.delitem( + sys.modules, + "aipass.seedgo.apps.handlers.aipass_standards.handler_import_content", + raising=False, + ) + + +# =========================================================================== +# handler_import_check tests +# =========================================================================== + + +class TestHandlerImportCheck: + """Tests for the handler_import_check checker.""" + + def test_branch_with_handler_import_passes(self, tmp_path: Path) -> None: + """apps/__init__.py containing 'from . import handlers' scores 100.""" + _write_file( + tmp_path / "apps" / "__init__.py", + "from . import handlers\n", + ) + from aipass.seedgo.apps.handlers.aipass_standards.handler_import_check import ( + check_branch, + ) + + result = check_branch(str(tmp_path)) + assert result["passed"] is True + assert result["score"] == 100 + assert result["standard"] == "HANDLER_IMPORT" + + def test_branch_missing_handler_import_fails(self, tmp_path: Path) -> None: + """apps/__init__.py without the handler import scores 0.""" + _write_file( + tmp_path / "apps" / "__init__.py", + "from . import modules\n", + ) + from aipass.seedgo.apps.handlers.aipass_standards.handler_import_check import ( + check_branch, + ) + + result = check_branch(str(tmp_path)) + assert result["passed"] is False + assert result["score"] == 0 + assert result["standard"] == "HANDLER_IMPORT" + failed = [c for c in result["checks"] if not c["passed"]] + assert len(failed) == 1 + assert "missing" in failed[0]["message"] + + def test_branch_no_apps_init_fails(self, tmp_path: Path) -> None: + """Branch with no apps/__init__.py at all scores 0.""" + # Create apps/ directory but no __init__.py + (tmp_path / "apps").mkdir(parents=True) + from aipass.seedgo.apps.handlers.aipass_standards.handler_import_check import ( + check_branch, + ) + + result = check_branch(str(tmp_path)) + assert result["passed"] is False + assert result["score"] == 0 + assert "not found" in result["checks"][0]["message"] + + def test_branch_bypassed(self, tmp_path: Path) -> None: + """Bypass rule matches produce score 100.""" + # No apps/ at all -- bypass should still pass + from aipass.seedgo.apps.handlers.aipass_standards.handler_import_check import ( + check_branch, + ) + + bypass = [{"standard": "handler_import"}] + result = check_branch(str(tmp_path), bypass_rules=bypass) + assert result["passed"] is True + assert result["score"] == 100 + + def test_branch_no_apps_dir(self, tmp_path: Path) -> None: + """Branch with no apps/ directory at all scores 0.""" + from aipass.seedgo.apps.handlers.aipass_standards.handler_import_check import ( + check_branch, + ) + + result = check_branch(str(tmp_path)) + assert result["passed"] is False + assert result["score"] == 0 + assert "not found" in result["checks"][0]["message"] + + def test_content_returns_string(self) -> None: + """handler_import_content.get_handler_import_standards() returns non-empty string.""" + from aipass.seedgo.apps.handlers.aipass_standards.handler_import_content import ( + get_handler_import_standards, + ) + + content = get_handler_import_standards() + assert isinstance(content, str) + assert len(content) > 0 + assert "HANDLER IMPORT" in content