Merge pull request #453 from AIOSAI/system/devpulse-featseedgo-add-handlerimport-standard-34-ruff-form
feat(system): feat(seedgo): add handler_import standard #34 + ruff format enforcement in ruff_check v1.2.0
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user