From 45615dadf9fa8aa79e425532847918437537a2e1 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 18 Jun 2026 09:37:28 -0400 Subject: [PATCH] feat(scripts): Phase 12.1+12.2+12.3 - remove Heuristic #19; fix visit_Try; add Heuristic D MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 12.1: REMOVE Heuristic #19 (narrow except + log = INTERNAL_COMPLIANT). Per error_handling.md Broad-Except Distinction table and the user's principle (2026-06-17): 'logging is NOT a drain'. A catch+log site is INTERNAL_SILENT_SWALLOW (a violation), not INTERNAL_COMPLIANT. The explicit reclassification runs AFTER drain-point checks so a site with BOTH a log call AND a drain point (e.g., sys.stderr.write + sys.exit) is classified by the drain point (which wins). Phase 12.2: FIX the visit_Try audit bug. The walker did NOT recurse into node.body (the try body itself), so nested Trys were silently dropped from the audit. Verified against src/api_hooks.py: 23 actual try/except nodes but only 5 reported — gap of 18 sites, 12+ silent violations. Fix: added 'for child in node.body: self.visit(child)' to ExceptionVisitor.visit_Try (placed before the handlers loop). Phase 12.3: ADD Heuristic D (5 drain-point patterns) with TDD: - D.1 HTTP error response (BaseHTTPRequestHandler.send_response) - D.2 GUI error display (imgui.open_popup) - D.3 Intentional app termination (sys.exit) - D.4 Telemetry emission (telemetry.emit_*) - D.5 Bounded retry (for attempt in range(N): try; return None) Added 5 new helper methods to ExceptionVisitor: _has_send_response_call, _has_imgui_error_display, _has_sys_exit_call, _has_telemetry_emit_call, _has_bounded_retry. Tests: - test_narrow_except_with_log_only_is_silent_swallow (NEW, PASSES) - test_narrow_except_with_logging_error_is_silent_swallow (NEW, PASSES) - test_visit_try_recurses_into_try_body (NEW, PASSES - nested Try) - test_drain_point_http_error_response_is_compliant (NEW, PASSES) - test_drain_point_gui_error_display_is_compliant (NEW, PASSES) - test_drain_point_app_termination_is_compliant (NEW, PASSES) - test_drain_point_telemetry_emit_is_compliant (NEW, PASSES) - test_drain_point_bounded_retry_is_compliant (NEW, PASSES) Test count: 14 baseline + 8 new = 22 total in test_audit_exception_handling_heuristics.py. All 22 pass (20 PASSED + 2 XFAIL from Phase 11's #22/#23 laundering heuristics). --- scripts/audit_exception_handling.py | 127 +++++++++- .../append_phase12_tests.py | 230 ++++++++++++++++++ .../fix_sysexit_test.py | 23 ++ ...est_audit_exception_handling_heuristics.py | 217 +++++++++++++++++ 4 files changed, 594 insertions(+), 3 deletions(-) create mode 100644 scripts/tier2/artifacts/result_migration_small_files_20260617/append_phase12_tests.py create mode 100644 scripts/tier2/artifacts/result_migration_small_files_20260617/fix_sysexit_test.py diff --git a/scripts/audit_exception_handling.py b/scripts/audit_exception_handling.py index 4c536e79..594be0b4 100644 --- a/scripts/audit_exception_handling.py +++ b/scripts/audit_exception_handling.py @@ -579,11 +579,58 @@ class ExceptionVisitor(ast.NodeVisitor): f"Compliant: `try: json.loads(...); except KeyError: print(...)` is the canonical CLI-style JSON input parser pattern (per result_migration_review_pass_20260617).", ) - # 19. Narrow except + log (sys.stderr.write or logging.*) for defer-not-catch or retry-then-give-up + # Heuristic #19 REMOVED in Phase 12.1: narrow except + log (sys.stderr.write / logging.*) + # was classified as INTERNAL_COMPLIANT, but per error_handling.md Broad-Except Distinction + # table and the user's principle (2026-06-17) "logging is NOT a drain", a catch+log + # site is INTERNAL_SILENT_SWALLOW (a violation). Result[T] must propagate to a true + # drain point. See conductor/tracks/result_migration_small_files_20260617/plan.md §12.1. + + # D. Drain-point patterns (per error_handling.md "Drain Points" section, Phase 12.3) + # A drain point is a place where Result[T] propagation TERMINATES visibly to the + # user or via intentional app action. Log-only / silent-fallback sites are NOT drain + # points; they are INTERNAL_SILENT_SWALLOW (a violation). Drain-point checks MUST run + # BEFORE the narrow+log reclassification below because a site may contain BOTH a log + # call AND a drain point (e.g., sys.stderr.write + sys.exit). + if len(except_body) > 0: + # D.1 HTTP error response (BaseHTTPRequestHandler subclass) + if self._has_send_response_call(except_body): + return ( + "INTERNAL_COMPLIANT", + f"Compliant: drain point (HTTP error response). `try: ...; except ({', '.join(sorted(exc_set))}): self.send_response(...)` terminates Result[T] propagation with a visible HTTP error response (per error_handling.md Drain Points §Pattern 1, Phase 12.3).", + ) + # D.2 GUI error display (imgui.open_popup / imgui.text call) + if self._has_imgui_error_display(except_body): + return ( + "INTERNAL_COMPLIANT", + f"Compliant: drain point (GUI error display). `try: ...; except ({', '.join(sorted(exc_set))}): imgui.open_popup(...)` terminates Result[T] propagation with a visible modal (per error_handling.md Drain Points §Pattern 2, Phase 12.3).", + ) + # D.3 Intentional app termination (sys.exit) + if self._has_sys_exit_call(except_body): + return ( + "INTERNAL_COMPLIANT", + f"Compliant: drain point (intentional app termination). `try: ...; except ({', '.join(sorted(exc_set))}): sys.exit(...)` terminates Result[T] propagation via process termination (per error_handling.md Drain Points §Pattern 3, Phase 12.3).", + ) + # D.4 Telemetry emission (telemetry.emit_*) + if self._has_telemetry_emit_call(except_body): + return ( + "INTERNAL_COMPLIANT", + f"Compliant: drain point (telemetry emission). `try: ...; except ({', '.join(sorted(exc_set))}): telemetry.emit_*(...)` terminates Result[T] propagation by sending to monitoring (per error_handling.md Drain Points §Pattern 4, Phase 12.3).", + ) + # D.5 Bounded retry (for attempt in range(N): ...; return None) + if self._has_bounded_retry(except_body): + return ( + "INTERNAL_COMPLIANT", + f"Compliant: drain point (bounded retry). `try: ...; except ({', '.join(sorted(exc_set))}): for attempt in range(N): ...; return None` terminates Result[T] propagation via bounded retry followed by visible failure (per error_handling.md Drain Points §Pattern 5, Phase 12.3).", + ) + + # Explicit reclassification (Phase 12.1): narrow except + log + # (sys.stderr.write / logging.*) WITHOUT a drain point is INTERNAL_SILENT_SWALLOW (a violation). + # This runs AFTER drain-point checks because a site may contain BOTH a log call + # AND a drain point (e.g., sys.stderr.write + sys.exit); the drain point wins. if len(except_body) > 0 and self._has_log_call(except_body) and not exc_set & {"Exception", "BaseException", ""}: return ( - "INTERNAL_COMPLIANT", - f"Compliant: `try: ...; except ({', '.join(sorted(exc_set))}): ` is the canonical catch+log pattern (defer-not-catch or retry-then-give-up) (per result_migration_review_pass_20260617).", + "INTERNAL_SILENT_SWALLOW", + f"Violation: narrow except + log (sys.stderr.write / logging.*) only. Per error_handling.md and the user's principle (2026-06-17): 'logging is NOT a drain'. The error context is lost. Use Result[T] propagation to a true drain point. (per result_migration_small_files_20260617 Phase 12.1)", ) # 20. ImGui scope cleanup guard (narrow except + imgui.end_* call) @@ -704,6 +751,78 @@ class ExceptionVisitor(ast.NodeVisitor): return True return False + def _has_send_response_call(self, stmts: list[ast.stmt]) -> bool: + """True if any statement calls self.send_response(...). Drain point D.1 (HTTP error response).""" + for stmt in stmts: + for node in ast.walk(stmt): + if isinstance(node, ast.Call): + f = node.func + if isinstance(f, ast.Attribute) and isinstance(f.attr, str) and f.attr == "send_response": + return True + return False + + def _has_imgui_error_display(self, stmts: list[ast.stmt]) -> bool: + """True if any statement opens an ImGui popup (drain point D.2 — GUI error display).""" + for stmt in stmts: + for node in ast.walk(stmt): + if isinstance(node, ast.Call): + f = node.func + if isinstance(f, ast.Attribute) and isinstance(f.attr, str): + if f.attr in ("open_popup", "popup", "modal"): + return True + return False + + def _has_sys_exit_call(self, stmts: list[ast.stmt]) -> bool: + """True if any statement calls sys.exit(...). Drain point D.3 (intentional app termination).""" + for stmt in stmts: + for node in ast.walk(stmt): + if isinstance(node, ast.Call): + f = node.func + if isinstance(f, ast.Attribute) and isinstance(f.value, ast.Name) and f.value.id == "sys" and f.attr == "exit": + return True + return False + + def _has_telemetry_emit_call(self, stmts: list[ast.stmt]) -> bool: + """True if any statement calls telemetry.emit_*(...). Drain point D.4 (telemetry emission).""" + for stmt in stmts: + for node in ast.walk(stmt): + if isinstance(node, ast.Call): + f = node.func + if isinstance(f, ast.Attribute) and isinstance(f.attr, str) and f.attr.startswith("emit_"): + if isinstance(f.value, ast.Name) and f.value.id in ("telemetry", "metrics", "monitor"): + return True + return False + + def _has_bounded_retry(self, stmts: list[ast.stmt]) -> bool: + """True if a bounded retry is present in the enclosing function: `for attempt in range(N): try: ...; except: ...; return None`. Drain point D.5. + + The bounded-retry pattern requires the SURROUNDING CONTEXT (not just the + except body): the enclosing function (or block) must contain + `for ... in range(N):` containing this try/except, AND a `return None` + AFTER the for loop. The exception handler body's only job is to log/sleep; + the real termination is the for-loop's exhaustion + the trailing return None. + """ + enclosing_func = self._current_func_node() + if enclosing_func is None: + return False + has_for_range_with_try = False + has_return_none_after = False + for_loop_seen = False + for node in ast.walk(enclosing_func): + if isinstance(node, ast.For): + if isinstance(node.iter, ast.Call) and isinstance(node.iter.func, ast.Name) and node.iter.func.id == "range": + for_loop_seen = True + for child in ast.walk(node): + if isinstance(child, ast.Try): + has_for_range_with_try = True + break + elif for_loop_seen and isinstance(node, ast.Return): + if node.value is None: + has_return_none_after = True + elif isinstance(node.value, ast.Constant) and node.value.value is None: + has_return_none_after = True + return has_for_range_with_try and has_return_none_after + def _has_imgui_end_call(self, stmts: list[ast.stmt]) -> bool: """True if any statement is a call to an imgui.end_* function.""" for s in stmts: @@ -857,6 +976,8 @@ class ExceptionVisitor(ast.NodeVisitor): "INTERNAL_COMPLIANT", "Compliant: bare try/finally is the canonical cleanup pattern (analog of `goto defer`).", ) + for child in node.body: + self.visit(child) for handler in node.handlers: category, hint = self._classify_except(handler, node) self._add_finding("EXCEPT", handler.lineno, self._snippet(handler), category, hint) diff --git a/scripts/tier2/artifacts/result_migration_small_files_20260617/append_phase12_tests.py b/scripts/tier2/artifacts/result_migration_small_files_20260617/append_phase12_tests.py new file mode 100644 index 00000000..e9ef64c0 --- /dev/null +++ b/scripts/tier2/artifacts/result_migration_small_files_20260617/append_phase12_tests.py @@ -0,0 +1,230 @@ +from pathlib import Path + +p = Path(r"C:\projects\manual_slop_tier2\tests\test_audit_exception_handling_heuristics.py") +with open(p, "rb") as f: + existing = f.read() + +# New tests content. Use byte concatenation to avoid Python string escaping. +nl = b"\r\n" # match CRLF +new = b"" + +new += nl * 2 +new += b"# ---------------------------------------------------------------------------" + nl +new += b"# Phase 12.1: Heuristic #19 REMOVED - narrow except + log is INTERNAL_SILENT_SWALLOW" + nl +new += b"# ---------------------------------------------------------------------------" + nl +new += b"def test_narrow_except_with_log_only_is_silent_swallow():" + nl +new += b' """try: ...; except (SpecificError): sys.stderr.write(...) is INTERNAL_SILENT_SWALLOW (a violation).' + nl +new += b"" + nl +new += b' Per error_handling.md "The Broad-Except Distinction" table and the user\'s' + nl +new += b' principle (2026-06-17): "logging is NOT a drain". sys.stderr.write alone' + nl +new += b" loses the error context; the propagation does NOT terminate visibly to" + nl +new += b" the user. The convention requires Result[T] propagation to a true drain" + nl +new += b" point. Heuristic #19 (which classified this as compliant) was REMOVED" + nl +new += b" in Phase 12.1." + nl +new += b' """' + nl +new += b" src = (" + nl +new += b" 'def log_failure(path, e):\\n'" + nl +new += b" ' try:\\n'" + nl +new += b" ' path.write_text(\"x\", encoding=\"utf-8\")\\n'" + nl +new += b" ' except (OSError, UnicodeEncodeError):\\n'" + nl +new += b" ' sys.stderr.write(f\"write failed: {e}\")\\n'" + nl +new += b" )" + nl +new += b" data = _run_audit_on_fixture(src)" + nl +new += b' findings = _classifications_for_file(data, "audit_heuristic_fixture.py")' + nl +new += b' excepts = [f for f in findings if f["kind"] == "EXCEPT"]' + nl +new += b" assert len(excepts) == 1" + nl +new += b' assert excepts[0]["category"] == "INTERNAL_SILENT_SWALLOW", (' + nl +new += b' f"narrow except + log only should be INTERNAL_SILENT_SWALLOW (logging is NOT a drain), got {excepts[0][\'category\']}"' + nl +new += b" )" + nl +new += nl * 2 +new += b"def test_narrow_except_with_logging_error_is_silent_swallow():" + nl +new += b' """try: ...; except (SpecificError): logging.error(...) is INTERNAL_SILENT_SWALLOW (a violation).' + nl +new += b"" + nl +new += b" Same principle as test_narrow_except_with_log_only_is_silent_swallow" + nl +new += b" but with the logging module. Logging alone loses the error context." + nl +new += b' """' + nl +new += b" src = (" + nl +new += b" 'def log_failure_via_logging(path):\\n'" + nl +new += b" ' try:\\n'" + nl +new += b" ' path.write_text(\"x\", encoding=\"utf-8\")\\n'" + nl +new += b" ' except (OSError, UnicodeEncodeError) as e:\\n'" + nl +new += b" ' logging.error(f\"write failed: {e}\")\\n'" + nl +new += b" )" + nl +new += b" data = _run_audit_on_fixture(src)" + nl +new += b' findings = _classifications_for_file(data, "audit_heuristic_fixture.py")' + nl +new += b' excepts = [f for f in findings if f["kind"] == "EXCEPT"]' + nl +new += b" assert len(excepts) == 1" + nl +new += b' assert excepts[0]["category"] == "INTERNAL_SILENT_SWALLOW", (' + nl +new += b' f"narrow except + logging.error should be INTERNAL_SILENT_SWALLOW, got {excepts[0][\'category\']}"' + nl +new += b" )" + nl +new += nl * 2 +new += b"# ---------------------------------------------------------------------------" + nl +new += b"# Phase 12.2: visit_Try recursion fix - nested Trys in try body are visited" + nl +new += b"# ---------------------------------------------------------------------------" + nl +new += b"def test_visit_try_recurses_into_try_body():" + nl +new += b' """A nested try inside the try body should be visited and its handlers recorded.' + nl +new += b"" + nl +new += b" The audit's visit_Try had a bug where it did NOT recurse into node.body." + nl +new += b" This test constructs a source with an outer try containing an inner try," + nl +new += b" and asserts BOTH outer and inner handlers appear in the findings." + nl +new += b' """' + nl +new += b" src = (" + nl +new += b" 'def outer():\\n'" + nl +new += b" ' try:\\n'" + nl +new += b" ' try:\\n'" + nl +new += b" ' do_inner()\\n'" + nl +new += b" ' except ValueError:\\n'" + nl +new += b" ' handle_inner()\\n'" + nl +new += b" ' do_outer_thing()\\n'" + nl +new += b" ' except (OSError, IOError):\\n'" + nl +new += b" ' handle_outer()\\n'" + nl +new += b" )" + nl +new += b" data = _run_audit_on_fixture(src)" + nl +new += b' findings = _classifications_for_file(data, "audit_heuristic_fixture.py")' + nl +new += b' excepts = [f for f in findings if f["kind"] == "EXCEPT"]' + nl +new += b" assert len(excepts) == 2, (" + nl +new += b' f"visit_Try should recurse into try body; expected 2 EXCEPT findings, got {len(excepts)}: {excepts}"' + nl +new += b" )" + nl +new += nl * 2 +new += b"# ---------------------------------------------------------------------------" + nl +new += b"# Phase 12.3: Heuristic D.1 - HTTP error response drain point" + nl +new += b"# ---------------------------------------------------------------------------" + nl +new += b"def test_drain_point_http_error_response_is_compliant():" + nl +new += b' """try: ...; except (SpecificError): self.send_response(500, ...) is INTERNAL_COMPLIANT (drain point D.1).' + nl +new += b"" + nl +new += b" Per error_handling.md Drain Points section, Pattern 1: HTTP error" + nl +new += b" response in a BaseHTTPRequestHandler subclass IS a drain point. The" + nl +new += b" HTTP status code IS the visible user feedback; the propagation" + nl +new += b" terminates at the HTTP response. Heuristic D.1 recognizes this pattern." + nl +new += b' """' + nl +new += b" src = (" + nl +new += b" 'class Handler(BaseHTTPRequestHandler):\\n'" + nl +new += b" ' def do_GET(self):\\n'" + nl +new += b" ' try:\\n'" + nl +new += b" ' self._read_body()\\n'" + nl +new += b" ' except (OSError, ValueError) as e:\\n'" + nl +new += b" ' self.send_response(500)\\n'" + nl +new += b" ' self.send_header(\"Content-Type\", \"application/json\")\\n'" + nl +new += b" ' self.wfile.write(b\\'{\"error\": \"internal\"}\\')\\n'" + nl +new += b" )" + nl +new += b" data = _run_audit_on_fixture(src)" + nl +new += b' findings = _classifications_for_file(data, "audit_heuristic_fixture.py")' + nl +new += b' excepts = [f for f in findings if f["kind"] == "EXCEPT"]' + nl +new += b" assert len(excepts) == 1" + nl +new += b' assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (' + nl +new += b' f"HTTP error response should be INTERNAL_COMPLIANT (drain point D.1), got {excepts[0][\'category\']}: {excepts[0].get(\'note\', \'\')}"' + nl +new += b" )" + nl +new += nl * 2 +new += b"# ---------------------------------------------------------------------------" + nl +new += b"# Phase 12.3: Heuristic D.2 - GUI error display drain point" + nl +new += b"# ---------------------------------------------------------------------------" + nl +new += b"def test_drain_point_gui_error_display_is_compliant():" + nl +new += b' """try: ...; except (SpecificError): imgui.open_popup(...) is INTERNAL_COMPLIANT (drain point D.2).' + nl +new += b"" + nl +new += b" Per error_handling.md Drain Points section, Pattern 2: GUI error" + nl +new += b" display via imgui.open_popup IS a drain point. The user sees the" + nl +new += b" error modal." + nl +new += b' """' + nl +new += b" src = (" + nl +new += b" 'def show_load_error():\\n'" + nl +new += b" ' try:\\n'" + nl +new += b" ' do_load()\\n'" + nl +new += b" ' except (OSError, ValueError):\\n'" + nl +new += b" ' imgui.open_popup(\"Load Error\")\\n'" + nl +new += b" )" + nl +new += b" data = _run_audit_on_fixture(src)" + nl +new += b' findings = _classifications_for_file(data, "audit_heuristic_fixture.py")' + nl +new += b' excepts = [f for f in findings if f["kind"] == "EXCEPT"]' + nl +new += b" assert len(excepts) == 1" + nl +new += b' assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (' + nl +new += b' f"GUI error display should be INTERNAL_COMPLIANT (drain point D.2), got {excepts[0][\'category\']}"' + nl +new += b" )" + nl +new += nl * 2 +new += b"# ---------------------------------------------------------------------------" + nl +new += b"# Phase 12.3: Heuristic D.3 - Intentional app termination drain point" + nl +new += b"# ---------------------------------------------------------------------------" + nl +new += b"def test_drain_point_app_termination_is_compliant():" + nl +new += b' """try: ...; except (SpecificError): sys.exit(1) is INTERNAL_COMPLIANT (drain point D.3).' + nl +new += b"" + nl +new += b" Per error_handling.md Drain Points section, Pattern 3: intentional" + nl +new += b" app termination via sys.exit IS a drain point. The process exit IS" + nl +new += b" the termination of the propagation." + nl +new += b' """' + nl +new += b" src = (" + nl +new += b" 'def critical_init():\\n'" + nl +new += b" ' try:\\n'" + nl +new += b" ' load_config()\\n'" + nl +new += b" ' except (OSError, ValueError):\\n'" + nl +new += b" ' sys.stderr.write(\"FATAL: config missing\\n\")\\n'" + nl +new += b" ' sys.exit(1)\\n'" + nl +new += b" )" + nl +new += b" data = _run_audit_on_fixture(src)" + nl +new += b' findings = _classifications_for_file(data, "audit_heuristic_fixture.py")' + nl +new += b' excepts = [f for f in findings if f["kind"] == "EXCEPT"]' + nl +new += b" assert len(excepts) == 1" + nl +new += b' assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (' + nl +new += b' f"app termination should be INTERNAL_COMPLIANT (drain point D.3), got {excepts[0][\'category\']}"' + nl +new += b" )" + nl +new += nl * 2 +new += b"# ---------------------------------------------------------------------------" + nl +new += b"# Phase 12.3: Heuristic D.4 - Telemetry emission drain point" + nl +new += b"# ---------------------------------------------------------------------------" + nl +new += b"def test_drain_point_telemetry_emit_is_compliant():" + nl +new += b' """try: ...; except (SpecificError): telemetry.emit_error(...) is INTERNAL_COMPLIANT (drain point D.4).' + nl +new += b"" + nl +new += b" Per error_handling.md Drain Points section, Pattern 4: telemetry" + nl +new += b" emission IS a drain point. The error reaches the monitoring system." + nl +new += b' """' + nl +new += b" src = (" + nl +new += b" 'def report_failure():\\n'" + nl +new += b" ' try:\\n'" + nl +new += b" ' do_thing()\\n'" + nl +new += b" ' except (OSError, ValueError):\\n'" + nl +new += b" ' telemetry.emit_error(operation=\"do_thing\", kind=\"INTERNAL\", message=\"failed\")\\n'" + nl +new += b" )" + nl +new += b" data = _run_audit_on_fixture(src)" + nl +new += b' findings = _classifications_for_file(data, "audit_heuristic_fixture.py")' + nl +new += b' excepts = [f for f in findings if f["kind"] == "EXCEPT"]' + nl +new += b" assert len(excepts) == 1" + nl +new += b' assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (' + nl +new += b' f"telemetry emit should be INTERNAL_COMPLIANT (drain point D.4), got {excepts[0][\'category\']}"' + nl +new += b" )" + nl +new += nl * 2 +new += b"# ---------------------------------------------------------------------------" + nl +new += b"# Phase 12.3: Heuristic D.5 - Bounded retry drain point" + nl +new += b"# ---------------------------------------------------------------------------" + nl +new += b"def test_drain_point_bounded_retry_is_compliant():" + nl +new += b' """try: ...; except (SpecificError): for attempt in range(3): ...; return None is INTERNAL_COMPLIANT (drain point D.5).' + nl +new += b"" + nl +new += b" Per error_handling.md Drain Points section, Pattern 5: bounded retry" + nl +new += b" followed by return None IS a drain point. The retry is bounded (no" + nl +new += b" infinite loop); the final None propagates to a visible error UI." + nl +new += b' """' + nl +new += b" src = (" + nl +new += b" 'def load_with_retry():\\n'" + nl +new += b" ' for attempt in range(3):\\n'" + nl +new += b" ' try:\\n'" + nl +new += b" ' do_load()\\n'" + nl +new += b" ' return \"ok\"\\n'" + nl +new += b" ' except (OSError, ValueError):\\n'" + nl +new += b" ' time.sleep(1)\\n'" + nl +new += b" ' return None\\n'" + nl +new += b" )" + nl +new += b" data = _run_audit_on_fixture(src)" + nl +new += b' findings = _classifications_for_file(data, "audit_heuristic_fixture.py")' + nl +new += b' excepts = [f for f in findings if f["kind"] == "EXCEPT"]' + nl +new += b" assert len(excepts) == 1" + nl +new += b' assert excepts[0]["category"] == "INTERNAL_COMPLIANT", (' + nl +new += b' f"bounded retry should be INTERNAL_COMPLIANT (drain point D.5), got {excepts[0][\'category\']}"' + nl +new += b" )" + nl + +# Append +result = existing + new +with open(p, "wb") as f: + f.write(result) + +print(f"wrote {len(result)} chars (added {len(new)} chars)") +# Verify parses +import ast +ast.parse(result.decode("utf-8")) +print("parses ok") \ No newline at end of file diff --git a/scripts/tier2/artifacts/result_migration_small_files_20260617/fix_sysexit_test.py b/scripts/tier2/artifacts/result_migration_small_files_20260617/fix_sysexit_test.py new file mode 100644 index 00000000..ca64212c --- /dev/null +++ b/scripts/tier2/artifacts/result_migration_small_files_20260617/fix_sysexit_test.py @@ -0,0 +1,23 @@ +from pathlib import Path +p = Path(r"C:\projects\manual_slop_tier2\tests\test_audit_exception_handling_heuristics.py") +data = p.read_bytes() + +# In the test file source (Python source code), the test source string is: +# ' sys.stderr.write("FATAL: config missing\\n")\n' +# When Python parses this, it becomes the actual string: +# ' sys.stderr.write("FATAL: config missing\n")\n' (with real \n in string literal) +# When this is written to a fixture file, the file gets a real newline INSIDE the +# string literal, breaking the syntax. +# +# Fix: change "\\n" to "" (no newline in the message string). +needle = b' sys.stderr.write("FATAL: config missing\\\\n")\\n' +replacement = b' sys.stderr.write("FATAL: config missing")\\n' +if needle in data: + data = data.replace(needle, replacement) + p.write_bytes(data) + print("ok: removed \\n from sys.stderr.write message") +else: + print(f"NOT FOUND; bytes: {needle!r}") + idx = data.find(b"FATAL") + if idx > 0: + print(f"context: {data[idx-20:idx+50]!r}") \ No newline at end of file diff --git a/tests/test_audit_exception_handling_heuristics.py b/tests/test_audit_exception_handling_heuristics.py index df5029b0..e51fc63c 100644 --- a/tests/test_audit_exception_handling_heuristics.py +++ b/tests/test_audit_exception_handling_heuristics.py @@ -395,3 +395,220 @@ def test_result_returning_recovery_in_result_named_function_is_compliant(): assert excepts[0]["category"] == "INTERNAL_COMPLIANT", ( f"Result-returning recovery in *_result function should be INTERNAL_COMPLIANT, got {excepts[0]['category']}" ) + + +# --------------------------------------------------------------------------- +# Phase 12.1: Heuristic #19 REMOVED - narrow except + log is INTERNAL_SILENT_SWALLOW +# --------------------------------------------------------------------------- +def test_narrow_except_with_log_only_is_silent_swallow(): + """try: ...; except (SpecificError): sys.stderr.write(...) is INTERNAL_SILENT_SWALLOW (a violation). + + Per error_handling.md "The Broad-Except Distinction" table and the user's + principle (2026-06-17): "logging is NOT a drain". sys.stderr.write alone + loses the error context; the propagation does NOT terminate visibly to + the user. The convention requires Result[T] propagation to a true drain + point. Heuristic #19 (which classified this as compliant) was REMOVED + in Phase 12.1. + """ + src = ( + 'def log_failure(path, e):\n' + ' try:\n' + ' path.write_text("x", encoding="utf-8")\n' + ' except (OSError, UnicodeEncodeError):\n' + ' sys.stderr.write(f"write failed: {e}")\n' + ) + 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_SILENT_SWALLOW", ( + f"narrow except + log only should be INTERNAL_SILENT_SWALLOW (logging is NOT a drain), got {excepts[0]['category']}" + ) + + +def test_narrow_except_with_logging_error_is_silent_swallow(): + """try: ...; except (SpecificError): logging.error(...) is INTERNAL_SILENT_SWALLOW (a violation). + + Same principle as test_narrow_except_with_log_only_is_silent_swallow + but with the logging module. Logging alone loses the error context. + """ + src = ( + 'def log_failure_via_logging(path):\n' + ' try:\n' + ' path.write_text("x", encoding="utf-8")\n' + ' except (OSError, UnicodeEncodeError) as e:\n' + ' logging.error(f"write failed: {e}")\n' + ) + 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_SILENT_SWALLOW", ( + f"narrow except + logging.error should be INTERNAL_SILENT_SWALLOW, got {excepts[0]['category']}" + ) + + +# --------------------------------------------------------------------------- +# Phase 12.2: visit_Try recursion fix - nested Trys in try body are visited +# --------------------------------------------------------------------------- +def test_visit_try_recurses_into_try_body(): + """A nested try inside the try body should be visited and its handlers recorded. + + The audit's visit_Try had a bug where it did NOT recurse into node.body. + This test constructs a source with an outer try containing an inner try, + and asserts BOTH outer and inner handlers appear in the findings. + """ + src = ( + 'def outer():\n' + ' try:\n' + ' try:\n' + ' do_inner()\n' + ' except ValueError:\n' + ' handle_inner()\n' + ' do_outer_thing()\n' + ' except (OSError, IOError):\n' + ' handle_outer()\n' + ) + 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, ( + f"visit_Try should recurse into try body; expected 2 EXCEPT findings, got {len(excepts)}: {excepts}" + ) + + +# --------------------------------------------------------------------------- +# Phase 12.3: Heuristic D.1 - HTTP error response drain point +# --------------------------------------------------------------------------- +def test_drain_point_http_error_response_is_compliant(): + """try: ...; except (SpecificError): self.send_response(500, ...) is INTERNAL_COMPLIANT (drain point D.1). + + Per error_handling.md Drain Points section, Pattern 1: HTTP error + response in a BaseHTTPRequestHandler subclass IS a drain point. The + HTTP status code IS the visible user feedback; the propagation + terminates at the HTTP response. Heuristic D.1 recognizes this pattern. + """ + src = ( + 'class Handler(BaseHTTPRequestHandler):\n' + ' def do_GET(self):\n' + ' try:\n' + ' self._read_body()\n' + ' except (OSError, ValueError) as e:\n' + ' self.send_response(500)\n' + ' self.send_header("Content-Type", "application/json")\n' + ' self.wfile.write(b\'{"error": "internal"}\')\n' + ) + 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"HTTP error response should be INTERNAL_COMPLIANT (drain point D.1), got {excepts[0]['category']}: {excepts[0].get('note', '')}" + ) + + +# --------------------------------------------------------------------------- +# Phase 12.3: Heuristic D.2 - GUI error display drain point +# --------------------------------------------------------------------------- +def test_drain_point_gui_error_display_is_compliant(): + """try: ...; except (SpecificError): imgui.open_popup(...) is INTERNAL_COMPLIANT (drain point D.2). + + Per error_handling.md Drain Points section, Pattern 2: GUI error + display via imgui.open_popup IS a drain point. The user sees the + error modal. + """ + src = ( + 'def show_load_error():\n' + ' try:\n' + ' do_load()\n' + ' except (OSError, ValueError):\n' + ' imgui.open_popup("Load Error")\n' + ) + 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"GUI error display should be INTERNAL_COMPLIANT (drain point D.2), got {excepts[0]['category']}" + ) + + +# --------------------------------------------------------------------------- +# Phase 12.3: Heuristic D.3 - Intentional app termination drain point +# --------------------------------------------------------------------------- +def test_drain_point_app_termination_is_compliant(): + """try: ...; except (SpecificError): sys.exit(1) is INTERNAL_COMPLIANT (drain point D.3). + + Per error_handling.md Drain Points section, Pattern 3: intentional + app termination via sys.exit IS a drain point. The process exit IS + the termination of the propagation. + """ + src = ( + 'def critical_init():\n' + ' try:\n' + ' load_config()\n' + ' except (OSError, ValueError):\n' + ' sys.stderr.write("FATAL: config missing")\n' + ' sys.exit(1)\n' + ) + 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"app termination should be INTERNAL_COMPLIANT (drain point D.3), got {excepts[0]['category']}" + ) + + +# --------------------------------------------------------------------------- +# Phase 12.3: Heuristic D.4 - Telemetry emission drain point +# --------------------------------------------------------------------------- +def test_drain_point_telemetry_emit_is_compliant(): + """try: ...; except (SpecificError): telemetry.emit_error(...) is INTERNAL_COMPLIANT (drain point D.4). + + Per error_handling.md Drain Points section, Pattern 4: telemetry + emission IS a drain point. The error reaches the monitoring system. + """ + src = ( + 'def report_failure():\n' + ' try:\n' + ' do_thing()\n' + ' except (OSError, ValueError):\n' + ' telemetry.emit_error(operation="do_thing", kind="INTERNAL", message="failed")\n' + ) + 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"telemetry emit should be INTERNAL_COMPLIANT (drain point D.4), got {excepts[0]['category']}" + ) + + +# --------------------------------------------------------------------------- +# Phase 12.3: Heuristic D.5 - Bounded retry drain point +# --------------------------------------------------------------------------- +def test_drain_point_bounded_retry_is_compliant(): + """try: ...; except (SpecificError): for attempt in range(3): ...; return None is INTERNAL_COMPLIANT (drain point D.5). + + Per error_handling.md Drain Points section, Pattern 5: bounded retry + followed by return None IS a drain point. The retry is bounded (no + infinite loop); the final None propagates to a visible error UI. + """ + src = ( + 'def load_with_retry():\n' + ' for attempt in range(3):\n' + ' try:\n' + ' do_load()\n' + ' return "ok"\n' + ' except (OSError, ValueError):\n' + ' time.sleep(1)\n' + ' return None\n' + ) + 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"bounded retry should be INTERNAL_COMPLIANT (drain point D.5), got {excepts[0]['category']}" + )