diff --git a/src/gui_2.py b/src/gui_2.py index 0b0ff243..89d2eb49 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -6906,13 +6906,43 @@ def render_tier_stream_panel(app: App, tier_key: str, stream_key: str | None) -> imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1)) render_selectable_label(app, f'stream_{tier_key}', content, width=-1, multiline=True, height=0) try: - if len(content) != app._tier_stream_last_len.get(stream_key, -1): - imgui.set_scroll_here_y(1.0) - app._tier_stream_last_len[stream_key] = len(content) - except (TypeError, AttributeError): - pass + _ = _tier_stream_scroll_sync_result(app, stream_key, content, imgui) + except Exception as e: + err = ErrorInfo( + kind=ErrorKind.INTERNAL, + message=f"tier stream scroll sync dispatch failed: {e}", + source="gui_2.render_tier_stream_panel.dispatcher", + original=e, + ) + if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] + app._last_request_errors.append(("render_tier_stream_panel.scroll_sync", err)) finally: imgui.end_child() + +def _tier_stream_scroll_sync_result(app: "App", stream_key: str, content: str, imgui_mod) -> Result[None]: + """Drain-aware variant of render_tier_stream_panel scroll-sync try block (L6908 INTERNAL_SILENT_SWALLOW). + + Extracts the narrow-except TypeError/AttributeError from + render_tier_stream_panel into a Result-returning helper. The narrow + except is the audit-classified SILENT_SWALLOW pattern: narrowing + + logging NOT a drain per the user's principle 2026-06-17. On exception, + converts to ErrorInfo. The caller (render_tier_stream_panel) drains + to app._last_request_errors. + + [C: src/gui_2.py:render_tier_stream_panel (L6908 caller)] + """ + try: + if len(content) != app._tier_stream_last_len.get(stream_key, -1): + imgui_mod.set_scroll_here_y(1.0) + app._tier_stream_last_len[stream_key] = len(content) + return Result(data=None) + except Exception as e: + return Result(data=None, errors=[ErrorInfo( + kind=ErrorKind.INTERNAL, + message=f"tier stream scroll sync failed: {e}", + source="gui_2._tier_stream_scroll_sync_result", + original=e, + )]) else: tier3_keys = [k for k in app.mma_streams if "Tier 3" in k] if not tier3_keys: diff --git a/tests/test_gui_2_result.py b/tests/test_gui_2_result.py index 224a75d6..ad845ea5 100644 --- a/tests/test_gui_2_result.py +++ b/tests/test_gui_2_result.py @@ -2158,4 +2158,48 @@ def test_phase_10_l4911_on_warmup_complete_callback_result_failure(): assert "status dict corrupted" in err.message +def test_phase_10_l6908_tier_stream_scroll_sync_result_success(): + """ + L6908 _tier_stream_scroll_sync_result returns Result(data=None) on success. + + The helper extracts the imgui.set_scroll_here_y + _tier_stream_last_len + update try/except from render_tier_stream_panel into a Result-returning + helper. On success, returns Result(data=None). The caller continues + the render loop normally. + """ + from unittest.mock import MagicMock + import src.gui_2 as gui2_mod + app = MagicMock() + len_map = MagicMock() + len_map.get.return_value = 0 + app._tier_stream_last_len = len_map + mock_imgui = MagicMock() + result = gui2_mod._tier_stream_scroll_sync_result(app, "stream_key", "content here", mock_imgui) + assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" + assert result.data is None + + +def test_phase_10_l6908_tier_stream_scroll_sync_result_failure(): + """ + L6908 _tier_stream_scroll_sync_result returns Result(data=None, errors=[ErrorInfo]) on failure. + + When the comparison or imgui.set_scroll_here_y raises (TypeError on + bad content, AttributeError on missing key), the helper converts to + ErrorInfo. The caller drains to app._last_request_errors. + """ + from unittest.mock import MagicMock + import src.gui_2 as gui2_mod + app = MagicMock() + bad_app = MagicMock() + bad_app._tier_stream_last_len.get.side_effect = AttributeError("missing key") + mock_imgui = MagicMock() + result = gui2_mod._tier_stream_scroll_sync_result(bad_app, "stream_key", "content", mock_imgui) + 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._tier_stream_scroll_sync_result" + assert "missing key" in err.message + +