facett-demo/tests/pop_ran_list.rs
//! **THE RAN-LIST + native↔wasm PARITY** — the headless proof that every demo tab
//! and (in particular) every `Sverige · Population` control actually RUNS, captured
//! as a readable list, and that the deck the wasm bundle ships is the SAME deck the
//! native tests cover.
//!
//! ## The bug this guards (RAGNARÖK)
//! The deployed wasm demo did not show the Sverige showcase's buttons/sliders. The
//! root cause was a **stale deployment** (a bundle built before the Sverige tab
//! existed) — the wasm *code* is correct: `DemoState::new()` builds the same deck on
//! both targets EXCEPT for the seven native-only korp caseworker-flow tabs, which the
//! deck (and LEGEND) gate behind `cfg(not(target_arch = "wasm32"))` — see
//! [`NATIVE_ONLY_TABS`]. These tests pin that invariant so a deck/control present
//! natively but missing from the shipped wasm surface is RED, while the seven
//! documented native-only tabs are the ONE allowed divergence:
//!
//! - [`ran_list_every_tab_and_sverige_controls_run`] — drive the WHOLE demo headless
//! (`egui_kittest` over the real `draw_app` path), walk every tab, click every
//! Sverige control by label, and read back the `runtrace` "what RAN" ledger from
//! `state_json["trace"]["ran"]`. Writes the list to
//! `$FACETT_RAN_LIST` (default `~/.cache/claude-build/facett-demo-ran-list.md`).
//! The wasm-surface assertion: the Sverige tab rendered and its `11M` / `⚡ select`
//! / slider / label / relation handlers fired — the SAME code that runs in wasm.
//! - [`native_wasm_deck_parity`] — the native deck == the wasm deck PLUS the seven
//! documented [`NATIVE_ONLY_TABS`]; equivalently the wasm bundle ships the native
//! deck MINUS exactly those seven. Sverige is present on both.
use egui_kittest::kittest::Queryable;
use facett_demo::{DemoState, LEGEND};
/// The **tabs that ship on the NATIVE desktop demo ONLY.**
///
/// The seven korp caseworker-flow surfaces — a permit Gantt, a support Kanban,
/// a deadline Calendar, a case-notification toast queue, a Markdown case-note pane, a
/// query SearchBar, and an attachment list — are heavyweight case-management tabs
/// that belong to the full desktop caseworker app, NOT the lightweight wasm web
/// demo. The two nornir-plugin decks (`Deploy key` / `Build Thing`) are native-only
/// for a harder reason: their nornir leaf deps (nornir-git → gix/tokio/russh;
/// nornir-build-thing) do not compile for wasm32 at all. Both the deck `vec!` and
/// the `LEGEND` in `facett-demo/src/lib.rs` gate these facets behind
/// `#[cfg(not(target_arch = "wasm32"))]`, so the wasm bundle ships the deck MINUS
/// exactly this list.
///
/// This is the ONE documented native↔wasm divergence. `native_wasm_deck_parity`
/// treats it as an explicit allowlist: the wasm surface is asserted to be the native
/// deck with *precisely* these removed, so any OTHER tab that goes missing from wasm
/// (i.e. a divergence that is NOT one of these seven) still turns the parity check
/// RED. To make a new tab native-only, add it here AND gate it in both places in
/// `lib.rs`; to add a shared tab, do neither and it flows to wasm automatically.
const NATIVE_ONLY_TABS: &[&str] = &[
"Bygglov", // facett-gantt — permit-case Gantt
"Support", // facett-kanban — support swimlane board
"Frister", // facett-calendar — deadline calendar
"Notifications", // facett-toast — case notification queue
"Anteckning", // facett-markdown — case note pane
"Search", // facett-search — query search bar
"Attachments", // facett-file — attachment list
// The two nornir-plugin decks (2026-08-20 roster completion): native-only
// because their nornir leaf deps do not build for wasm32 (nornir-git pulls
// gix + tokio + russh; nornir-build-thing is a native plugin seam).
"Deploy key", // facett-deploykey — 🔑 SSH deploy-key surface
"Build Thing", // facett-buildthing — ⚒ build-variant deck
];
/// The default ran-list path (overridable via `$FACETT_RAN_LIST`).
fn ran_list_path() -> String {
std::env::var("FACETT_RAN_LIST")
.unwrap_or_else(|_| format!("{}/.cache/claude-build/facett-demo-ran-list.md", std::env::var("HOME").unwrap_or_default()))
}
/// Build a headless harness over the whole demo (the real `draw_app` path the
/// eframe binary + the wasm bundle both run — CLI/GUI/wasm parity).
fn demo_harness() -> egui_kittest::Harness<'static, DemoState> {
let mut demo = DemoState::new();
// Close the legend so it doesn't overlap the tab bar in the headless layout.
demo.show_about = false;
egui_kittest::Harness::builder()
.with_size(egui::vec2(1280.0, 900.0))
.build_ui_state(|ui, demo: &mut DemoState| demo.draw_app(ui), demo)
}
/// INJECT-ASSERT (THE RAN-LIST + the wasm-surface proof): drive the whole demo
/// headless, render every tab, and CLICK every Sverige control by its label. Read
/// back the `runtrace` ledger (folded into `state_json["trace"]["ran"]`) — one row
/// per component/control that ACTUALLY RAN, with a count. Assert the Sverige tab
/// rendered and every one of its controls fired (the exact code that runs in the
/// shipped wasm bundle), then write the readable list to `$FACETT_RAN_LIST`.
#[test]
fn ran_list_every_tab_and_sverige_controls_run() {
// Start from a clean ledger so the list reflects THIS drive only.
facett_core::runtrace::reset();
let mut h = demo_harness();
let _ = h.run_ok();
// ── 1. Walk every tab: activate each one (the robot-addressable equivalent of
// clicking its header — `Deck::set_active_by_title`) → it renders, so
// `deck.render:<title>` is recorded for each. Activating by title rather
// than pointer-hit-testing the tab HEADER is robust to the wrapped tab
// bar: past ~63 facets a couple of headers on a wrapped row fall outside
// the robot's AccessKit viewport and a raw header click silently misses
// them, even though the tab is live (the `set_active_by_title` seam exists
// for exactly this — see its doc + `deck_no_error_atoms`). ──
let titles: Vec<String> = h.state().deck.titles().iter().map(|s| s.to_string()).collect();
// The NATIVE deck: `EXPECTED_FACET_COUNT` facets = the shared (native == wasm)
// tabs + the 7 native-only korp caseworker-flow tabs (Bygglov / Support /
// Frister / Notifications / Anteckning / Search / Attachments). This drive is
// native, so it sees them all; the wasm bundle ships the shared subset (see
// `NATIVE_ONLY_TABS` and `native_wasm_deck_parity`).
//
// DERIVED, not typed. A literal `72` sat here and went stale the moment `0b12255`
// added the `Errors` and `Manual` tabs — this assertion was already failing on
// `main`, as a second copy of a number `src/lib.rs` already owned. There is now
// exactly one copy.
assert_eq!(
titles.len(),
facett_demo::EXPECTED_FACET_COUNT,
"the native deck has all {} facets (the shared gallery tabs + the 9 native-only \
tabs: Bygglov / Support / Frister / Notifications / Anteckning / Search / \
Attachments / Deploy key / Build Thing)",
facett_demo::EXPECTED_FACET_COUNT,
);
for t in &titles {
assert!(h.state_mut().set_active_tab(t), "tab `{t}` is activatable");
let _ = h.run_ok();
}
// ── 2. Land on the Sverige tab and CLICK every control by label (the exact
// handlers that must run in wasm). ──
assert!(h.state_mut().set_active_tab("Sverige"), "Sverige tab exists + is selectable");
let _ = h.run_ok();
// The Sverige controls, by their on-screen labels. Each click fires the handler
// we instrumented with `runtrace::ran(...)` in `pop_tab::ui`.
for label in [
"11M", // → ctl:11M (canonical population button)
"by-kommun", // → ctl:colour:by-kommun
"debt-heat", // → ctl:colour:debt-heat
"✨ bloom", // → ctl:bloom
"⚡ select", // → ctl:select (the click-shockwave)
"Address", // → ctl:label:Address (a label toggle)
"OWES", // → ctl:relation:OWES (a relation toggle)
] {
if let Some(node) = h.query_by_label(label) {
node.click();
let _ = h.run_ok();
}
}
// ── 3. Read back the ledger from the live demo's state_json. ──
let state = h.state().state_json();
let ran = state["deck"]["trace"]["ran"].as_object().expect("trace.ran is an object").clone();
// The wasm-surface assertions: the Sverige tab RENDERED and each control RAN.
let got = |k: &str| ran.get(k).and_then(|v| v.as_u64()).unwrap_or(0);
assert!(got("deck.render:Sverige") > 0, "the Sverige tab RENDERED in the live drive");
assert!(got("PopGraphTab::ui") > 0, "the Sverige tab's ui() RAN");
assert!(got("PopGraphTab::ctl:11M") > 0, "the `11M` button handler RAN (the canonical population button)");
assert!(got("PopGraphTab::ctl:select") > 0, "the `⚡ select` shockwave handler RAN");
assert!(got("PopGraphTab::ctl:bloom") > 0, "the `✨ bloom` toggle handler RAN");
assert!(got("PopGraphTab::ctl:colour:debt-heat") > 0, "the debt-heat colour handler RAN");
assert!(got("PopGraphTab::ctl:label:Address") > 0, "an Address label-toggle handler RAN");
assert!(got("PopGraphTab::ctl:relation:OWES") > 0, "an OWES relation-toggle handler RAN");
// Every tab rendered at least once (the deck.render:<title> rows).
for t in &titles {
assert!(got(&format!("deck.render:{t}")) > 0, "tab `{t}` rendered in the drive");
}
// ── 4. Write THE LIST (component/tab/control → ran? → count). ──
let mut rows: Vec<(String, u64)> = ran.iter().map(|(k, v)| (k.clone(), v.as_u64().unwrap_or(0))).collect();
rows.sort();
let mut md = String::new();
md.push_str("# facett-demo — what RAN (native headless drive)\n\n");
md.push_str("Driven via `egui_kittest` over the real `draw_app` path (the SAME code the\n");
md.push_str("wasm bundle runs). One row per component/tab/control that actually executed.\n\n");
md.push_str("| component / tab / control | ran? | count |\n|---|---|---|\n");
for (k, c) in &rows {
md.push_str(&format!("| `{k}` | ✅ | {c} |\n"));
}
md.push_str(&format!("\n**{} distinct surfaces ran.**\n", rows.len()));
// The native-vs-wasm note: the deck is cfg-independent EXCEPT for the seven
// native-only korp caseworker-flow tabs; the wasm path is read live via
// `window.__facett_state()` (the JS hook).
md.push_str("\n## native ↔ wasm\n\n");
md.push_str("The deck assembly (`DemoState::new`) is `cfg`-gated ONLY for the seven native-only\n");
md.push_str("korp caseworker-flow tabs (Bygglov / Support / Frister / Notifications / Anteckning /\n");
md.push_str("Search / Attachments); the wasm bundle ships the same deck MINUS exactly those seven.\n");
md.push_str("On wasm the identical ledger is read live from the browser via `window.__facett_state()`\n");
md.push_str("→ `state_json.deck.trace.ran` — a divergence beyond the seven documented native-only\n");
md.push_str("tabs (a shared tab/control present here but not running in the shipped wasm) is RED.\n");
let path = ran_list_path();
if let Some(parent) = std::path::Path::new(&path).parent() {
let _ = std::fs::create_dir_all(parent);
}
std::fs::write(&path, &md).expect("write the ran-list");
eprintln!("[ran-list] wrote {} surfaces → {path}", rows.len());
}
/// INJECT-ASSERT (native↔wasm deck PARITY): the deck's tab set + the LEGEND are
/// `cfg`-independent EXCEPT for the seven native-only korp caseworker-flow tabs
/// ([`NATIVE_ONLY_TABS`]), which both the `FacetDeck::new(vec![…])` and the `LEGEND`
/// in `DemoState::new` gate behind `#[cfg(not(target_arch = "wasm32"))]`. So the deck
/// the wasm bundle ships is the native deck MINUS *exactly* those seven, and the
/// Sverige showcase is present on both. A shared tab present natively but dropped on
/// wasm — a divergence that is NOT one of the seven — is the bug this pins RED.
#[test]
fn native_wasm_deck_parity() {
let demo = DemoState::new();
let titles: Vec<String> = demo.deck.titles().iter().map(|s| s.to_string()).collect();
// The LEGEND catalogue and the live deck agree on Sverige.
let legend_keys: Vec<&str> = LEGEND.iter().map(|(k, _)| *k).collect();
assert!(legend_keys.contains(&"Sverige"), "the LEGEND lists the Sverige showcase");
assert!(titles.iter().any(|t| t == "Sverige"), "the live deck holds the Sverige tab");
assert!(titles.iter().any(|t| t == "Velo L1"), "the live deck holds the Velo L1 tab");
// ── The canonical NATIVE deck: 72 facets in deck order (the 65 shared tabs +
// the 7 native-only korp caseworker-flow tabs, which sit between `System Map`
// and the trailing `scene` shell). This drive is native, so the live deck ==
// this full list; any add/remove/reorder of a native tab turns it RED. ──
const EXPECTED_NATIVE: &[&str] = &[
"graph", "graphview", "Graphs", "Universe", "metro", "Velo L1", "Sverige", "graph3d", "graph3d-given", "rabbit3d", "avocado3d",
"nav", "shell", "overlay", "widgets", "Layers", "primitives", "depgraph", "pipeline", "Falsett", "synthetic-sdp", "SDP", "brokkr", "runa", "releaseview", "timeline",
"plandag", "grid", "table", "card", "form", "pop", "plot",
"boxplot", "violin", "heatmap", "treemap", "candles", "map", "syschart",
"geomap", "OSM 2D", "OSM 3D", "icebergview", "docview", "log", "stat", "warehouse",
"tabbar", "decograph", "falkor", "matrix", "jobs", "zone", "Jobs", "graphpan", "security", "helix", "dna", "timehelix",
"console", "Open File", "video", "Git", "warehousedeck", "codevault", "System Map",
// ── the 2026-08-20 roster completion (the 11 shared, wasm-clean tabs) ──
"cicd", "brokkrview", "lineage", "Fleet",
"holger — registry & artifact browser",
"Ingest", "Sign in", "Binary analysis", "World coderoom", "Claude", "About",
// ── the nine native-only tabs (== NATIVE_ONLY_TABS): the seven korp
// caseworker-flow tabs + the two nornir-plugin decks ──
"Bygglov", "Support", "Frister", "Notifications", "Anteckning", "Search", "Attachments",
"Deploy key", "Build Thing",
"scene",
// The error-code showcase pair (`0b12255`) and the GFX v2 renderer-track
// showcase. The first two were missing here, which is why this test was
// already red on `main`.
"Errors", "Manual", "GFX v2",
];
assert_eq!(titles, EXPECTED_NATIVE, "the native deck is the canonical gallery, in order");
// ── The by-construction half: MEMBERSHIP is derived, only ORDER is typed. ──
//
// `EXPECTED_NATIVE` is a genuine ORDER guard, and deck order is not legend order
// (the `scene` shell sits after the native-only tabs in the deck and before them
// in `LEGEND`), so the list itself cannot be generated. But its *contents* can be
// checked against the roster `src/lib.rs` maintains — and that is precisely what
// went wrong: this list silently lacked two tabs that were in the deck, in
// `LEGEND`, and in the mega matrix.
//
// So: the SET of expected titles must equal `LEGEND` minus the `menu` command
// palette (a `CommandPalette`, never a deck tab). Add a tab and forget this list
// and it goes red naming the tab, instead of failing with two 75-entry arrays to
// diff by eye.
{
use std::collections::BTreeSet;
let expected: BTreeSet<&str> = EXPECTED_NATIVE.iter().copied().collect();
assert_eq!(expected.len(), EXPECTED_NATIVE.len(), "no title appears twice in EXPECTED_NATIVE");
let legend: BTreeSet<&str> =
facett_demo::LEGEND.iter().map(|(k, _)| *k).filter(|k| *k != "menu").collect();
let missing: Vec<&&str> = legend.difference(&expected).collect();
assert!(missing.is_empty(), "tabs in LEGEND but missing from EXPECTED_NATIVE (add them, in deck order): {missing:?}");
let extra: Vec<&&str> = expected.difference(&legend).collect();
assert!(extra.is_empty(), "EXPECTED_NATIVE names tabs that are not in LEGEND (stale): {extra:?}");
}
// The allowlist is LIVE, not stale: every native-only tab really is in the deck
// (a NATIVE_ONLY_TABS entry that named a tab which no longer exists would silently
// weaken the parity check — this refuses that).
for t in NATIVE_ONLY_TABS {
assert!(titles.iter().any(|x| x == t), "native-only tab `{t}` must exist in the native deck");
}
// ── PARITY: the wasm web bundle ships the native deck MINUS exactly the seven
// documented native-only tabs. Derive that surface from the (native) deck the
// SAME way the `#[cfg(target_arch = "wasm32")]` build drops them, and assert
// its shape against a COMPUTED count (not a fresh magic number). An UNINTENDED
// omission — a tab gone from wasm that ISN'T one of the seven — makes either
// `titles != EXPECTED_NATIVE` above or this derivation go RED, so the check
// still bites on real native↔wasm divergence. ──
let wasm_deck: Vec<&String> = titles.iter().filter(|t| !NATIVE_ONLY_TABS.contains(&t.as_str())).collect();
assert_eq!(
wasm_deck.len(),
titles.len() - NATIVE_ONLY_TABS.len(),
"the wasm deck == the native deck minus exactly the {} native-only tabs",
NATIVE_ONLY_TABS.len(),
);
// (A literal `assert_eq!(wasm_deck.len(), 65, …)` used to sit here, directly under
// a comment promising "a COMPUTED count (not a fresh magic number)". It is gone
// rather than corrected: the assertion immediately above already derives the same
// fact from `titles.len()` and `NATIVE_ONLY_TABS`, so the literal added no
// coverage and did add a third place to forget when a tab is added. It was
// already stale by two.)
// Not one of the seven leaked into the wasm surface, and Sverige survives.
for t in NATIVE_ONLY_TABS {
assert!(!wasm_deck.iter().any(|x| x.as_str() == *t), "native-only `{t}` must NOT appear in the wasm deck");
}
assert!(wasm_deck.iter().any(|t| t.as_str() == "Sverige"), "the wasm deck keeps the Sverige showcase");
// Defence in depth: if this test is ever compiled for wasm (wasm-pack), the live
// deck there IS the 65-tab subset — assert that directly so a real wasm build that
// drops a shared tab (or keeps a native-only one) is caught on-target too.
#[cfg(target_arch = "wasm32")]
{
let wasm_expected: Vec<&str> =
EXPECTED_NATIVE.iter().copied().filter(|t| !NATIVE_ONLY_TABS.contains(t)).collect();
assert_eq!(titles, wasm_expected, "the wasm deck is the native deck minus the 7 native-only tabs");
}
// Sverige carries its real controls' state (the showcase is populated, not a
// shell): the analytic 11M scale + three linked views, which is exactly what the
// wasm bundle renders the buttons/sliders over.
let s = demo.deck.state_json();
let sv = &s["facets"]["Sverige"];
assert_eq!(sv["component"], "PopGraphTab", "Sverige is the population showcase");
assert_eq!(sv["views"].as_array().unwrap().len(), 3, "three linked Sverige views");
}