Files
DOLPHIN/Observability/TUI/test_dolphin_tui_malformed_json.py
hjnormey 01c19662cb 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.
2026-04-21 16:58:38 +02:00

273 lines
10 KiB
Python
Executable File

# Tests for graceful handling of malformed JSON in HZ values.
# Validates: Requirements 12.3
# The TUI MUST NOT crash when any individual HZ key contains malformed JSON.
# Malformed JSON MUST result in all fields being None (no crash, no exception).
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,
DataSnapshot,
DolphinDataFetcher,
)
# ---------------------------------------------------------------------------
# Malformed JSON inputs to test
# ---------------------------------------------------------------------------
MALFORMED_JSON_INPUTS = [
"{bad json",
"not-json",
"null",
"[]",
"123",
"",
"{",
"}",
"{'key': 'value'}", # single quotes — invalid JSON
"undefined",
"NaN",
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_fetcher():
return DolphinDataFetcher()
def _make_mock_map_returning(value):
"""Return a mock IMap whose .get(key).result() returns the given value."""
future = MagicMock()
future.result.return_value = value
hz_map = MagicMock()
hz_map.get.return_value = future
hz_map.key_set.return_value = future
return hz_map
def _make_fetcher_with_malformed_json(malformed: str):
"""Create a DolphinDataFetcher whose hz_client returns malformed JSON for every map key."""
fetcher = DolphinDataFetcher()
fetcher.hz_connected = True
mock_map = _make_mock_map_returning(malformed)
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_malformed_json(malformed: str):
fetcher = _make_fetcher_with_malformed_json(malformed)
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())
# ---------------------------------------------------------------------------
# _parse_scan: malformed JSON -> all None, no exception
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS)
def test_parse_scan_malformed_no_crash(bad_json):
"""_parse_scan must not raise on malformed JSON."""
fetcher = _make_fetcher()
result = fetcher._parse_scan(bad_json) # must not raise
assert isinstance(result, dict)
@pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS)
def test_parse_scan_malformed_returns_none_fields(bad_json):
"""_parse_scan must return all-None fields for malformed JSON."""
fetcher = _make_fetcher()
result = fetcher._parse_scan(bad_json)
for key in ("scan_number", "vel_div", "w50_velocity", "w750_velocity",
"instability_50", "scan_bridge_ts", "scan_age_s"):
assert result[key] is None, f"_parse_scan({bad_json!r})[{key!r}] should be None"
assert result["asset_prices"] == {}
# ---------------------------------------------------------------------------
# _parse_safety: malformed JSON -> all None, no exception
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS)
def test_parse_safety_malformed_no_crash(bad_json):
"""_parse_safety must not raise on malformed JSON."""
fetcher = _make_fetcher()
result = fetcher._parse_safety(bad_json) # must not raise
assert isinstance(result, dict)
@pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS)
def test_parse_safety_malformed_returns_none_fields(bad_json):
"""_parse_safety must return all-None fields for malformed JSON."""
fetcher = _make_fetcher()
result = fetcher._parse_safety(bad_json)
for key in ("posture", "rm", "cat1", "cat2", "cat3", "cat4", "cat5"):
assert result[key] is None, f"_parse_safety({bad_json!r})[{key!r}] should be None"
# ---------------------------------------------------------------------------
# _parse_heartbeat: malformed JSON -> all None, no exception
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS)
def test_parse_heartbeat_malformed_no_crash(bad_json):
"""_parse_heartbeat must not raise on malformed JSON."""
fetcher = _make_fetcher()
result = fetcher._parse_heartbeat(bad_json) # must not raise
assert isinstance(result, dict)
@pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS)
def test_parse_heartbeat_malformed_returns_none_fields(bad_json):
"""_parse_heartbeat must return all-None fields for malformed JSON."""
fetcher = _make_fetcher()
result = fetcher._parse_heartbeat(bad_json)
for key in ("heartbeat_ts", "heartbeat_phase", "heartbeat_flow", "heartbeat_age_s"):
assert result[key] is None, f"_parse_heartbeat({bad_json!r})[{key!r}] should be None"
# ---------------------------------------------------------------------------
# fetch() with malformed JSON from HZ -> DataSnapshot, no crash
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("bad_json", ["{bad json", "not-json", "null", "[]", "123", ""])
def test_fetch_malformed_json_returns_datasnapshot(bad_json):
"""fetch() must return a DataSnapshot even when HZ returns malformed JSON."""
snap = _fetch_with_malformed_json(bad_json)
assert isinstance(snap, DataSnapshot)
@pytest.mark.parametrize("bad_json", ["{bad json", "not-json", "null", "[]", "123", ""])
def test_fetch_malformed_json_scan_fields_none(bad_json):
"""fetch() with malformed JSON must produce None scan fields."""
snap = _fetch_with_malformed_json(bad_json)
assert snap.scan_number is None
assert snap.vel_div is None
assert snap.w50_velocity is None
assert snap.instability_50 is None
assert snap.scan_bridge_ts is None
assert snap.scan_age_s is None
@pytest.mark.parametrize("bad_json", ["{bad json", "not-json", "null", "[]", "123", ""])
def test_fetch_malformed_json_safety_fields_none(bad_json):
"""fetch() with malformed JSON must produce None safety fields."""
snap = _fetch_with_malformed_json(bad_json)
assert snap.posture is None
assert snap.rm is None
assert snap.cat1 is None
@pytest.mark.parametrize("bad_json", ["{bad json", "not-json", "null", "[]", "123", ""])
def test_fetch_malformed_json_heartbeat_fields_none(bad_json):
"""fetch() with malformed JSON must produce None heartbeat fields."""
snap = _fetch_with_malformed_json(bad_json)
assert snap.heartbeat_ts is None
assert snap.heartbeat_phase is None
assert snap.heartbeat_age_s is None
@pytest.mark.parametrize("bad_json", ["{bad json", "not-json", "null", "[]", "123", ""])
def test_fetch_malformed_json_no_crash(bad_json):
"""fetch() must not raise any exception when HZ returns malformed JSON."""
# If this doesn't raise, the test passes
snap = _fetch_with_malformed_json(bad_json)
assert snap is not None
# ---------------------------------------------------------------------------
# fmt_float, fmt_pnl, color_age handle None gracefully (parse errors -> None fields)
# ---------------------------------------------------------------------------
def test_fmt_float_handles_none_from_parse_error():
"""fmt_float(None) must return '--' — parse errors produce None fields."""
assert fmt_float(None) == "--"
def test_fmt_pnl_handles_none_from_parse_error():
"""fmt_pnl(None) must return ('white', '--') — parse errors produce None fields."""
color, text = fmt_pnl(None)
assert text == "--"
assert color == "white"
def test_color_age_handles_none_from_parse_error():
"""color_age(None) must return ('dim', 'N/A') — parse errors produce None fields."""
color, text = color_age(None)
assert text == "N/A"
assert color == "dim"
def test_all_none_snapshot_fmt_float_no_crash():
"""All float fields on an all-None DataSnapshot must format without crashing."""
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 == "--", f"fmt_float({field_name}=None) should be '--', got {result!r}"
def test_all_none_snapshot_fmt_pnl_no_crash():
"""PnL fields on an all-None DataSnapshot must format without crashing."""
snap = DataSnapshot()
for field_name in ("pnl", "nautilus_pnl"):
val = getattr(snap, field_name)
color, text = fmt_pnl(val)
assert text == "--"
assert color == "white"
def test_all_none_snapshot_color_age_no_crash():
"""Age fields on an all-None DataSnapshot must format without crashing."""
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"
assert color == "dim"