bit-forecast-arena / tests /test_ui.py
Bit-Trading-Company's picture
CI deploy local
1e94a0b verified
Raw
History Blame Contribute Delete
17.9 kB
"""The rendering layer: the design ports faithfully, and stays capability-gated.
Two classes of test here.
*Fidelity*: every stylesheet the design system ships actually reaches the page,
and nothing forces height from `vh` (which ratchets the Spaces iframe). These
catch the failure where the app looks approximately right in a way that is hard
to name.
*Gating*: the UI decides what to draw from an adapter's declared capabilities
and never from a model's name. The fixture adapters below exist so that claim
can be tested without loading a single weight.
"""
from __future__ import annotations
import re
import numpy as np
import pandas as pd
import pytest
from src import config, view
from src.adapters.base import (OUTPUT_OHLCV_PATHS, OUTPUT_QUANTILE_LINE,
Capabilities, ForecastAdapter, ForecastResult)
from src.ui import chart, chrome, shell
from tests.fixture import future_ts, synth
from bit_ui import theme
# --------------------------------------------------------------------------
# Fixture adapters -- declared capabilities, no weights
# --------------------------------------------------------------------------
class _Fixture(ForecastAdapter):
"""An adapter that declares whatever the test needs it to."""
family = "fixture"
def __init__(self, output=OUTPUT_QUANTILE_LINE, hardware="cpu"):
super().__init__("fixture/model", revision="test")
self._output, self._hardware = output, hardware
def capabilities(self):
return Capabilities(output=self._output, hardware=self._hardware,
max_context=256)
def load(self, model_id=None, revision=None):
self._model = True
self._resolved_revision = "test"
return self
def predict(self, context_ohlcv, horizon=6, n_samples=8, seed=0,
issued_ts=None):
last = float(context_ohlcv["close"].iloc[-1])
q = np.tile(np.array([0.97, 0.99, 1.0, 1.01, 1.03]) * last, (horizon, 1))
paths = None
if self._output == OUTPUT_OHLCV_PATHS:
paths = np.tile(last, (n_samples, horizon, 5)).astype("float64")
return ForecastResult(
quantiles=q, levels=config.QUANTILE_LEVELS, horizon=horizon,
context_len=len(context_ohlcv), inference_version="fixture",
seed=seed, n_samples=n_samples, paths=paths)
class _Run:
"""The shape `shell` expects from `runtime.ForecastRun`."""
def __init__(self, adapter, context, horizon=6, slug="fixture"):
self.model_slug = slug
self.model_id = "fixture/model"
self.family = "fixture"
self.asset, self.timeframe, self.horizon = "BTC-USD", "1h", horizon
self.context = context
self.result = adapter.predict(context, horizon=horizon)
self.issued_ts = pd.to_datetime(context["ts"], utc=True).iloc[-1]
self.target_ts = future_ts(context, horizon)
self.capabilities = adapter.capabilities().as_dict()
self.forecast_id, self.archived_rows, self.elapsed_s = "fid", horizon, 0.1
def _data(runs, state, **over):
models = [{
"slug": r.model_slug, "name": r.model_slug, "family": "fixture",
"size": "1M", "caps": r.capabilities, "color": "var(--accent-amber-strong)",
"grade": "-", "grade_color": "var(--text-tertiary)",
"resolved_label": "UNRESOLVED",
} for r in runs] or [{
"slug": "fixture", "name": "fixture", "family": "fixture", "size": "1M",
"caps": {"output": OUTPUT_QUANTILE_LINE, "hardware": "cpu"},
"color": "var(--accent-amber-strong)", "grade": "-",
"grade_color": "var(--text-tertiary)", "resolved_label": "UNRESOLVED"}]
data = {
"models": models,
"models_by_slug": {m["slug"]: m for m in models},
"panels": {r.model_slug: {"empty": True} for r in runs},
"runs": runs,
"chart": (chart.build(runs[0].context, runs, "BTC-USD", runs[0].horizon)
if runs else {}),
"volatility": {"rows": [], "verdict": "", "note": ""},
"stats": [], "standings_rows": [], "families": ("kronos",),
"coverage": {"scope": "", "pct_label": "—", "pct_width": "0%", "rows": []},
"nav": {}, "archived_total": 0, "resolved_total": 0,
"resolver_last_ran": "NEVER", "runtime_hint": "~1S",
"quota_blocked": False, "footer_resources": (), "status_line": "",
"error": None,
}
data.update(over)
return data
@pytest.fixture
def context():
return synth(300)
def _state(**over):
state = view.default_state()
state["selected"] = ["fixture"]
state.update(over)
return state
# --------------------------------------------------------------------------
# 6. Capability gating (fixture adapter)
# --------------------------------------------------------------------------
def test_quantile_model_never_reaches_the_ghost_path_code(context):
adapter = _Fixture(output=OUTPUT_QUANTILE_LINE).load()
run = _Run(adapter, context)
html = shell.chart_section(_state(), _data([run], _state()))
assert "Quantile line only" in html
assert "Sampled OHLCV paths" not in html
# The ghost renderer's own opacity value must appear nowhere.
assert 'opacity="0.17"' not in html
def test_path_model_gets_the_ghost_control_and_the_ghosts(context):
adapter = _Fixture(output=OUTPUT_OHLCV_PATHS).load()
run = _Run(adapter, context)
html = shell.chart_section(_state(ghosts=True), _data([run], _state()))
assert "Sampled OHLCV paths" in html
assert "Quantile line only" not in html
def test_ghosts_are_suppressed_when_the_toggle_is_off(context):
adapter = _Fixture(output=OUTPUT_OHLCV_PATHS).load()
run = _Run(adapter, context)
drawing = chart.build(context, [run], "BTC-USD", run.horizon, show_ghosts=False)
assert 'opacity="0.17"' not in drawing["svg"]
drawing_on = chart.build(context, [run], "BTC-USD", run.horizon, show_ghosts=True)
assert 'opacity="0.17"' in drawing_on["svg"]
def test_chart_ignores_paths_when_the_capability_does_not_declare_them(context):
"""A result carrying paths but declaring `quantile_line` draws no ghosts.
This is the gate that matters: rendering keys off the declaration, not off
whether the array happens to be populated.
"""
adapter = _Fixture(output=OUTPUT_OHLCV_PATHS).load()
run = _Run(adapter, context)
assert run.result.paths is not None
run.capabilities = {"output": OUTPUT_QUANTILE_LINE, "hardware": "cpu"}
drawing = chart.build(context, [run], "BTC-USD", run.horizon, show_ghosts=True)
assert 'opacity="0.17"' not in drawing["svg"]
def test_hardware_capability_drives_the_cpu_gpu_chip(context):
for hardware, expected in (("cpu", "CPU · instant"), ("gpu", "your GPU quota")):
adapter = _Fixture(hardware=hardware).load()
run = _Run(adapter, context)
html = shell.models_section(_state(), _data([run], _state()))
assert expected in html
def test_no_model_family_is_named_in_the_rendering_layer():
"""The UI must not special-case a model by name.
Capability flags are the whole interface between adapters and rendering;
a name check here is how that boundary rots.
"""
import pathlib
for name in ("shell.py", "chart.py"):
source = (pathlib.Path(__file__).resolve().parents[1]
/ "src" / "ui" / name).read_text().lower()
# Prose in docstrings is fine; a comparison against a model name is not.
for family in ("kronos", "chronos", "timesfm"):
assert f'== "{family}' not in source
assert f"'{family}'" not in source.replace("'kronos, chronos or timesfm'", "")
# --------------------------------------------------------------------------
# Design fidelity
# --------------------------------------------------------------------------
def test_every_design_system_stylesheet_reaches_the_page():
"""Keyed on a rule only that file contains.
Vendoring the token files and forgetting `base.css` is the classic version
of this bug: the app inherits the framework's rounded corners and roomy
padding and looks approximately right in a way nobody can name.
"""
css = chrome.full_css()
markers = {
"colors.css": "--bg-canvas",
"typography.css": "--text-2xs",
"spacing.css": "--tracking-wider",
"base.css": "user-select",
}
for filename, marker in markers.items():
assert marker in css, f"{filename} did not reach the page ({marker} missing)"
def test_arena_css_reaches_the_page_too():
"""`theme.full_css()` alone would silently drop the Arena's own rules."""
css = chrome.full_css()
for marker in (".fa-rail", ".fa-picker", ".fa-card", ".fa-chart"):
assert marker in css
def test_nothing_forces_height_from_vh(context):
"""`height:100vh` inside the Spaces iframe ratchets the page height."""
adapter = _Fixture(output=OUTPUT_OHLCV_PATHS).load()
run = _Run(adapter, context)
state = _state()
html = shell.page(state, _data([run], state))
offenders = theme.find_forced_vh(html)
assert not offenders, f"forced vh heights: {offenders}"
def test_chart_uses_the_designs_geometry(context):
adapter = _Fixture().load()
run = _Run(adapter, context)
svg = chart.build(context, [run], "BTC-USD", run.horizon)["svg"]
assert 'viewBox="0 0 1200 352"' in svg
def test_interpolated_values_are_escaped(context):
"""Model ids and user text reach the markup; they must not become markup."""
state = _state(hfid='"><script>alert(1)</script>')
html = shell._enroll(state, _data([], state))
assert "<script>alert(1)" not in html
assert "&lt;script&gt;" in html or "&quot;&gt;" in html
# --------------------------------------------------------------------------
# 9. Error states
# --------------------------------------------------------------------------
@pytest.mark.parametrize("kind,needle", [
("quota", "SIGN IN"),
("load_failure", "DUPLICATE THIS SPACE"),
("no_data", "∅"),
])
def test_designed_error_states_render(kind, needle):
state = _state()
err = {"kind": kind, "title": "Something went wrong",
"message": "the detail", "login_url": "#", "duplicate_url": "#"}
html = shell.chart_section(state, _data([], state, error=err))
assert needle in html
assert "the detail" in html
assert "DISMISS" in html
def test_quota_banner_renders_when_blocked():
state = _state()
html = shell.controls(state, _data([], state, quota_blocked=True,
quota_msg="GPU-tier model — sign in",
runtime_hint="BLOCKED"))
assert "GPU-tier model" in html
assert "BLOCKED" in html
def test_rejected_enrollment_renders_its_message():
state = _state(enroll_ok=False,
enroll_note="'nope' is not a supported adapter family.")
html = shell._enroll(state, _data([], state))
assert "not a supported adapter family" in html
assert "var(--fin-down)" in html
def test_empty_track_record_renders_the_designed_state():
state = _state()
html = shell.trackrecord_section(state, _data([], state))
assert "No history yet" in html
assert "MAKE THE FIRST FORECAST" in html
# --------------------------------------------------------------------------
# Bridge
# --------------------------------------------------------------------------
def test_actions_off_the_allow_list_are_dropped():
assert chrome.parse_action("definitely-not-an-action:1|n") is None
assert chrome.parse_action("run:go|n1") is not None
def test_emit_refuses_an_action_it_does_not_know():
with pytest.raises(Exception):
chrome.emit("not-a-real-action", "x")
def test_sidebar_collapse_is_not_a_server_action():
"""Routing it through Gradio would re-render the whole page to move a rail."""
assert "sidebar" not in chrome.ALLOWED_KEYS
# --------------------------------------------------------------------------
# The bridge actually reaches the markup
# --------------------------------------------------------------------------
def test_every_control_carries_a_real_data_bit_attribute(context):
"""Regression: `emit()` returns the attribute *value*, not the attribute.
Interpolating it bare renders `<button run:go>` -- an unknown attribute,
no `data-bit`, so the delegated listener never matches and every control on
the page silently does nothing. Nothing else in the suite caught that,
because the markup was still well-formed and still contained the action
strings. This asserts the attribute, not the string.
"""
adapter = _Fixture(output=OUTPUT_OHLCV_PATHS).load()
run = _Run(adapter, context)
state = _state()
html = shell.page(state, _data([run], state))
buttons = re.findall(r"<button\b[^>]*>", html)
assert len(buttons) > 12, f"only {len(buttons)} buttons rendered"
# The sidebar toggle is the one deliberate exception: collapsing is a
# client-side class flip (`data-bit-sidebar-toggle`), never a server
# action, because routing it through Gradio would re-render the whole page
# to move a rail 160px.
without = [b for b in buttons
if "data-bit=" not in b and "data-bit-sidebar-toggle" not in b]
assert not without, f"buttons with no data-bit: {without[:3]}"
# And the actions that reach it must be ones the bridge accepts.
for value in re.findall(r'data-bit="([^"]+)"', html):
assert chrome.parse_action(f"{value}|nonce") is not None, \
f"{value!r} would be dropped by the bridge"
def test_the_run_control_is_present_and_addressable(context):
"""The single most important control on the page."""
state = _state()
html = shell.controls(state, _data([], state))
assert 'data-bit="run:go"' in html
def test_the_no_gpu_state_offers_the_only_thing_that_helps():
"""On CPU hardware a GPU-tier model cannot be made to work by waiting."""
state = _state()
err = {"kind": "no_gpu", "title": "GPU hardware not attached",
"message": "kronos-small needs GPU hardware.",
"duplicate_url": "https://example.invalid?duplicate=true"}
html = shell.chart_section(state, _data([], state, error=err))
assert "RUN IT ON YOUR OWN HARDWARE" in html
assert "needs GPU hardware" in html
def test_matchup_draws_fans_but_never_ghosts(context):
"""Three models' sampled paths over one another is a smear.
The design gates ghosts on playground mode as well as on capability, and
the chart panel already hides the toggle in matchup -- but the drawing call
has to agree, or the ghosts render with no way to turn them off.
"""
adapter = _Fixture(output=OUTPUT_OHLCV_PATHS).load()
runs = [_Run(adapter, context, slug=f"m{i}") for i in range(3)]
state = _state(mode="matchup", selected=[r.model_slug for r in runs],
ghosts=True)
html = shell.chart_section(state, _data(runs, state))
assert "Sampled OHLCV paths" not in html, "ghost toggle offered in matchup"
drawing = chart.build(context, runs, "BTC-USD", runs[0].horizon,
show_ghosts=False)
assert 'opacity="0.17"' not in drawing["svg"]
# --------------------------------------------------------------------------
# Cached forecasts must be labelled as cached
# --------------------------------------------------------------------------
def _cached(run):
run.from_cache = True
return run
def test_a_cached_forecast_says_when_it_was_made(context):
adapter = _Fixture(output=OUTPUT_QUANTILE_LINE).load()
run = _cached(_Run(adapter, context))
state = _state()
html = shell.chart_section(state, _data([run], state))
assert "LAST ARCHIVED" in html
assert "ISSUED" in html
assert "FROZEN" in html
def test_a_freshly_issued_forecast_is_not_labelled_archived(context):
adapter = _Fixture(output=OUTPUT_QUANTILE_LINE).load()
run = _Run(adapter, context) # from_cache defaults to False
state = _state()
html = shell.chart_section(state, _data([run], state))
assert "LAST ARCHIVED" not in html
def test_a_cached_backfill_says_so(context):
adapter = _Fixture(output=OUTPUT_QUANTILE_LINE).load()
run = _cached(_Run(adapter, context))
run.backfilled = True
state = _state()
assert "BACKFILL" in shell.chart_section(state, _data([run], state))
def test_a_path_model_with_no_archived_paths_says_why_it_is_empty(context):
"""Offering a toggle that draws nothing is worse than explaining."""
adapter = _Fixture(output=OUTPUT_OHLCV_PATHS).load()
run = _cached(_Run(adapter, context))
run.result.paths = None # archived before paths were stored
state = _state(ghosts=True)
html = shell.chart_section(state, _data([run], state))
assert "Paths not archived" in html
assert "Sampled OHLCV paths" not in html, "offered a toggle with nothing to draw"
def test_a_path_model_with_archived_paths_still_offers_the_toggle(context):
adapter = _Fixture(output=OUTPUT_OHLCV_PATHS).load()
run = _cached(_Run(adapter, context))
assert run.result.paths is not None
state = _state(ghosts=True)
html = shell.chart_section(state, _data([run], state))
assert "Sampled OHLCV paths" in html
assert "Paths not archived" not in html
@pytest.mark.parametrize("delta,expected", [
(pd.Timedelta(seconds=30), "just now"),
(pd.Timedelta(minutes=20), "20m ago"),
(pd.Timedelta(hours=3), "3h ago"),
(pd.Timedelta(days=4), "4d ago"),
])
def test_age_reads_in_the_coarsest_honest_unit(delta, expected):
now = pd.Timestamp("2026-08-21T12:00:00Z")
assert view.age_label(now - delta, now=now) == expected