1c565da7a0
PR2 of the test_full_live_workflow_imgui_assert fix sequence.
When an ImGui scope mismatch (IM_ASSERT(Missing End())) fires in
immapp.run (e.g. after cumulative state corruption from prior sims'
panel renders), the RuntimeError propagates out of app.run(). The
controller's _io_pool gets shut down via __del__/finalization. The
hook server (separate ThreadingHTTPServer) survives. Subsequent test
clicks fail with 'cannot schedule new futures after shutdown' and
the test times out after 120s with no clear signal of what went
wrong.
This commit:
1. Wraps immapp.run in try/except RuntimeError in gui_2.py:618.
On assertion: logs the error to stderr (NOT silent), records
it on controller._gui_degraded_reason and _last_imgui_assert,
and returns from run() so the hook server keeps serving.
2. Adds _gui_degraded_reason and _last_imgui_assert to
AppController.__init__ (initialized to None).
3. Adds /api/gui_health endpoint in api_hooks.py:148. Returns
{healthy, degraded_reason, last_assert, io_pool_alive}.
4. Adds ApiHookClient.get_gui_health() with the matching unit
tests (3 mocked tests + 1 live test).
Per user feedback 2026-06-08:
- The wrap does NOT silently swallow the error. It logs at ERROR
level and surfaces it via the health endpoint.
- Tests can call client.get_gui_health() to detect a degraded GUI
and fail fast with a clear message.
TDD: tests written first, confirmed to fail, then fix applied.
34/34 unit tests pass. 1/1 live test passes (live_gui health
endpoint reports healthy=True on fresh subprocess).
60 lines
1.9 KiB
Python
60 lines
1.9 KiB
Python
"""Tests for ApiHookClient.get_gui_health and the /api/gui_health endpoint.
|
|
|
|
The endpoint exposes the controller's GUI health state (degraded or not)
|
|
to tests so they can fail fast with a clear message when the GUI
|
|
crashed (e.g. due to an ImGui IM_ASSERT).
|
|
"""
|
|
import pytest
|
|
from unittest.mock import patch
|
|
import sys
|
|
import os
|
|
|
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
|
|
from src.api_hook_client import ApiHookClient
|
|
|
|
|
|
def test_get_gui_health_calls_endpoint() -> None:
|
|
"""get_gui_health hits GET /api/gui_health and returns the dict."""
|
|
client = ApiHookClient()
|
|
with patch.object(client, "_make_request") as mock_make:
|
|
mock_make.return_value = {
|
|
"healthy": True,
|
|
"degraded_reason": None,
|
|
"last_assert": None,
|
|
"io_pool_alive": True,
|
|
}
|
|
health = client.get_gui_health()
|
|
assert health == {
|
|
"healthy": True,
|
|
"degraded_reason": None,
|
|
"last_assert": None,
|
|
"io_pool_alive": True,
|
|
}
|
|
mock_make.assert_any_call("GET", "/api/gui_health")
|
|
|
|
|
|
def test_get_gui_health_handles_empty_response() -> None:
|
|
"""get_gui_health returns a default healthy dict on empty/invalid response."""
|
|
client = ApiHookClient()
|
|
with patch.object(client, "_make_request") as mock_make:
|
|
mock_make.return_value = None
|
|
health = client.get_gui_health()
|
|
assert health["healthy"] is True
|
|
assert health["degraded_reason"] is None
|
|
|
|
|
|
def test_get_gui_health_reports_degraded_state() -> None:
|
|
"""get_gui_health returns degraded=True when the controller has a degraded_reason."""
|
|
client = ApiHookClient()
|
|
with patch.object(client, "_make_request") as mock_make:
|
|
mock_make.return_value = {
|
|
"healthy": False,
|
|
"degraded_reason": "immapp.run raised RuntimeError: IM_ASSERT(Missing End())",
|
|
"last_assert": "Traceback (most recent call last):\n File immui.cpp\nIM_ASSERT",
|
|
"io_pool_alive": True,
|
|
}
|
|
health = client.get_gui_health()
|
|
assert health["healthy"] is False
|
|
assert "IM_ASSERT" in health["degraded_reason"]
|