fix(seedgo): AST checker accuracy arc — 13 false positives gone fleet-wide, 2 legit hooks bypasses documented, checkers/audit/checklist aligned, fixture refreshed, stale bypass entries purged (devpulse/memory/seedgo). Fleet audit 100 pct. S330 night arc
This commit is contained in:
@@ -11,6 +11,14 @@ PyPI version — not the changelog header.
|
||||
|
||||
## [2026-07-21]
|
||||
|
||||
**fix(seedgo)** — checker accuracy arc (S330): AST-based import analysis
|
||||
lands in the checkers (dead_code, encapsulation, handlers, readme,
|
||||
test_quality, unused_function) — 13 false positives eliminated fleet-wide,
|
||||
2 real hooks imports that legitimately bypass the pattern documented instead
|
||||
of suppressed. branch_audit, checklist and ignore_handler aligned; provider
|
||||
hooks snapshot fixture refreshed; stale bypass entries for deleted tools
|
||||
purged across branches (devpulse, memory, seedgo, hooks). Fleet audit 100%.
|
||||
|
||||
**feat(hooks)** — hook sound layer + temporal grounding. Sounds now mirror
|
||||
the log across the hook fleet (prompt, lifecycle, notification, security
|
||||
handlers) — audible liveness for the whole layer, verified live (2465 green,
|
||||
|
||||
@@ -70,71 +70,6 @@
|
||||
"file": "tests/test_owner_guard.py",
|
||||
"reason": "Unit test imports the owner guard handler (its SUT) and patches spawn.registry's get_owner/is_owner — the guard is a shared handler primitive with no apps/modules/ command entry point. Same direct-SUT pattern as test_compass_store.py. #681."
|
||||
},
|
||||
{
|
||||
"standard": "encapsulation",
|
||||
"file": "tools/spot_check.py",
|
||||
"reason": "Standalone diagnostic tool script, not a handler."
|
||||
},
|
||||
{
|
||||
"standard": "debug_print",
|
||||
"file": "tools/spot_check.py",
|
||||
"reason": "Standalone CLI tool — print() is the intended output method."
|
||||
},
|
||||
{
|
||||
"standard": "silent_catch",
|
||||
"file": "tools/hook_engine_poc/engine.py",
|
||||
"reason": "POC hook engine — stdlib only, no prax logger. Uses sys.stderr for error reporting."
|
||||
},
|
||||
{
|
||||
"standard": "error_handling",
|
||||
"file": "tools/hook_engine_poc/engine.py",
|
||||
"reason": "POC hook engine — exception handlers write to stderr, not prax logger."
|
||||
},
|
||||
{
|
||||
"standard": "silent_catch",
|
||||
"file": "tools/hook_engine_poc/test_engine.py",
|
||||
"reason": "POC test harness — catches test exceptions to report pass/fail, writes to stderr."
|
||||
},
|
||||
{
|
||||
"standard": "imports",
|
||||
"file": "tools/hook_engine_poc/test_engine.py",
|
||||
"reason": "POC test harness — sys.path needed to import engine.py from same directory."
|
||||
},
|
||||
{
|
||||
"standard": "trigger",
|
||||
"file": "tools/hook_engine_poc/test_engine.py",
|
||||
"reason": "POC test harness — .unlink() clears ephemeral JSONL log between tests, not a tracked file."
|
||||
},
|
||||
{
|
||||
"standard": "help_text",
|
||||
"file": "tools/hook_engine_poc/test_engine.py",
|
||||
"reason": "POC test harness — usage example in docstring."
|
||||
},
|
||||
{
|
||||
"standard": "debug_print",
|
||||
"file": "tools/rm_shim/redteam_suite.py",
|
||||
"reason": "Standalone red-team diagnostic runner (FPLAN-0250 Phase 6) — print() IS the report output, same as broker_acceptance_test.py."
|
||||
},
|
||||
{
|
||||
"standard": "encapsulation",
|
||||
"file": "tools/rm_shim/redteam_suite.py",
|
||||
"reason": "Red-team tool imports the real broker daemon/client + sandbox module directly to exercise them under live conditions — that is the point of an integration probe, not a handler."
|
||||
},
|
||||
{
|
||||
"standard": "imports",
|
||||
"file": "tools/rm_shim/redteam_suite.py",
|
||||
"reason": "Standalone script run via 'python tools/...' — sys.path insert lets it import the production modules it red-teams without being pip-installed."
|
||||
},
|
||||
{
|
||||
"standard": "help_text",
|
||||
"file": "tools/rm_shim/redteam_suite.py",
|
||||
"reason": "Diagnostic script — docstring shows the 'python tools/...' invocation; it is not a drone-routed module."
|
||||
},
|
||||
{
|
||||
"standard": "documentation",
|
||||
"file": "tools/rm_shim/redteam_suite.py",
|
||||
"reason": "Result.ok/bad are 2-line internal report helpers in a diagnostic script — self-evident, docstrings redundant."
|
||||
},
|
||||
{
|
||||
"standard": "encapsulation",
|
||||
"file": "tests/test_compass_store.py",
|
||||
|
||||
@@ -759,31 +759,6 @@
|
||||
"standard": "meta",
|
||||
"reason": "Test file — META block present at lines 1-7; hook false-positive on test file format."
|
||||
},
|
||||
{
|
||||
"file": "tools/migrate_entries.py",
|
||||
"standard": "architecture",
|
||||
"reason": "Standalone utility script in tools/ — intentionally outside 3-layer apps/ structure."
|
||||
},
|
||||
{
|
||||
"file": "tools/strip_trinity_limits.py",
|
||||
"standard": "architecture",
|
||||
"reason": "One-time migration script in tools/ — intentionally outside 3-layer apps/ structure."
|
||||
},
|
||||
{
|
||||
"file": "tools/strip_trinity_limits.py",
|
||||
"standard": "log_visibility",
|
||||
"reason": "Standalone script — cannot import prax logger. Uses stdlib logging."
|
||||
},
|
||||
{
|
||||
"file": "tools/migrate_entries.py",
|
||||
"standard": "silent_catch",
|
||||
"reason": "Standalone script — cannot import prax logger. Errors reported via stderr print + result dict."
|
||||
},
|
||||
{
|
||||
"file": "tools/migrate_entries.py",
|
||||
"standard": "debug_print",
|
||||
"reason": "Standalone script — print(stderr) is the error reporting mechanism. No prax available."
|
||||
},
|
||||
{
|
||||
"file": "tests/test_detector.py",
|
||||
"standard": "architecture",
|
||||
|
||||
@@ -195,11 +195,6 @@
|
||||
"standard": "deep_nesting",
|
||||
"reason": "scan() depth 6 — state machine tracking file types across standards with nested conditionals, inherent to triplet completeness detection"
|
||||
},
|
||||
{
|
||||
"file": "tools/triplet_scanner.py",
|
||||
"standard": "deep_nesting",
|
||||
"reason": "scan() depth 6 — state machine tracking file types across standards with nested conditionals, inherent to triplet completeness detection"
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/aipass_standards/introspection_check.py",
|
||||
"standard": "deep_nesting",
|
||||
|
||||
@@ -120,9 +120,9 @@ seedgo/
|
||||
│ │ ├── branch_audit.py # Per-branch scoring engine
|
||||
│ │ ├── discovery.py # Branch discovery (CWD-first registry)
|
||||
│ │ └── audit_display.py # Rich result formatting
|
||||
│ ├── bypass/ # Bypass system
|
||||
│ ├── bypass/ # Bypass + ignore systems
|
||||
│ │ ├── bypass_handler.py # .seedgo/bypass.json loader
|
||||
│ │ └── ignore_handler.py # .seedgo/ignore patterns
|
||||
│ │ └── ignore_handler.py # Audit ignore patterns + .seedgoignore engine
|
||||
│ ├── config/ # Configuration handlers
|
||||
│ ├── diagnostics/ # Pyright integration + branch discovery
|
||||
│ ├── json/ # JSON tracking (json_handler)
|
||||
@@ -146,6 +146,8 @@ seedgo/
|
||||
|
||||
**Bypass system:** `.seedgo/bypass.json` per branch. Each entry has file, standard, optional lines, and required reason. Checkers call `is_bypassed()` per violation. Bypass is intentional documented deviation, not ignoring.
|
||||
|
||||
**`.seedgoignore` (throwaway paths, no reason required):** Drop a `.seedgoignore` file into any directory to exclude matching files/dirs from scans, audits, and the per-file checklist — same gitignore-style patterns and per-directory nesting semantics as a real `.gitignore` (via `pathspec`). Scope is exactly that directory's subtree; a nested `.seedgoignore` adds further excludes on top of any ancestor's, it doesn't replace them. A global default (`tools/`) applies fleet-wide with zero setup, since every branch's `apps/tools/` is deliberate throwaway prototyping space — quick scripts for fast answers, not standards-compliant by design. Unlike bypass (documented exception to a specific standard on a specific file), `.seedgoignore` removes the file from consideration entirely and needs no reason. It does **not** touch diagnostics (ruff/pyright) — those keep running on ignored files so auto-fix still catches real errors while you write; only standards checks skip them. See `ignore_handler.load_ignore_entries()` / `is_seedgo_ignored()`.
|
||||
|
||||
---
|
||||
|
||||
## The 40 Standards
|
||||
|
||||
@@ -24,6 +24,7 @@ from pathlib import Path
|
||||
from aipass.prax import logger
|
||||
from aipass.seedgo.apps.handlers.json import json_handler
|
||||
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import is_seedgo_ignored, load_ignore_entries
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.skip_dirs import SOURCE_SKIP_DIRS, is_disabled_file
|
||||
|
||||
AUDIT_SCOPE = "branch_level"
|
||||
@@ -42,12 +43,17 @@ _SKIP_DIRS = SOURCE_SKIP_DIRS | {"tests", "json_templates"}
|
||||
# =============================================
|
||||
|
||||
|
||||
def _should_skip(path: Path) -> bool:
|
||||
"""Check whether any parent directory component is in the skip set or file is disabled."""
|
||||
return any(part in _SKIP_DIRS for part in path.parts) or is_disabled_file(path.name)
|
||||
def _should_skip(path: Path, branch_root: Path, ignore_entries: list) -> bool:
|
||||
"""Check whether any parent directory component is in the skip set, file is
|
||||
disabled, or the path is excluded via .seedgoignore / the global default."""
|
||||
return (
|
||||
any(part in _SKIP_DIRS for part in path.parts)
|
||||
or is_disabled_file(path.name)
|
||||
or is_seedgo_ignored(str(path), branch_root, ignore_entries)
|
||||
)
|
||||
|
||||
|
||||
def _collect_scannable_files(apps_dir: Path) -> list[Path]:
|
||||
def _collect_scannable_files(apps_dir: Path, branch_root: Path, ignore_entries: list) -> list[Path]:
|
||||
"""
|
||||
Collect .py files from apps/modules/ and apps/handlers/ that should be
|
||||
checked for usage. Skips __init__.py, __pycache__, .archive, etc.
|
||||
@@ -60,20 +66,20 @@ def _collect_scannable_files(apps_dir: Path) -> list[Path]:
|
||||
for py_file in subdir.rglob("*.py"):
|
||||
if py_file.name == "__init__.py":
|
||||
continue
|
||||
if _should_skip(py_file):
|
||||
if _should_skip(py_file, branch_root, ignore_entries):
|
||||
continue
|
||||
targets.append(py_file)
|
||||
return sorted(targets)
|
||||
|
||||
|
||||
def _collect_source_text(apps_dir: Path) -> str:
|
||||
def _collect_source_text(apps_dir: Path, branch_root: Path, ignore_entries: list) -> str:
|
||||
"""
|
||||
Read ALL .py files under apps/ into a single string for searching.
|
||||
This is the corpus we search for references.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
for py_file in apps_dir.rglob("*.py"):
|
||||
if _should_skip(py_file):
|
||||
if _should_skip(py_file, branch_root, ignore_entries):
|
||||
continue
|
||||
try:
|
||||
parts.append(py_file.read_text(encoding="utf-8", errors="ignore"))
|
||||
@@ -266,7 +272,8 @@ def check_branch(branch_path: str, bypass_rules: list | None = None) -> dict:
|
||||
entry_point_name = branch_name
|
||||
|
||||
# Collect scannable files
|
||||
targets = _collect_scannable_files(apps_dir)
|
||||
ignore_entries = load_ignore_entries(bp)
|
||||
targets = _collect_scannable_files(apps_dir, bp, ignore_entries)
|
||||
if not targets:
|
||||
result = {
|
||||
"passed": True,
|
||||
@@ -287,7 +294,7 @@ def check_branch(branch_path: str, bypass_rules: list | None = None) -> dict:
|
||||
return result
|
||||
|
||||
# Build the source corpus (all .py content from apps/)
|
||||
source_text = _collect_source_text(apps_dir)
|
||||
source_text = _collect_source_text(apps_dir, bp, ignore_entries)
|
||||
|
||||
# Check each target for references
|
||||
total_files = len(targets)
|
||||
|
||||
@@ -28,6 +28,11 @@ from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed
|
||||
# Audit scope: all Python files
|
||||
AUDIT_SCOPE = "all_files"
|
||||
|
||||
# Cross-branch/cross-package handler imports are import-statement checks, so a
|
||||
# violation hiding in a package marker file must not be invisible to the audit
|
||||
# the way it would be for content checkers (dead code, naming, etc.).
|
||||
INCLUDE_INIT_FILES = True
|
||||
|
||||
|
||||
def _find_registry() -> Path:
|
||||
"""Find AIPASS_REGISTRY.json by walking up from this file's location."""
|
||||
|
||||
@@ -13,9 +13,10 @@ Validates handler compliance with AIPass handler standards.
|
||||
Checks handler independence, auto-detection pattern, no orchestration.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, Iterator, List, Optional, Tuple
|
||||
|
||||
from aipass.prax import logger
|
||||
from aipass.seedgo.apps.handlers.json import json_handler
|
||||
@@ -24,6 +25,11 @@ from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed
|
||||
# Audit scope: all Python files
|
||||
AUDIT_SCOPE = "all_files"
|
||||
|
||||
# Cross-handler/orchestration imports are purely import-statement checks, so a
|
||||
# violation hiding in a package marker file must not be invisible to the audit
|
||||
# the way it would be for content checkers (dead code, naming, etc.).
|
||||
INCLUDE_INIT_FILES = True
|
||||
|
||||
|
||||
def check_module(module_path: str, bypass_rules: list | None = None) -> Dict:
|
||||
"""
|
||||
@@ -121,6 +127,39 @@ def check_module(module_path: str, bypass_rules: list | None = None) -> Dict:
|
||||
return {"passed": overall_passed, "checks": checks, "score": score, "standard": "HANDLERS"}
|
||||
|
||||
|
||||
def _iter_import_modules(content: str) -> Iterator[Tuple[int, str, List[str]]]:
|
||||
"""
|
||||
Parse content and yield (line_number, dotted_module, imported_names) for every
|
||||
real import statement — string literals, comments and docstrings can never
|
||||
produce a hit because the AST only contains actual import nodes.
|
||||
|
||||
Relative imports (from . import x) are omitted entirely: they can't reference
|
||||
an absolute dotted path like "apps.handlers"/"apps.modules" and are always
|
||||
same-package, so callers don't need to special-case them.
|
||||
"""
|
||||
try:
|
||||
tree = ast.parse(content)
|
||||
except SyntaxError as e:
|
||||
logger.info("Skipping import scan: SyntaxError during parse: %s", e)
|
||||
return
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
if node.level:
|
||||
continue
|
||||
yield node.lineno, node.module or "", [alias.name for alias in node.names]
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
yield node.lineno, alias.name, []
|
||||
|
||||
|
||||
def _line_text(lines: List[str], lineno: int, fallback: str) -> str:
|
||||
"""Return the source line at 1-indexed lineno, or a fallback if out of range."""
|
||||
if 0 < lineno <= len(lines):
|
||||
return lines[lineno - 1].strip()
|
||||
return fallback
|
||||
|
||||
|
||||
def check_handler_independence(content: str, lines: List[str], module_path: str) -> Dict:
|
||||
"""
|
||||
Check handler independence - no cross-handler imports except defaults
|
||||
@@ -141,58 +180,26 @@ def check_handler_independence(content: str, lines: List[str], module_path: str)
|
||||
own_package = path_parts[i + 1]
|
||||
break
|
||||
|
||||
in_docstring = False
|
||||
for i, line in enumerate(lines, 1):
|
||||
stripped = line.strip()
|
||||
|
||||
# Track docstrings (handle both single-line and multi-line)
|
||||
if stripped.startswith('"""') or stripped.startswith("'''"):
|
||||
quote_marker = '"""' if stripped.startswith('"""') else "'''"
|
||||
# Count occurrences of the quote marker
|
||||
quote_count = stripped.count(quote_marker)
|
||||
if quote_count >= 2:
|
||||
# Single-line docstring (opening and closing on same line)
|
||||
continue # Skip this line but don't toggle state
|
||||
else:
|
||||
# Multi-line docstring boundary
|
||||
in_docstring = not in_docstring
|
||||
|
||||
# Skip docstrings, comments and empty lines
|
||||
if in_docstring or not stripped or stripped.startswith("#"):
|
||||
for lineno, module, names in _iter_import_modules(content):
|
||||
if "apps.handlers" not in module:
|
||||
continue
|
||||
|
||||
# Check for handler imports
|
||||
if "apps.handlers" in stripped and ("from " in stripped or "import " in stripped):
|
||||
# Skip if in a string (rough check)
|
||||
if '"from ' in stripped or "'from " in stripped:
|
||||
continue
|
||||
# Allowed: Default handlers (json_handler)
|
||||
if module.endswith("handlers.json") and "json_handler" in names:
|
||||
continue
|
||||
|
||||
# Extract code part (before comment)
|
||||
code_part = stripped.split("#")[0] if "#" in stripped else stripped
|
||||
# Allowed: Same package absolute imports
|
||||
if own_package and f"handlers.{own_package}" in module:
|
||||
continue
|
||||
|
||||
if "apps.handlers" not in code_part:
|
||||
continue
|
||||
|
||||
# Allowed: Default handlers (json_handler)
|
||||
if "handlers.json import json_handler" in code_part:
|
||||
continue
|
||||
|
||||
# Allowed: Same package imports (relative imports like "from .decorators")
|
||||
if code_part.strip().startswith("from ."):
|
||||
continue
|
||||
|
||||
# Allowed: Same package absolute imports
|
||||
if own_package and f"handlers.{own_package}" in code_part:
|
||||
continue
|
||||
|
||||
# Forbidden: Cross-handler imports
|
||||
forbidden_imports.append(f"line {i}: {stripped}")
|
||||
# Forbidden: Cross-handler imports
|
||||
forbidden_imports.append(f"line {lineno}: {_line_text(lines, lineno, module)}")
|
||||
|
||||
if forbidden_imports:
|
||||
return {
|
||||
"name": "Handler independence",
|
||||
"passed": False,
|
||||
"message": f"Cross-handler imports detected (except defaults): {forbidden_imports[0]}",
|
||||
"message": "Cross-handler imports detected (except defaults): " + "; ".join(forbidden_imports),
|
||||
}
|
||||
|
||||
return {"name": "Handler independence", "passed": True, "message": "No forbidden cross-handler imports detected"}
|
||||
@@ -239,50 +246,22 @@ def check_no_orchestration(content: str, lines: List[str]) -> Optional[Dict]:
|
||||
"""
|
||||
module_imports = []
|
||||
|
||||
in_docstring = False
|
||||
for i, line in enumerate(lines, 1):
|
||||
stripped = line.strip()
|
||||
|
||||
# Track docstrings (handle both single-line and multi-line)
|
||||
if stripped.startswith('"""') or stripped.startswith("'''"):
|
||||
quote_marker = '"""' if stripped.startswith('"""') else "'''"
|
||||
# Count occurrences of the quote marker
|
||||
quote_count = stripped.count(quote_marker)
|
||||
if quote_count >= 2:
|
||||
# Single-line docstring (opening and closing on same line)
|
||||
continue # Skip this line but don't toggle state
|
||||
else:
|
||||
# Multi-line docstring boundary
|
||||
in_docstring = not in_docstring
|
||||
|
||||
# Skip docstrings, comments and empty lines
|
||||
if in_docstring or not stripped or stripped.startswith("#"):
|
||||
for lineno, module, _names in _iter_import_modules(content):
|
||||
if "apps.modules" not in module:
|
||||
continue
|
||||
|
||||
# Check for module imports
|
||||
if "apps.modules" in stripped and ("from " in stripped or "import " in stripped):
|
||||
# Skip if in a string
|
||||
if '"from ' in stripped or "'from " in stripped:
|
||||
continue
|
||||
# Allowed: Service imports (prax.apps.modules.logger, cli.apps.modules)
|
||||
if "prax.apps.modules.logger" in module or "cli.apps.modules" in module:
|
||||
continue
|
||||
|
||||
# Extract code part
|
||||
code_part = stripped.split("#")[0] if "#" in stripped else stripped
|
||||
|
||||
if "apps.modules" not in code_part:
|
||||
continue
|
||||
|
||||
# Allowed: Service imports (prax.apps.modules.logger, cli.apps.modules)
|
||||
if "prax.apps.modules.logger" in code_part or "cli.apps.modules" in code_part:
|
||||
continue
|
||||
|
||||
# Forbidden: Module imports (orchestration)
|
||||
module_imports.append(f"line {i}: {stripped}")
|
||||
# Forbidden: Module imports (orchestration)
|
||||
module_imports.append(f"line {lineno}: {_line_text(lines, lineno, module)}")
|
||||
|
||||
if module_imports:
|
||||
return {
|
||||
"name": "No orchestration",
|
||||
"passed": False,
|
||||
"message": f"Handler imports modules (orchestration): {module_imports[0]}",
|
||||
"message": "Handler imports modules (orchestration): " + "; ".join(module_imports),
|
||||
}
|
||||
|
||||
return {"name": "No orchestration", "passed": True, "message": "No module imports detected (pure implementation)"}
|
||||
|
||||
@@ -31,6 +31,7 @@ from aipass.prax import logger
|
||||
from aipass.seedgo.apps.handlers.json import json_handler
|
||||
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.skip_dirs import SOURCE_SKIP_DIRS, is_disabled_file
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import is_seedgo_ignored, load_ignore_entries
|
||||
|
||||
# Audit scope: entry points only (apps/{name}.py)
|
||||
AUDIT_SCOPE = "entry_point"
|
||||
@@ -503,12 +504,16 @@ def _extract_test_counts(content: str) -> List[int]:
|
||||
def _count_test_functions(tests_dir: Path) -> int:
|
||||
"""Count `def test_` functions in all test_*.py files under tests/."""
|
||||
count = 0
|
||||
branch_root = tests_dir.parent
|
||||
ignore_entries = load_ignore_entries(branch_root)
|
||||
test_func_pattern = re.compile(r"^\s*def\s+test_", re.MULTILINE)
|
||||
for test_file in tests_dir.rglob("test_*.py"):
|
||||
if any(part in SOURCE_SKIP_DIRS for part in test_file.relative_to(tests_dir).parts):
|
||||
continue
|
||||
if is_disabled_file(test_file.name):
|
||||
continue
|
||||
if is_seedgo_ignored(str(test_file), branch_root, ignore_entries):
|
||||
continue
|
||||
try:
|
||||
source = test_file.read_text(encoding="utf-8")
|
||||
count += len(test_func_pattern.findall(source))
|
||||
|
||||
@@ -30,6 +30,7 @@ from aipass.prax import logger
|
||||
from aipass.seedgo.apps.handlers.json import json_handler
|
||||
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.skip_dirs import SOURCE_SKIP_DIRS, is_disabled_file
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import is_seedgo_ignored, load_ignore_entries
|
||||
|
||||
AUDIT_SCOPE = "branch_level"
|
||||
|
||||
@@ -180,7 +181,7 @@ def _should_skip_file(name: str) -> bool:
|
||||
return is_disabled_file(name)
|
||||
|
||||
|
||||
def _find_test_files_broad(branch_path: Path) -> list[Path]:
|
||||
def _find_test_files_broad(branch_path: Path, ignore_entries: list) -> list[Path]:
|
||||
"""Find all test files for module coverage analysis.
|
||||
|
||||
Broader than _find_all_test_files — also finds scattered test files
|
||||
@@ -197,6 +198,8 @@ def _find_test_files_broad(branch_path: Path) -> list[Path]:
|
||||
continue
|
||||
if any(_should_skip_dir(part) for part in py_file.relative_to(tests_dir).parts):
|
||||
continue
|
||||
if is_seedgo_ignored(str(py_file), branch_path, ignore_entries):
|
||||
continue
|
||||
resolved = py_file.resolve()
|
||||
if resolved not in seen:
|
||||
seen.add(resolved)
|
||||
@@ -208,6 +211,8 @@ def _find_test_files_broad(branch_path: Path) -> list[Path]:
|
||||
continue
|
||||
if py_file.name in ("__init__.py", "conftest.py") or _should_skip_file(py_file.name):
|
||||
continue
|
||||
if is_seedgo_ignored(str(py_file), branch_path, ignore_entries):
|
||||
continue
|
||||
if py_file.name.startswith("test_") or py_file.name.endswith("_test.py"):
|
||||
resolved = py_file.resolve()
|
||||
if resolved not in seen:
|
||||
@@ -234,7 +239,7 @@ def _analyze_test_file_imports(source: str) -> set[str]:
|
||||
return tested_modules
|
||||
|
||||
|
||||
def _collect_testable_modules(branch_path: Path) -> set[str]:
|
||||
def _collect_testable_modules(branch_path: Path, ignore_entries: list) -> set[str]:
|
||||
"""Collect module names from apps/modules/ and apps/handlers/.
|
||||
|
||||
Returns set of module names:
|
||||
@@ -255,17 +260,21 @@ def _collect_testable_modules(branch_path: Path) -> set[str]:
|
||||
and item.suffix == ".py"
|
||||
and item.name != "__init__.py"
|
||||
and not _should_skip_file(item.name)
|
||||
and not is_seedgo_ignored(str(item), branch_path, ignore_entries)
|
||||
):
|
||||
modules.add(item.stem)
|
||||
|
||||
handlers_dir = apps_dir / "handlers"
|
||||
if handlers_dir.is_dir():
|
||||
for item in sorted(handlers_dir.iterdir()):
|
||||
if _should_skip_dir(item.name):
|
||||
if _should_skip_dir(item.name) or is_seedgo_ignored(str(item), branch_path, ignore_entries):
|
||||
continue
|
||||
if item.is_dir() and item.name != "__pycache__":
|
||||
has_py = any(
|
||||
f.suffix == ".py" and f.name != "__init__.py" and not _should_skip_file(f.name)
|
||||
f.suffix == ".py"
|
||||
and f.name != "__init__.py"
|
||||
and not _should_skip_file(f.name)
|
||||
and not is_seedgo_ignored(str(f), branch_path, ignore_entries)
|
||||
for f in item.iterdir()
|
||||
if f.is_file()
|
||||
)
|
||||
@@ -282,7 +291,7 @@ def _collect_testable_modules(branch_path: Path) -> set[str]:
|
||||
return modules
|
||||
|
||||
|
||||
def _find_all_test_files(branch_path: Path) -> list[Path]:
|
||||
def _find_all_test_files(branch_path: Path, ignore_entries: list) -> list[Path]:
|
||||
"""Find all test files and conftest.py in the branch's tests/ directory.
|
||||
|
||||
Scans for any test_*.py file plus conftest.py -- no naming requirements.
|
||||
@@ -295,6 +304,8 @@ def _find_all_test_files(branch_path: Path) -> list[Path]:
|
||||
for p in sorted(tests_dir.iterdir()):
|
||||
if not p.is_file() or p.suffix != ".py":
|
||||
continue
|
||||
if is_seedgo_ignored(str(p), branch_path, ignore_entries):
|
||||
continue
|
||||
if p.name.startswith("test_") or p.name == "conftest.py":
|
||||
results.append(p)
|
||||
|
||||
@@ -395,7 +406,8 @@ def check_branch(branch_path: str, bypass_rules: list | None = None) -> dict:
|
||||
}
|
||||
|
||||
# Phase 1: Find all test files
|
||||
test_files = _find_all_test_files(bp)
|
||||
ignore_entries = load_ignore_entries(bp)
|
||||
test_files = _find_all_test_files(bp, ignore_entries)
|
||||
|
||||
if not test_files:
|
||||
checks.append(
|
||||
@@ -470,7 +482,7 @@ def check_branch(branch_path: str, bypass_rules: list | None = None) -> dict:
|
||||
|
||||
# Phase 4: Module coverage (category 11 — from test_coverage_check.py)
|
||||
# Uses broader file discovery + import-based module mapping
|
||||
broad_test_files = _find_test_files_broad(bp)
|
||||
broad_test_files = _find_test_files_broad(bp, ignore_entries)
|
||||
total_tests = 0
|
||||
tested_modules: set[str] = set()
|
||||
for tf in broad_test_files:
|
||||
@@ -483,7 +495,7 @@ def check_branch(branch_path: str, bypass_rules: list | None = None) -> dict:
|
||||
if total_tests == 0:
|
||||
tested_modules = set()
|
||||
|
||||
all_modules = _collect_testable_modules(bp)
|
||||
all_modules = _collect_testable_modules(bp, ignore_entries)
|
||||
total_modules = len(all_modules)
|
||||
|
||||
# 3 module coverage items
|
||||
|
||||
@@ -32,6 +32,7 @@ from pathlib import Path
|
||||
from aipass.prax import logger
|
||||
from aipass.seedgo.apps.handlers.json import json_handler
|
||||
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import is_seedgo_ignored, load_ignore_entries
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.skip_dirs import SOURCE_SKIP_DIRS, is_disabled_file
|
||||
|
||||
AUDIT_SCOPE = "branch_level"
|
||||
@@ -60,18 +61,23 @@ _MAIN_BLOCK_RE = re.compile(
|
||||
# -- File collection ----------------------------------------------------------
|
||||
|
||||
|
||||
def _should_skip(path: Path) -> bool:
|
||||
"""Return True if any path component is in the skip set or file is disabled."""
|
||||
return any(part in SKIP_DIRS for part in path.parts) or is_disabled_file(path.name)
|
||||
def _should_skip(path: Path, branch_root: Path, ignore_entries: list) -> bool:
|
||||
"""Return True if any path component is in the skip set, file is disabled,
|
||||
or the path is excluded via .seedgoignore / the global default."""
|
||||
return (
|
||||
any(part in SKIP_DIRS for part in path.parts)
|
||||
or is_disabled_file(path.name)
|
||||
or is_seedgo_ignored(str(path), branch_root, ignore_entries)
|
||||
)
|
||||
|
||||
|
||||
def _collect_python_files(branch_path: Path) -> list[Path]:
|
||||
def _collect_python_files(branch_path: Path, ignore_entries: list) -> list[Path]:
|
||||
"""Collect all .py files in the branch, skipping irrelevant dirs."""
|
||||
files: list[Path] = []
|
||||
if not branch_path.is_dir():
|
||||
return files
|
||||
for py_file in branch_path.rglob("*.py"):
|
||||
if _should_skip(py_file):
|
||||
if _should_skip(py_file, branch_path, ignore_entries):
|
||||
continue
|
||||
files.append(py_file)
|
||||
return sorted(files)
|
||||
@@ -216,7 +222,8 @@ def check_branch(branch_path: str, bypass_rules: list | None = None) -> dict:
|
||||
}
|
||||
|
||||
# Phase 1: Collect all .py files
|
||||
py_files = _collect_python_files(branch)
|
||||
ignore_entries = load_ignore_entries(branch)
|
||||
py_files = _collect_python_files(branch, ignore_entries)
|
||||
if not py_files:
|
||||
json_handler.log_operation(
|
||||
"check_completed",
|
||||
|
||||
@@ -42,19 +42,27 @@ def discover_checkers(pack_path: Path | None = None) -> Dict[str, Any]:
|
||||
return checkers
|
||||
|
||||
|
||||
def _collect_py_files(branch_path: Path) -> List[Dict[str, str]]:
|
||||
"""Collect auditable .py files from apps/, respecting ignore patterns."""
|
||||
def _collect_py_files(branch_path: Path, include_init: bool = False) -> List[Dict[str, str]]:
|
||||
"""Collect auditable .py files from apps/, respecting ignore patterns.
|
||||
|
||||
__init__.py package markers are excluded by default — most checkers are
|
||||
content-focused (dead code, naming, nesting) and __init__.py is typically
|
||||
boilerplate. Pass include_init=True for import-statement checkers, where a
|
||||
real cross-handler import hiding in a package marker must not go unseen.
|
||||
"""
|
||||
apps_dir = branch_path / "apps"
|
||||
if not apps_dir.exists():
|
||||
return []
|
||||
ign = ignore_handler.get_audit_ignore_patterns()
|
||||
ignore_entries = ignore_handler.load_ignore_entries(branch_path)
|
||||
return [
|
||||
{"file": str(f), "name": f.name}
|
||||
for f in apps_dir.rglob("*.py")
|
||||
if f.name != "__init__.py"
|
||||
if (include_init or f.name != "__init__.py")
|
||||
and not is_disabled_file(f.name)
|
||||
and not is_throwaway_path(str(f))
|
||||
and not any(p in str(f).lower() for p in ign)
|
||||
and not ignore_handler.is_seedgo_ignored(str(f), branch_path, ignore_entries)
|
||||
]
|
||||
|
||||
|
||||
@@ -132,6 +140,7 @@ def audit_branch(branch: Dict[str, str], bypass_rules: list, pack_path: Path | N
|
||||
"""Audit a branch for standards compliance. Returns backward-compatible dict."""
|
||||
entry_file, branch_path = branch["entry_file"], Path(branch["path"])
|
||||
checkers, all_files = discover_checkers(pack_path), _collect_py_files(branch_path)
|
||||
files_with_init: List[Dict[str, str]] | None = None
|
||||
|
||||
# Discover diagnostics checker from handlers/diagnostics/ (outside pack dirs)
|
||||
diag_mod = _load_diagnostics_checker()
|
||||
@@ -161,7 +170,12 @@ def audit_branch(branch: Dict[str, str], bypass_rules: list, pack_path: Path | N
|
||||
results[name], scores[name] = {"passed": False, "score": 0, "error": str(e)}, 0
|
||||
# All-files scope: scan every .py file, override score with average
|
||||
if scope == "all_files" and all_files:
|
||||
v, s = _run_all_files(checker, name, all_files, bypass_rules)
|
||||
scan_files = all_files
|
||||
if getattr(checker, "INCLUDE_INIT_FILES", False):
|
||||
if files_with_init is None:
|
||||
files_with_init = _collect_py_files(branch_path, include_init=True)
|
||||
scan_files = files_with_init
|
||||
v, s = _run_all_files(checker, name, scan_files, bypass_rules)
|
||||
all_violations[name] = v
|
||||
if s:
|
||||
avg_score = int(sum(s) / len(s))
|
||||
|
||||
@@ -11,14 +11,23 @@ Ignore Pattern Configuration Handler
|
||||
|
||||
Provides ignore patterns for audit file filtering, template baseline checking,
|
||||
and deprecated pattern tracking. Pure configuration with helper functions.
|
||||
|
||||
Also provides the .seedgoignore engine — a gitignore-style dotfile droppable
|
||||
into any directory (per-directory scope, same nesting semantics as .gitignore)
|
||||
plus a global default so agents' tools/ dirs are ignored fleet-wide with zero
|
||||
per-branch setup. See DEFAULT_IGNORE_PATTERNS / is_seedgo_ignored().
|
||||
"""
|
||||
|
||||
# =============================================
|
||||
# IMPORTS
|
||||
# =============================================
|
||||
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import pathspec
|
||||
|
||||
from aipass.prax import logger
|
||||
from aipass.seedgo.apps.handlers.json import json_handler
|
||||
|
||||
# =============================================
|
||||
@@ -111,6 +120,95 @@ def get_deprecated_patterns() -> dict:
|
||||
return DEPRECATED_PATTERNS.copy()
|
||||
|
||||
|
||||
# =============================================
|
||||
# SEEDGO_IGNORE — gitignore-style, per-directory
|
||||
# =============================================
|
||||
|
||||
# Dotfile name — droppable into any directory in a branch, gitignore-style
|
||||
# patterns (via pathspec), scoped to that directory's subtree exactly like a
|
||||
# real .gitignore.
|
||||
IGNORE_FILENAME = ".seedgoignore"
|
||||
|
||||
# Global default — applied to every branch with zero per-branch setup.
|
||||
# Agents' tools/ dirs are deliberate throwaway prototyping space (quick
|
||||
# scripts for fast answers) — not standards-compliant by design, and that's
|
||||
# fine and wanted (Patrick ruling).
|
||||
DEFAULT_IGNORE_PATTERNS: List[str] = [
|
||||
"tools/",
|
||||
]
|
||||
|
||||
|
||||
def _iter_seedgo_ignore_files(branch_root: Path) -> List[Path]:
|
||||
"""Return every .seedgoignore file under branch_root, shallowest first."""
|
||||
return sorted(branch_root.rglob(IGNORE_FILENAME), key=lambda p: len(p.parts))
|
||||
|
||||
|
||||
def load_ignore_entries(branch_root: Path) -> List[Tuple[str, "pathspec.PathSpec"]]:
|
||||
"""Build the ordered (scope, PathSpec) list for a branch.
|
||||
|
||||
scope "" is the global default and applies branch-wide. Each discovered
|
||||
.seedgoignore file adds a scope equal to its own directory (relative to
|
||||
branch_root) — its patterns are relative to that directory and only
|
||||
match within its subtree, same nesting semantics as a real .gitignore.
|
||||
|
||||
Args:
|
||||
branch_root: Absolute path to the branch root.
|
||||
|
||||
Returns:
|
||||
Ordered list of (scope, PathSpec) pairs, global default first.
|
||||
"""
|
||||
root = Path(branch_root).resolve()
|
||||
entries: List[Tuple[str, "pathspec.PathSpec"]] = [
|
||||
("", pathspec.PathSpec.from_lines("gitignore", DEFAULT_IGNORE_PATTERNS))
|
||||
]
|
||||
|
||||
for ignore_file in _iter_seedgo_ignore_files(root):
|
||||
scope = ignore_file.parent.relative_to(root).as_posix()
|
||||
if scope == ".":
|
||||
scope = ""
|
||||
try:
|
||||
lines = ignore_file.read_text(encoding="utf-8").splitlines()
|
||||
except OSError as exc:
|
||||
logger.info("[ignore_handler] Cannot read %s: %s", ignore_file, exc)
|
||||
continue
|
||||
entries.append((scope, pathspec.PathSpec.from_lines("gitignore", lines)))
|
||||
|
||||
json_handler.log_operation("seedgo_ignore_loaded", {"branch_root": str(root), "files": len(entries) - 1})
|
||||
return entries
|
||||
|
||||
|
||||
def is_seedgo_ignored(
|
||||
file_path: str, branch_root: Path, entries: Optional[List[Tuple[str, "pathspec.PathSpec"]]] = None
|
||||
) -> bool:
|
||||
"""Check whether file_path is ignored via .seedgoignore or the global default.
|
||||
|
||||
Args:
|
||||
file_path: Absolute path to the candidate file.
|
||||
branch_root: Absolute path to the branch root.
|
||||
entries: Pre-loaded result of load_ignore_entries(); loaded fresh if omitted.
|
||||
|
||||
Returns:
|
||||
True when scans/audits/checklists/checker-style enforcement should skip this path.
|
||||
"""
|
||||
root = Path(branch_root).resolve()
|
||||
try:
|
||||
rel = Path(file_path).resolve().relative_to(root).as_posix()
|
||||
except ValueError as exc:
|
||||
logger.info("[ignore_handler] %s not under branch root %s: %s", file_path, root, exc)
|
||||
return False
|
||||
|
||||
if entries is None:
|
||||
entries = load_ignore_entries(root)
|
||||
|
||||
for scope, spec in entries:
|
||||
if scope and rel != scope and not rel.startswith(scope + "/"):
|
||||
continue
|
||||
sub_rel = rel[len(scope) + 1 :] if scope else rel
|
||||
if spec.match_file(sub_rel):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# =============================================
|
||||
# MODULE INITIALIZATION
|
||||
# =============================================
|
||||
|
||||
@@ -45,6 +45,9 @@ from aipass.seedgo.apps.handlers.bypass.bypass_handler import (
|
||||
# Throwaway / prototype detection
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.skip_dirs import is_prototype_file, is_throwaway_path
|
||||
|
||||
# .seedgoignore — gitignore-style per-directory + global default (tools/)
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import is_seedgo_ignored
|
||||
|
||||
# JSON handler for tracking
|
||||
from aipass.seedgo.apps.handlers.json import json_handler
|
||||
|
||||
@@ -140,6 +143,10 @@ def run_checklist(file_path: str, pack_name: str = "aipass", prototype: bool = F
|
||||
if prototype or is_prototype_file(resolved):
|
||||
return [{"standard": "(skip)", "passed": True, "detail": "Prototype mode — standards skipped"}]
|
||||
|
||||
branch_path = _resolve_branch_path(resolved)
|
||||
if branch_path is not None and is_seedgo_ignored(resolved, branch_path):
|
||||
return [{"standard": "(skip)", "passed": True, "detail": "Ignored via .seedgoignore"}]
|
||||
|
||||
# Discover pack path
|
||||
pack_path = _resolve_pack_path(pack_name)
|
||||
if pack_path is None:
|
||||
@@ -191,20 +198,28 @@ def _resolve_pack_path(pack_name: str) -> Optional[Path]:
|
||||
return None
|
||||
|
||||
|
||||
def _load_bypass_for_file(file_path: str) -> list:
|
||||
"""Load bypass rules for the branch containing file_path."""
|
||||
def _resolve_branch_path(file_path: str) -> Optional[Path]:
|
||||
"""Resolve the absolute branch root containing file_path, or None."""
|
||||
branch = get_branch_from_path(file_path)
|
||||
if branch is None:
|
||||
return []
|
||||
return None
|
||||
raw_path = branch.get("path", "")
|
||||
if not raw_path:
|
||||
return []
|
||||
return None
|
||||
bp = Path(raw_path)
|
||||
if not bp.is_absolute():
|
||||
from aipass.seedgo.apps.handlers.bypass.bypass_handler import _find_registry
|
||||
|
||||
registry_path = _find_registry()
|
||||
bp = (registry_path.parent / bp).resolve()
|
||||
return bp
|
||||
|
||||
|
||||
def _load_bypass_for_file(file_path: str) -> list:
|
||||
"""Load bypass rules for the branch containing file_path."""
|
||||
bp = _resolve_branch_path(file_path)
|
||||
if bp is None:
|
||||
return []
|
||||
return load_bypass_rules(str(bp))
|
||||
|
||||
|
||||
|
||||
+99
-29
@@ -1,5 +1,21 @@
|
||||
{
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:presence_gate"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:persistent_alert"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
@@ -40,6 +56,22 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:compass_recall"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:feedback_pulse"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
@@ -48,15 +80,53 @@
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
},
|
||||
{
|
||||
"matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreToolUse"
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:context_gauge",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:temporal",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Notification": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py Notification"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py Stop"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py SessionStart:cadence_reset",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -72,6 +142,17 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreToolUse"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubagentStop": [
|
||||
{
|
||||
"hooks": [
|
||||
@@ -82,26 +163,6 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py Stop"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Notification": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py Notification"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"matcher": "manual",
|
||||
@@ -162,14 +223,23 @@
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionStart": [
|
||||
},
|
||||
{
|
||||
"matcher": "manual",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py SessionStart:cadence_reset",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreCompact:pre_compact_prep",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "auto",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreCompact:pre_compact_prep",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
|
||||
@@ -221,6 +221,111 @@ def test_get_deprecated_patterns_returns_dict():
|
||||
assert isinstance(value, str)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests -- .seedgoignore engine (load_ignore_entries / is_seedgo_ignored)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_global_default_ignores_tools_dir_with_no_dotfile(tmp_path):
|
||||
"""Global default (tools/) applies branch-wide even with zero .seedgoignore files."""
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import is_seedgo_ignored
|
||||
|
||||
tools_file = tmp_path / "apps" / "tools" / "scratch.py"
|
||||
tools_file.parent.mkdir(parents=True)
|
||||
tools_file.write_text("pass", encoding="utf-8")
|
||||
normal_file = tmp_path / "apps" / "modules" / "real.py"
|
||||
normal_file.parent.mkdir(parents=True)
|
||||
normal_file.write_text("pass", encoding="utf-8")
|
||||
|
||||
assert is_seedgo_ignored(str(tools_file), tmp_path) is True
|
||||
assert is_seedgo_ignored(str(normal_file), tmp_path) is False
|
||||
|
||||
|
||||
def test_seedgo_ignore_dotfile_scoped_to_its_own_directory(tmp_path):
|
||||
"""A .seedgoignore dropped in a subdir only affects that subdir's subtree, not siblings."""
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import is_seedgo_ignored
|
||||
|
||||
scratch_dir = tmp_path / "apps" / "handlers" / "experiment"
|
||||
scratch_dir.mkdir(parents=True)
|
||||
(scratch_dir / ".seedgoignore").write_text("*.draft.py\n", encoding="utf-8")
|
||||
(scratch_dir / "wip.draft.py").write_text("pass", encoding="utf-8")
|
||||
|
||||
sibling_dir = tmp_path / "apps" / "handlers" / "other"
|
||||
sibling_dir.mkdir(parents=True)
|
||||
(sibling_dir / "wip.draft.py").write_text("pass", encoding="utf-8")
|
||||
|
||||
assert is_seedgo_ignored(str(scratch_dir / "wip.draft.py"), tmp_path) is True
|
||||
assert is_seedgo_ignored(str(sibling_dir / "wip.draft.py"), tmp_path) is False
|
||||
|
||||
|
||||
def test_seedgo_ignore_supports_gitignore_style_patterns(tmp_path):
|
||||
"""Comments, blank lines, and negation follow standard gitignore semantics."""
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import is_seedgo_ignored
|
||||
|
||||
(tmp_path / ".seedgoignore").write_text(
|
||||
"\n".join(["# comment", "", "scratch/", "!scratch/keep_me.py"]),
|
||||
encoding="utf-8",
|
||||
)
|
||||
scratch_dir = tmp_path / "scratch"
|
||||
scratch_dir.mkdir()
|
||||
(scratch_dir / "throwaway.py").write_text("pass", encoding="utf-8")
|
||||
(scratch_dir / "keep_me.py").write_text("pass", encoding="utf-8")
|
||||
|
||||
assert is_seedgo_ignored(str(scratch_dir / "throwaway.py"), tmp_path) is True
|
||||
assert is_seedgo_ignored(str(scratch_dir / "keep_me.py"), tmp_path) is False
|
||||
|
||||
|
||||
def test_seedgo_ignore_nested_scopes_both_apply(tmp_path):
|
||||
"""A nested .seedgoignore adds to (not replaces) any ancestor .seedgoignore scopes."""
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import is_seedgo_ignored
|
||||
|
||||
(tmp_path / ".seedgoignore").write_text("*.rootskip\n", encoding="utf-8")
|
||||
child_dir = tmp_path / "apps" / "child"
|
||||
child_dir.mkdir(parents=True)
|
||||
(child_dir / ".seedgoignore").write_text("*.childskip\n", encoding="utf-8")
|
||||
(child_dir / "a.rootskip").write_text("pass", encoding="utf-8")
|
||||
(child_dir / "b.childskip").write_text("pass", encoding="utf-8")
|
||||
(child_dir / "c.py").write_text("pass", encoding="utf-8")
|
||||
|
||||
assert is_seedgo_ignored(str(child_dir / "a.rootskip"), tmp_path) is True
|
||||
assert is_seedgo_ignored(str(child_dir / "b.childskip"), tmp_path) is True
|
||||
assert is_seedgo_ignored(str(child_dir / "c.py"), tmp_path) is False
|
||||
|
||||
|
||||
def test_is_seedgo_ignored_path_outside_branch_root_returns_false(tmp_path):
|
||||
"""A file outside branch_root cannot be resolved to a relative path — treated as not ignored."""
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import is_seedgo_ignored
|
||||
|
||||
branch_root = tmp_path / "branch"
|
||||
branch_root.mkdir()
|
||||
outside_file = tmp_path / "elsewhere" / "file.py"
|
||||
outside_file.parent.mkdir()
|
||||
outside_file.write_text("pass", encoding="utf-8")
|
||||
|
||||
assert is_seedgo_ignored(str(outside_file), branch_root) is False
|
||||
|
||||
|
||||
def test_load_ignore_entries_default_only_when_no_dotfiles(tmp_path):
|
||||
"""With no .seedgoignore files present, only the global default scope is returned."""
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import load_ignore_entries
|
||||
|
||||
entries = load_ignore_entries(tmp_path)
|
||||
assert len(entries) == 1
|
||||
assert entries[0][0] == ""
|
||||
|
||||
|
||||
def test_is_seedgo_ignored_accepts_precomputed_entries(tmp_path):
|
||||
"""Passing pre-loaded entries skips the internal reload — same result as omitting it."""
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import is_seedgo_ignored, load_ignore_entries
|
||||
|
||||
tools_file = tmp_path / "tools" / "scratch.py"
|
||||
tools_file.parent.mkdir(parents=True)
|
||||
tools_file.write_text("pass", encoding="utf-8")
|
||||
|
||||
entries = load_ignore_entries(tmp_path)
|
||||
assert is_seedgo_ignored(str(tools_file), tmp_path, entries) is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests -- utils.is_bypassed name-scoped bypass
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -158,6 +158,42 @@ from aipass.seedgo.apps.modules.scanner import scan_all
|
||||
result = check_handlers(fp, bypass_rules=bypass)
|
||||
assert result["score"] == 100
|
||||
|
||||
def test_handlers_reports_all_forbidden_imports(self, tmp_path: Path) -> None:
|
||||
"""A file with two cross-handler imports must report both, not just the first."""
|
||||
handler_dir = tmp_path / "apps" / "handlers" / "mypack"
|
||||
handler_dir.mkdir(parents=True)
|
||||
code = """\
|
||||
from aipass.seedgo.apps.handlers.error import error_handler
|
||||
from aipass.seedgo.apps.handlers.file import file_handler
|
||||
|
||||
def do_stuff():
|
||||
return True
|
||||
"""
|
||||
fp = str(handler_dir / "double_violation.py")
|
||||
Path(fp).write_text(code, encoding="utf-8")
|
||||
result = check_handlers(fp)
|
||||
independence = next(c for c in result["checks"] if c["name"] == "Handler independence")
|
||||
assert "line 1" in independence["message"]
|
||||
assert "line 2" in independence["message"]
|
||||
|
||||
def test_handlers_reports_all_orchestration_imports(self, tmp_path: Path) -> None:
|
||||
"""A file with two module (orchestration) imports must report both, not just the first."""
|
||||
handler_dir = tmp_path / "apps" / "handlers" / "mypack"
|
||||
handler_dir.mkdir(parents=True)
|
||||
code = """\
|
||||
from aipass.seedgo.apps.modules.scanner import scan_all
|
||||
from aipass.seedgo.apps.modules.reporter import report_all
|
||||
|
||||
def do_stuff():
|
||||
return scan_all()
|
||||
"""
|
||||
fp = str(handler_dir / "double_orchestration.py")
|
||||
Path(fp).write_text(code, encoding="utf-8")
|
||||
result = check_handlers(fp)
|
||||
orchestration = next(c for c in result["checks"] if c["name"] == "No orchestration")
|
||||
assert "line 1" in orchestration["message"]
|
||||
assert "line 2" in orchestration["message"]
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# 3. hardcoded_key_check
|
||||
|
||||
@@ -238,6 +238,21 @@ class TestDeadCodeCheck:
|
||||
assert result["score"] == 100
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_dead_code_ignores_tools_dir_by_default(self, mock_json, tmp_path: Path) -> None:
|
||||
"""Orphan modules under apps/tools/ are skipped via the global .seedgoignore default."""
|
||||
branch = _make_branch(tmp_path)
|
||||
_write_file(
|
||||
branch / "apps" / "tools" / "scratch.py",
|
||||
"def lonely_function():\n return None\n",
|
||||
)
|
||||
_write_file(
|
||||
branch / "apps" / (branch.name + ".py"),
|
||||
"def handle_command(): pass\n",
|
||||
)
|
||||
result = dead_code_check_branch(str(branch))
|
||||
assert result["score"] == 100
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# =============================================
|
||||
# 5. test_quality_check (check_branch)
|
||||
@@ -466,3 +481,16 @@ class TestUnusedFunctionCheck:
|
||||
result = unused_function_check_branch(str(branch), bypass_rules=bypass)
|
||||
assert result["score"] == 100
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_unused_function_respects_seedgo_ignore_dotfile(self, mock_json, tmp_path: Path) -> None:
|
||||
"""A .seedgoignore dropped alongside an unused function's file suppresses the flag."""
|
||||
branch = _make_branch(tmp_path)
|
||||
experiment_dir = branch / "apps" / "handlers" / "experiment"
|
||||
_write_file(experiment_dir / ".seedgoignore", "*\n")
|
||||
_write_file(
|
||||
experiment_dir / "scratch.py",
|
||||
"def never_called():\n pass\n",
|
||||
)
|
||||
result = unused_function_check_branch(str(branch))
|
||||
assert result["score"] == 100
|
||||
assert result["passed"] is True
|
||||
|
||||
@@ -132,6 +132,45 @@ def test_handler_independence_same_package(tmp_path):
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_handler_independence_fstring_guard_text_not_flagged(tmp_path):
|
||||
"""An f-string that mentions apps.handlers as help text is not a real import."""
|
||||
content = (
|
||||
"def guard():\n"
|
||||
" raise ImportError(\n"
|
||||
' f" from aipass.seedgo.apps.handlers.error import error_handler\\n"\n'
|
||||
" )\n"
|
||||
)
|
||||
handler_path = str(tmp_path / "apps" / "handlers" / "audit" / "guard.py")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_handler_independence,
|
||||
)
|
||||
|
||||
result = check_handler_independence(content, _lines(content), handler_path)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_handler_independence_flags_real_import_alongside_guard_text(tmp_path):
|
||||
"""A real cross-handler import is still caught even when guard text sits nearby."""
|
||||
content = (
|
||||
"from aipass.seedgo.apps.handlers.error import error_handler\n"
|
||||
"\n"
|
||||
"def guard():\n"
|
||||
" raise ImportError(\n"
|
||||
' f" from aipass.seedgo.apps.handlers.error import error_handler\\n"\n'
|
||||
" )\n"
|
||||
)
|
||||
handler_path = str(tmp_path / "apps" / "handlers" / "audit" / "mixed.py")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_handler_independence,
|
||||
)
|
||||
|
||||
result = check_handler_independence(content, _lines(content), handler_path)
|
||||
assert result["passed"] is False
|
||||
assert "line 1" in result["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 2. handlers_check -- check_auto_detection
|
||||
# ===========================================================================
|
||||
@@ -238,6 +277,51 @@ def test_no_orchestration_in_docstring():
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_no_orchestration_fstring_guard_text_not_flagged():
|
||||
"""An import-guard error message built from an f-string is not a real import.
|
||||
|
||||
Regression test: apps/handlers/__init__.py raises ImportError with help text
|
||||
like `f" from {MY_BRANCH}.apps.modules.<module> import <function>\\n"` —
|
||||
the old text-matching check flagged this guard text as orchestration.
|
||||
"""
|
||||
content = (
|
||||
"MY_BRANCH = 'aipass.seedgo'\n"
|
||||
"def guard():\n"
|
||||
" raise ImportError(\n"
|
||||
' f" from {MY_BRANCH}.apps.modules.<module> import <function>\\n"\n'
|
||||
" )\n"
|
||||
)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_no_orchestration,
|
||||
)
|
||||
|
||||
result = check_no_orchestration(content, _lines(content))
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_no_orchestration_flags_real_import_alongside_guard_text():
|
||||
"""A real module import is still caught even when guard text sits nearby."""
|
||||
content = (
|
||||
"from aipass.seedgo.apps.modules.scanner import scan_all\n"
|
||||
"\n"
|
||||
"def guard():\n"
|
||||
" raise ImportError(\n"
|
||||
' f" from aipass.seedgo.apps.modules.<module> import <function>\\n"\n'
|
||||
" )\n"
|
||||
)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_no_orchestration,
|
||||
)
|
||||
|
||||
result = check_no_orchestration(content, _lines(content))
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "line 1" in result["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 4. log_handler_check -- check_no_raw_file_handler
|
||||
# ===========================================================================
|
||||
|
||||
@@ -22,6 +22,11 @@ def _mock_infrastructure(monkeypatch):
|
||||
"""Mock heavy infrastructure imports for checklist."""
|
||||
import sys
|
||||
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import (
|
||||
is_seedgo_ignored as real_is_seedgo_ignored,
|
||||
load_ignore_entries as real_load_ignore_entries,
|
||||
)
|
||||
|
||||
mock_logger = MagicMock()
|
||||
mock_console = MagicMock()
|
||||
mock_error = MagicMock()
|
||||
@@ -67,6 +72,12 @@ def _mock_infrastructure(monkeypatch):
|
||||
bypass_mod.load_bypass_rules = MagicMock(return_value=[])
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.bypass_handler", bypass_mod)
|
||||
|
||||
ignore_mod = MagicMock()
|
||||
ignore_mod.is_seedgo_ignored = real_is_seedgo_ignored
|
||||
ignore_mod.load_ignore_entries = real_load_ignore_entries
|
||||
bypass_pkg.ignore_handler = ignore_mod
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.ignore_handler", ignore_mod)
|
||||
|
||||
# Force re-import
|
||||
monkeypatch.delitem(sys.modules, "aipass.seedgo.apps.modules.checklist", raising=False)
|
||||
|
||||
@@ -222,6 +233,31 @@ def test_run_checklist_prototype_marker_skips(tmp_path, monkeypatch):
|
||||
assert "prototype" in results[0]["detail"].lower()
|
||||
|
||||
|
||||
def test_run_checklist_seedgo_ignore_skips(tmp_path, monkeypatch):
|
||||
"""A file under apps/tools/ is skipped via the global .seedgoignore default."""
|
||||
import sys
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards import skip_dirs
|
||||
|
||||
monkeypatch.setattr(skip_dirs, "_get_temp_roots", lambda: [])
|
||||
|
||||
branch_mod = MagicMock()
|
||||
branch_mod.get_branch_from_path = MagicMock(return_value={"path": str(tmp_path)})
|
||||
branch_mod.load_bypass_rules = MagicMock(return_value=[])
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.bypass_handler", branch_mod)
|
||||
|
||||
from aipass.seedgo.apps.modules.checklist import run_checklist
|
||||
|
||||
tools_dir = tmp_path / "apps" / "tools"
|
||||
tools_dir.mkdir(parents=True)
|
||||
f = tools_dir / "scratch.py"
|
||||
f.write_text("x = 1\n", encoding="utf-8")
|
||||
results = run_checklist(str(f))
|
||||
assert len(results) == 1
|
||||
assert results[0]["passed"] is True
|
||||
assert "seedgoignore" in results[0]["detail"].lower()
|
||||
|
||||
|
||||
def test_run_checklist_normal_file_still_audited(tmp_path, monkeypatch):
|
||||
"""A normal file without markers/temp path is still fully audited."""
|
||||
import sys
|
||||
|
||||
@@ -32,8 +32,15 @@ def _mock_infrastructure(monkeypatch):
|
||||
mock_console = MagicMock()
|
||||
mock_json_handler = MagicMock()
|
||||
mock_json_handler.log_operation = MagicMock(return_value=True)
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import (
|
||||
is_seedgo_ignored as real_is_seedgo_ignored,
|
||||
load_ignore_entries as real_load_ignore_entries,
|
||||
)
|
||||
|
||||
mock_ignore_handler = MagicMock()
|
||||
mock_ignore_handler.get_audit_ignore_patterns = MagicMock(return_value=[])
|
||||
mock_ignore_handler.is_seedgo_ignored = real_is_seedgo_ignored
|
||||
mock_ignore_handler.load_ignore_entries = real_load_ignore_entries
|
||||
mock_scan_branch = MagicMock(return_value=None)
|
||||
|
||||
# -- prax ---------------------------------------------------------------
|
||||
@@ -1087,6 +1094,27 @@ class TestCollectPyFiles:
|
||||
assert "handler.py" in names
|
||||
assert "__init__.py" not in names
|
||||
|
||||
def test_collects_init_files_when_requested(self, tmp_path, monkeypatch):
|
||||
"""include_init=True keeps __init__.py — for import checkers that must not miss them."""
|
||||
import sys
|
||||
|
||||
skip_dirs = sys.modules.get("aipass.seedgo.apps.handlers.aipass_standards.skip_dirs")
|
||||
if skip_dirs:
|
||||
monkeypatch.setattr(skip_dirs, "_get_temp_roots", lambda: [])
|
||||
|
||||
from aipass.seedgo.apps.handlers.audit.branch_audit import (
|
||||
_collect_py_files,
|
||||
)
|
||||
|
||||
apps_dir = tmp_path / "apps"
|
||||
apps_dir.mkdir()
|
||||
(apps_dir / "__init__.py").write_text("", encoding="utf-8")
|
||||
(apps_dir / "module.py").write_text("pass", encoding="utf-8")
|
||||
result = _collect_py_files(tmp_path, include_init=True)
|
||||
names = [f["name"] for f in result]
|
||||
assert "module.py" in names
|
||||
assert "__init__.py" in names
|
||||
|
||||
def test_respects_ignore_patterns(self, tmp_path, monkeypatch):
|
||||
"""Files matching ignore patterns are excluded."""
|
||||
import sys
|
||||
@@ -1134,6 +1162,29 @@ class TestCollectPyFiles:
|
||||
assert "module.py" in names
|
||||
assert "dashboard_sync(disabled).py" not in names
|
||||
|
||||
def test_respects_seedgo_ignore_tools_dir(self, tmp_path, monkeypatch):
|
||||
"""Files under apps/tools/ are excluded via the global .seedgoignore default."""
|
||||
import sys
|
||||
|
||||
skip_dirs = sys.modules.get("aipass.seedgo.apps.handlers.aipass_standards.skip_dirs")
|
||||
if skip_dirs:
|
||||
monkeypatch.setattr(skip_dirs, "_get_temp_roots", lambda: [])
|
||||
|
||||
from aipass.seedgo.apps.handlers.audit.branch_audit import (
|
||||
_collect_py_files,
|
||||
)
|
||||
|
||||
apps_dir = tmp_path / "apps"
|
||||
apps_dir.mkdir()
|
||||
(apps_dir / "module.py").write_text("pass", encoding="utf-8")
|
||||
tools_dir = apps_dir / "tools"
|
||||
tools_dir.mkdir()
|
||||
(tools_dir / "scratch.py").write_text("pass", encoding="utf-8")
|
||||
result = _collect_py_files(tmp_path)
|
||||
names = [f["name"] for f in result]
|
||||
assert "module.py" in names
|
||||
assert "scratch.py" not in names
|
||||
|
||||
|
||||
class TestExtractBranchLevelViolations:
|
||||
"""Tests for _extract_branch_level_violations."""
|
||||
@@ -1462,6 +1513,7 @@ def _make_checker(
|
||||
checker = MagicMock()
|
||||
checker.AUDIT_SCOPE = scope
|
||||
checker.FILE_FILTER = None
|
||||
checker.INCLUDE_INIT_FILES = False
|
||||
|
||||
if has_check_module:
|
||||
default_mod = {
|
||||
@@ -1676,6 +1728,41 @@ class TestAuditBranch:
|
||||
result = branch_audit.audit_branch(branch, [])
|
||||
assert "naming_violations" in result
|
||||
|
||||
def test_all_files_scope_include_init_files_opt_in(self, tmp_path, monkeypatch):
|
||||
"""Checker with INCLUDE_INIT_FILES=True sees __init__.py; a normal checker does not."""
|
||||
from aipass.seedgo.apps.handlers.audit import branch_audit
|
||||
|
||||
branch, branch_path = _setup_branch(tmp_path)
|
||||
apps_dir = Path(branch_path) / "apps"
|
||||
(apps_dir / "__init__.py").write_text("pass", encoding="utf-8")
|
||||
|
||||
import_checker = _make_checker(scope="all_files")
|
||||
import_checker.INCLUDE_INIT_FILES = True
|
||||
normal_checker = _make_checker(scope="all_files")
|
||||
|
||||
# tmp_path lives under the system temp dir, which is_throwaway_path
|
||||
# filters out wholesale — neutralize that so _collect_py_files sees
|
||||
# the fixture's files, same as it would for a real branch on disk.
|
||||
monkeypatch.setattr(branch_audit, "is_throwaway_path", lambda path_str: False)
|
||||
monkeypatch.setattr(
|
||||
branch_audit,
|
||||
"discover_checkers",
|
||||
lambda pack_path=None: {"handlers": import_checker, "naming": normal_checker},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
branch_audit,
|
||||
"_load_diagnostics_checker",
|
||||
lambda: None,
|
||||
)
|
||||
monkeypatch.setattr(branch_audit, "scan_branch", lambda p: None)
|
||||
|
||||
branch_audit.audit_branch(branch, [])
|
||||
|
||||
import_checked_names = {Path(c.args[0]).name for c in import_checker.check_module.call_args_list}
|
||||
normal_checked_names = {Path(c.args[0]).name for c in normal_checker.check_module.call_args_list}
|
||||
assert "__init__.py" in import_checked_names
|
||||
assert "__init__.py" not in normal_checked_names
|
||||
|
||||
def test_dynamic_post_check(self, tmp_path, monkeypatch):
|
||||
"""check_branch_post discovered and called."""
|
||||
from aipass.seedgo.apps.handlers.audit import branch_audit
|
||||
|
||||
@@ -48,6 +48,10 @@ def _mock_infrastructure(monkeypatch):
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json.json_handler", json_mod)
|
||||
|
||||
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed as real_is_bypassed
|
||||
from aipass.seedgo.apps.handlers.bypass.ignore_handler import (
|
||||
is_seedgo_ignored as real_is_seedgo_ignored,
|
||||
load_ignore_entries as real_load_ignore_entries,
|
||||
)
|
||||
|
||||
bypass_pkg = MagicMock()
|
||||
bypass_utils = MagicMock()
|
||||
@@ -55,6 +59,8 @@ def _mock_infrastructure(monkeypatch):
|
||||
bypass_pkg.utils = bypass_utils
|
||||
bypass_ignore = MagicMock()
|
||||
bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[])
|
||||
bypass_ignore.is_seedgo_ignored = real_is_seedgo_ignored
|
||||
bypass_ignore.load_ignore_entries = real_load_ignore_entries
|
||||
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.utils", bypass_utils)
|
||||
|
||||
Reference in New Issue
Block a user