Files
momentum-system/tests/smoke_dashboard_headless.py
T
momentum devandClaude Fable 5 e2b3c1ceae operator-plane: headless smoke + effective-config panel + ledger + unpin ruff
Step 6: tests/smoke_dashboard_headless.py renders the real page in a headless
browser and fails on any JS console/pageerror (standalone script, not
pytest-collected; CI non-blocking first). admin_api gains GET /api/config/effective
(per-key masked value + source: overrides.env vs compose, missing .env.example
keys flagged as drift) + an admin.html table (values XSS-escaped). dashboard.html
gains the Experiment Ledger panel reading the plan-006 verdicts from the snapshot
(empty-state until the tables exist). Removes the last CI ruff per-file pin
(api/main.py:F821) now that os/hmac are imported; scoped ruff is green unpinned.
Marks plan 007 DONE in plans/README.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:36:00 -04:00

100 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Headless dashboard smoke test (plan 007 step 6).
Renders the real dashboard page in a headless browser and FAILS on any JS console
/ page error — the net that would have caught the do_POST NameError's sibling
class (a runtime JS bug curl can't see). Standalone script (NOT a pytest module —
the filename has no ``test_`` prefix so pytest never collects it); wired into CI
as a NON-BLOCKING step first (loopback binds are flaky on WSL hosts), promote to
blocking after one green week.
Setup (once per environment):
pip install playwright && python -m playwright install chrome-headless-shell
# minimal Linux/WSL: if the browser won't launch with a libasound error, stub it
mkdir -p ~/.lib-stubs && touch ~/.lib-stubs/libasound.so.2
export LD_LIBRARY_PATH=~/.lib-stubs:$LD_LIBRARY_PATH
Exit codes: 0 = no JS errors (pass); 1 = JS errors or assertion failure;
2 = setup missing (playwright/browser unavailable — treated as skip in CI).
"""
from __future__ import annotations
import os
import sys
import uuid
# run standalone (`python tests/smoke_dashboard_headless.py`) -> put the repo root
# on the path so ``api`` / ``storage`` import without an installed package.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def _drop_schema(schema: str) -> None:
try:
import psycopg2
url = os.environ.get("DATABASE_URL")
if url:
conn = psycopg2.connect(url)
conn.autocommit = True
conn.cursor().execute(f'DROP SCHEMA IF EXISTS "{schema}" CASCADE')
conn.close()
except Exception: # noqa: BLE001
pass
def main() -> int:
os.environ.setdefault("PG_SKIP_SCHEMA", "1")
schema = f"mem_{uuid.uuid4().hex[:12]}"
os.environ["MOMENTUM_PG_SCHEMA"] = schema
from api.main import DashboardState, serve_in_background
from storage.event_store import EventStore
store = EventStore("momentum") # creates the isolated schema
state = DashboardState("momentum", execution_service=None)
httpd, _ = serve_in_background(state, host="127.0.0.1", port=0)
port = httpd.server_address[1]
errors: list[str] = []
try:
try:
from playwright.sync_api import sync_playwright
except Exception as exc: # noqa: BLE001
print(f"SKIP — playwright not installed: {exc}")
return 2
with sync_playwright() as p:
try:
browser = p.chromium.launch(channel="chrome-headless-shell",
args=["--no-sandbox"])
except Exception: # noqa: BLE001 — fall back to a plain chromium install
try:
browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
except Exception as exc: # noqa: BLE001
print(f"SKIP — no launchable browser: {exc}")
return 2
page = browser.new_page()
page.on("console", lambda m: errors.append(m.text) if m.type == "error" else None)
page.on("pageerror", lambda e: errors.append(str(e)))
# NOT networkidle: the dashboard holds an SSE stream open (+ polling), so
# it never idles. Load the DOM, then settle so the snapshot fetch + render
# run and any JS error surfaces.
page.goto(f"http://127.0.0.1:{port}", wait_until="domcontentloaded")
page.wait_for_timeout(2500)
assert page.locator(".panel").count() >= 8, "panels missing"
assert page.locator("#mode-badge").count() == 1, "badge missing"
browser.close()
finally:
httpd.shutdown()
store.close()
_drop_schema(schema)
if errors:
print(f"FAIL — {len(errors)} JS errors: {errors}")
return 1
print("0 JS errors")
return 0
if __name__ == "__main__":
sys.exit(main())