Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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");
}