From e14271c5ee2b679987c15ed26753e3e903b4a565 Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 12 Jun 2026 08:11:04 +0100 Subject: [PATCH] test: mode registry consistency checks; fail fast if registry missing Also documents the registry-driven mode integration in CLAUDE.md. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 8 +++++++- templates/index.html | 7 +++++-- tests/test_mode_registry.py | 39 +++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/test_mode_registry.py diff --git a/CLAUDE.md b/CLAUDE.md index 10154a5..a158a4c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,7 +161,13 @@ Each signal type has its own Flask blueprint: - **Templates**: `templates/index.html` (main SPA), `templates/partials/modes/*.html` (sidebar panels), `templates/partials/nav.html` (global nav) - **JS Modules**: `static/js/modes/*.js` - IIFE pattern per mode (e.g., `WeatherSat`, `SSTV`, `Meshtastic`) - **CSS**: `static/css/modes/*.css` - scoped styles per mode, CSS variables for theming (`--bg-card`, `--accent-cyan`, `--font-mono`) -- **Mode Integration**: Each mode needs entries in `index.html` at ~12 points: CSS include, welcome card, partial include, visuals container, JS include, `validModes` set, `modeGroups` map, classList toggle, `modeNames`, visuals display toggle, titles, and init call in `switchMode()` +- **Mode Integration**: Each mode is declared once in `static/js/mode-registry.js` + (label, group, elementId, module, init/destroy hooks, visuals flag). The + catalog, sidebar toggles, destroy map, visuals list, and init dispatch in + `templates/index.html` are all derived from it. A new mode additionally needs: + its partial in `templates/partials/modes/`, entries in the CSS/JS lazy-load + asset maps in `index.html`, and its include in the partials block. + `tests/test_mode_registry.py` enforces registry/asset consistency. ### Docker - `Dockerfile` - Single-stage build with all SDR tools compiled from source (dump1090, AIS-catcher, slowrx, SatDump, etc.). CMD runs `start.sh` (gunicorn + gevent) diff --git a/templates/index.html b/templates/index.html index 3aeaefb..0239041 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3815,6 +3815,9 @@ } // Mode from query string (e.g., /?mode=wifi) + if (!window.INTERCEPT_MODES) { + throw new Error('mode-registry.js failed to load — the SPA cannot start'); + } let pendingStartMode = null; const modeCatalog = {}; for (const [mode, def] of Object.entries(window.INTERCEPT_MODES)) { @@ -4918,7 +4921,7 @@ refreshDroneDevices(); } - // Module destroy is now handled by moduleDestroyMap above. + // Module destroy is now handled by the mode registry (static/js/mode-registry.js). // Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm) const reconBtn = document.getElementById('reconBtn'); @@ -4999,7 +5002,7 @@ } if (requestId !== modeSwitchRequestId) return; - // Waterfall destroy is now handled by moduleDestroyMap above. + // Waterfall destroy is now handled by the mode registry (static/js/mode-registry.js). const totalMs = Math.round(performance.now() - switchStartMs); console.info( diff --git a/tests/test_mode_registry.py b/tests/test_mode_registry.py new file mode 100644 index 0000000..8b4bb26 --- /dev/null +++ b/tests/test_mode_registry.py @@ -0,0 +1,39 @@ +"""Consistency checks between the mode registry and the template/assets.""" + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +REGISTRY = ROOT / "static" / "js" / "mode-registry.js" +INDEX = ROOT / "templates" / "index.html" + + +def _registry_modes() -> set[str]: + src = REGISTRY.read_text() + return set(re.findall(r"^\s{4}([a-z_]+):\s*\{", src, re.M)) + + +def test_registry_has_all_modes(): + """The registry must declare a sane number of modes (28 at creation).""" + modes = _registry_modes() + assert len(modes) >= 28, f"registry lost modes: {sorted(modes)}" + + +def test_registry_modes_have_partials(): + """Every partial included by index.html must exist on disk.""" + html = INDEX.read_text() + partials = set(re.findall(r"partials/modes/([\w.-]+)\.html", html)) + for partial in partials: + assert (ROOT / "templates" / "partials" / "modes" / f"{partial}.html").exists(), ( + f"index.html includes missing partial: {partial}" + ) + + +def test_no_orphan_mode_assets(): + """Every modes/*.js and modes/*.css file is referenced somewhere.""" + referenced = INDEX.read_text() + REGISTRY.read_text() + # ground_station_waterfall.js belongs to the satellite dashboard + referenced += (ROOT / "templates" / "satellite_dashboard.html").read_text() + for asset_dir, ext in [("static/js/modes", ".js"), ("static/css/modes", ".css")]: + for f in (ROOT / asset_dir).glob(f"*{ext}"): + assert f.name in referenced, f"orphaned mode asset: {f}"