fix: show scan error as TUI toast before exit

Scan errors were stored in _scan_error and re-raised only after the
user closed the TUI, making the app appear stuck while producing a
confusing post-exit traceback.

Extract _notify_scan_error and call it from all three error branches
in _start_scan_thread so a persistent Textual toast is shown
immediately when the scan thread fails.
This commit is contained in:
Mads Hvelplund 2026-06-19 12:16:21 +02:00
parent c91e4ca1b9
commit 4bc1430d23
No known key found for this signature in database
GPG key ID: 08208EA18E154F00
4 changed files with 60 additions and 0 deletions

View file

@ -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)

View file

View file

View file

@ -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]