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.
273 lines
10 KiB
Python
Executable File
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"
|