diff --git a/strix/interface/tui/app.py b/strix/interface/tui/app.py index e8fac0e4..aefd4b04 100644 --- a/strix/interface/tui/app.py +++ b/strix/interface/tui/app.py @@ -1378,12 +1378,15 @@ class StrixTUIApp(App): # type: ignore[misc] except (ConnectionError, TimeoutError) as e: logging.exception("Network error during scan") self._scan_error = e + self._notify_scan_error(e) except RuntimeError as e: logging.exception("Runtime error during scan") self._scan_error = e + self._notify_scan_error(e) except Exception as e: logging.exception("Unexpected error during scan") self._scan_error = e + self._notify_scan_error(e) finally: with contextlib.suppress(Exception): loop.run_until_complete( @@ -1399,6 +1402,16 @@ class StrixTUIApp(App): # type: ignore[misc] self._scan_thread = threading.Thread(target=scan_target, daemon=True) self._scan_thread.start() + def _notify_scan_error(self, exc: Exception) -> None: + """Dispatch a persistent error toast from any thread.""" + with contextlib.suppress(Exception): + self.call_from_thread( + self.notify, + f"Scan failed: {type(exc).__name__}: {exc}", + severity="error", + timeout=0, + ) + def _capture_sdk_event(self, agent_id: str, event: Any) -> None: try: self.call_from_thread(self._record_sdk_event, agent_id, event) diff --git a/tests/interface/__init__.py b/tests/interface/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/interface/tui/__init__.py b/tests/interface/tui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/interface/tui/test_app.py b/tests/interface/tui/test_app.py new file mode 100644 index 00000000..b2789710 --- /dev/null +++ b/tests/interface/tui/test_app.py @@ -0,0 +1,47 @@ +"""Tests for StrixTUIApp._notify_scan_error — Fix 2: in-TUI error display.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +from strix.interface.tui.app import StrixTUIApp + + +def _mock_app() -> MagicMock: + return MagicMock(spec=StrixTUIApp) + + +def test_notify_scan_error_dispatches_toast() -> None: + """_notify_scan_error must call notify via call_from_thread with error text.""" + app = _mock_app() + exc = RuntimeError("something went wrong") + + StrixTUIApp._notify_scan_error(app, exc) + + app.call_from_thread.assert_called_once_with( + app.notify, + "Scan failed: RuntimeError: something went wrong", + severity="error", + timeout=0, + ) + + +def test_notify_scan_error_suppresses_call_from_thread_failure() -> None: + """If call_from_thread itself raises, the exception must be swallowed.""" + app = _mock_app() + app.call_from_thread.side_effect = RuntimeError("TUI not ready") + + # Must not propagate the exception. + StrixTUIApp._notify_scan_error(app, ValueError("boom")) + + +def test_notify_scan_error_formats_exception_type() -> None: + """Toast text must include the exception's class name and message.""" + app = _mock_app() + exc = ConnectionError("host unreachable") + + StrixTUIApp._notify_scan_error(app, exc) + + args = app.call_from_thread.call_args.args + assert "ConnectionError" in args[1] + assert "host unreachable" in args[1]