initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree
Includes core prod + GREEN/BLUE subsystems: - prod/ (BLUE harness, configs, scripts, docs) - nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved) - adaptive_exit/ (AEM engine + models/bucket_assignments.pkl) - Observability/ (EsoF advisor, TUI, dashboards) - external_factors/ (EsoF producer) - mc_forewarning_qlabs_fork/ (MC regime/envelope) Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
This commit is contained in:
291
Observability/TUI/test_dolphin_tui_prefect_offline.py
Executable file
291
Observability/TUI/test_dolphin_tui_prefect_offline.py
Executable file
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
test_dolphin_tui_prefect_offline.py
|
||||
|
||||
Verifies Prefect-offline behavior of DolphinDataFetcher and PrefectPanel.
|
||||
|
||||
Tests:
|
||||
- test_fetch_prefect_returns_false_on_connection_error
|
||||
- test_fetch_prefect_returns_false_on_timeout
|
||||
- test_fetch_prefect_returns_false_on_non_200
|
||||
- test_fetch_prefect_does_not_crash
|
||||
- test_snapshot_prefect_offline_fields
|
||||
- test_prefect_panel_shows_offline_text
|
||||
|
||||
All tests are self-contained and do NOT require a live Prefect instance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import types
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Make sure the TUI module is importable from this directory
|
||||
# ---------------------------------------------------------------------------
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stub hazelcast so the import succeeds without the package
|
||||
# ---------------------------------------------------------------------------
|
||||
_hz_stub = types.ModuleType("hazelcast")
|
||||
_hz_stub.HazelcastClient = MagicMock()
|
||||
sys.modules.setdefault("hazelcast", _hz_stub)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stub textual so dolphin_tui imports cleanly without a terminal
|
||||
# ---------------------------------------------------------------------------
|
||||
for _mod in [
|
||||
"textual",
|
||||
"textual.app",
|
||||
"textual.containers",
|
||||
"textual.widgets",
|
||||
]:
|
||||
if _mod not in sys.modules:
|
||||
sys.modules[_mod] = types.ModuleType(_mod)
|
||||
|
||||
_textual_app = sys.modules["textual.app"]
|
||||
_textual_app.App = object
|
||||
_textual_app.ComposeResult = object
|
||||
|
||||
_textual_containers = sys.modules["textual.containers"]
|
||||
_textual_containers.Horizontal = object
|
||||
|
||||
_textual_widgets = sys.modules["textual.widgets"]
|
||||
_textual_widgets.Static = object
|
||||
_textual_widgets.VerticalScroll = object
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stub httpx — we will patch individual methods per test
|
||||
# ---------------------------------------------------------------------------
|
||||
if "httpx" not in sys.modules:
|
||||
_httpx_stub = types.ModuleType("httpx")
|
||||
_httpx_stub.AsyncClient = MagicMock()
|
||||
_httpx_stub.ConnectError = type("ConnectError", (Exception,), {})
|
||||
_httpx_stub.TimeoutException = type("TimeoutException", (Exception,), {})
|
||||
sys.modules["httpx"] = _httpx_stub
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Now import the module under test
|
||||
# ---------------------------------------------------------------------------
|
||||
from dolphin_tui import ( # noqa: E402
|
||||
DolphinDataFetcher,
|
||||
DataSnapshot,
|
||||
PrefectPanel,
|
||||
)
|
||||
import httpx # noqa: E402 (the stub or real module)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _run(coro):
|
||||
"""Run a coroutine in the current event loop."""
|
||||
return asyncio.get_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
def _make_fetcher() -> DolphinDataFetcher:
|
||||
fetcher = DolphinDataFetcher(hz_host="localhost", hz_port=5701)
|
||||
fetcher._start_reconnect = MagicMock() # prevent background reconnect tasks
|
||||
return fetcher
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFetchPrefectConnectionError(unittest.TestCase):
|
||||
"""test_fetch_prefect_returns_false_on_connection_error
|
||||
|
||||
When httpx raises ConnectError, fetch_prefect() must return (False, []).
|
||||
"""
|
||||
|
||||
def test_fetch_prefect_returns_false_on_connection_error(self):
|
||||
fetcher = _make_fetcher()
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=False)
|
||||
mock_client.get = AsyncMock(side_effect=httpx.ConnectError("refused"))
|
||||
|
||||
with patch.object(httpx, "AsyncClient", return_value=mock_client):
|
||||
result = _run(fetcher.fetch_prefect())
|
||||
|
||||
self.assertEqual(result, (False, []))
|
||||
|
||||
|
||||
class TestFetchPrefectTimeout(unittest.TestCase):
|
||||
"""test_fetch_prefect_returns_false_on_timeout
|
||||
|
||||
When httpx raises TimeoutException, fetch_prefect() must return (False, []).
|
||||
"""
|
||||
|
||||
def test_fetch_prefect_returns_false_on_timeout(self):
|
||||
fetcher = _make_fetcher()
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=False)
|
||||
mock_client.get = AsyncMock(side_effect=httpx.TimeoutException("timed out"))
|
||||
|
||||
with patch.object(httpx, "AsyncClient", return_value=mock_client):
|
||||
result = _run(fetcher.fetch_prefect())
|
||||
|
||||
self.assertEqual(result, (False, []))
|
||||
|
||||
|
||||
class TestFetchPrefectNon200(unittest.TestCase):
|
||||
"""test_fetch_prefect_returns_false_on_non_200
|
||||
|
||||
When /api/health returns HTTP 503, fetch_prefect() must return (False, []).
|
||||
"""
|
||||
|
||||
def test_fetch_prefect_returns_false_on_non_200(self):
|
||||
fetcher = _make_fetcher()
|
||||
|
||||
health_resp = MagicMock()
|
||||
health_resp.status_code = 503
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=False)
|
||||
mock_client.get = AsyncMock(return_value=health_resp)
|
||||
|
||||
with patch.object(httpx, "AsyncClient", return_value=mock_client):
|
||||
healthy, flows = _run(fetcher.fetch_prefect())
|
||||
|
||||
self.assertFalse(healthy, "healthy must be False when /api/health returns 503")
|
||||
# flows may be empty or populated depending on whether the flows call was made;
|
||||
# the key requirement is that healthy is False
|
||||
self.assertIsInstance(flows, list)
|
||||
|
||||
|
||||
class TestFetchPrefectDoesNotCrash(unittest.TestCase):
|
||||
"""test_fetch_prefect_does_not_crash
|
||||
|
||||
fetch_prefect() must never raise, even on unexpected exceptions.
|
||||
"""
|
||||
|
||||
def test_fetch_prefect_does_not_crash_on_unexpected_exception(self):
|
||||
fetcher = _make_fetcher()
|
||||
|
||||
# Simulate AsyncClient itself raising an unexpected error
|
||||
with patch.object(httpx, "AsyncClient", side_effect=RuntimeError("unexpected")):
|
||||
try:
|
||||
result = _run(fetcher.fetch_prefect())
|
||||
except Exception as exc:
|
||||
self.fail(f"fetch_prefect() raised unexpectedly: {exc}")
|
||||
|
||||
self.assertEqual(result, (False, []))
|
||||
|
||||
def test_fetch_prefect_does_not_crash_on_os_error(self):
|
||||
fetcher = _make_fetcher()
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=False)
|
||||
mock_client.get = AsyncMock(side_effect=OSError("network unreachable"))
|
||||
|
||||
with patch.object(httpx, "AsyncClient", return_value=mock_client):
|
||||
try:
|
||||
result = _run(fetcher.fetch_prefect())
|
||||
except Exception as exc:
|
||||
self.fail(f"fetch_prefect() raised unexpectedly: {exc}")
|
||||
|
||||
self.assertEqual(result, (False, []))
|
||||
|
||||
|
||||
class TestSnapshotPrefectOfflineFields(unittest.TestCase):
|
||||
"""test_snapshot_prefect_offline_fields
|
||||
|
||||
When fetch_prefect() returns (False, []), the assembled DataSnapshot
|
||||
must have prefect_healthy=False and prefect_flows=[].
|
||||
"""
|
||||
|
||||
def test_snapshot_prefect_offline_fields(self):
|
||||
snap = DataSnapshot(
|
||||
prefect_healthy=False,
|
||||
prefect_flows=[],
|
||||
)
|
||||
|
||||
self.assertFalse(snap.prefect_healthy, "prefect_healthy must be False")
|
||||
self.assertEqual(snap.prefect_flows, [], "prefect_flows must be empty list")
|
||||
|
||||
def test_snapshot_default_is_offline(self):
|
||||
"""Default DataSnapshot should represent offline state."""
|
||||
snap = DataSnapshot()
|
||||
|
||||
self.assertFalse(snap.prefect_healthy)
|
||||
self.assertEqual(snap.prefect_flows, [])
|
||||
|
||||
|
||||
class TestPrefectPanelShowsOfflineText(unittest.TestCase):
|
||||
"""test_prefect_panel_shows_offline_text
|
||||
|
||||
When DataSnapshot.prefect_healthy=False, PrefectPanel._render_markup()
|
||||
must contain the string "PREFECT OFFLINE".
|
||||
"""
|
||||
|
||||
def test_prefect_panel_shows_offline_text_when_unhealthy(self):
|
||||
panel = PrefectPanel()
|
||||
snap = DataSnapshot(prefect_healthy=False, prefect_flows=[])
|
||||
|
||||
panel._snap = snap
|
||||
markup = panel._render_markup()
|
||||
|
||||
self.assertIn(
|
||||
"PREFECT OFFLINE",
|
||||
markup,
|
||||
f"Expected 'PREFECT OFFLINE' in panel markup, got:\n{markup}",
|
||||
)
|
||||
|
||||
def test_prefect_panel_shows_offline_text_when_snap_is_none(self):
|
||||
"""Panel must show PREFECT OFFLINE when no snapshot has been set."""
|
||||
panel = PrefectPanel()
|
||||
# _snap defaults to None
|
||||
|
||||
markup = panel._render_markup()
|
||||
|
||||
self.assertIn(
|
||||
"PREFECT OFFLINE",
|
||||
markup,
|
||||
f"Expected 'PREFECT OFFLINE' when snap is None, got:\n{markup}",
|
||||
)
|
||||
|
||||
def test_prefect_panel_does_not_show_offline_when_healthy(self):
|
||||
"""Sanity check: healthy snapshot should NOT show PREFECT OFFLINE."""
|
||||
panel = PrefectPanel()
|
||||
snap = DataSnapshot(prefect_healthy=True, prefect_flows=[])
|
||||
|
||||
panel._snap = snap
|
||||
markup = panel._render_markup()
|
||||
|
||||
self.assertNotIn(
|
||||
"PREFECT OFFLINE",
|
||||
markup,
|
||||
f"'PREFECT OFFLINE' should not appear when prefect_healthy=True",
|
||||
)
|
||||
self.assertIn("PREFECT ✓", markup)
|
||||
|
||||
def test_update_data_does_not_crash_when_offline(self):
|
||||
"""update_data() must not raise when called with an offline snapshot."""
|
||||
panel = PrefectPanel()
|
||||
# Patch the inherited update() method (from Static/object) so it's a no-op
|
||||
panel.update = MagicMock()
|
||||
|
||||
snap = DataSnapshot(prefect_healthy=False, prefect_flows=[])
|
||||
|
||||
try:
|
||||
panel.update_data(snap)
|
||||
except Exception as exc:
|
||||
self.fail(f"update_data() raised unexpectedly: {exc}")
|
||||
|
||||
panel.update.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user