116 lines
4.9 KiB
Python
116 lines
4.9 KiB
Python
"""Cockpit reorg: grouped nav (Live/Backtest/Research) resolves, the at-a-glance Overview home renders
|
|
book-status + edges + a chart, and the rendered pages stay leak-clean (no absolute app links / external
|
|
CDNs) so the nginx tailnet proxy never exposes the internal address. All seeded in-memory — no DB, no net."""
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
import re
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
|
|
from fxhnt.adapters.web.app import create_app
|
|
from fxhnt.application.forward_models import BacktestSummary
|
|
from fxhnt.application.forward_models import ForwardNavRow as Row
|
|
from fxhnt.application.forward_models import ForwardSummary
|
|
|
|
# Every nav-group link in base.html. Each must resolve 200 (forward track empty is fine — routes degrade).
|
|
_NAV_LINKS = ["/", "/paper", "/paper/replay", "/paper/sim", "/funnel", "/factory"]
|
|
|
|
|
|
def _client(*, seed_headline: bool = True) -> TestClient:
|
|
repo = ForwardNavRepo("sqlite://")
|
|
repo.migrate()
|
|
if seed_headline:
|
|
at = dt.datetime(2026, 6, 24, 12, 0)
|
|
# Multi-day forward history for the bybit_4edge headline book so the NAV curve + countdown render.
|
|
rows = [Row("bybit_4edge", f"2026-06-{5 + i:02d}", 0.004, 1.0 + 0.004 * (i + 1)) for i in range(8)]
|
|
repo.upsert_rows(rows, at)
|
|
repo.upsert_summary(
|
|
ForwardSummary("bybit_4edge", "2026-06-12", 8, 1.032, 0.032, 1.4, -0.02),
|
|
"WAIT", "8/60 forward days", at)
|
|
repo.upsert_backtest_summary(BacktestSummary(
|
|
"bybit_4edge", "2026-05-30", cagr=0.30, ann_vol=0.20, sharpe=1.5, max_drawdown=-0.16,
|
|
passed=True, dsr=0.7, is_sharpe=1.6, oos_sharpe=1.3, pvalue=0.01), at)
|
|
return TestClient(create_app(repo))
|
|
|
|
|
|
# ---- grouped nav --------------------------------------------------------------------------------------
|
|
|
|
def test_nav_groups_present() -> None:
|
|
r = _client().get("/")
|
|
assert r.status_code == 200
|
|
for label in ("Live", "Backtest", "Research", "Overview", "Paper", "Replay", "Run sim", "Funnel", "Factory"):
|
|
assert label in r.text, f"nav label {label!r} missing"
|
|
|
|
|
|
def test_every_nav_link_resolves() -> None:
|
|
c = _client()
|
|
for href in _NAV_LINKS:
|
|
assert c.get(href).status_code == 200, f"nav link {href} did not resolve"
|
|
|
|
|
|
def test_active_section_highlighted() -> None:
|
|
c = _client()
|
|
# Backtest section is active on /paper/sim; Live section active on the home page.
|
|
assert 'class="group active"' in c.get("/paper/sim").text
|
|
assert 'class="group active"' in c.get("/").text
|
|
|
|
|
|
# ---- at-a-glance overview home ------------------------------------------------------------------------
|
|
|
|
def test_overview_shows_book_status_and_countdown() -> None:
|
|
r = _client().get("/")
|
|
assert r.status_code == 200
|
|
assert "Fund status" in r.text
|
|
assert "Forward Sharpe" in r.text and "OOS Sharpe" in r.text and "Max drawdown" in r.text
|
|
# gate countdown computed from gate_spec.min_days (60) - days (8) = 52, not hardcoded.
|
|
assert "52" in r.text and "days left" in r.text
|
|
assert "Bybit 4-edge book" in r.text # headline display name
|
|
|
|
|
|
def test_overview_shows_edges_and_links() -> None:
|
|
r = _client().get("/")
|
|
assert r.status_code == 200
|
|
assert "Edges in the book" in r.text
|
|
assert 'class="fleet"' in r.text # the per-edge table is preserved
|
|
assert "/strategy/" in r.text # each edge links to its detail
|
|
|
|
|
|
def test_overview_renders_forward_nav_chart() -> None:
|
|
r = _client().get("/")
|
|
assert r.status_code == 200
|
|
assert "Forward NAV" in r.text
|
|
assert "<svg" in r.text and "<polyline" in r.text # inline-SVG chart rendered, no JS lib
|
|
|
|
|
|
def test_overview_honest_when_no_forward_history() -> None:
|
|
# No headline seeded: home must still render (never 500) and be honest about the missing track.
|
|
r = _client(seed_headline=False).get("/")
|
|
assert r.status_code == 200
|
|
assert "Fund status" in r.text
|
|
|
|
|
|
# ---- leak-clean guard (protects the nginx tailnet proxy) ----------------------------------------------
|
|
|
|
# An absolute http(s) URL anywhere in the rendered HTML. The only legitimate absolute URL is the SVG
|
|
# xmlns namespace (www.w3.org), which is not a fetched resource — allow exactly that, reject everything
|
|
# else (CDNs, absolute app/host links would leak the internal tailnet address).
|
|
_ABS_URL = re.compile(r"https?://(?!www\.w3\.org/)[^\s\"'<>]+")
|
|
|
|
|
|
def _assert_leak_clean(html: str) -> None:
|
|
hits = _ABS_URL.findall(html)
|
|
assert not hits, f"leak-clean violation: absolute URLs in rendered HTML: {hits}"
|
|
|
|
|
|
def test_home_is_leak_clean() -> None:
|
|
_assert_leak_clean(_client().get("/").text)
|
|
|
|
|
|
def test_base_chrome_is_leak_clean() -> None:
|
|
# base.html chrome (nav, header, vendored htmx/img) must emit only relative URLs across every route.
|
|
c = _client()
|
|
for href in _NAV_LINKS:
|
|
_assert_leak_clean(c.get(href).text)
|