diff --git a/src/gui_2.py b/src/gui_2.py index 23b5702b..1145af65 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -745,33 +745,43 @@ def _post_init(self) -> None: self.runner_params.callbacks.post_init = _profiled_post_init self._fetch_models(self.current_provider) md_options = markdown_helper.get_renderer().options - #Note(Ed): Exception(Thirdparty) - try: - immapp.run(self.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=md_options)) - except RuntimeError as _immapp_exc: - # ImGui scope errors (IM_ASSERT) and other native-bundle exceptions - # surface as RuntimeError. Per user feedback 2026-06-08, do not - # silently swallow — record the failure on the controller so the - # /api/gui_health endpoint and the GUI logs can surface it. Keep the - # process alive so the hook server (separate thread) can continue - # serving tests; the next test can detect the degraded state and - # fail fast with a clear message. - if hasattr(self, "controller") and self.controller is not None: - self.controller._gui_degraded_reason = ( - f"immapp.run raised {type(_immapp_exc).__name__}: {_immapp_exc}" - ) - self.controller._last_imgui_assert = traceback.format_exc() - print( - f"[GUI-DEGRADED] immapp.run raised: {_immapp_exc}", - file = sys.stderr, - flush = True, - ) - print(self.controller._last_imgui_assert if hasattr(self, "controller") and self.controller else "", - file=sys.stderr, flush=True) - return - # On exit (only reached on clean shutdown) - self.shutdown() - session_logger.close_session() +def _run_immapp_result(app: "App") -> Result[None]: + """Drain-aware variant of App.run immapp.run() call (L728 INTERNAL_SILENT_SWALLOW). + + Extracts the thirdparty immapp.run() invocation from App.run into a + Result-returning helper. On exception (RuntimeError from IM_ASSERT or + other native-bundle errors), converts to ErrorInfo. The legacy run + method sets controller._gui_degraded_reason and _last_imgui_assert + (the canonical degradation drain), appends to _startup_timeline_errors, + and returns. NO logging: logging is NOT a drain per the user's + principle 2026-06-17. + + [C: src/gui_2.py:App.run (L728 legacy wrapper)] + """ + #Note(Ed): Exception(Thirdparty) + try: + immapp.run(app.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=markdown_helper.get_renderer().options)) + return Result(data=None) + except Exception as e: + return Result(data=None, errors=[ErrorInfo( + kind=ErrorKind.INTERNAL, + message=f"immapp.run raised {type(e).__name__}: {e}", + source="gui_2._run_immapp_result", + original=e, + )]) + + run_result = _run_immapp_result(self) + if not run_result.ok: + err = run_result.errors[0] + if hasattr(self, "controller") and self.controller is not None: + self.controller._gui_degraded_reason = err.message + self.controller._last_imgui_assert = traceback.format_exception(type(err.original), err.original, err.original.__traceback__) if err.original else "" + if not hasattr(self, '_startup_timeline_errors'): self._startup_timeline_errors = [] + self._startup_timeline_errors.append(("run.immapp", err)) + return + # On exit (only reached on clean shutdown) + self.shutdown() + session_logger.close_session() def _load_fonts(self) -> None: from src.startup_profiler import startup_profiler diff --git a/tests/test_gui_2_result.py b/tests/test_gui_2_result.py index 744c650a..07691191 100644 --- a/tests/test_gui_2_result.py +++ b/tests/test_gui_2_result.py @@ -1857,4 +1857,50 @@ def test_phase_10_l612_post_init_callback_result_failure(): assert "controller not ready" in err.message +def test_phase_10_l728_run_immapp_result_success(): + """ + L728 _run_immapp_result returns Result(data=None) on success. + + The helper extracts the immapp.run() call from App.run into a Result-returning + helper. On success, returns Result(data=None). The legacy run method + proceeds to self.shutdown() and session_logger.close_session(). + """ + from unittest.mock import MagicMock, patch + import src.gui_2 as gui2_mod + app = MagicMock() + app.runner_params = MagicMock() + with patch("src.gui_2.immapp") as mock_immapp: + mock_immapp.AddOnsParams.return_value = MagicMock() + result = gui2_mod._run_immapp_result(app) + assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" + assert result.data is None + mock_immapp.run.assert_called_once() + + +def test_phase_10_l728_run_immapp_result_failure(): + """ + L728 _run_immapp_result returns Result(data=None, errors=[ErrorInfo]) on failure. + + When immapp.run() raises RuntimeError (IM_ASSERT, native bundle crash, etc.), + the helper converts to ErrorInfo. The legacy run method sets the + controller._gui_degraded_reason and _last_imgui_assert drain attributes, + appends to _startup_timeline_errors, and returns. + """ + from unittest.mock import MagicMock, patch + import src.gui_2 as gui2_mod + app = MagicMock() + app.runner_params = MagicMock() + app.controller = MagicMock() + with patch("src.gui_2.immapp") as mock_immapp: + mock_immapp.AddOnsParams.return_value = MagicMock() + mock_immapp.run.side_effect = RuntimeError("IM_ASSERT: invalid scope") + result = gui2_mod._run_immapp_result(app) + assert not result.ok, f"Expected ok=False on failure, got data: {result.data}" + assert result.data is None + assert result.errors, "Expected at least one error on failure" + err = result.errors[0] + assert err.source == "gui_2._run_immapp_result" + assert "IM_ASSERT" in err.message + +