diff --git a/src/gui_2.py b/src/gui_2.py index be970b6e..ce4a62fb 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -1165,12 +1165,35 @@ def _shutdown_save_ini_result(app: "App") -> Result[None]: # the cost of the first full-UI render. Splitting them attributes the gap. if not getattr(self, "_gui_func_entered", False): self._gui_func_entered = True - try: - init_ts = getattr(self.controller, "_init_start_ts", None) - if init_ts is not None: - sys.stderr.write(f"[startup] first _gui_func entry at {(time.time() - init_ts) * 1000:.1f}ms after init (window/GL + font/style/post_init callbacks done)\n") - sys.stderr.flush() - except Exception: pass + log_result = _gui_func_entry_log_result(self) + if not log_result.ok: + if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] + self._last_request_errors.append(("_gui_func.entry_log", log_result.errors[0])) + +def _gui_func_entry_log_result(app: "App") -> Result[None]: + """Drain-aware variant of App._gui_func startup-timing log (L1152 INTERNAL_SILENT_SWALLOW). + + Extracts the first-frame startup-timing sys.stderr.write/flush from + App._gui_func into a Result-returning helper. On exception (broken pipe, + OSError on the stderr stream), converts to ErrorInfo (logging NOT a drain + per the user's principle 2026-06-17). The legacy _gui_func method + drains to self._last_request_errors. + + [C: src/gui_2.py:App._gui_func (L1152 legacy wrapper)] + """ + try: + init_ts = getattr(app.controller, "_init_start_ts", None) + if init_ts is not None: + sys.stderr.write(f"[startup] first _gui_func entry at {(time.time() - init_ts) * 1000:.1f}ms after init (window/GL + font/style/post_init callbacks done)\n") + sys.stderr.flush() + return Result(data=None) + except Exception as e: + return Result(data=None, errors=[ErrorInfo( + kind=ErrorKind.INTERNAL, + message=f"gui_func entry log failed: {e}", + source="gui_2._gui_func_entry_log_result", + original=e, + )]) # One-shot: kick off the controller's heavy-module warmup on the shared # io_pool once the FIRST frame has actually been painted. Waiting one frame diff --git a/tests/test_gui_2_result.py b/tests/test_gui_2_result.py index 7ac173ad..d4521bfa 100644 --- a/tests/test_gui_2_result.py +++ b/tests/test_gui_2_result.py @@ -1947,4 +1947,47 @@ def test_phase_10_l1052_shutdown_save_ini_result_failure(): assert "disk full" in err.message +def test_phase_10_l1152_gui_func_entry_log_result_success(): + """ + L1152 _gui_func_entry_log_result returns Result(data=None) on success. + + The helper extracts the startup-timing sys.stderr.write from + App._gui_func into a Result-returning helper. On success, returns + Result(data=None). The legacy _gui_func method proceeds to the rest + of the render loop. + """ + from unittest.mock import MagicMock + import src.gui_2 as gui2_mod + app = MagicMock() + app.controller = MagicMock() + app.controller._init_start_ts = 1000.0 + result = gui2_mod._gui_func_entry_log_result(app) + assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" + assert result.data is None + + +def test_phase_10_l1152_gui_func_entry_log_result_failure(): + """ + L1152 _gui_func_entry_log_result returns Result(data=None, errors=[ErrorInfo]) on failure. + + When the sys.stderr.write or sys.stderr.flush raises (e.g., broken pipe), + the helper converts to ErrorInfo. The legacy _gui_func method drains to + self._last_request_errors and proceeds with the rest of the render loop + (preserving the original behavior of continuing past the failure). + """ + from unittest.mock import MagicMock, patch + import src.gui_2 as gui2_mod + app = MagicMock() + app.controller = MagicMock() + app.controller._init_start_ts = 1000.0 + with patch("src.gui_2.sys.stderr.write", side_effect=OSError("broken pipe")): + result = gui2_mod._gui_func_entry_log_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._gui_func_entry_log_result" + assert "broken pipe" in err.message + +