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>
100 lines
3.9 KiB
Python
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())
|