# Tests for graceful "N/A" / "--" rendering when HZ maps return None for all keys. # Validates: Requirements 12.3 # The TUI MUST NOT crash when any individual HZ key is missing or contains # malformed JSON. Missing fields MUST render as "--" or "N/A". import asyncio import sys import os import types from unittest.mock import MagicMock, patch, AsyncMock import pytest # --------------------------------------------------------------------------- # Ensure the TUI module is importable without textual/hazelcast/httpx installed # --------------------------------------------------------------------------- sys.path.insert(0, os.path.dirname(__file__)) for _mod in ("textual", "textual.app", "textual.widgets", "textual.containers", "httpx", "hazelcast"): if _mod not in sys.modules: sys.modules[_mod] = types.ModuleType(_mod) import textual.app as _textual_app import textual.widgets as _textual_widgets import textual.containers as _textual_containers _textual_app.App = object _textual_app.ComposeResult = object _textual_widgets.Static = object _textual_widgets.VerticalScroll = object _textual_containers.Horizontal = object from dolphin_tui import ( color_age, fmt_float, fmt_pnl, rm_bar, posture_color, status_color, DataSnapshot, DolphinDataFetcher, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_mock_map_returning_none(): """Return a mock IMap whose .get(key).result() always returns None.""" future = MagicMock() future.result.return_value = None hz_map = MagicMock() hz_map.get.return_value = future hz_map.key_set.return_value = future return hz_map def _make_fetcher_with_mock_client(): """Create a DolphinDataFetcher whose hz_client returns None for every map key.""" fetcher = DolphinDataFetcher() fetcher.hz_connected = True mock_map = _make_mock_map_returning_none() map_future = MagicMock() map_future.result.return_value = mock_map mock_client = MagicMock() mock_client.get_map.return_value = map_future fetcher.hz_client = mock_client return fetcher def _run(coro): return asyncio.get_event_loop().run_until_complete(coro) def _fetch_with_empty_maps(): fetcher = _make_fetcher_with_mock_client() with patch.object(fetcher, "fetch_prefect", new=AsyncMock(return_value=(False, []))): with patch.object(fetcher, "tail_log", return_value=[]): with patch.object(fetcher, "_start_reconnect", return_value=None): return _run(fetcher.fetch()) # --------------------------------------------------------------------------- # Formatting helpers: None inputs # --------------------------------------------------------------------------- def test_fmt_float_none_returns_double_dash(): assert fmt_float(None) == "--" def test_fmt_float_none_custom_decimals(): assert fmt_float(None, decimals=2) == "--" def test_fmt_pnl_none_returns_double_dash(): color, text = fmt_pnl(None) assert text == "--" def test_fmt_pnl_none_returns_white_color(): color, text = fmt_pnl(None) assert color == "white" def test_color_age_none_returns_na(): color, text = color_age(None) assert text == "N/A" def test_color_age_none_returns_dim(): color, text = color_age(None) assert color == "dim" def test_rm_bar_none_returns_double_dash(): assert rm_bar(None) == "--" def test_posture_color_none_returns_dim(): assert posture_color(None) == "dim" def test_status_color_none_returns_dim(): assert status_color(None) == "dim" # --------------------------------------------------------------------------- # Sync parsers: None input -> all-None output, no crash # --------------------------------------------------------------------------- def test_parse_scan_none_no_crash(): fetcher = DolphinDataFetcher() result = fetcher._parse_scan(None) assert result["scan_number"] is None assert result["vel_div"] is None assert result["w50_velocity"] is None assert result["w750_velocity"] is None assert result["instability_50"] is None assert result["scan_bridge_ts"] is None assert result["scan_age_s"] is None assert result["asset_prices"] == {} def test_parse_safety_none_no_crash(): fetcher = DolphinDataFetcher() result = fetcher._parse_safety(None) for key in ("posture", "rm", "cat1", "cat2", "cat3", "cat4", "cat5"): assert result[key] is None, "Expected {} to be None".format(key) def test_parse_heartbeat_none_no_crash(): fetcher = DolphinDataFetcher() result = fetcher._parse_heartbeat(None) for key in ("heartbeat_ts", "heartbeat_phase", "heartbeat_flow", "heartbeat_age_s"): assert result[key] is None, "Expected {} to be None".format(key) # --------------------------------------------------------------------------- # fetch() with all-None HZ maps -> DataSnapshot with all HZ fields None # --------------------------------------------------------------------------- def test_fetch_empty_maps_returns_datasnapshot(): snap = _fetch_with_empty_maps() assert isinstance(snap, DataSnapshot) def test_fetch_empty_maps_hz_connected_true(): snap = _fetch_with_empty_maps() assert snap.hz_connected is True def test_fetch_empty_maps_scan_fields_none(): snap = _fetch_with_empty_maps() assert snap.scan_number is None assert snap.vel_div is None assert snap.w50_velocity is None assert snap.w750_velocity is None assert snap.instability_50 is None assert snap.scan_bridge_ts is None assert snap.scan_age_s is None def test_fetch_empty_maps_safety_fields_none(): snap = _fetch_with_empty_maps() assert snap.posture is None assert snap.rm is None assert snap.cat1 is None assert snap.cat2 is None assert snap.cat3 is None assert snap.cat4 is None assert snap.cat5 is None def test_fetch_empty_maps_extf_fields_none(): snap = _fetch_with_empty_maps() assert snap.funding_btc is None assert snap.dvol_btc is None assert snap.fng is None assert snap.vix is None assert snap.exf_age_s is None def test_fetch_empty_maps_state_fields_none(): snap = _fetch_with_empty_maps() assert snap.capital is None assert snap.pnl is None assert snap.trades is None assert snap.nautilus_capital is None assert snap.nautilus_pnl is None assert snap.nautilus_trades is None def test_fetch_empty_maps_heartbeat_fields_none(): snap = _fetch_with_empty_maps() assert snap.heartbeat_ts is None assert snap.heartbeat_phase is None assert snap.heartbeat_age_s is None def test_fetch_empty_maps_meta_health_fields_none(): snap = _fetch_with_empty_maps() assert snap.meta_rm is None assert snap.meta_status is None assert snap.m1_proc is None assert snap.m2_heartbeat is None assert snap.m3_data is None def test_fetch_empty_maps_obf_top_empty_list(): snap = _fetch_with_empty_maps() assert snap.obf_top == [] # --------------------------------------------------------------------------- # All-None DataSnapshot: formatting helpers must not crash # --------------------------------------------------------------------------- def test_all_none_snap_fmt_float_fields(): snap = DataSnapshot() for field_name in ( "vel_div", "w50_velocity", "w750_velocity", "instability_50", "acb_boost", "acb_beta", "funding_btc", "dvol_btc", "fng", "vix", "capital", "pnl", "rm", ): val = getattr(snap, field_name) result = fmt_float(val) assert result == "--", "fmt_float({}=None) should be '--', got {!r}".format(field_name, result) def test_all_none_snap_fmt_pnl_fields(): snap = DataSnapshot() for field_name in ("pnl", "nautilus_pnl"): val = getattr(snap, field_name) color, text = fmt_pnl(val) assert text == "--", "fmt_pnl({}=None) text should be '--'".format(field_name) assert color == "white" def test_all_none_snap_color_age_fields(): snap = DataSnapshot() for field_name in ("scan_age_s", "exf_age_s", "esof_age_s", "heartbeat_age_s"): val = getattr(snap, field_name) color, text = color_age(val) assert text == "N/A", "color_age({}=None) text should be 'N/A'".format(field_name) assert color == "dim" def test_all_none_snap_rm_bar(): snap = DataSnapshot() assert rm_bar(snap.rm) == "--" def test_all_none_snap_posture_color(): snap = DataSnapshot() assert posture_color(snap.posture) == "dim" def test_all_none_snap_status_color(): snap = DataSnapshot() assert status_color(snap.meta_status) == "dim"