Files
DOLPHIN/Observability/TUI/test_dolphin_tui.py

2071 lines
70 KiB
Python
Raw Normal View History

"""
Tests for dolphin_tui helper utilities.
Validates: Requirements 3.2
"""
import sys
import os
# Allow importing dolphin_tui without its optional heavy deps (textual, hazelcast, httpx)
sys.path.insert(0, os.path.dirname(__file__))
import types
# Stub out optional third-party imports so the module loads in a test environment
for _mod in ("textual", "textual.app", "textual.widgets", "textual.containers", "httpx", "hazelcast"):
if _mod not in sys.modules:
sys.modules[_mod] = types.ModuleType(_mod)
# Provide minimal stubs for textual symbols used at module level
import textual as _textual
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 rm_bar, fmt_pnl, posture_color, status_color # noqa: E402
# ---------------------------------------------------------------------------
# Unit tests
# ---------------------------------------------------------------------------
def test_rm_bar_none_returns_placeholder():
assert rm_bar(None) == "--"
def test_rm_bar_zero_all_empty():
result = rm_bar(0.0, width=10)
assert result == "[░░░░░░░░░░] 0.00"
def test_rm_bar_one_all_filled():
result = rm_bar(1.0, width=10)
assert result == "[██████████] 1.00"
def test_rm_bar_midpoint():
result = rm_bar(0.5, width=10)
assert result == "[█████░░░░░] 0.50"
def test_rm_bar_default_width_length():
result = rm_bar(0.82)
# default width=20 → length should be 20 + 7
# '[' (1) + width bar chars + '] ' (2) + '{rm:.2f}' (4 chars for [0.00,1.00]) = width + 7
assert len(result) == 27
def test_rm_bar_format_suffix():
result = rm_bar(0.82, width=20)
assert result.endswith("] 0.82")
# ---------------------------------------------------------------------------
# Property-based test
# Validates: Requirements 3.2
# Property: rm_bar(rm, width) output length always equals width + 4
# for any rm in [0.0, 1.0] and any valid width.
# ---------------------------------------------------------------------------
from hypothesis import given, settings
import hypothesis.strategies as st
@given(
rm=st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
width=st.integers(min_value=1, max_value=200),
)
@settings(max_examples=500)
def test_rm_bar_length_property(rm: float, width: int) -> None:
"""**Validates: Requirements 3.2**
Property: for any rm in [0.0, 1.0] and any positive integer width,
len(rm_bar(rm, width)) == width + 7.
The +7 accounts for:
'[' (1) + bar chars (width) + '] ' (2) + '{rm:.2f}' (4 chars for [0.00,1.00])
= 1 + width + 2 + 4 = width + 7
"""
result = rm_bar(rm, width=width)
assert len(result) == width + 7, (
f"rm_bar({rm!r}, width={width!r}) returned {result!r} "
f"with length {len(result)}, expected {width + 7}"
)
# ---------------------------------------------------------------------------
# fmt_pnl tests
# Validates: Requirements 7.3
# ---------------------------------------------------------------------------
def test_fmt_pnl_none():
color, text = fmt_pnl(None)
assert text == "--"
assert color == "white"
def test_fmt_pnl_positive():
color, text = fmt_pnl(1240.50)
assert color == "green"
assert text == "+$1,240.50"
def test_fmt_pnl_negative():
color, text = fmt_pnl(-320.75)
assert color == "red"
assert text == "-$320.75"
def test_fmt_pnl_zero():
color, text = fmt_pnl(0.0)
assert color == "white"
assert text == "$0.00"
def test_fmt_pnl_large_positive():
color, text = fmt_pnl(1_234_567.89)
assert color == "green"
assert text == "+$1,234,567.89"
# ---------------------------------------------------------------------------
# Property-based test for fmt_pnl
# Validates: Requirements 7.3
# Property: fmt_pnl always returns a (str, str) tuple; color is one of the
# three valid values; text starts with '+$', '-$', or '$'.
# ---------------------------------------------------------------------------
@given(v=st.floats(allow_nan=False, allow_infinity=False))
@settings(max_examples=500)
def test_fmt_pnl_property(v: float) -> None:
"""**Validates: Requirements 7.3**
Property: fmt_pnl(v) always returns a valid (color, text) tuple for any
finite float, with color in {green, red, white} and text formatted as
a dollar amount with sign prefix.
"""
color, text = fmt_pnl(v)
assert color in ("green", "red", "white")
assert "$" in text
if v > 0:
assert color == "green"
assert text.startswith("+$")
elif v < 0:
assert color == "red"
assert text.startswith("-$")
else:
assert color == "white"
# ---------------------------------------------------------------------------
# posture_color tests
# Validates: Requirements 3.1
# ---------------------------------------------------------------------------
def test_posture_color_apex():
assert posture_color("APEX") == "green"
def test_posture_color_stalker():
assert posture_color("STALKER") == "yellow"
def test_posture_color_turtle():
assert posture_color("TURTLE") == "dark_orange"
def test_posture_color_hibernate():
assert posture_color("HIBERNATE") == "red"
def test_posture_color_unknown():
assert posture_color("UNKNOWN_POSTURE") == "dim"
def test_posture_color_none():
assert posture_color(None) == "dim"
def test_posture_color_empty_string():
assert posture_color("") == "dim"
# ---------------------------------------------------------------------------
# Property-based test for posture_color
# Validates: Requirements 3.1
# Property: posture_color always returns a non-empty string; known postures
# return their mapped color; anything else returns "dim".
# ---------------------------------------------------------------------------
from dolphin_tui import POSTURE_COLORS # noqa: E402
@given(posture=st.text())
@settings(max_examples=500)
def test_posture_color_property(posture: str) -> None:
"""**Validates: Requirements 3.1**
Property: posture_color(posture) always returns a non-empty string.
Known postures map to their defined color; all others return "dim".
"""
result = posture_color(posture)
assert isinstance(result, str)
assert len(result) > 0
if posture in POSTURE_COLORS:
assert result == POSTURE_COLORS[posture]
else:
assert result == "dim"
# ---------------------------------------------------------------------------
# status_color tests
# Validates: Requirements 3.3
# ---------------------------------------------------------------------------
def test_status_color_green():
assert status_color("GREEN") == "green"
def test_status_color_degraded():
assert status_color("DEGRADED") == "yellow"
def test_status_color_critical():
assert status_color("CRITICAL") == "dark_orange"
def test_status_color_dead():
assert status_color("DEAD") == "red"
def test_status_color_unknown():
assert status_color("UNKNOWN_STATUS") == "dim"
def test_status_color_none():
assert status_color(None) == "dim"
def test_status_color_empty_string():
assert status_color("") == "dim"
# ---------------------------------------------------------------------------
# Property-based test for status_color
# Validates: Requirements 3.3
# Property: status_color always returns a non-empty string; known statuses
# return their mapped color; anything else returns "dim".
# ---------------------------------------------------------------------------
from dolphin_tui import STATUS_COLORS # noqa: E402
@given(status=st.text())
@settings(max_examples=500)
def test_status_color_property(status: str) -> None:
"""**Validates: Requirements 3.3**
Property: status_color(status) always returns a non-empty string.
Known statuses map to their defined color; all others return "dim".
"""
result = status_color(status)
assert isinstance(result, str)
assert len(result) > 0
if status in STATUS_COLORS:
assert result == STATUS_COLORS[status]
else:
assert result == "dim"
# ---------------------------------------------------------------------------
# SystemHealthPanel tests
# Validates: Requirements 6.1, 6.2, 6.3
# ---------------------------------------------------------------------------
from dolphin_tui import SystemHealthPanel, DataSnapshot # noqa: E402
def _make_panel() -> SystemHealthPanel:
"""Create a SystemHealthPanel with Static stubbed to object."""
return SystemHealthPanel.__new__(SystemHealthPanel)
def _init_panel() -> SystemHealthPanel:
panel = SystemHealthPanel.__new__(SystemHealthPanel)
panel._snap = None
return panel
def test_system_health_panel_no_snap_shows_dashes():
"""With no snapshot, all fields render as '--'."""
panel = _init_panel()
markup = panel._render_markup()
assert "rm_meta: --" in markup
assert "M1: --" in markup
assert "M5: --" in markup
assert "● --" in markup
def test_system_health_panel_rm_meta_formatted():
"""rm_meta is formatted to 3 decimal places."""
panel = _init_panel()
panel._snap = DataSnapshot(meta_rm=0.9234567)
markup = panel._render_markup()
assert "rm_meta: 0.923" in markup
def test_system_health_panel_m_scores_formatted():
"""M1-M5 scores are formatted to 1 decimal place."""
panel = _init_panel()
panel._snap = DataSnapshot(
m1_proc=1.0,
m2_heartbeat=0.9,
m3_data=0.8,
m4_cp=0.75,
m5_coh=0.5,
)
markup = panel._render_markup()
assert "M1: 1.0" in markup
assert "M2: 0.9" in markup
assert "M3: 0.8" in markup
assert "M4: 0.8" in markup # 0.75 rounds to 0.8 with 1 decimal
assert "M5: 0.5" in markup
def test_system_health_panel_status_green():
"""GREEN status renders with [green] markup."""
panel = _init_panel()
panel._snap = DataSnapshot(meta_status="GREEN")
markup = panel._render_markup()
assert "[green]● GREEN[/green]" in markup
def test_system_health_panel_status_degraded():
"""DEGRADED status renders with [yellow] markup."""
panel = _init_panel()
panel._snap = DataSnapshot(meta_status="DEGRADED")
markup = panel._render_markup()
assert "[yellow]● DEGRADED[/yellow]" in markup
def test_system_health_panel_status_critical():
"""CRITICAL status renders with [dark_orange] markup."""
panel = _init_panel()
panel._snap = DataSnapshot(meta_status="CRITICAL")
markup = panel._render_markup()
assert "[dark_orange]● CRITICAL[/dark_orange]" in markup
def test_system_health_panel_status_dead():
"""DEAD status renders with [red] markup."""
panel = _init_panel()
panel._snap = DataSnapshot(meta_status="DEAD")
markup = panel._render_markup()
assert "[red]● DEAD[/red]" in markup
def test_system_health_panel_none_fields_show_dashes():
"""None fields in a snapshot render as '--', not crash."""
panel = _init_panel()
panel._snap = DataSnapshot(
meta_rm=None,
meta_status=None,
m1_proc=None,
m2_heartbeat=None,
m3_data=None,
m4_cp=None,
m5_coh=None,
)
markup = panel._render_markup()
assert "rm_meta: --" in markup
assert "● --" in markup
def test_system_health_panel_title_present():
"""Panel markup always includes the SYSTEM HEALTH title."""
panel = _init_panel()
markup = panel._render_markup()
assert "SYSTEM HEALTH" in markup
def test_system_health_panel_update_data_stores_snap():
"""update_data stores the snapshot (update() call is a no-op on stub)."""
panel = _init_panel()
# Patch update to be a no-op since Static is stubbed
panel.update = lambda _: None
snap = DataSnapshot(meta_rm=0.5, meta_status="GREEN")
panel.update_data(snap)
assert panel._snap is snap
# ---------------------------------------------------------------------------
# AlphaEnginePanel tests
# Validates: Requirements 3.1, 3.2, 3.3, 3.4, 12.3, 15.3
# ---------------------------------------------------------------------------
from dolphin_tui import AlphaEnginePanel # noqa: E402
def _init_alpha_panel() -> AlphaEnginePanel:
panel = AlphaEnginePanel.__new__(AlphaEnginePanel)
panel._snap = None
return panel
def test_alpha_panel_no_snap_shows_dashes():
"""With no snapshot, all fields render as '--'."""
panel = _init_alpha_panel()
markup = panel._render_markup()
assert "Posture:" in markup
assert "--" in markup
assert "Rm:" in markup
assert "ACB:" in markup
assert "Cat1:" in markup
def test_alpha_panel_title_present():
"""Panel markup always includes the ALPHA ENGINE title."""
panel = _init_alpha_panel()
markup = panel._render_markup()
assert "ALPHA ENGINE" in markup
def test_alpha_panel_posture_apex_green():
"""APEX posture renders with [green] markup (Req 3.1)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(posture="APEX")
markup = panel._render_markup()
assert "[green]APEX[/green]" in markup
def test_alpha_panel_posture_stalker_yellow():
"""STALKER posture renders with [yellow] markup (Req 3.1)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(posture="STALKER")
markup = panel._render_markup()
assert "[yellow]STALKER[/yellow]" in markup
def test_alpha_panel_posture_turtle_dark_orange():
"""TURTLE posture renders with [dark_orange] markup (Req 3.1)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(posture="TURTLE")
markup = panel._render_markup()
assert "[dark_orange]TURTLE[/dark_orange]" in markup
def test_alpha_panel_posture_hibernate_red():
"""HIBERNATE posture renders with [red] markup (Req 3.1)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(posture="HIBERNATE")
markup = panel._render_markup()
assert "[red]HIBERNATE[/red]" in markup
def test_alpha_panel_posture_none_shows_dim_dash():
"""None posture renders as dim '--' (Req 12.3)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(posture=None)
markup = panel._render_markup()
assert "[dim]--[/dim]" in markup
def test_alpha_panel_rm_bar_rendered( ):
"""Rm value renders as ASCII bar (Req 3.2)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(rm=0.5)
markup = panel._render_markup()
assert "" in markup
assert "" in markup
assert "0.50" in markup
def test_alpha_panel_rm_none_shows_dash():
"""None Rm renders as '--' (Req 12.3)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(rm=None)
markup = panel._render_markup()
assert "Rm: --" in markup
def test_alpha_panel_acb_boost_beta_formatted():
"""ACB boost and beta render with 2 decimal places (Req 3.3)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(acb_boost=1.55, acb_beta=0.80)
markup = panel._render_markup()
assert "1.55x" in markup
assert "β=0.80" in markup
def test_alpha_panel_acb_none_shows_dashes():
"""None ACB fields render as '--' (Req 12.3)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(acb_boost=None, acb_beta=None)
markup = panel._render_markup()
assert "ACB: --x β=--" in markup
def test_alpha_panel_cat_values_rendered():
"""Cat1-Cat5 values render with 2 decimal places (Req 3.4)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(
cat1=0.9, cat2=0.8, cat3=0.7, cat4=1.0, cat5=0.5
)
markup = panel._render_markup()
assert "0.90" in markup
assert "0.80" in markup
assert "0.70" in markup
assert "1.00" in markup
assert "0.50" in markup
def test_alpha_panel_cat_none_shows_dashes():
"""None Cat fields render as '--' (Req 12.3)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(
cat1=None, cat2=None, cat3=None, cat4=None, cat5=None
)
markup = panel._render_markup()
assert "--" in markup
def test_alpha_panel_all_none_no_crash():
"""All-None snapshot never crashes (Req 12.3)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(
posture=None, rm=None,
acb_boost=None, acb_beta=None,
cat1=None, cat2=None, cat3=None, cat4=None, cat5=None,
)
markup = panel._render_markup()
assert "ALPHA ENGINE" in markup
def test_alpha_panel_update_data_stores_snap():
"""update_data stores the snapshot."""
panel = _init_alpha_panel()
panel.update = lambda _: None
snap = DataSnapshot(posture="APEX", rm=0.82)
panel.update_data(snap)
assert panel._snap is snap
def test_alpha_panel_cat_right_aligned():
"""Cat values are right-aligned within their field (Req 15.3)."""
panel = _init_alpha_panel()
panel._snap = DataSnapshot(cat1=0.9)
markup = panel._render_markup()
# The value should be right-aligned (padded with spaces on the left)
assert " 0.90" in markup
# ---------------------------------------------------------------------------
# ExtFPanel tests
# Validates: Requirements 4.1, 4.2, 4.3, 12.3
# ---------------------------------------------------------------------------
from dolphin_tui import ExtFPanel # noqa: E402
def _init_extf_panel() -> ExtFPanel:
panel = ExtFPanel.__new__(ExtFPanel)
panel._snap = None
return panel
def test_extf_panel_title_present():
"""Panel markup always includes the ExtF title."""
panel = _init_extf_panel()
markup = panel._render_markup()
assert "ExtF" in markup
def test_extf_panel_no_snap_shows_dashes():
"""With no snapshot, all fields render as '--'."""
panel = _init_extf_panel()
markup = panel._render_markup()
assert "funding: --" in markup
assert "dvol: --" in markup
assert "fng: --" in markup
assert "taker: --" in markup
assert "vix: --" in markup
assert "ls_btc: --" in markup
def test_extf_panel_numeric_fields_formatted( ):
"""Numeric fields render with 4 decimal places (Req 4.1)."""
panel = _init_extf_panel()
panel._snap = DataSnapshot(
funding_btc=-0.0123,
dvol_btc=62.4,
fng=28.0,
taker=0.81,
vix=18.2,
ls_btc=0.48,
)
markup = panel._render_markup()
assert "funding: -0.0123" in markup
assert "dvol: 62.4000" in markup
assert "fng: 28.0000" in markup
assert "taker: 0.8100" in markup
assert "vix: 18.2000" in markup
assert "ls_btc: 0.4800" in markup
def test_extf_panel_acb_ready_true_green():
"""acb_ready=True renders as green ✓ (Req 4.2)."""
panel = _init_extf_panel()
panel._snap = DataSnapshot(acb_ready=True)
markup = panel._render_markup()
assert "[green]✓[/green]" in markup
def test_extf_panel_acb_ready_false_red():
"""acb_ready=False renders as red ✗ (Req 4.2)."""
panel = _init_extf_panel()
panel._snap = DataSnapshot(acb_ready=False)
markup = panel._render_markup()
assert "[red]✗[/red]" in markup
def test_extf_panel_acb_ready_none_dim_dash():
"""acb_ready=None renders as dim '--' (Req 12.3)."""
panel = _init_extf_panel()
panel._snap = DataSnapshot(acb_ready=None)
markup = panel._render_markup()
assert "[dim]--[/dim]" in markup
def test_extf_panel_acb_present_displayed():
"""acb_present string is displayed as-is (Req 4.2)."""
panel = _init_extf_panel()
panel._snap = DataSnapshot(acb_present="9/9")
markup = panel._render_markup()
assert "9/9" in markup
def test_extf_panel_acb_present_none_shows_dash():
"""acb_present=None renders as '--' (Req 12.3)."""
panel = _init_extf_panel()
panel._snap = DataSnapshot(acb_present=None)
markup = panel._render_markup()
assert "ACB: --" in markup
def test_extf_panel_age_fresh_green():
"""Fresh age (< STALE_WARN) renders with green color (Req 4.3)."""
panel = _init_extf_panel()
panel._snap = DataSnapshot(exf_age_s=5.0)
markup = panel._render_markup()
assert "[green]5.0s ●[/green]" in markup
def test_extf_panel_age_stale_yellow():
"""Stale age (>= STALE_WARN, < STALE_DEAD) renders with yellow color (Req 4.3)."""
panel = _init_extf_panel()
panel._snap = DataSnapshot(exf_age_s=30.0)
markup = panel._render_markup()
assert "[yellow]30.0s ●[/yellow]" in markup
def test_extf_panel_age_dead_red():
"""Dead age (>= STALE_DEAD) renders with red color (Req 4.3)."""
panel = _init_extf_panel()
panel._snap = DataSnapshot(exf_age_s=90.0)
markup = panel._render_markup()
assert "[red]90.0s ●[/red]" in markup
def test_extf_panel_age_none_dim_na():
"""None age renders as dim N/A (Req 12.3)."""
panel = _init_extf_panel()
panel._snap = DataSnapshot(exf_age_s=None)
markup = panel._render_markup()
assert "[dim]N/A ●[/dim]" in markup
def test_extf_panel_all_none_no_crash():
"""All-None snapshot never crashes (Req 12.3)."""
panel = _init_extf_panel()
panel._snap = DataSnapshot(
funding_btc=None, dvol_btc=None, fng=None,
taker=None, vix=None, ls_btc=None,
acb_ready=None, acb_present=None, exf_age_s=None,
)
markup = panel._render_markup()
assert "ExtF" in markup
def test_extf_panel_update_data_stores_snap():
"""update_data stores the snapshot."""
panel = _init_extf_panel()
panel.update = lambda _: None
snap = DataSnapshot(funding_btc=-0.012, acb_ready=True, exf_age_s=3.5)
panel.update_data(snap)
assert panel._snap is snap
# ---------------------------------------------------------------------------
# CapitalPanel tests
# Validates: Requirements 7.1, 7.2, 7.3, 12.3
# ---------------------------------------------------------------------------
from dolphin_tui import CapitalPanel # noqa: E402
def _init_capital_panel() -> CapitalPanel:
panel = CapitalPanel.__new__(CapitalPanel)
panel._snap = None
return panel
def test_capital_panel_title_present():
"""Panel markup always includes the CAPITAL / PnL title."""
panel = _init_capital_panel()
markup = panel._render_markup()
assert "CAPITAL / PnL" in markup
def test_capital_panel_no_snap_shows_dashes():
"""With no snapshot, all fields render as '--'."""
panel = _init_capital_panel()
markup = panel._render_markup()
assert "Blue capital: --" in markup
assert "Drawdown: --" in markup
assert "Peak: --" in markup
assert "Trades: --" in markup
assert "Nautilus cap: --" in markup
assert "Naut trades: --" in markup
def test_capital_panel_capital_formatted():
"""Blue capital renders as $X,XXX.XX (Req 7.1)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(capital=124532.10)
markup = panel._render_markup()
assert "Blue capital: $124,532.10" in markup
def test_capital_panel_drawdown_as_percentage():
"""Drawdown stored as ratio renders as percentage (Req 7.1)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(drawdown=-0.0321)
markup = panel._render_markup()
assert "Drawdown: -3.21%" in markup
def test_capital_panel_peak_formatted():
"""Peak capital renders as $X,XXX.XX (Req 7.1)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(peak_capital=128750.00)
markup = panel._render_markup()
assert "Peak: $128,750.00" in markup
def test_capital_panel_pnl_positive_green():
"""Positive PnL renders with green color (Req 7.3)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(pnl=1240.50)
markup = panel._render_markup()
assert "[green]+$1,240.50[/green]" in markup
def test_capital_panel_pnl_negative_red():
"""Negative PnL renders with red color (Req 7.3)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(pnl=-320.75)
markup = panel._render_markup()
assert "[red]-$320.75[/red]" in markup
def test_capital_panel_pnl_zero_white():
"""Zero PnL renders with white color (Req 7.3)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(pnl=0.0)
markup = panel._render_markup()
assert "[white]$0.00[/white]" in markup
def test_capital_panel_pnl_none_white_dash():
"""None PnL renders as white '--' (Req 12.3)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(pnl=None)
markup = panel._render_markup()
assert "[white]--[/white]" in markup
def test_capital_panel_trades_displayed():
"""Trades count renders as integer string (Req 7.1)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(trades=12)
markup = panel._render_markup()
assert "Trades: 12" in markup
def test_capital_panel_nautilus_capital_formatted():
"""Nautilus capital renders as $X,XXX.XX (Req 7.2)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(nautilus_capital=124532.10)
markup = panel._render_markup()
assert "Nautilus cap: $124,532.10" in markup
def test_capital_panel_nautilus_pnl_positive_green():
"""Positive Nautilus PnL renders with green color (Req 7.2, 7.3)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(nautilus_pnl=1240.50)
markup = panel._render_markup()
assert "[green]+$1,240.50[/green]" in markup
def test_capital_panel_nautilus_pnl_negative_red():
"""Negative Nautilus PnL renders with red color (Req 7.2, 7.3)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(nautilus_pnl=-500.00)
markup = panel._render_markup()
assert "[red]-$500.00[/red]" in markup
def test_capital_panel_nautilus_trades_displayed():
"""Nautilus trades count renders as integer string (Req 7.2)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(nautilus_trades=7)
markup = panel._render_markup()
assert "Naut trades: 7" in markup
def test_capital_panel_nautilus_posture_apex_green():
"""Nautilus APEX posture renders with green color (Req 7.2)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(nautilus_posture="APEX")
markup = panel._render_markup()
assert "[green]APEX[/green]" in markup
def test_capital_panel_nautilus_posture_hibernate_red():
"""Nautilus HIBERNATE posture renders with red color (Req 7.2)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(nautilus_posture="HIBERNATE")
markup = panel._render_markup()
assert "[red]HIBERNATE[/red]" in markup
def test_capital_panel_nautilus_posture_none_dim_dash():
"""None Nautilus posture renders as dim '--' (Req 12.3)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(nautilus_posture=None)
markup = panel._render_markup()
assert "[dim]--[/dim]" in markup
def test_capital_panel_all_none_no_crash():
"""All-None snapshot never crashes (Req 12.3)."""
panel = _init_capital_panel()
panel._snap = DataSnapshot(
capital=None, drawdown=None, peak_capital=None,
pnl=None, trades=None,
nautilus_capital=None, nautilus_pnl=None,
nautilus_trades=None, nautilus_posture=None,
)
markup = panel._render_markup()
assert "CAPITAL / PnL" in markup
def test_capital_panel_update_data_stores_snap():
"""update_data stores the snapshot."""
panel = _init_capital_panel()
panel.update = lambda _: None
snap = DataSnapshot(capital=100000.0, pnl=500.0)
panel.update_data(snap)
assert panel._snap is snap
# ---------------------------------------------------------------------------
# PrefectPanel tests
# Validates: Requirements 8.1, 8.2, 8.3, 8.4
# ---------------------------------------------------------------------------
from dolphin_tui import PrefectPanel, PREFECT_FLOW_STATUS_COLORS # noqa: E402
def _init_prefect_panel() -> PrefectPanel:
panel = PrefectPanel.__new__(PrefectPanel)
panel._snap = None
return panel
def test_prefect_panel_title_present():
"""Panel markup always includes the PREFECT FLOWS title."""
panel = _init_prefect_panel()
markup = panel._render_markup()
assert "PREFECT FLOWS" in markup
def test_prefect_panel_no_snap_shows_offline():
"""With no snapshot, shows PREFECT OFFLINE in red."""
panel = _init_prefect_panel()
markup = panel._render_markup()
assert "[red][PREFECT OFFLINE][/red]" in markup
def test_prefect_panel_healthy_true_shows_green_badge():
"""prefect_healthy=True shows green [PREFECT ✓] badge (Req 8.1)."""
panel = _init_prefect_panel()
panel._snap = DataSnapshot(prefect_healthy=True)
markup = panel._render_markup()
assert "[green][PREFECT ✓][/green]" in markup
def test_prefect_panel_healthy_false_shows_offline_red():
"""prefect_healthy=False shows red [PREFECT OFFLINE] badge (Req 8.4)."""
panel = _init_prefect_panel()
panel._snap = DataSnapshot(prefect_healthy=False)
markup = panel._render_markup()
assert "[red][PREFECT OFFLINE][/red]" in markup
def test_prefect_panel_completed_status_green():
"""COMPLETED status renders with [green] markup (Req 8.3)."""
panel = _init_prefect_panel()
panel._snap = DataSnapshot(
prefect_healthy=True,
prefect_flows=[{"name": "my-flow", "status": "COMPLETED", "start_time": None, "duration": None}],
)
markup = panel._render_markup()
assert "[green]COMPLETED[/green]" in markup
def test_prefect_panel_running_status_cyan():
"""RUNNING status renders with [cyan] markup (Req 8.3)."""
panel = _init_prefect_panel()
panel._snap = DataSnapshot(
prefect_healthy=True,
prefect_flows=[{"name": "my-flow", "status": "RUNNING", "start_time": None, "duration": None}],
)
markup = panel._render_markup()
assert "[cyan]RUNNING[/cyan]" in markup
def test_prefect_panel_failed_status_red():
"""FAILED status renders with [red] markup (Req 8.3)."""
panel = _init_prefect_panel()
panel._snap = DataSnapshot(
prefect_healthy=True,
prefect_flows=[{"name": "my-flow", "status": "FAILED", "start_time": None, "duration": None}],
)
markup = panel._render_markup()
assert "[red]FAILED[/red]" in markup
def test_prefect_panel_flow_name_displayed():
"""Flow name is displayed in the panel (Req 8.2)."""
panel = _init_prefect_panel()
panel._snap = DataSnapshot(
prefect_healthy=True,
prefect_flows=[{"name": "dolphin-daily-run", "status": "COMPLETED", "start_time": None, "duration": None}],
)
markup = panel._render_markup()
assert "dolphin-daily-run" in markup
def test_prefect_panel_duration_formatted_minutes_seconds():
"""Duration of 135s formats as '2m 15s' (Req 8.2)."""
from dolphin_tui import _fmt_flow_duration
assert _fmt_flow_duration(135.0) == "2m 15s"
def test_prefect_panel_duration_formatted_seconds_only():
"""Duration of 45s formats as '45s' (Req 8.2)."""
from dolphin_tui import _fmt_flow_duration
assert _fmt_flow_duration(45.0) == "45s"
def test_prefect_panel_duration_none_shows_dash():
"""None duration formats as '--' (Req 12.3)."""
from dolphin_tui import _fmt_flow_duration
assert _fmt_flow_duration(None) == "--"
def test_prefect_panel_start_time_formatted_hhmm_utc():
"""start_time ISO string formats as HH:MM UTC (Req 8.2)."""
from dolphin_tui import _fmt_flow_start
result = _fmt_flow_start("2025-01-15T14:30:00Z")
assert result == "14:30 UTC"
def test_prefect_panel_start_time_none_shows_dash():
"""None start_time formats as '--' (Req 12.3)."""
from dolphin_tui import _fmt_flow_start
assert _fmt_flow_start(None) == "--"
def test_prefect_panel_all_none_flow_fields_no_crash():
"""Flow entry with all-None fields renders without crashing (Req 12.3)."""
panel = _init_prefect_panel()
panel._snap = DataSnapshot(
prefect_healthy=True,
prefect_flows=[{"name": None, "status": None, "start_time": None, "duration": None}],
)
markup = panel._render_markup()
assert "PREFECT FLOWS" in markup
def test_prefect_panel_update_data_stores_snap():
"""update_data stores the snapshot."""
panel = _init_prefect_panel()
panel.update = lambda _: None
snap = DataSnapshot(prefect_healthy=True, prefect_flows=[])
panel.update_data(snap)
assert panel._snap is snap
def test_prefect_flow_status_colors_constant():
"""PREFECT_FLOW_STATUS_COLORS has the required keys and values."""
assert PREFECT_FLOW_STATUS_COLORS["COMPLETED"] == "green"
assert PREFECT_FLOW_STATUS_COLORS["RUNNING"] == "cyan"
assert PREFECT_FLOW_STATUS_COLORS["FAILED"] == "red"
assert PREFECT_FLOW_STATUS_COLORS["CRASHED"] == "red"
assert PREFECT_FLOW_STATUS_COLORS["PENDING"] == "yellow"
assert PREFECT_FLOW_STATUS_COLORS["LATE"] == "yellow"
# ---------------------------------------------------------------------------
# OBFPanel tests
# Validates: Requirements 1.3
# ---------------------------------------------------------------------------
from dolphin_tui import OBFPanel # noqa: E402
def _init_obf_panel() -> OBFPanel:
panel = OBFPanel.__new__(OBFPanel)
panel._snap = None
return panel
def test_obf_panel_title_present():
"""Panel markup always includes the OBF TOP ASSETS title."""
panel = _init_obf_panel()
markup = panel._render_markup()
assert "OBF TOP ASSETS" in markup
def test_obf_panel_empty_obf_top_shows_no_data():
"""Empty obf_top shows 'No OBF data' in dim (Req 1.3)."""
panel = _init_obf_panel()
panel._snap = DataSnapshot(obf_top=[])
markup = panel._render_markup()
assert "No OBF data" in markup
def test_obf_panel_no_snap_shows_no_data():
"""With no snapshot, shows 'No OBF data'."""
panel = _init_obf_panel()
markup = panel._render_markup()
assert "No OBF data" in markup
def test_obf_panel_asset_name_displayed():
"""Asset name is displayed in the panel (Req 1.3)."""
panel = _init_obf_panel()
panel._snap = DataSnapshot(obf_top=[
{"asset": "BTCUSDT", "imbalance": 0.18, "fill_prob": 0.75, "depth_quality": 0.90},
])
markup = panel._render_markup()
assert "BTCUSDT" in markup
def test_obf_panel_imbalance_positive_sign():
"""Positive imbalance shows with + sign and 2 decimals (Req 1.3)."""
panel = _init_obf_panel()
panel._snap = DataSnapshot(obf_top=[
{"asset": "BTCUSDT", "imbalance": 0.18, "fill_prob": 0.75, "depth_quality": 0.90},
])
markup = panel._render_markup()
assert "+0.18" in markup
def test_obf_panel_imbalance_negative_sign():
"""Negative imbalance shows with - sign and 2 decimals (Req 1.3)."""
panel = _init_obf_panel()
panel._snap = DataSnapshot(obf_top=[
{"asset": "ETHUSDT", "imbalance": -0.05, "fill_prob": 0.60, "depth_quality": 0.80},
])
markup = panel._render_markup()
assert "-0.05" in markup
def test_obf_panel_fill_prob_formatted():
"""fill_prob is formatted with 2 decimal places (Req 1.3)."""
panel = _init_obf_panel()
panel._snap = DataSnapshot(obf_top=[
{"asset": "BTCUSDT", "imbalance": 0.10, "fill_prob": 0.75, "depth_quality": 0.90},
])
markup = panel._render_markup()
assert "0.75" in markup
def test_obf_panel_depth_quality_formatted():
"""depth_quality is formatted with 2 decimal places (Req 1.3)."""
panel = _init_obf_panel()
panel._snap = DataSnapshot(obf_top=[
{"asset": "BTCUSDT", "imbalance": 0.10, "fill_prob": 0.75, "depth_quality": 0.92},
])
markup = panel._render_markup()
assert "0.92" in markup
def test_obf_panel_none_fields_show_dashes():
"""None fields in an entry render as '--', no crash (Req 12.3)."""
panel = _init_obf_panel()
panel._snap = DataSnapshot(obf_top=[
{"asset": "BTCUSDT", "imbalance": None, "fill_prob": None, "depth_quality": None},
])
markup = panel._render_markup()
assert "BTCUSDT" in markup
assert "--" in markup
def test_obf_panel_update_data_stores_snap():
"""update_data stores the snapshot."""
panel = _init_obf_panel()
panel.update = lambda _: None
snap = DataSnapshot(obf_top=[{"asset": "BTCUSDT", "imbalance": 0.1, "fill_prob": 0.5, "depth_quality": 0.8}])
panel.update_data(snap)
assert panel._snap is snap
# ---------------------------------------------------------------------------
# LogPanel tests
# Validates: Requirements 9.1, 9.2, 9.3, 9.4
# ---------------------------------------------------------------------------
from dolphin_tui import LogPanel # noqa: E402
def _init_log_panel() -> LogPanel:
panel = LogPanel.__new__(LogPanel)
panel._snap = None
panel._lines = []
panel._log_path = "run_logs/meta_health.log"
return panel
def test_log_panel_is_verticalscroll_subclass():
"""LogPanel extends VerticalScroll (Req 9.1)."""
import textual.widgets as _tw
assert issubclass(LogPanel, _tw.VerticalScroll)
def test_log_panel_update_data_stores_snap():
"""update_data stores the snapshot (Req 9.1)."""
panel = _init_log_panel()
snap = DataSnapshot(log_lines=["line1", "line2"])
panel.update_data(snap)
assert panel._snap is snap
def test_log_panel_log_lines_joined_with_newlines():
"""Log lines are joined with newlines in the content (Req 9.2)."""
panel = _init_log_panel()
panel._lines = ["line one", "line two", "line three"]
content = panel._build_content()
assert "line one" in content
assert "line two" in content
assert "line three" in content
# Lines should be separated by newlines
assert content.count("\n") >= 2
def test_log_panel_empty_log_lines_renders_title():
"""Empty log_lines renders the title line (Req 9.3)."""
panel = _init_log_panel()
panel._lines = []
content = panel._build_content()
assert "LOG TAIL" in content
def test_log_panel_log_not_found_passthrough():
"""'Log not found' message from fetcher is displayed (Req 9.4)."""
panel = _init_log_panel()
snap = DataSnapshot(log_lines=["Log not found: run_logs/meta_health.log"])
panel.update_data(snap)
content = panel._build_content()
assert "Log not found" in content
def test_log_panel_update_data_sets_lines():
"""update_data populates _lines from snap.log_lines."""
panel = _init_log_panel()
snap = DataSnapshot(log_lines=["alpha", "beta", "gamma"])
panel.update_data(snap)
assert panel._lines == ["alpha", "beta", "gamma"]
def test_log_panel_title_line_in_content():
"""Panel content includes the title line with meta_health.log reference (Req 9.1)."""
panel = _init_log_panel()
panel._lines = ["some log line"]
content = panel._build_content()
assert "LOG TAIL" in content
assert "meta_health.log" in content
# ---------------------------------------------------------------------------
# Terminal size check tests
# Validates: Requirements 14.4
# ---------------------------------------------------------------------------
from dolphin_tui import MIN_TERM_WIDTH, MIN_TERM_HEIGHT # noqa: E402
def test_min_term_width_is_120():
"""MIN_TERM_WIDTH constant must be 120 (Req 14.4)."""
assert MIN_TERM_WIDTH == 120
def test_min_term_height_is_30():
"""MIN_TERM_HEIGHT constant must be 30 (Req 14.4)."""
assert MIN_TERM_HEIGHT == 30
def test_terminal_too_small_width():
"""Width below 120 is considered too small (Req 14.4)."""
assert 119 < MIN_TERM_WIDTH
def test_terminal_too_small_height():
"""Height below 30 is considered too small (Req 14.4)."""
assert 29 < MIN_TERM_HEIGHT
def test_terminal_exact_minimum_not_too_small():
"""Exactly 120×30 is not too small (Req 14.4)."""
width, height = MIN_TERM_WIDTH, MIN_TERM_HEIGHT
too_small = width < MIN_TERM_WIDTH or height < MIN_TERM_HEIGHT
assert not too_small
def test_terminal_one_below_width_is_too_small():
"""119×30 is too small (Req 14.4)."""
width, height = MIN_TERM_WIDTH - 1, MIN_TERM_HEIGHT
too_small = width < MIN_TERM_WIDTH or height < MIN_TERM_HEIGHT
assert too_small
def test_terminal_one_below_height_is_too_small():
"""120×29 is too small (Req 14.4)."""
width, height = MIN_TERM_WIDTH, MIN_TERM_HEIGHT - 1
too_small = width < MIN_TERM_WIDTH or height < MIN_TERM_HEIGHT
assert too_small
def test_terminal_both_below_minimum_is_too_small():
"""80×24 is too small (Req 14.4)."""
width, height = 80, 24
too_small = width < MIN_TERM_WIDTH or height < MIN_TERM_HEIGHT
assert too_small
def test_terminal_large_size_not_too_small():
"""200×50 is not too small (Req 14.4)."""
width, height = 200, 50
too_small = width < MIN_TERM_WIDTH or height < MIN_TERM_HEIGHT
assert not too_small
def test_warning_message_contains_minimum_dimensions():
"""Warning message text includes the minimum dimensions (Req 14.4)."""
# Simulate the warning text that _check_terminal_size() would produce
width, height = 80, 24
msg = (
f"⚠ Terminal too small — resize to "
f"{MIN_TERM_WIDTH}×{MIN_TERM_HEIGHT} minimum\n"
f"(current: {width}×{height})"
)
assert "120" in msg
assert "30" in msg
assert "80" in msg
assert "24" in msg
assert "Terminal too small" in msg
# ===========================================================================
# Task 6.1 — HZ Offline Tests
# Validates: Requirements 11.2, 12.1, 12.3
# ===========================================================================
import asyncio
from unittest.mock import MagicMock, patch, AsyncMock
from dolphin_tui import ( # noqa: E402
DataSnapshot,
DolphinDataFetcher,
HeaderBar,
SystemHealthPanel,
AlphaEnginePanel,
ScanBridgePanel,
ExtFPanel,
EsoFPanel,
CapitalPanel,
PrefectPanel,
OBFPanel,
LogPanel,
)
# ---------------------------------------------------------------------------
# Helper: build a fully-None HZ DataSnapshot (hz_connected=False)
# ---------------------------------------------------------------------------
def _offline_snapshot(**overrides) -> DataSnapshot:
"""Return a DataSnapshot with hz_connected=False and all HZ fields None."""
return DataSnapshot(
hz_connected=False,
prefect_connected=False,
prefect_healthy=False,
prefect_flows=[],
log_lines=[],
# All HZ-derived fields are None by default (DataSnapshot defaults)
**overrides,
)
# ---------------------------------------------------------------------------
# 6.1a — HeaderBar shows [HZ ✗] when hz_connected=False
# Validates: Requirements 11.2
# ---------------------------------------------------------------------------
def test_header_hz_disconnected_badge():
"""HeaderBar renders [HZ ✗] in red when hz_connected=False (Req 11.2)."""
header = HeaderBar.__new__(HeaderBar)
header._hz_connected = False
header._meta_status = None
markup = header._render_markup()
assert "[red][HZ ✗][/red]" in markup
assert "[green][HZ ✓][/green]" not in markup
def test_header_hz_connected_badge():
"""HeaderBar renders [HZ ✓] in green when hz_connected=True (Req 11.2)."""
header = HeaderBar.__new__(HeaderBar)
header._hz_connected = True
header._meta_status = None
markup = header._render_markup()
assert "[green][HZ ✓][/green]" in markup
assert "[red][HZ ✗][/red]" not in markup
def test_header_update_status_disconnected():
"""update_status(False, None) sets _hz_connected=False and renders [HZ ✗]."""
header = HeaderBar.__new__(HeaderBar)
header._hz_connected = True
header._meta_status = None
# Stub update() so it doesn't call Textual internals
header.update = lambda _: None
header.update_status(False, None)
assert header._hz_connected is False
markup = header._render_markup()
assert "[red][HZ ✗][/red]" in markup
# ---------------------------------------------------------------------------
# 6.1b — DolphinDataFetcher.fetch() returns hz_connected=False when HZ unreachable
# Validates: Requirements 12.1
# ---------------------------------------------------------------------------
def test_fetcher_fetch_returns_snapshot_when_hz_none():
"""fetch() returns DataSnapshot(hz_connected=False) when hz_client is None."""
fetcher = DolphinDataFetcher.__new__(DolphinDataFetcher)
fetcher.hz_client = None
fetcher.hz_connected = False
fetcher._running = True
fetcher._reconnect_task = None
fetcher._reconnect_backoff = 5.0
fetcher._reconnect_backoff_initial = 5.0
fetcher._reconnect_backoff_max = 60.0
fetcher._reconnect_backoff_multiplier = 1.5
fetcher.log_path = "run_logs/meta_health.log"
# Stub _start_reconnect to be a no-op
fetcher._start_reconnect = lambda: None
# Stub fetch_prefect to return (False, [])
async def _fake_prefect():
return (False, [])
fetcher.fetch_prefect = _fake_prefect
# Stub tail_log to return []
fetcher.tail_log = lambda path, n=50: []
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert isinstance(snap, DataSnapshot)
assert snap.hz_connected is False
# All HZ-derived fields must be None
assert snap.scan_number is None
assert snap.vel_div is None
assert snap.posture is None
assert snap.rm is None
assert snap.capital is None
assert snap.meta_rm is None
assert snap.meta_status is None
def test_fetcher_connect_hz_failure_sets_disconnected():
"""connect_hz() returns False and sets hz_connected=False when HZ raises."""
fetcher = DolphinDataFetcher(hz_host="127.0.0.1", hz_port=9999)
fetcher._start_reconnect = lambda: None # suppress background task
with patch("dolphin_tui.HAZELCAST_AVAILABLE", True):
with patch("dolphin_tui.hazelcast") as mock_hz:
mock_hz.HazelcastClient.side_effect = ConnectionRefusedError("refused")
result = asyncio.get_event_loop().run_until_complete(fetcher.connect_hz())
assert result is False
assert fetcher.hz_connected is False
assert fetcher.hz_client is None
# ---------------------------------------------------------------------------
# 6.1c — _apply_snapshot() does not raise with all-None snapshot
# Validates: Requirements 12.1, 12.3
# ---------------------------------------------------------------------------
def test_apply_snapshot_all_none_no_crash():
"""_apply_snapshot() with all-None HZ fields never raises (Req 12.1, 12.3)."""
snap = _offline_snapshot()
# Exercise every panel's _render_markup() directly (no Textual app needed)
panels = [
_make_system_health_panel(snap),
_make_alpha_panel(snap),
_make_scan_bridge_panel(snap),
_make_extf_panel(snap),
_make_esof_panel(snap),
_make_capital_panel(snap),
_make_prefect_panel(snap),
_make_obf_panel(snap),
]
for panel in panels:
markup = panel._render_markup()
assert markup # non-empty string, no exception
def _make_system_health_panel(snap: DataSnapshot) -> SystemHealthPanel:
p = SystemHealthPanel.__new__(SystemHealthPanel)
p._snap = snap
return p
def _make_alpha_panel(snap: DataSnapshot) -> AlphaEnginePanel:
p = AlphaEnginePanel.__new__(AlphaEnginePanel)
p._snap = snap
return p
def _make_scan_bridge_panel(snap: DataSnapshot) -> ScanBridgePanel:
p = ScanBridgePanel.__new__(ScanBridgePanel)
p._snap = snap
return p
def _make_extf_panel(snap: DataSnapshot) -> ExtFPanel:
p = ExtFPanel.__new__(ExtFPanel)
p._snap = snap
return p
def _make_esof_panel(snap: DataSnapshot) -> EsoFPanel:
p = EsoFPanel.__new__(EsoFPanel)
p._snap = snap
return p
def _make_capital_panel(snap: DataSnapshot) -> CapitalPanel:
p = CapitalPanel.__new__(CapitalPanel)
p._snap = snap
return p
def _make_prefect_panel(snap: DataSnapshot) -> PrefectPanel:
p = PrefectPanel.__new__(PrefectPanel)
p._snap = snap
return p
def _make_obf_panel(snap: DataSnapshot) -> OBFPanel:
p = OBFPanel.__new__(OBFPanel)
p._snap = snap
return p
# ---------------------------------------------------------------------------
# 6.1d — All panels render "--" (not real data) when hz_connected=False
# Validates: Requirements 12.1, 12.3
# ---------------------------------------------------------------------------
def test_system_health_panel_offline_shows_dashes():
"""SystemHealthPanel shows '--' for all HZ fields when offline (Req 12.3)."""
snap = _offline_snapshot()
p = _make_system_health_panel(snap)
markup = p._render_markup()
assert "rm_meta: --" in markup
assert "M1: --" in markup
assert "M5: --" in markup
assert "● --" in markup
def test_alpha_panel_offline_shows_dashes():
"""AlphaEnginePanel shows '--' for all HZ fields when offline (Req 12.3)."""
snap = _offline_snapshot()
p = _make_alpha_panel(snap)
markup = p._render_markup()
assert "[dim]--[/dim]" in markup # posture
assert "Rm: --" in markup
def test_scan_bridge_panel_offline_shows_dashes():
"""ScanBridgePanel shows '--' for all HZ fields when offline (Req 12.3)."""
snap = _offline_snapshot()
p = _make_scan_bridge_panel(snap)
markup = p._render_markup()
assert "Scan #--" in markup
assert "vel_div: --" in markup
assert "bridge_ts: --" in markup
def test_extf_panel_offline_shows_dashes():
"""ExtFPanel shows '--' for all HZ fields when offline (Req 12.3)."""
snap = _offline_snapshot()
p = _make_extf_panel(snap)
markup = p._render_markup()
assert "funding: --" in markup
assert "dvol: --" in markup
assert "fng: --" in markup
def test_esof_panel_offline_shows_dashes():
"""EsoFPanel shows '--' for all HZ fields when offline (Req 12.3)."""
snap = _offline_snapshot()
p = _make_esof_panel(snap)
markup = p._render_markup()
assert "Moon: --" in markup
assert "Session: --" in markup
assert "MC pos: --" in markup
def test_capital_panel_offline_shows_dashes():
"""CapitalPanel shows '--' for all HZ fields when offline (Req 12.3)."""
snap = _offline_snapshot()
p = _make_capital_panel(snap)
markup = p._render_markup()
assert "Blue capital: --" in markup
assert "Drawdown: --" in markup
assert "Nautilus cap: --" in markup
def test_prefect_panel_offline_shows_offline():
"""PrefectPanel shows PREFECT OFFLINE when prefect_healthy=False (Req 8.4)."""
snap = _offline_snapshot()
p = _make_prefect_panel(snap)
markup = p._render_markup()
assert "[red][PREFECT OFFLINE][/red]" in markup
def test_obf_panel_offline_shows_no_data():
"""OBFPanel shows 'No OBF data' when obf_top is empty (Req 1.3)."""
snap = _offline_snapshot()
p = _make_obf_panel(snap)
markup = p._render_markup()
assert "No OBF data" in markup
# ---------------------------------------------------------------------------
# 6.1e — Header shows [HZ ✗] for offline snapshot
# Validates: Requirements 11.2
# ---------------------------------------------------------------------------
def test_header_offline_snapshot_shows_hz_cross():
"""HeaderBar shows [HZ ✗] when snapshot has hz_connected=False (Req 11.2)."""
snap = _offline_snapshot()
header = HeaderBar.__new__(HeaderBar)
header._hz_connected = snap.hz_connected
header._meta_status = snap.meta_status
markup = header._render_markup()
assert "[red][HZ ✗][/red]" in markup
# ---------------------------------------------------------------------------
# 6.1f — Property-based test: _apply_snapshot never raises for any all-None snapshot
# Validates: Requirements 12.1, 12.3
# Property: for any DataSnapshot with hz_connected=False and all-None HZ fields,
# every panel's _render_markup() never raises and returns a non-empty string.
# ---------------------------------------------------------------------------
from hypothesis import given, settings # noqa: E402 (already imported above, re-import is fine)
import hypothesis.strategies as st # noqa: E402
@given(
ts=st.floats(min_value=0.0, max_value=1e12, allow_nan=False, allow_infinity=False),
prefect_healthy=st.booleans(),
log_lines=st.lists(st.text(max_size=80), max_size=10),
)
@settings(max_examples=200)
def test_apply_snapshot_never_raises_property(
ts: float,
prefect_healthy: bool,
log_lines: list,
) -> None:
"""**Validates: Requirements 12.1, 12.3**
Property: for any DataSnapshot with hz_connected=False and all HZ-derived
fields at their None defaults, every panel's _render_markup() never raises
and always returns a non-empty string.
"""
snap = DataSnapshot(
ts=ts,
hz_connected=False,
prefect_connected=prefect_healthy,
prefect_healthy=prefect_healthy,
prefect_flows=[],
log_lines=log_lines,
# All HZ fields remain None (DataSnapshot defaults)
)
panels = [
_make_system_health_panel(snap),
_make_alpha_panel(snap),
_make_scan_bridge_panel(snap),
_make_extf_panel(snap),
_make_esof_panel(snap),
_make_capital_panel(snap),
_make_prefect_panel(snap),
_make_obf_panel(snap),
]
for panel in panels:
result = panel._render_markup()
assert isinstance(result, str)
assert len(result) > 0
# Header also must not raise
header = HeaderBar.__new__(HeaderBar)
header._hz_connected = False
header._meta_status = None
header_markup = header._render_markup()
assert "[red][HZ ✗][/red]" in header_markup
# ===========================================================================
# Task 6.2 — HZ Online Tests
# Validates: Requirements 1.2, 2.12.4, 3.13.4, 4.14.3, 5.15.2,
# 6.16.3, 7.17.3, 11.2, 13.1
# ===========================================================================
import time as _time_module # noqa: E402 (time already imported at module level)
import json as _json_module # noqa: E402 (json already imported at module level)
# ---------------------------------------------------------------------------
# Fixture JSON payloads
# ---------------------------------------------------------------------------
FIXTURE_EIGEN_SCAN = _json_module.dumps({
"scan_number": 8634,
"vel_div": -0.0312,
"w50_velocity": -0.0421,
"w750_velocity": -0.0109,
"instability_50": 0.0234,
"asset_prices": {"BTCUSDT": 65000.0, "ETHUSDT": 3200.0},
"bridge_ts": "2026-04-02T10:30:00+00:00",
})
FIXTURE_SAFETY = _json_module.dumps({
"posture": "APEX",
"Rm": 0.82,
"Cat1": 0.9, "Cat2": 0.8, "Cat3": 0.7, "Cat4": 1.0, "Cat5": 0.9,
})
FIXTURE_STATE_LATEST = _json_module.dumps({
"capital": 124532.10,
"drawdown": -0.0321,
"peak_capital": 128750.00,
"pnl": 1240.50,
"trades": 12,
})
FIXTURE_STATE_NAUTILUS = _json_module.dumps({
"capital": 124532.10,
"pnl": 1240.50,
"trades": 7,
"posture": "APEX",
"param_hash": "abc123",
})
FIXTURE_EXF = _json_module.dumps({
"funding_btc": -0.0123,
"dvol_btc": 62.4,
"fng": 28.0,
"taker": 0.81,
"vix": 18.2,
"ls_btc": 0.48,
"_acb_ready": True,
"_acb_present": "9/9",
"_pushed_at": "2026-04-02T10:29:55+00:00",
})
FIXTURE_ESOF = _json_module.dumps({
"moon_phase_name": "Waxing Gibbous",
"mercury_retrograde": False,
"liquidity_session": "London",
"market_cycle_position": 0.42,
"_pushed_at": "2026-04-02T10:29:50+00:00",
})
FIXTURE_META_HEALTH = _json_module.dumps({
"rm_meta": 0.923,
"status": "GREEN",
"m1_proc": 1.0,
"m2_heartbeat": 1.0,
"m3_data_freshness": 1.0,
"m4_control_plane": 1.0,
"m5_coherence": 1.0,
})
FIXTURE_HEARTBEAT = _json_module.dumps({
"ts": _time_module.time() - 2.0,
"phase": "RUNNING",
"flow": "nautilus_prefect",
})
# ---------------------------------------------------------------------------
# Helper: build a MagicMock HZ client returning fixture data
# ---------------------------------------------------------------------------
def _make_mock_hz_client():
"""Build a MagicMock HZ client that returns fixture data for all known maps/keys."""
_data = {
("DOLPHIN_FEATURES", "latest_eigen_scan"): FIXTURE_EIGEN_SCAN,
("DOLPHIN_FEATURES", "acb_boost"): _json_module.dumps({"boost": 1.55, "beta": 0.80}),
("DOLPHIN_FEATURES", "exf_latest"): FIXTURE_EXF,
("DOLPHIN_FEATURES", "esof_latest"): FIXTURE_ESOF,
("DOLPHIN_SAFETY", "latest"): FIXTURE_SAFETY,
("DOLPHIN_STATE_BLUE", "latest"): FIXTURE_STATE_LATEST,
("DOLPHIN_STATE_BLUE", "latest_nautilus"): FIXTURE_STATE_NAUTILUS,
("DOLPHIN_HEARTBEAT", "nautilus_flow_heartbeat"): FIXTURE_HEARTBEAT,
("DOLPHIN_META_HEALTH", "latest"): FIXTURE_META_HEALTH,
}
def _make_imap(map_name):
imap = MagicMock()
def _get(key):
future = MagicMock()
future.result.return_value = _data.get((map_name, key))
return future
imap.get.side_effect = _get
# key_set for OBF shards — return empty
ks = MagicMock()
ks.result.return_value = []
imap.key_set.return_value = ks
return imap
client = MagicMock()
def _get_map(name):
future = MagicMock()
future.result.return_value = _make_imap(name)
return future
client.get_map.side_effect = _get_map
return client
# ---------------------------------------------------------------------------
# Helper: build a DolphinDataFetcher wired to the mock HZ client
# ---------------------------------------------------------------------------
def _make_online_fetcher() -> DolphinDataFetcher:
fetcher = DolphinDataFetcher.__new__(DolphinDataFetcher)
fetcher.hz_host = "dolphin.taile8ad92.ts.net"
fetcher.hz_port = 5701
fetcher.hz_client = _make_mock_hz_client()
fetcher.hz_connected = True
fetcher._running = True
fetcher._reconnect_task = None
fetcher._reconnect_backoff = 5.0
fetcher._reconnect_backoff_initial = 5.0
fetcher._reconnect_backoff_max = 60.0
fetcher._reconnect_backoff_multiplier = 1.5
fetcher.log_path = "run_logs/meta_health.log"
fetcher._start_reconnect = lambda: None
async def _fake_prefect():
return (True, [{"name": "test-flow", "status": "COMPLETED", "start_time": None, "duration": 60.0}])
fetcher.fetch_prefect = _fake_prefect
fetcher.tail_log = lambda path, n=50: ["10:30:01 [INFO] RM_META=0.923 [GREEN]"]
return fetcher
# ---------------------------------------------------------------------------
# 6.2a — fetch() with mock HZ returns hz_connected=True
# Validates: Requirements 1.2, 11.2
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_returns_connected_snapshot():
"""fetch() with mock HZ client returns DataSnapshot(hz_connected=True)."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert isinstance(snap, DataSnapshot)
assert snap.hz_connected is True
# ---------------------------------------------------------------------------
# 6.2b — Scan fields populated from fixture
# Validates: Requirements 2.1, 2.3
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_scan_fields_populated():
"""fetch() populates scan_number, vel_div, w50_velocity, w750_velocity, instability_50."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert snap.scan_number == 8634
assert snap.vel_div is not None
assert snap.w50_velocity is not None
assert snap.w750_velocity is not None
assert snap.instability_50 is not None
# ---------------------------------------------------------------------------
# 6.2c — Safety fields populated from fixture
# Validates: Requirements 3.1, 3.2, 3.4
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_safety_fields_populated():
"""fetch() populates posture, rm, and cat1-cat5 from DOLPHIN_SAFETY."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert snap.posture == "APEX"
assert snap.rm == 0.82
assert snap.cat1 is not None
assert snap.cat2 is not None
assert snap.cat3 is not None
assert snap.cat4 is not None
assert snap.cat5 is not None
# ---------------------------------------------------------------------------
# 6.2d — State (Blue) fields populated from fixture
# Validates: Requirements 7.1
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_state_fields_populated():
"""fetch() populates capital, drawdown, peak_capital, pnl, trades from DOLPHIN_STATE_BLUE."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert snap.capital is not None
assert snap.drawdown is not None
assert snap.peak_capital is not None
assert snap.pnl is not None
assert snap.trades is not None
assert abs(snap.capital - 124532.10) < 0.01
assert abs(snap.drawdown - (-0.0321)) < 1e-6
assert snap.trades == 12
# ---------------------------------------------------------------------------
# 6.2e — Nautilus state fields populated from fixture
# Validates: Requirements 7.2
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_nautilus_fields_populated():
"""fetch() populates nautilus_capital, nautilus_pnl, nautilus_trades, nautilus_posture."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert snap.nautilus_capital is not None
assert snap.nautilus_pnl is not None
assert snap.nautilus_trades is not None
assert snap.nautilus_posture is not None
# ---------------------------------------------------------------------------
# 6.2f — ExtF fields populated from fixture
# Validates: Requirements 4.1, 4.2, 4.3
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_extf_fields_populated():
"""fetch() populates all ExtF fields including acb_ready, acb_present, exf_age_s."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert snap.funding_btc is not None
assert snap.dvol_btc is not None
assert snap.fng is not None
assert snap.taker is not None
assert snap.vix is not None
assert snap.ls_btc is not None
assert snap.acb_ready is not None
assert snap.acb_present is not None
assert snap.exf_age_s is not None
# ---------------------------------------------------------------------------
# 6.2g — EsoF fields populated from fixture
# Validates: Requirements 5.1, 5.2
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_esof_fields_populated():
"""fetch() populates moon_phase, mercury_retro, liquidity_session, market_cycle_pos, esof_age_s."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert snap.moon_phase is not None
assert snap.mercury_retro is not None
assert snap.liquidity_session is not None
assert snap.market_cycle_pos is not None
assert snap.esof_age_s is not None
# ---------------------------------------------------------------------------
# 6.2h — Meta health fields populated from fixture
# Validates: Requirements 6.1, 6.2, 6.3
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_meta_health_populated():
"""fetch() populates meta_rm, meta_status, and M1-M5 from DOLPHIN_META_HEALTH."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert snap.meta_rm is not None
assert abs(snap.meta_rm - 0.923) < 1e-6
assert snap.meta_status == "GREEN"
assert snap.m1_proc is not None
assert snap.m2_heartbeat is not None
assert snap.m3_data is not None
assert snap.m4_cp is not None
assert snap.m5_coh is not None
# ---------------------------------------------------------------------------
# 6.2i — Heartbeat fields populated from fixture
# Validates: Requirements 1.2
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_heartbeat_populated():
"""fetch() populates heartbeat_ts, heartbeat_phase, heartbeat_flow, heartbeat_age_s >= 0."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert snap.heartbeat_ts is not None
assert snap.heartbeat_phase is not None
assert snap.heartbeat_flow is not None
assert snap.heartbeat_age_s is not None
assert snap.heartbeat_age_s >= 0
# ---------------------------------------------------------------------------
# 6.2j — Scan age computed from bridge_ts
# Validates: Requirements 2.2, 2.4
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_scan_age_computed():
"""fetch() computes scan_age_s as a non-negative float from bridge_ts."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert snap.scan_age_s is not None
assert snap.scan_age_s >= 0
# ---------------------------------------------------------------------------
# 6.2k — All panels render real data (not "--") for key fields
# Validates: Requirements 1.2, 2.1, 3.1, 6.1, 7.1
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_all_panels_render_real_data():
"""With a populated snapshot, panels render real values not '--' for key fields."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
scan_panel = _make_scan_bridge_panel(snap)
scan_markup = scan_panel._render_markup()
assert "8634" in scan_markup
alpha_panel = _make_alpha_panel(snap)
alpha_markup = alpha_panel._render_markup()
assert "APEX" in alpha_markup
assert "0.82" in alpha_markup
capital_panel = _make_capital_panel(snap)
capital_markup = capital_panel._render_markup()
assert "124,532.10" in capital_markup
health_panel = _make_system_health_panel(snap)
health_markup = health_panel._render_markup()
assert "GREEN" in health_markup
# ---------------------------------------------------------------------------
# 6.2l — HeaderBar shows [HZ ✓] when hz_connected=True
# Validates: Requirements 11.2
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_header_shows_hz_tick():
"""HeaderBar with hz_connected=True shows [green][HZ ✓][/green]."""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
header = HeaderBar.__new__(HeaderBar)
header._hz_connected = snap.hz_connected
header._meta_status = snap.meta_status
markup = header._render_markup()
assert "[green][HZ ✓][/green]" in markup
assert "[red][HZ ✗][/red]" not in markup
# ---------------------------------------------------------------------------
# 6.2m — fetch() completes within 1.5s (Req 13.1)
# Validates: Requirements 13.1
# ---------------------------------------------------------------------------
def test_fetcher_fetch_hz_online_completes_within_poll_cycle():
"""fetch() with mock HZ completes in under 1.5 seconds (Req 13.1)."""
fetcher = _make_online_fetcher()
t0 = _time_module.time()
asyncio.get_event_loop().run_until_complete(fetcher.fetch())
elapsed = _time_module.time() - t0
assert elapsed < 1.5, f"fetch() took {elapsed:.3f}s, expected < 1.5s"
# ---------------------------------------------------------------------------
# 6.2n — Property-based test: key fields never None with valid fixture data
# Validates: Requirements 1.2, 2.1, 3.1, 6.1, 7.1
# Property: given valid fixture data (deterministic mock), fetch() always returns
# a snapshot where scan_number, posture, rm, capital, meta_rm, meta_status
# are non-None.
# ---------------------------------------------------------------------------
@given(st.just(None))
@settings(max_examples=10)
def test_fetch_hz_online_snapshot_fields_never_none_property(_: None) -> None:
"""**Validates: Requirements 1.2, 2.1, 3.1, 6.1, 7.1**
Property: with valid fixture data, fetch() always returns a snapshot where
the key fields (scan_number, posture, rm, capital, meta_rm, meta_status)
are non-None across 10 repeated calls (confirms stability of the mock).
"""
fetcher = _make_online_fetcher()
snap = asyncio.get_event_loop().run_until_complete(fetcher.fetch())
assert snap.hz_connected is True
assert snap.scan_number is not None
assert snap.posture is not None
assert snap.rm is not None
assert snap.capital is not None
assert snap.meta_rm is not None
assert snap.meta_status is not None