Files
manual_slop/tests/test_audit_exception_handling_heuristics.py
T
ed 8ea2ffc3e8 feat(scripts): Phase 10.3 heuristics - reclassify 14 UNCLEAR sites
Adds 5 new heuristics (#22-#26) to scripts/audit_exception_handling.py
that recognize narrow-catch + non-Result patterns added in Phase 3-8:

22. Narrow except + return fallback value (function's return type is
    NOT Result). Catches: project_manager.py:get_git_commit,
    aggregate.py:is_absolute_with_drive, etc.

23. Narrow except + use error inline (except body uses e/exc in a
    non-pass way). Catches: session_logger.py:log_tool_call,
    summarize.py:_summarise_python, etc.

24. Narrow except + assign fallback (var = <value>, no return).
    Catches: file_cache.py:mtime cache, etc.

25. Narrow except + uses traceback module (e.g., traceback.format_exc()).
    Catches: aggregate.py file read with traceback, etc.

26. Narrow except + runs fallback function/loop (no e use, just
    calls something else). Catches: aggregate.py AST skeleton fallback,
    markdown_helper.py render_table fallback, etc.

Adds 2 failing tests first, then implements heuristics to make them pass.

Result: 14 UNCLEAR sites reclassified as INTERNAL_COMPLIANT.
After Phase 10.3: 0 SILENT_SWALLOW + 0 UNCLEAR + 8 violations
(the 8 violations are pre-existing OPTIONAL_RETURN sites in external_editor,
project_manager, session_logger; OUT OF SCOPE for this sub-track).
2026-06-17 22:59:12 -04:00

349 lines
14 KiB
Python

"""Tests for the new heuristics added in scripts/audit_exception_handling.py.
Each test creates a fixture file with a specific pattern and runs the audit
script against it, verifying the pattern is classified correctly.
The new heuristics (added by result_migration_review_pass_20260617):
1. list.index with ValueError fallback to default index (INTERNAL_COMPLIANT)
2. dict lookup (KeyError) with default value (INTERNAL_COMPLIANT)
3. datetime.fromisoformat(s) with ValueError: None (INTERNAL_COMPLIANT)
4. Path.resolve(strict=True) with OSError/ValueError fallback (INTERNAL_COMPLIANT)
5. Path.relative_to with ValueError: pass (INTERNAL_COMPLIANT)
6. asyncio.get_running_loop() with RuntimeError: asyncio.run() (INTERNAL_COMPLIANT)
7. Narrow except (ImportError, AttributeError) + fallback stub (INTERNAL_COMPLIANT)
8. raise NotImplementedError() as entire function body (INTERNAL_PROGRAMMER_RAISE)
9. if None: raise ImportError() validation pattern (INTERNAL_PROGRAMMER_RAISE)
10. try/except (json.JSONDecodeError, KeyError) around JSON parse + print + return (INTERNAL_COMPLIANT)
"""
from __future__ import annotations
import json
import subprocess
import sys
import textwrap
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "audit_exception_handling.py"
def _run_audit_on_fixture(source: str) -> dict:
"""Run the audit script on a fixture and return the parsed JSON output."""
# Use a temp dir outside tests/artifacts/ to avoid the audit's
# default artifacts-exclusion filter.
import tempfile
tmpdir = Path(tempfile.mkdtemp(prefix="audit_fixture_"))
fixture = tmpdir / "audit_heuristic_fixture.py"
fixture.write_text(textwrap.dedent(source), encoding="utf-8")
try:
result = subprocess.run(
[sys.executable, str(SCRIPT), "--json", "--src", str(tmpdir), "--verbose"],
capture_output=True,
text=True,
check=False,
cwd=str(ROOT),
)
finally:
if fixture.exists():
fixture.unlink()
if tmpdir.exists():
tmpdir.rmdir()
if result.returncode not in (0, 1):
raise RuntimeError(f"audit failed: {result.stderr}")
return json.loads(result.stdout)
def _classifications_for_file(data: dict, filename_suffix: str) -> list[dict]:
"""Return all findings for files whose path ends with `filename_suffix`."""
return [
f
for file_info in data.get("files", [])
for f in file_info.get("findings", [])
if file_info["filename"].endswith(filename_suffix)
]
# ---------------------------------------------------------------------------
# Heuristic 1: list.index with ValueError fallback to default index
# ---------------------------------------------------------------------------
def test_list_index_valueerror_fallback_is_compliant():
"""try: list.index(x); except ValueError: idx = N is compliant."""
src = '''
def func(items, target):
try:
idx = items.index(target)
except ValueError:
idx = 0
return idx
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
assert len(excepts) == 1, f"expected 1 except, got {len(excepts)}: {excepts}"
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
f"list.index+ValueError fallback should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
)
# ---------------------------------------------------------------------------
# Heuristic 2: KeyError lookup with default value
# ---------------------------------------------------------------------------
def test_keyerror_lookup_with_default_is_compliant():
"""try: dict[x]; except KeyError: val = default is compliant."""
src = '''
def func(d, key):
try:
val = d[key]
except KeyError:
val = "default"
return val
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
assert len(excepts) == 1
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
f"KeyError+default should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
)
# ---------------------------------------------------------------------------
# Heuristic 3: datetime.fromisoformat(s) with ValueError: None
# ---------------------------------------------------------------------------
def test_fromisoformat_valueerror_none_is_compliant():
"""try: datetime.fromisoformat(s); except ValueError: x = None is compliant."""
src = '''
import datetime
def func(s):
try:
d = datetime.datetime.fromisoformat(s)
except ValueError:
d = None
return d
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
assert len(excepts) == 1
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
f"fromisoformat+ValueError:None should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
)
# ---------------------------------------------------------------------------
# Heuristic 4: Path.resolve(strict=True) with OSError/ValueError fallback
# ---------------------------------------------------------------------------
def test_path_resolve_strict_fallback_is_compliant():
"""try: Path(p).resolve(strict=True); except (OSError, ValueError): Path(p).resolve() is compliant."""
src = '''
from pathlib import Path
def func(p):
try:
rp = Path(p).resolve(strict=True)
except (OSError, ValueError):
rp = Path(p).resolve()
return rp
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
assert len(excepts) == 1
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
f"Path.resolve(strict=True)+fallback should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
)
# ---------------------------------------------------------------------------
# Heuristic 5: Path.relative_to with ValueError: pass
# ---------------------------------------------------------------------------
def test_path_relative_to_valueerror_is_compliant():
"""try: rp.relative_to(base); except ValueError: pass is compliant (canonical subpath check)."""
src = '''
from pathlib import Path
def is_subpath(rp, base):
try:
rp.relative_to(base)
return True
except ValueError:
return False
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
assert len(excepts) == 1
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
f"Path.relative_to+ValueError should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
)
# ---------------------------------------------------------------------------
# Heuristic 6: asyncio.get_running_loop() with RuntimeError: asyncio.run()
# ---------------------------------------------------------------------------
def test_asyncio_get_running_loop_fallback_is_compliant():
"""try: get_running_loop(); except RuntimeError: asyncio.run() is compliant."""
src = '''
import asyncio
def bridge(coro):
try:
loop = asyncio.get_running_loop()
return loop
except RuntimeError:
return asyncio.run(coro)
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
assert len(excepts) == 1
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
f"get_running_loop+RuntimeError fallback should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
)
# ---------------------------------------------------------------------------
# Heuristic 7: narrow except (ImportError, AttributeError) + fallback stub
# ---------------------------------------------------------------------------
def test_import_attr_fallback_stub_is_compliant():
"""narrow except (ImportError, AttributeError) with fallback attribute assignment is compliant."""
src = '''
class Stub:
available = False
def get_thing():
try:
import real_module
return real_module
except (ImportError, ModuleNotFoundError, AttributeError):
return Stub()
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
assert len(excepts) == 1
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
f"narrow except + fallback stub should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
)
# ---------------------------------------------------------------------------
# Heuristic 8: raise NotImplementedError() as entire function body
# ---------------------------------------------------------------------------
def test_raise_notimplemented_entire_body_is_programmer_error():
"""raise NotImplementedError() as the entire function body is INTERNAL_PROGRAMMER_RAISE."""
src = '''
class Base:
def abstract_method(self):
raise NotImplementedError()
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
raises = [f for f in findings if f["kind"] == "RAISE"]
assert len(raises) == 1
assert raises[0]["category"] == "INTERNAL_PROGRAMMER_RAISE", (
f"raise NotImplementedError() in entire body should be INTERNAL_PROGRAMMER_RAISE, got {raises[0]['category']}"
)
# ---------------------------------------------------------------------------
# Heuristic 9: if None: raise ImportError() validation
# ---------------------------------------------------------------------------
def test_validation_raise_is_programmer_error():
"""if <var> is None: raise ImportError() is INTERNAL_PROGRAMMER_RAISE."""
src = '''
def func(dep):
if dep is None:
raise ImportError("dependency missing")
return dep
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
raises = [f for f in findings if f["kind"] == "RAISE"]
assert len(raises) == 1
assert raises[0]["category"] == "INTERNAL_PROGRAMMER_RAISE", (
f"validation raise should be INTERNAL_PROGRAMMER_RAISE, got {raises[0]['category']}"
)
# ---------------------------------------------------------------------------
# Heuristic 10: try/except (json.JSONDecodeError, KeyError) + print + return
# ---------------------------------------------------------------------------
def test_json_parse_with_print_is_compliant():
"""try/except (json.JSONDecodeError, KeyError) around JSON parse with print is compliant."""
src = '''
import json
def parse(s):
try:
data = json.loads(s)
if not isinstance(data, list):
print("not a list")
return
return data
except json.JSONDecodeError as e:
print(f"json error: {e}")
except KeyError as e:
print(f"missing key: {e}")
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
assert len(excepts) == 2
for e in excepts:
assert e["category"] == "INTERNAL_COMPLIANT", (
f"json parse with print should be INTERNAL_COMPLIANT, got {e['category']}"
)
# ---------------------------------------------------------------------------
# Heuristic 22: Narrow except + return fallback value
# ---------------------------------------------------------------------------
def test_narrow_except_returns_fallback_is_compliant():
"""try: <call>; except SpecificError: return <fallback> (in a non-Result-returning function) is compliant.
The function returns a meaningful fallback that the caller can check.
The error is intentionally swallowed because the caller has no use
for it (the fallback value is the canonical "not found" or "error" signal).
This is the pattern used by src/project_manager.py:get_git_commit,
src/aggregate.py:is_absolute_with_drive, src/models.py:load_mcp_configuration, etc.
"""
src = '''
def get_git_commit(git_dir):
try:
r = subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True, cwd=git_dir, timeout=5)
return r.stdout.strip() if r.returncode == 0 else ""
except (OSError, subprocess.SubprocessError, subprocess.TimeoutExpired):
return ""
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
assert len(excepts) == 1
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
f"narrow except returning fallback should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
)
# ---------------------------------------------------------------------------
# Heuristic 23: Narrow except + use error inline (formatted into another value)
# ---------------------------------------------------------------------------
def test_narrow_except_uses_error_inline_is_compliant():
"""try: <call>; except SpecificError as exc: <use exc inline> is compliant.
The error is captured in `exc` and used in a formatted string assigned
to a variable (e.g., ps1_name = f"(write error: {exc})"). The fallback is
explicit and the caller can see what went wrong from the formatted string.
This is the pattern used by src/session_logger.py:log_tool_call.
"""
src = '''
def write_script(ps1_path, script):
try:
ps1_path.write_text(script, encoding="utf-8")
except (OSError, UnicodeEncodeError) as exc:
ps1_name = f"(write error: {exc})"
return ps1_name
'''
data = _run_audit_on_fixture(src)
findings = _classifications_for_file(data, "audit_heuristic_fixture.py")
excepts = [f for f in findings if f["kind"] == "EXCEPT"]
assert len(excepts) == 1
assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (
f"narrow except using error inline should be INTERNAL_COMPLIANT, got {excepts[0]['category']}"
)