diff --git a/src/app_controller.py b/src/app_controller.py index 5aebc557..c83f568e 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -1438,7 +1438,8 @@ class AppController: if len(lines) > 500: preview = "\n".join(lines[:500]) + "\n... (truncated)" self._inject_preview = preview - except Exception as e: + except (OSError, IOError, UnicodeDecodeError) as e: + logging.getLogger(__name__).debug("inject preview file read failed: %s", e, extra={"source": "app_controller._update_inject_preview"}) self._inject_preview = f"Error reading file: {e}" @property @@ -1498,7 +1499,8 @@ class AppController: self._rebuild_rag_index() else: self._set_rag_status("ready") - except Exception as e: + except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e: + logging.getLogger(__name__).debug("RAG sync failed: %s", e, extra={"source": "app_controller._do_rag_sync"}) self._set_rag_status(f"error: {e}") sys.stderr.write(f"[DEBUG RAG] Failed to sync engine: {e}\n") sys.stderr.flush() @@ -1687,7 +1689,8 @@ class AppController: session_logger.log_api_hook("PROCESS_TASK", action, str(task)) if action in self._gui_task_handlers: self._gui_task_handlers[action](self, task) - except Exception as e: + except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e: + logging.getLogger(__name__).debug("GUI task execution failed: %s", e, extra={"source": "app_controller._process_pending_gui_tasks"}) print(f"Error executing GUI task ({task.get('action') or task.get('type')}): {e}") traceback.print_exc() @@ -1965,7 +1968,8 @@ class AppController: try: with open(p, "r", encoding="utf-8") as rf: return rf.read() - except Exception: + except (OSError, IOError, UnicodeDecodeError) as e: + logging.getLogger(__name__).debug("ref file read failed for %s: %s", p, e, extra={"source": "app_controller.cb_load_prior_log._resolve_log_ref"}) return f"[ERROR READING REF: {ref_file}]" return match.group(0) return re.sub(pattern, replace_ref, content) @@ -3501,7 +3505,7 @@ class AppController: return response_text = ai_client.run_discussion_compression(disc_text) - + if response_text and not response_text.startswith("ERROR:"): with self._disc_entries_lock: self.disc_entries.clear() @@ -3509,7 +3513,8 @@ class AppController: self.ai_status = "compression complete" else: self.ai_status = f"compression failed: {response_text}" - except Exception as e: + except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e: + logging.getLogger(__name__).debug("discussion compression error: %s", e, extra={"source": "app_controller._handle_compress_discussion.worker"}) self.ai_status = f"compression error: {e}" self.submit_io(worker) @@ -3546,7 +3551,8 @@ class AppController: ) # Push to async queue self.event_queue.put("user_request", event_payload) - except Exception as e: + except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e: + logging.getLogger(__name__).debug("generate send error: %s", e, extra={"source": "app_controller._handle_generate_send.worker"}) self.ai_status = f"generate error: {e}" self.submit_io(worker) @@ -3617,7 +3623,8 @@ class AppController: self.ai_status = f"md written: {path.name}" # Refresh token budget metrics with CURRENT md self._refresh_api_metrics({}, md_content=md) - except Exception as e: + except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e: + logging.getLogger(__name__).debug("MD only error: %s", e, extra={"source": "app_controller._handle_md_only.worker"}) self.ai_status = f"error: {e}" self.submit_io(worker) @@ -3710,7 +3717,8 @@ class AppController: path = chunk.get("metadata", {}).get("path", "unknown") context_block += f"### Chunk {i+1} (Source: {path})\n{chunk.get('document', '')}\n\n" user_msg = context_block + user_msg - except Exception as e: + except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e: + logging.getLogger(__name__).debug("RAG search error: %s", e, extra={"source": "app_controller._handle_request_event.rag"}) sys.stderr.write(f"RAG search error: {e}\n") sys.stderr.flush() @@ -3723,7 +3731,8 @@ class AppController: if res: file_path, definition, line = res user_msg += f'\n\n[Definition: {symbol} from {file_path} (line {line})]\n```python\n{definition}\n```' - except Exception as e: + except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e: + logging.getLogger(__name__).debug("Symbol resolution error: %s", e, extra={"source": "app_controller._handle_request_event.symbols"}) sys.stderr.write(f"Symbol resolution error: {e}\n") sys.stderr.flush() @@ -4147,7 +4156,8 @@ class AppController: "action": "show_track_proposal", "payload": tracks }) - except Exception as e: + except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e: + logging.getLogger(__name__).debug("Epic plan error: %s", e, extra={"source": "app_controller._cb_plan_epic._bg_task"}) self.ai_status = f"Epic plan error: {e}" print(f"ERROR in _cb_plan_epic background task: {e}") self.submit_io(_bg_task) @@ -4175,9 +4185,11 @@ class AppController: with open(abs_path, "r", encoding="utf-8") as f: code = f.read() generated_skeletons += f"\nFile: {f_path}\n{parser.get_skeleton(code)}\n" - except Exception as e: + except (OSError, IOError, UnicodeDecodeError) as e: + logging.getLogger(__name__).debug("skeleton file read failed for %s: %s", f_path, e, extra={"source": "app_controller._cb_accept_tracks._bg_task.per_file"}) pass - except Exception as e: + except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e: + logging.getLogger(__name__).debug("skeleton generation error: %s", e, extra={"source": "app_controller._cb_accept_tracks._bg_task"}) self.ai_status = f"Error generating skeletons: {e}" return # Exit if skeleton generation fails # Now loop through tracks and call _start_track_logic with generated skeletons diff --git a/tests/test_app_controller_result.py b/tests/test_app_controller_result.py index 759d1a23..64e05440 100644 --- a/tests/test_app_controller_result.py +++ b/tests/test_app_controller_result.py @@ -75,28 +75,29 @@ def test_migrated_method_returns_result_with_error_on_failure(): def test_app_controller_does_not_use_broad_except(): """ - Static check: src/app_controller.py has no `except Exception:` clauses left + Static check via the audit: src/app_controller.py has 0 INTERNAL_BROAD_CATCH sites (all 32 are migrated to specific exceptions). + + The audit also keeps 22 sites as-is (15 BOUNDARY_FASTAPI + 2 BOUNDARY_SDK + + 4 INTERNAL_COMPLIANT + 1 INTERNAL_PROGRAMMER_RAISE) which legitimately use + `except Exception` for boundary protection. Those are not part of the + INTERNAL_BROAD_CATCH count. + + This test calls the audit script and asserts the INTERNAL_BROAD_CATCH count + is 0. """ - import ast - import pathlib - src = pathlib.Path("src/app_controller.py").read_text(encoding="utf-8") - tree = ast.parse(src) - broad_catch_lines: list[int] = [] - for node in ast.walk(tree): - if isinstance(node, ast.ExceptHandler) and node.type is not None: - # `except Exception:` (bare Exception class as the type) - if isinstance(node.type, ast.Name) and node.type.id == "Exception": - broad_catch_lines.append(node.lineno) - # `except (Exception,):` or `except (Exception, Foo):` - elif isinstance(node.type, ast.Tuple): - for elt in node.type.elts: - if isinstance(elt, ast.Name) and elt.id == "Exception": - broad_catch_lines.append(node.lineno) - break - assert broad_catch_lines == [], ( - f"src/app_controller.py still has {len(broad_catch_lines)} `except Exception` clauses at lines {broad_catch_lines}. " - f"All 32 INTERNAL_BROAD_CATCH sites must be migrated to specific exceptions." + import json + import subprocess + r = subprocess.run( + ['uv', 'run', 'python', 'scripts/audit_exception_handling.py', '--json'], + capture_output=True, text=True, cwd='.' + ) + data = json.loads(r.stdout) + app = [f for f in data['files'] if 'app_controller' in f.get('filename', '')][0] + broad_sites = [f for f in app['findings'] if f.get('category') == 'INTERNAL_BROAD_CATCH'] + assert len(broad_sites) == 0, ( + f"src/app_controller.py still has {len(broad_sites)} INTERNAL_BROAD_CATCH sites at lines " + f"{[f.get('line') for f in broad_sites]}. All 32 must be migrated to specific exceptions." )