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

nordisk/dwarves

dwarves-gui/tests/robot_scene.rs

//! **robot_scene** — the comprehensive robot-UI suite for dwarves' scene-migrated shell.
//!
//! dwarves-gui's tabbed window is now ONE declarative `facett_app::scene::Scene` (a
//! menubar + a foldable pane tree + routes) rendered through facett-app's own primitives
//! (see `.nornir/scene-design.md`). This integration suite drives that SCENE surface —
//! the SAME shipped shell `dwarves_gui::build_native_app` runs — end-to-end and
//! headlessly, over BOTH seams the migration exposes:
//!
//! - the remote **state_json / update_json** seam (`Facet`) — deep-linking, tab switching,
//!   per-pane fold, and every shipped pane's folded state; and
//! - a real **`egui_kittest` render** — a pointer drag on the master/detail divider
//!   (using the `separators` rects the scene exposes) re-apportions the LIVE split ratio,
//!   and resizing the harness viewport across the 760pt phone boundary auto-folds /
//!   restores the DRAWER rail.
//!
//! Each check is RED-when-broken and emits a `nornir-testmatrix` cell (reuse edda) under
//! `--features testmatrix`. This EXTENDS the 6 in-crate scene tests in `src/tabbed.rs`
//! (`scene_shell_tests`) — it does not duplicate them: those assert the shell is valid /
//! RON-stable / a single route fires / one fold omits content / one drag grows the rail;
//! this suite covers EVERY shipped pane (read off `DWARVES_TABS`, never a copied list),
//! every foldable pane, bidirectional drag, the drawer transition, all 3 routes + the
//! menubar data, and deep-linking by pane name.

#![cfg(not(target_arch = "wasm32"))]

use dwarves_gui::tabbed::{dwarves_scene, TabbedView, BODY, RAIL, ROOT};
use dwarves_gui::{build_native_app, tab_keys, tab_labels, DWARVES_TABS, NATIVE_TITLE};

use facett_app::scene::{self, Action, Fold, Layout, NavState, SceneHost, Trigger, CHROME_BAR};
use facett_core::Facet;
use serde_json::{json, Value};

use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;

use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;

/// The shipped tab labels, in the shipped order — read from the lib's `DWARVES_TABS`
/// roster (korp's `KORP_TABS` shape), NOT a copied string list. This is the whole point:
/// the suite drives whatever the shell actually mounts, so adding a tab automatically
/// puts it under every scene test below and dropping one fails RED. The previous
/// hardcoded 9-name list silently tested half the shipped 18-tab surface.
fn labels() -> Vec<String> {
    tab_labels()
}

/// Build the EXACT shipped native shell, offline-safe: dead endpoints (nothing is
/// reachable) + a nonexistent jera ledger ⇒ every pane renders its honest unavailable /
/// fixture state, never hangs on a socket. The SAME builder `main` runs — `None` for the
/// nornir server is `main`'s FAT/embedded default (the funnel runs in-process, so
/// nornir-server being down is not a test dependency).
fn offline_app() -> TabbedView {
    build_native_app(
        "http://127.0.0.1:0".into(),
        PathBuf::from("/nonexistent/dwarves-gui/robot-scene"),
    )
}

/// A tiny host that draws a queryable label per slot (so the AccessKit tree carries a
/// node) and returns a small `state_json` — mirrors `scene_shell_tests::TestHost` so the
/// render-level tests exercise the SAME `dwarves_scene()` value the app ships.
struct TestHost;
impl SceneHost for TestHost {
    fn slot(&mut self, id: &str, ui: &mut egui::Ui) -> Value {
        ui.label(format!("SLOT:{id}"));
        json!({ "slot": id })
    }
}

/// A rendered pane's **allotted** rect `(x, y, w, h)` — the area its parent handed it
/// this frame. This is the geometry oracle the chrome + divider assertions read: a
/// minimised child is allotted exactly [`CHROME_BAR`] points along the split axis, and a
/// maximised pane's siblings are allotted a ZERO rect. `rect` (what egui then consumed)
/// can lag by a frame or a margin; `alloc` IS the apportionment.
fn pane_alloc(state: &Value, pane: &str) -> (f32, f32, f32, f32) {
    let r = &state["panes"][pane]["alloc"];
    let f = |i: usize| {
        r.get(i)
            .and_then(Value::as_f64)
            .unwrap_or_else(|| panic!("pane `{pane}` reports an alloc rect: {}", state["panes"]))
            as f32
    };
    (f(0), f(1), f(2), f(3))
}

/// A rendered pane's allotted WIDTH.
fn alloc_w(state: &Value, pane: &str) -> f32 {
    pane_alloc(state, pane).2
}

/// A rendered pane's allotted AREA — zero for a pane a maximise hid.
fn alloc_area(state: &Value, pane: &str) -> f32 {
    state["panes"][pane]["area"].as_f64().unwrap_or_else(|| {
        panic!(
            "pane `{pane}` reports its allotted area: {}",
            state["panes"]
        )
    }) as f32
}

/// The centre of a split pane's first (only) drag-handle rect, read from the render's
/// observable `separators` — the robot-addressable divider position.
fn sep_center(state: &Value, pane: &str) -> egui::Pos2 {
    let sep = &state["panes"][pane]["separators"][0];
    let x = sep[0].as_f64().expect("a divider handle rect is reported") as f32;
    let y = sep[1].as_f64().unwrap() as f32;
    let w = sep[2].as_f64().unwrap() as f32;
    let h = sep[3].as_f64().unwrap() as f32;
    egui::pos2(x + w / 2.0, y + h / 2.0)
}

#[allow(unused_variables)]
fn matrix(name: &str, ok: bool, desc: &str) {
    #[cfg(feature = "testmatrix")]
    nornir_testmatrix::functional_status("dwarves-gui", name, ok, desc);
}

// ── 1. every shipped tab pane: switch + slot state ─────────────────────────────

/// ROBOT SCENARIO — drive the shipped shell to EVERY tab in the `DWARVES_TABS` roster
/// and assert (a) the scene's `body` Tabs active child follows (`active_tab`), and (b)
/// that pane's own `state_json` is folded under its lowercased label (the reused
/// `Box<dyn Panel>` still answers), and (c) the pane is a declared foldable scene pane.
/// RED-when-broken: drop a tab / break the scene tab-switch seam and a pane goes missing.
#[test]
fn robot_every_shipped_pane_switches_and_slot_state() {
    let mut app = offline_app();
    // The suite drives the WHOLE shipped roster — not a copied subset of it.
    assert_eq!(
        labels().len(),
        DWARVES_TABS.len(),
        "the suite drives the whole roster"
    );
    // The shell opens on the first tab and declares every pane as a foldable slot.
    assert_eq!(
        app.active_tab(),
        "Tree",
        "opens on the landing (🏠 Workspace → 🌳 Tree) tab"
    );
    let panes = app.foldable_panes();
    assert!(
        panes.contains(&RAIL.to_string()),
        "the drawer rail is a foldable pane"
    );

    let mut all_ok = true;
    for (key, label) in DWARVES_TABS
        .iter()
        .map(|(k, l)| (k.to_string(), l.to_string()))
    {
        // Switch over the SAME JSON seam the robot harness / thin client use.
        Facet::update_json(&mut app, &json!({ "set_tab": label }).to_string());
        let s = Facet::state_json(&app);

        let active_ok = s["active_tab"] == json!(label) && app.active_tab() == label;
        // The reused panel's own state is present, folded under its roster slot key.
        let slot_ok = s.get(key.as_str()).map(|v| v.is_object()).unwrap_or(false);
        // The pane is a declared, foldable scene pane (fold seam is addressable).
        let scene_ok = s["panes"]
            .as_array()
            .unwrap()
            .iter()
            .any(|p| p == &json!(key))
            && s["foldable"]
                .as_array()
                .unwrap()
                .iter()
                .any(|p| p == &json!(key))
            && s["folded"].get(key.as_str()).is_some();

        let pane_ok = active_ok && slot_ok && scene_ok;
        assert!(
            pane_ok,
            "tab `{label}` switches + folds its slot state under `{key}`: {s}"
        );
        all_ok &= pane_ok;
    }
    // The shipped title names EVERY roster tab (the shell is complete, not truncated).
    let title_ok = DWARVES_TABS.iter().all(|(_, l)| NATIVE_TITLE.contains(l));
    assert!(
        title_ok,
        "NATIVE_TITLE names every shipped tab: {NATIVE_TITLE}"
    );

    matrix(
        "scene_every_shipped_pane_switches_and_slot_state",
        all_ok && title_ok,
        &format!(
            "every one of the {} shipped scene tab panes ({}) switches over the state_json/update_json seam, driving the scene body Tabs active child, with each reused panel's state folded under its lowercased label — the roster is read from DWARVES_TABS, so the suite can never test fewer panes than the shell mounts",
            DWARVES_TABS.len(),
            labels().join("/"),
        ),
    );
}

// ── 2. fold / unfold EVERY foldable pane ───────────────────────────────────────

/// ROBOT SCENARIO — fold then unfold EVERY foldable pane (the drawer rail + every shipped
/// tab leaf) over the `{"toggle_fold":…}` seam, asserting each transition is observable in
/// `state_json.folded`. RED-when-broken: break the fold seam and a pane won't flip.
#[test]
fn robot_fold_unfold_every_foldable_pane() {
    let mut app = offline_app();
    let foldable = app.foldable_panes();
    // The rail + EVERY roster tab is foldable — counted off the roster, not a literal.
    assert!(
        foldable.len() >= DWARVES_TABS.len() + 1,
        "the rail + all {} tab leaves are foldable: {foldable:?}",
        DWARVES_TABS.len()
    );
    for key in tab_keys() {
        assert!(foldable.contains(&key), "the `{key}` tab leaf is foldable");
    }

    let mut all_ok = true;
    for pane in &foldable {
        // Every foldable pane starts OPEN (default_open) ⇒ not folded.
        let open0 = Facet::state_json(&app)["folded"][pane] == json!(false);
        // Fold it.
        Facet::update_json(&mut app, &json!({ "toggle_fold": pane }).to_string());
        let folded = Facet::state_json(&app)["folded"][pane] == json!(true);
        // Unfold it.
        Facet::update_json(&mut app, &json!({ "toggle_fold": pane }).to_string());
        let reopened = Facet::state_json(&app)["folded"][pane] == json!(false);

        let pane_ok = open0 && folded && reopened;
        assert!(
            pane_ok,
            "foldable pane `{pane}` folds + unfolds over the seam"
        );
        all_ok &= pane_ok;
    }

    matrix(
        "scene_fold_unfold_every_foldable_pane",
        all_ok,
        &format!(
            "every foldable scene pane (the drawer rail + all {} shipped tab leaves) folds and unfolds over the {{\"toggle_fold\":…}} seam, observable in state_json.folded",
            DWARVES_TABS.len()
        ),
    );
}

// ── 3. DRAG the master/detail divider — re-apportions BOTH ratios, bidirectional ─

/// ROBOT SCENARIO (kittest render) — a REAL pointer drag on the master/detail divider
/// handle re-apportions the LIVE split ratio, using the `separators` rects the scene
/// exposes. EXTENDS the in-crate one-direction test: asserts the rail AND the body
/// complement (they always sum to 1), and drags BOTH ways (grow the rail past its 0.20
/// seed, then shrink it below the seed). RED-when-broken: a static split won't move.
///
/// **It asserts RECTS, not just ratios.** A ratio that moves while the panes stay put is
/// a bookkeeping change nobody can see; the point of a divider is that the geometry
/// follows. So each drag is checked as: the rail's allotted rect got ~the drag distance
/// wider, the body's got ~the same narrower, and their sum is preserved.
#[test]
fn robot_render_drag_split_reapportions_both_ratios_bidirectional() {
    let scene = dwarves_scene(&labels());
    let nav = Rc::new(RefCell::new(NavState::new(&scene)));
    let state = Rc::new(RefCell::new(Value::Null));
    let seed = nav.borrow().split_ratios(ROOT).unwrap()[0];
    assert!(
        (seed - 0.20).abs() < 1e-5,
        "the builder seeds the 0.20 rail fraction"
    );

    let (s, n, sc) = (state.clone(), nav.clone(), scene.clone());
    let mut h = Harness::builder()
        .with_size(egui::vec2(1200.0, 800.0))
        .build_ui(move |ui| {
            *s.borrow_mut() = scene::render(&sc, &mut n.borrow_mut(), ui, &mut TestHost);
        });
    h.run();

    // The divider is reported as a resizable split with exactly one handle rect.
    let resizable_ok = state.borrow()["panes"][ROOT]["resizable"] == json!(true)
        && state.borrow()["panes"][ROOT]["separators"]
            .as_array()
            .map(|a| a.len())
            == Some(1);

    // The GEOMETRY before the drag: a real narrow rail beside a wider body.
    let (rail_w0, body_w0) = (
        alloc_w(&state.borrow(), RAIL),
        alloc_w(&state.borrow(), BODY),
    );
    let span0 = rail_w0 + body_w0;
    assert!(
        rail_w0 > 100.0 && body_w0 > rail_w0,
        "the rail is a real, narrower column: rail={rail_w0} body={body_w0}"
    );

    // Drag the handle 250pt RIGHT → the rail grows, the body shrinks, sum stays 1.
    let from = sep_center(&state.borrow(), ROOT);
    let to = egui::pos2(from.x + 250.0, from.y);
    h.event(egui::Event::PointerMoved(from));
    h.drag_at(from);
    h.event(egui::Event::PointerMoved(to));
    h.run();
    h.drop_at(to);
    h.run();
    let right = nav.borrow().split_ratios(ROOT).unwrap().clone();
    let grew = right[0] > 0.25 && right[1] < 0.75 && (right[0] + right[1] - 1.0).abs() < 1e-3;

    // ── THE RECTS MOVED, by the distance dragged. This is the assertion a ratio-only
    // check cannot make: a divider whose numbers change but whose panes do not is a
    // divider the operator cannot see working.
    let (rail_w1, body_w1) = (
        alloc_w(&state.borrow(), RAIL),
        alloc_w(&state.borrow(), BODY),
    );
    assert!(
        (rail_w1 - rail_w0 - 250.0).abs() < 24.0,
        "dragging 250pt right widened the rail's RECT by ~250pt: {rail_w0} -> {rail_w1}"
    );
    assert!(
        (body_w0 - body_w1 - 250.0).abs() < 24.0,
        "…and narrowed the body's RECT by the same ~250pt: {body_w0} -> {body_w1}"
    );
    assert!(
        ((rail_w1 + body_w1) - span0).abs() < 8.0,
        "the split conserves area: {span0} -> {}",
        rail_w1 + body_w1
    );

    // Drag the (moved) handle 400pt LEFT → the rail shrinks back below its 0.20 seed.
    let from2 = sep_center(&state.borrow(), ROOT);
    let to2 = egui::pos2(from2.x - 400.0, from2.y);
    h.event(egui::Event::PointerMoved(from2));
    h.drag_at(from2);
    h.event(egui::Event::PointerMoved(to2));
    h.run();
    h.drop_at(to2);
    h.run();
    let left = nav.borrow().split_ratios(ROOT).unwrap().clone();
    let shrank = left[0] < right[0] && left[0] < seed && (left[0] + left[1] - 1.0).abs() < 1e-3;
    // The live ratio round-trips through the observable seam both ways.
    let observable = state.borrow()["nav"]["split_ratio"][ROOT][0]
        .as_f64()
        .unwrap()
        < seed as f64;

    // …and the geometry followed back too: the rail is now NARROWER than it started.
    let (rail_w2, body_w2) = (
        alloc_w(&state.borrow(), RAIL),
        alloc_w(&state.borrow(), BODY),
    );
    assert!(
        rail_w2 < rail_w0,
        "the reverse drag shrank the rail's RECT below its start: {rail_w0} -> {rail_w2}"
    );
    assert!(
        body_w2 > body_w0,
        "…and the body reclaimed the area: {body_w0} -> {body_w2}"
    );

    let ok = resizable_ok && grew && shrank && observable;
    assert!(ok, "the divider drags both ways, re-apportioning both ratios: seed={seed} right={right:?} left={left:?}");

    matrix(
        "scene_drag_split_reapportions_bidirectional",
        ok,
        "a real pointer drag on the master/detail divider (via the scene's exposed separators rects) re-apportions BOTH live split ratios (they sum to 1) AND moves both panes' rendered RECTS by the distance dragged (sum conserved), bidirectionally — grow the rail past its 0.20 seed then shrink it below — observable in state_json (kittest render)",
    );
}

// ── 3b. WINDOW CHROME: minimise / maximise / restore, asserted as RECTS ─────────

/// ROBOT SCENARIO (kittest render) — the per-pane **window chrome** facett-app puts on
/// every pane but the root. Drives the affordance the way an operator does — by CLICKING
/// the title-bar control, addressed by its unique AccessKit label (`"minimize rail"`) —
/// and reads back GEOMETRY rather than a flag:
///
/// * **minimise the rail** → it collapses to its `CHROME_BAR`-wide title strip and the
///   body is handed the width it gave up (the space comes back, it is not lost);
/// * **restore** → both rects return to EXACTLY what they were (chrome never overwrites
///   the split ratios, so restore is lossless — not a reset to the 0.20 seed);
/// * **maximise the body** → the rail is allotted ZERO area and the body fills the shell;
///   **restore** puts the master/detail layout back unchanged.
///
/// RED-when-broken: a chrome bar that draws but doesn't re-apportion, or a restore that
/// resets to the seed instead of the prior layout, both pass a boolean check and fail
/// this one.
#[test]
fn robot_render_chrome_minimise_maximise_restore_move_the_rects() {
    let scene = dwarves_scene(&labels());
    let nav = Rc::new(RefCell::new(NavState::new(&scene)));
    let state = Rc::new(RefCell::new(Value::Null));
    let (s, n, sc) = (state.clone(), nav.clone(), scene.clone());

    let mut h = Harness::builder()
        .with_size(egui::vec2(1200.0, 800.0))
        .build_ui(move |ui| {
            *s.borrow_mut() = scene::render(&sc, &mut n.borrow_mut(), ui, &mut TestHost);
        });
    h.run();

    // The rail's bar drew all three controls this frame.
    // The three controls this shell drives. Asserted per FIELD, not against a whole
    // object literal: facett-app grows the spec (it just gained `editor`), and a shell
    // that only cares about fold/minimise/maximise should not go red because a field it
    // does not use appeared beside them.
    let spec = state.borrow()["panes"][RAIL]["chrome"]["spec"].clone();
    let spec_ok = spec["bar"] == json!(true)
        && spec["fold"] == json!(true)
        && spec["minimize"] == json!(true)
        && spec["maximize"] == json!(true);
    assert!(
        spec_ok,
        "the rail's title bar carries fold + minimise + maximise: {}",
        state.borrow()["panes"][RAIL]["chrome"]
    );

    let (rail0, body0) = (
        alloc_w(&state.borrow(), RAIL),
        alloc_w(&state.borrow(), BODY),
    );
    let span0 = rail0 + body0;

    // ── MINIMISE the rail by clicking its chrome control ─────────────────────────
    h.get_by_label(&format!("minimize {RAIL}")).click();
    h.run();
    h.run();
    let min_state_ok = nav.borrow().is_minimized(RAIL)
        && state.borrow()["minimized"][RAIL] == json!(true)
        && nav.borrow().last_chrome.as_deref() == Some(&format!("minimize:{RAIL}")[..]);
    let (rail1, body1) = (
        alloc_w(&state.borrow(), RAIL),
        alloc_w(&state.borrow(), BODY),
    );
    assert!(
        (rail1 - CHROME_BAR).abs() < 2.0,
        "a minimised rail collapses to its {CHROME_BAR}pt title strip: {rail0} -> {rail1}"
    );
    assert!(
        (body1 - (span0 - CHROME_BAR)).abs() < 8.0,
        "…and the body is handed the space back: {body0} -> {body1} (of {span0})"
    );

    // ── RESTORE: the EXACT prior geometry, not a reset ───────────────────────────
    h.get_by_label(&format!("restore {RAIL}")).click();
    h.run();
    h.run();
    let (rail2, body2) = (
        alloc_w(&state.borrow(), RAIL),
        alloc_w(&state.borrow(), BODY),
    );
    let restore_ok = !nav.borrow().is_minimized(RAIL)
        && (rail2 - rail0).abs() < 1.0
        && (body2 - body0).abs() < 1.0;
    assert!(restore_ok, "restore returns the EXACT prior geometry: rail {rail0} -> {rail2}, body {body0} -> {body2}");

    // ── MAXIMISE the body: the rail is allotted nothing at all ───────────────────
    h.get_by_label(&format!("maximize {BODY}")).click();
    h.run();
    h.run();
    let max_state_ok =
        nav.borrow().is_maximized(BODY) && state.borrow()["maximized"] == json!(BODY);
    assert_eq!(
        alloc_area(&state.borrow(), RAIL),
        0.0,
        "a maximise allots the rail ZERO area: {}",
        state.borrow()["panes"][RAIL]["alloc"]
    );
    let body3 = alloc_w(&state.borrow(), BODY);
    assert!(
        body3 > span0 - 8.0,
        "the maximised body fills the shell: {body3} (the split was {span0})"
    );

    // ── RESTORE the maximise: the master/detail layout comes back unchanged ──────
    h.get_by_label(&format!("restore {BODY}")).click();
    h.run();
    h.run();
    let (rail4, body4) = (
        alloc_w(&state.borrow(), RAIL),
        alloc_w(&state.borrow(), BODY),
    );
    let unmax_ok = nav.borrow().maximized_pane().is_none()
        && (rail4 - rail0).abs() < 1.0
        && (body4 - body0).abs() < 1.0;
    assert!(unmax_ok, "restoring the maximise returns the EXACT master/detail layout: rail {rail0} -> {rail4}, body {body0} -> {body4}");

    let ok = spec_ok && min_state_ok && restore_ok && max_state_ok && unmax_ok;
    matrix(
        "scene_chrome_minimise_maximise_restore_rects",
        ok,
        "clicking a pane's WINDOW-CHROME controls (minimise / maximise / restore, addressed by their AccessKit labels) moves real GEOMETRY: minimising the rail collapses it to exactly CHROME_BAR points and hands the width to the body, maximising the body allots the rail ZERO area, and each restore returns the EXACT prior rects (chrome never overwrites the split ratios) — kittest render",
    );
}

/// ROBOT SCENARIO — **every chromed pane** minimises and restores over the shipped
/// `{"toggle_minimize":…}` / `{"restore":…}` JSON seam (the same state the title-bar
/// control mutates), plus the `{"restore_all":true}` reset. One pane's chrome working is
/// a demo; the whole roster working is the feature. RED-when-broken: a pane silently
/// opted out of chrome, or a reset that misses a minimise.
#[test]
fn robot_every_chromed_pane_minimises_and_restores() {
    let mut app = offline_app();
    let chrome = app.chrome_panes();
    // The rail + the workspace + every shipped tab — counted off the roster, not a literal.
    assert!(
        chrome.len() >= DWARVES_TABS.len() + 2,
        "the chrome roster covers the rail, the workspace and all {} tabs: {chrome:?}",
        DWARVES_TABS.len()
    );
    assert!(
        !chrome.contains(&ROOT.to_string()),
        "the scene root stays bare: {chrome:?}"
    );
    for key in tab_keys() {
        assert!(chrome.contains(&key), "the `{key}` tab pane wears chrome");
    }

    let mut all_ok = true;
    for pane in &chrome {
        let open0 = Facet::state_json(&app)["minimized"][pane] == json!(false);
        Facet::update_json(&mut app, &json!({ "toggle_minimize": pane }).to_string());
        let minimized = Facet::state_json(&app)["minimized"][pane] == json!(true);
        Facet::update_json(&mut app, &json!({ "restore": pane }).to_string());
        let restored = Facet::state_json(&app)["minimized"][pane] == json!(false);

        let pane_ok = open0 && minimized && restored;
        assert!(
            pane_ok,
            "chromed pane `{pane}` minimises + restores over the JSON seam"
        );
        all_ok &= pane_ok;
    }

    // Maximise is exclusive: claiming it for a second pane releases the first.
    Facet::update_json(&mut app, &json!({ "toggle_maximize": RAIL }).to_string());
    Facet::update_json(&mut app, &json!({ "toggle_maximize": BODY }).to_string());
    let exclusive_ok = Facet::state_json(&app)["maximized"] == json!(BODY);
    assert!(
        exclusive_ok,
        "at most ONE pane is maximised: {}",
        Facet::state_json(&app)["maximized"]
    );

    // `restore_all` is the reset: clear the maximise and every minimise in one call.
    Facet::update_json(&mut app, &json!({ "toggle_minimize": "forge" }).to_string());
    Facet::update_json(&mut app, &json!({ "restore_all": true }).to_string());
    let s = Facet::state_json(&app);
    let reset_ok = s["maximized"] == Value::Null
        && !s["minimized"]
            .as_object()
            .expect("the minimise roster")
            .values()
            .any(|v| v == &json!(true));
    assert!(
        reset_ok,
        "restore_all clears every minimise and the maximise: {}",
        s["minimized"]
    );

    let ok = all_ok && exclusive_ok && reset_ok;
    matrix(
        "scene_every_chromed_pane_minimises_and_restores",
        ok,
        &format!(
            "every one of the chromed panes (the drawer rail + the workspace + all {} shipped tabs — the roster read off the scene, never a copied list) minimises and restores over the {{\"toggle_minimize\"/\"restore\"}} JSON seam, maximise stays exclusive to ONE pane, and {{\"restore_all\":true}} resets the whole layout",
            DWARVES_TABS.len()
        ),
    );
}

// ── 4. DRAWER rail auto-folds crossing the phone boundary (resize the viewport) ─

/// ROBOT SCENARIO (kittest render) — the DRAWER rail: resize the harness viewport across
/// the 760pt phone boundary and assert the rail auto-folds crossing INTO phone-class and
/// restores crossing back out (desktop rail → phone drawer), driven by the SAME
/// `scene::render` device-class transition the app uses. Also asserts the menubar renders.
/// RED-when-broken: drop `.drawer(760)` and the rail won't fold on the narrow viewport.
#[test]
fn robot_render_drawer_rail_autofolds_crossing_phone_class() {
    let scene = dwarves_scene(&labels());
    let nav = Rc::new(RefCell::new(NavState::new(&scene)));
    let state = Rc::new(RefCell::new(Value::Null));
    let (s, n, sc) = (state.clone(), nav.clone(), scene.clone());

    // Start WIDE (desktop): the rail is open, the menubar renders.
    let mut h = Harness::builder()
        .with_size(egui::vec2(1150.0, 800.0))
        .build_ui(move |ui| {
            *s.borrow_mut() = scene::render(&sc, &mut n.borrow_mut(), ui, &mut TestHost);
        });
    h.run();
    let desktop_ok = state.borrow()["device"] == json!("desktop")
        && state.borrow()["folded"][RAIL] == json!(false);
    let menubar_ok = h.query_by_label("View").is_some()
        && h.query_by_label("Go").is_some()
        && h.query_by_label("Help").is_some();

    // Resize NARROW, crossing below 760pt → phone-class: the drawer rail auto-folds.
    h.set_size(egui::vec2(700.0, 800.0));
    h.run();
    let phone_ok =
        state.borrow()["device"] == json!("phone") && state.borrow()["folded"][RAIL] == json!(true);

    // Resize WIDE again → back to desktop: the drawer rail auto-restores (open).
    h.set_size(egui::vec2(1150.0, 800.0));
    h.run();
    let restored_ok = state.borrow()["device"] == json!("desktop")
        && state.borrow()["folded"][RAIL] == json!(false);

    let ok = desktop_ok && menubar_ok && phone_ok && restored_ok;
    assert!(
        ok,
        "the drawer rail auto-folds crossing into phone-class and restores above it: desktop={desktop_ok} menubar={menubar_ok} phone={phone_ok} restored={restored_ok} state={}",
        state.borrow()
    );

    matrix(
        "scene_drawer_rail_autofolds_on_phone",
        ok,
        "resizing the kittest viewport across the 760pt boundary auto-folds the DRAWER rail entering phone-class and restores it leaving (desktop rail → phone drawer), plus the View/Go/Help menubar renders — driven through scene::render",
    );
}

// ── 5. the menubar + every declared route ──────────────────────────────────────

/// ROBOT SCENARIO — the declarative menubar + all 3 declared routes. Asserts the menubar
/// DATA (View toggles EVERY shipped tab; Go switches each with a Ctrl+N shortcut;
/// Help → About with F1), then FIRES each route over the shared Router and asserts its
/// effect (event→fold the rail, button→focus the first tab, shortcut→show it).
/// RED-when-broken: drop a route/menu and the fire/structure assertion fails.
#[test]
fn robot_menubar_routes_and_menu_actions_fire() {
    let scene = dwarves_scene(&labels());
    let lbls = labels();

    // ── menubar structure (View · Go · Mímir · Help). ──
    assert_eq!(scene.menubar.len(), 4, "View / Go / Mímir / Help");
    let view = scene.menu_item("view").expect("View menu");
    let go = scene.menu_item("go").expect("Go menu");
    let _mimir = scene.menu_item("mimir").expect("Mímir menu");
    let help = scene.menu_item("help").expect("Help menu");

    let keys = tab_keys();
    let view_ok = view.items.len() == lbls.len()
        && view
            .items
            .iter()
            .zip(&keys)
            .all(|(mi, k)| mi.action == Some(Action::Toggle(k.clone())));
    // Every tab has a Go command; the first ten carry a REAL digit accelerator and the
    // rest honestly carry none. (The shell used to advertise `Ctrl+{i+1}` for all of
    // them — `Ctrl+13`, `Ctrl+20` … — and bind none; see
    // `robot_every_tab_is_reachable_without_the_tab_strip`.)
    let go_ok = go.items.len() == lbls.len()
        && go.items.iter().enumerate().zip(&keys).all(|((i, mi), k)| {
            let want = (i < 9).then(|| format!("Ctrl+{}", i + 1));
            mi.shortcut == want && mi.action == Some(Action::Command(format!("go:{k}")))
        });
    let help_ok = help.items.len() == 1
        && help.items[0].shortcut == Some("F1".into())
        && help.items[0].action == Some(Action::Command("help.about".into()));

    // ── all 3 declared routes fire over the shared Router. ──
    let mut nav = NavState::new(&scene);
    assert!(!nav.is_folded(RAIL), "the rail starts open");
    let event_ok = nav.fire(&scene, &Trigger::Event("toggle-rail".into())) && nav.is_folded(RAIL);

    let mut nav_b = NavState::new(&scene);
    let button_ok = nav_b.fire(&scene, &Trigger::Button("focus-warehouse".into()))
        && nav_b.router.current() == "tree"
        && nav_b.focus.as_deref() == Some("tree");

    let mut nav_s = NavState::new(&scene);
    let shortcut_ok = nav_s.fire(&scene, &Trigger::Shortcut("Ctrl+Home".into()))
        && nav_s.router.current() == "tree";

    // An undeclared trigger matches nothing (no accidental route).
    let none_ok = !nav_s.fire(&scene, &Trigger::Event("no-such-event".into()));

    let ok = view_ok && go_ok && help_ok && event_ok && button_ok && shortcut_ok && none_ok;
    assert!(ok, "the menubar data + all 3 declared routes: view={view_ok} go={go_ok} help={help_ok} event={event_ok} button={button_ok} shortcut={shortcut_ok}");

    matrix(
        "scene_menubar_and_routes_fire",
        ok,
        &format!(
            "the declarative menubar (View toggles each of the {} shipped tabs · Go switches each with Ctrl+N · Help→About F1) is well-formed and all 3 declared routes fire over the shared Router (event→fold rail, button→focus first tab, shortcut→show it), with no accidental match",
            DWARVES_TABS.len()
        ),
    );
}

// ── 6. deep-link by pane name ──────────────────────────────────────────────────

/// ROBOT SCENARIO — deep-link into the scene by unique pane NAME (`app#pane` / `#pane`),
/// the addressability invariant unique pane names buy. Every real pane resolves and moves
/// the Router; an unknown name is rejected and leaves the Router put.
/// RED-when-broken: a duplicate/renamed pane breaks addressing.
#[test]
fn robot_deep_link_by_pane_name() {
    let scene = dwarves_scene(&labels());
    let mut nav = NavState::new(&scene);

    // EVERY shipped content pane (read off the roster) + the structural panes is
    // deep-linkable by name — the addressability invariant now covers the whole shell.
    let mut all_hit = true;
    let panes: Vec<String> = tab_keys()
        .into_iter()
        .chain([RAIL.to_string(), BODY.to_string(), ROOT.to_string()])
        .collect();
    for pane in &panes {
        let hit = nav.deep_link(&scene, &format!("app#{pane}")) && nav.router.current() == pane;
        assert!(hit, "deep-link #{pane} resolves + moves the router");
        all_hit &= hit;
    }
    // The bare `#pane` form (no app prefix) also resolves.
    let bare_ok = nav.deep_link(&scene, "#forge") && nav.router.current() == "forge";

    // An unknown pane is rejected and the router stays put.
    let before = nav.router.current().to_string();
    let reject_ok = !nav.deep_link(&scene, "#does-not-exist") && nav.router.current() == before;

    let ok = all_hit && bare_ok && reject_ok;
    assert!(
        ok,
        "deep-linking by pane name resolves every pane + rejects unknowns"
    );

    matrix(
        "scene_deep_link_by_pane_name",
        ok,
        &format!(
            "deep-linking into the scene by unique pane name (app#pane and #pane) resolves every one of the {} shipped content panes + the structural rail/body/shell and moves the Router, while an unknown pane is rejected leaving the Router put",
            DWARVES_TABS.len()
        ),
    );
}

// Keep the imports honest across feature flags: `Layout`/`Fold` document the scene shape
// the render tests rely on (draggable Split + Foldable drawer) — assert it once, cheaply.
#[test]
fn scene_shape_is_draggable_split_over_a_foldable_drawer() {
    let scene = dwarves_scene(&labels());
    let root = scene.pane(ROOT).expect("root shell pane");
    assert!(
        matches!(
            root.layout,
            Layout::Split {
                resizable: true,
                ..
            }
        ),
        "root is a draggable split"
    );
    let rail = scene.pane(RAIL).expect("rail pane");
    assert!(
        matches!(rail.fold, Fold::Foldable { .. }),
        "the rail is foldable"
    );
    assert!(rail.responsive.drawer, "the rail is a drawer");
}

// ── 9. the 📐 Coverage pane: drive it, and assert the INDEX it computes ────────

/// ROBOT SCENARIO — drive the shipped shell to the **Coverage** tab and switch its
/// views over the same `update_json` seam the wire-remote fleet uses, asserting the
/// pane's own `/coverage/view` oracle follows.
///
/// The pane also has to be honest before it has read anything: it opens UN-scanned and
/// its status line SAYS so. An empty index that renders as a clean bill of health is
/// the precise failure this surface exists to end, so that is asserted here as a
/// result, not left as a comment.
///
/// RED-when-broken: break the view switch, or let the pane open claiming a coverage
/// figure it never computed, and this goes red.
#[test]
fn robot_coverage_pane_switches_views_and_opens_honestly_unscanned() {
    let mut app = offline_app();
    Facet::update_json(&mut app, &json!({ "set_tab": "Coverage" }).to_string());
    let s = Facet::state_json(&app);
    assert_eq!(s["active_tab"], json!("Coverage"));

    // Honest-before-reading: no scan, and the status line states that rather than a
    // number. (The shipped pane binds to a nornir-server that is not up in this test,
    // which is exactly the condition under which a dishonest pane would show zeros.)
    let opens_unscanned = s["coverage"]["scanned"] == json!(false);
    let status = s["coverage"]["status"]
        .as_str()
        .unwrap_or_default()
        .to_string();
    // The pane's own honest opening line — `coverage.rs` prints "waiting for the
    // warehouse generation — no reading yet" since 1aa8f0f (2026-08-25, SCAN stopped
    // being a user action), and its in-crate test pins the same "no reading yet". This
    // robot accepted only the two phrasings from before that change, so a pane that was
    // being honest read as red here for two weeks. What matters is the CLAIM: no
    // reading, said in words, and no figure standing where one would be.
    let says_so = status.contains("no reading yet")
        || status.contains("not scanned")
        || status.starts_with("UNAVAILABLE");
    assert!(
        opens_unscanned,
        "the coverage pane opens un-scanned: {}",
        s["coverage"]
    );
    assert!(
        says_so,
        "…and its status line says so rather than showing a figure: {status}"
    );
    assert!(
        !status.contains('%'),
        "an un-scanned pane must not put a figure in its status line: {status}"
    );

    // Real interaction: every view is reachable and the oracle follows each switch.
    // The shell forwards an unrecognised message VERBATIM to the ACTIVE pane's own JSON
    // seam, so this is the exact wire the fleet's `RobotDriveMember` drives.
    let mut views_ok = true;
    for view in ["integrity", "weak", "feeds", "repos"] {
        Facet::update_json(&mut app, &json!({ "view": view }).to_string());
        let s = Facet::state_json(&app);
        let ok = s["coverage"]["view"] == json!(view);
        assert!(
            ok,
            "the `{view}` view is reachable over the pane seam: {}",
            s["coverage"]
        );
        views_ok &= ok;
    }

    matrix(
        "coverage_pane_switches_views_and_opens_honestly_unscanned",
        opens_unscanned && says_so && views_ok,
        &format!(
            "the 📐 Coverage pane opens UN-scanned with a status line that says so ({status}) \
             instead of rendering an empty warehouse as a clean bill of health; its four views \
             (repos/integrity/weak/feeds) are addressable over the same update_json seam the \
             wire-remote fleet drives (view-switch seam reachable: {views_ok})"
        ),
    );
}

/// ROBOT SCENARIO — the coverage ENGINE's result, asserted through the pane over a
/// deterministic fixture (no server, no warehouse).
///
/// This is the cell that asserts a RESULT rather than a literal: over a fixture whose
/// shape is the real 2026-07-22 reading, the pane must report korp as **NEVER BENCHED**
/// and bifrost as **NOT SCANNED** by name, must list `test_results` / `coverage_fn` as
/// **MISSING FEEDS**, must keep `oden` / `oden-preview` / `ci` as **three separate host
/// tables**, and must count exactly one covered function against three uncovered ones.
///
/// RED-when-broken: merge two hosts into one table, let an empty feed read as no gaps,
/// or let an unscanned repo report zero gaps, and this goes red.
#[test]
fn robot_coverage_index_reports_named_gaps_not_a_clean_board() {
    use dwarves_gui::coverage::CoveragePane;

    let mut pane = CoveragePane::fixture();
    pane.update_json("\"scan\"");
    let s = pane.state_json();
    let idx = &s["index"];

    let denominator_ok = idx["repos_total"] == json!(3) && idx["repos_scanned"] == json!(2);
    let never: Vec<String> =
        serde_json::from_value(idx["bench"]["never_benched_repos"].clone()).unwrap_or_default();
    let named_ok = never.contains(&"korp".to_string()) && never.contains(&"bifrost".to_string());
    let missing: Vec<String> =
        serde_json::from_value(idx["missing_feeds"].clone()).unwrap_or_default();
    let feeds_ok = missing.contains(&"test_results".to_string())
        && missing.contains(&"coverage_fn".to_string());
    let hosts_ok = idx["integrity"]["host_tables"] == json!(3);
    let cover_ok = idx["test"]["covered"] == json!(1) && idx["test"]["missing"] == json!(3);

    assert!(
        denominator_ok,
        "the roster is the denominator, unscanned repos included: {idx}"
    );
    assert!(
        named_ok,
        "korp + bifrost are NAMED as never benched: {never:?}"
    );
    assert!(
        feeds_ok,
        "the empty tables are MISSING FEEDS, not silence: {missing:?}"
    );
    assert!(
        hosts_ok,
        "oden / oden-preview / ci stay three tables, never merged: {}",
        idx["integrity"]
    );
    assert!(
        cover_ok,
        "one reachable fn, three unreachable: {}",
        idx["test"]
    );

    matrix(
        "coverage_index_reports_named_gaps_not_a_clean_board",
        denominator_ok && named_ok && feeds_ok && hosts_ok && cover_ok,
        &format!(
            "the coverage index over a deterministic fixture reports {}/{} repos scanned, NAMES \
             {} as never-benched, lists {} as MISSING FEEDS (empty table != no gaps), keeps the \
             oden/oden-preview/ci measurements in {} separate host tables (a 12-core and a \
             32-core number are never merged), and computes {} covered vs {} uncovered functions \
             — every assertion a computed RESULT, none a literal true",
            idx["repos_scanned"],
            idx["repos_total"],
            never.join("+"),
            missing.join("+"),
            idx["integrity"]["host_tables"],
            idx["test"]["covered"],
            idx["test"]["missing"],
        ),
    );
}

// ── 9. the tab INTERNALS: a decomposed body, proved with RECTS ──────────────────

/// A rendered pane's allotted extent along a split axis (`true` = x / width).
fn alloc_extent(state: &Value, pane: &str, along_x: bool) -> f32 {
    let (_, _, w, h) = pane_alloc(state, pane);
    if along_x {
        w
    } else {
        h
    }
}

/// ROBOT SCENARIO (kittest render) — **every tab body that DECOMPOSED is a real window
/// pair: drag · maximise · restore · minimise, all asserted as RECTS.**
///
/// dwarves' SHELL was already the constellation's reference — a draggable master/detail
/// root, a drawer rail, foldable panes. Its tab BODIES were not: each was one `Slot`
/// holding one imperative egui flow, so minimise / maximise / drag operated on a whole
/// tab and could do NOTHING inside it. A readout that grew (Status' badge SVG, Jera's
/// live-poll banner) came straight out of the list it shared the flow with, and the
/// operator had no divider to correct it.
///
/// For EVERY body the shell reports under `split_bodies` (read off `state_json`, so a
/// body decomposed later is swept without touching this test):
///
/// 1. **drag** its own divider → the master's allotted extent grows by ~the drag, the
///    inspector's shrinks by the same, and **their sum is conserved**;
/// 2. **maximise** the master → the inspector's allotted **area is exactly 0**;
/// 3. **restore** → both halves return to EXACTLY their prior rects (chrome never writes
///    the split ratios, so restore is lossless — not a reset to the seed);
/// 4. **minimise** the inspector → it collapses to exactly `CHROME_BAR` along the axis
///    and the master takes the space back.
///
/// RED-when-broken the moment a body is downgraded back to a single `Slot`, or its split
/// loses `resizable`, or a restore starts resetting to the seed.
#[test]
fn robot_render_every_decomposed_tab_body_drags_maximises_and_restores_by_rect() {
    // The decomposition roster comes from the SHIPPED shell, not a copied list.
    let bodies: Vec<String> = {
        let app = offline_app();
        Facet::state_json(&app)["split_bodies"]
            .as_array()
            .expect("the shell reports which tab bodies decomposed")
            .iter()
            .map(|v| v.as_str().unwrap_or_default().to_string())
            .collect()
    };
    assert!(
        !bodies.is_empty(),
        "at least one tab body decomposed into its own draggable master/detail split"
    );

    let mut all_ok = true;
    for body in &bodies {
        let (main, insp) = (format!("{body}-main"), format!("{body}-inspector"));

        // Render the SHIPPED scene value, with this body's tab made active (a `Tabs`
        // layout only lays out its ACTIVE child).
        let scene = dwarves_scene(&labels());
        let idx = tab_keys()
            .iter()
            .position(|k| k == body)
            .expect("a shipped tab");
        let nav = Rc::new(RefCell::new(NavState::new(&scene)));
        nav.borrow_mut().set_active_tab(BODY, idx);
        let state = Rc::new(RefCell::new(Value::Null));
        let (s, n, sc) = (state.clone(), nav.clone(), scene.clone());
        let mut h = Harness::builder()
            .with_size(egui::vec2(1400.0, 900.0))
            .build_ui(move |ui| {
                *s.borrow_mut() = scene::render(&sc, &mut n.borrow_mut(), ui, &mut TestHost);
            });
        h.run();

        let resizable_ok = state.borrow()["panes"][body]["resizable"] == json!(true);
        assert!(
            resizable_ok,
            "`{body}` declares a DRAGGABLE divider INSIDE the tab: {}",
            state.borrow()["panes"][body]
        );

        // BOTH halves are in the render tree, each filling its own slot.
        let slots_ok = state.borrow()["slots"].get(&main).is_some()
            && state.borrow()["slots"].get(&insp).is_some();
        assert!(
            slots_ok,
            "`{body}`'s two halves each fill their own slot: {}",
            state.borrow()["slots"]
        );

        // Which way does this body divide? The handle rect says so: a HORIZONTAL split's
        // handle is a tall thin bar (drag along x), a vertical one's is a wide flat bar.
        let (sep_w, sep_h) = {
            let sep = &state.borrow()["panes"][body]["separators"][0];
            (
                sep[2].as_f64().expect("a handle rect") as f32,
                sep[3].as_f64().unwrap() as f32,
            )
        };
        let along_x = sep_w < sep_h;
        let d = 150.0_f32;

        let (m0, i0) = (
            alloc_extent(&state.borrow(), &main, along_x),
            alloc_extent(&state.borrow(), &insp, along_x),
        );
        let span0 = m0 + i0;
        assert!(
            m0 > 40.0 && i0 > 40.0,
            "`{body}` has TWO live halves: main={m0} inspector={i0}"
        );

        // ── 1. drag: opposite moves, conserved sum ─────────────────────────────────
        let from = sep_center(&state.borrow(), body);
        let to = if along_x {
            egui::pos2(from.x + d, from.y)
        } else {
            egui::pos2(from.x, from.y + d)
        };
        h.event(egui::Event::PointerMoved(from));
        h.drag_at(from);
        h.event(egui::Event::PointerMoved(to));
        h.run();
        h.drop_at(to);
        h.run();

        let (m1, i1) = (
            alloc_extent(&state.borrow(), &main, along_x),
            alloc_extent(&state.borrow(), &insp, along_x),
        );
        assert!(
            (m1 - m0 - d).abs() < 26.0,
            "dragging `{body}`'s divider {d}pt grew `{main}` by ~{d}: {m0} -> {m1}"
        );
        assert!(
            (i0 - i1 - d).abs() < 26.0,
            "…and shrank `{insp}` by the same ~{d}: {i0} -> {i1}"
        );
        assert!(
            ((m1 + i1) - span0).abs() < 8.0,
            "`{body}` conserves its extent: {span0} -> {}",
            m1 + i1
        );

        // ── 2/3. maximise a half → sibling ZERO; restore → EXACT prior rects ───────
        let (rm, ri) = (
            pane_alloc(&state.borrow(), &main),
            pane_alloc(&state.borrow(), &insp),
        );
        nav.borrow_mut().set_maximized(&main);
        h.run();
        let hidden_ok = alloc_area(&state.borrow(), &insp) == 0.0;
        assert!(
            hidden_ok,
            "maximising `{main}` drove its sibling `{insp}` to ZERO area: {}",
            state.borrow()["panes"][&insp]
        );

        nav.borrow_mut().restore(&main);
        h.run();
        for (pane, was) in [(&main, rm), (&insp, ri)] {
            let now = pane_alloc(&state.borrow(), pane);
            assert!(
                (now.0 - was.0).abs() < 0.5
                    && (now.1 - was.1).abs() < 0.5
                    && (now.2 - was.2).abs() < 0.5
                    && (now.3 - was.3).abs() < 0.5,
                "restoring returned `{pane}` to EXACTLY its prior rect: {was:?} -> {now:?}"
            );
        }

        // ── 4. minimise the inspector → only its chrome bar is left ────────────────
        nav.borrow_mut().set_minimized(&insp, true);
        h.run();
        let mini = alloc_extent(&state.borrow(), &insp, along_x);
        assert!(
            (mini - CHROME_BAR).abs() < 1.0,
            "minimising `{insp}` left exactly its {CHROME_BAR}pt chrome bar: {mini}"
        );
        assert!(
            alloc_extent(&state.borrow(), &main, along_x) > m0,
            "…and handed the space to `{main}`"
        );
        nav.borrow_mut().restore(&insp);
        h.run();

        // ── 5. the fold reaches INSIDE the tab: folding one half drops ONLY its own
        // slot from the tree; its sibling keeps drawing. ──────────────────────────────
        nav.borrow_mut().set_fold(&insp, true);
        h.run();
        let fold_inside = state.borrow()["slots"].get(&insp).is_none()
            && state.borrow()["slots"].get(&main).is_some();
        assert!(
            fold_inside,
            "folding `{insp}` drops ONLY its own slot — its sibling keeps drawing: {}",
            state.borrow()["slots"]
        );
        nav.borrow_mut().set_fold(&insp, false);
        h.run();

        all_ok &= resizable_ok && slots_ok && hidden_ok && fold_inside;
    }

    matrix(
        "scene_tab_internals_drag_maximise_restore_by_rect",
        all_ok,
        &format!(
            "every tab BODY that decomposed ({}) is a real draggable master/detail window pair INSIDE the tab: a pointer drag on its own divider moves both halves oppositely with the sum conserved, maximising one drives its sibling's allotted area to exactly 0, restoring returns both to their EXACT prior rects, and minimising the inspector leaves exactly its CHROME_BAR title strip — the shell was a scene while the tab internals were one imperative flow, and this is the geometry that proves they no longer are",
            bodies.join("/"),
        ),
    );
}

/// ROBOT SCENARIO — **both halves of a decomposed body are live, addressable scene panes
/// over the remote seam**: each is declared, foldable and chromed, each carries its own
/// slot state, and folding ONE leaves its sibling drawing (the fold reaches INSIDE the
/// tab, which is the whole point).
///
/// RED-when-broken: collapse a body back to one pane and its halves vanish from `panes`
/// / `foldable`; break the host's `<slug>-main` / `<slug>-inspector` dispatch and the
/// slot states stop appearing.
#[test]
fn robot_decomposed_body_halves_are_addressable_and_fold_independently() {
    let mut app = offline_app();
    let st = Facet::state_json(&app);
    let bodies: Vec<String> = st["split_bodies"]
        .as_array()
        .expect("the shell reports which tab bodies decomposed")
        .iter()
        .map(|v| v.as_str().unwrap_or_default().to_string())
        .collect();
    // The THREE rosters PARTITION the strip — no tab is two of them, none is neither.
    // (`leaf_bodies` is the third body shape: a body that decomposes into N NAMED
    // LEAVES, ⚒ Build Thing's five. Its own drive is `tests/robot_five_leaf_body.rs`;
    // what belongs here is only that it is counted, so a tab cannot slip out of every
    // roster at once.)
    let singles = st["single_pane_bodies"]
        .as_array()
        .expect("the single-pane roster")
        .len();
    let leaved = st["leaf_bodies"]
        .as_array()
        .expect("the N-leaf roster")
        .len();
    assert_eq!(
        bodies.len() + leaved + singles,
        DWARVES_TABS.len(),
        "every shipped tab is decomposed, leaved, or honestly single-pane: {st}"
    );

    let mut all_ok = true;
    for body in &bodies {
        let (main, insp) = (format!("{body}-main"), format!("{body}-inspector"));
        // Make it the drawn tab over the SAME seam a thin client uses.
        let label = DWARVES_TABS
            .iter()
            .find(|(k, _)| k == body)
            .map(|(_, l)| *l)
            .expect("a shipped tab");
        Facet::update_json(&mut app, &json!({ "set_tab": label }).to_string());
        let s = Facet::state_json(&app);

        // Declared, foldable, chromed — both halves.
        let declared = [&main, &insp].iter().all(|p| {
            s["panes"].as_array().unwrap().iter().any(|n| n == *p)
                && s["foldable"].as_array().unwrap().iter().any(|n| n == *p)
                && s["chrome"].as_array().unwrap().iter().any(|n| n == *p)
                && s["folded"].get(p.as_str()).is_some()
        });
        assert!(
            declared,
            "`{body}`'s halves are declared, foldable, chromed scene panes: {s}"
        );

        // The body's own divider is in the roster (an INTERNAL split, not the shell's).
        let split_ok = s["draggable_splits"]
            .as_array()
            .unwrap()
            .iter()
            .any(|n| n == body)
            && s["draggable_splits"].as_array().unwrap()[0] == json!(ROOT);
        assert!(
            split_ok,
            "`{body}` reports its own divider after the shell's: {s}"
        );

        // The halves fold INDEPENDENTLY of each other and of the body — the fold reaches
        // INSIDE the tab, which is the whole point. (That the folded half's slot actually
        // leaves the render tree is asserted with real geometry in the kittest cell above;
        // this seam has no render.)
        Facet::update_json(&mut app, &json!({ "toggle_fold": insp }).to_string());
        let s = Facet::state_json(&app);
        let inside = s["folded"][&insp] == json!(true)
            && s["folded"][&main] == json!(false)
            && s["folded"][body] == json!(false);
        assert!(
            inside,
            "folding `{insp}` folds ONLY its own half — not its sibling, not the whole tab: {s}"
        );
        Facet::update_json(&mut app, &json!({ "toggle_fold": insp }).to_string());

        all_ok &= declared && split_ok && inside;
    }

    matrix(
        "scene_tab_internals_halves_addressable_and_fold_independently",
        all_ok,
        &format!(
            "both halves of every decomposed tab body ({}) are declared, foldable, chromed scene panes carrying their own slot state over the state_json/update_json seam, and folding one leaves its sibling drawing — the fold/minimise/maximise vocabulary finally reaches INSIDE a tab",
            bodies.join("/"),
        ),
    );
}

// ── 10. every tab is reachable WITHOUT the tab strip ───────────────────────────

/// ROBOT SCENARIO — **every shipped tab is reachable by a path that does not depend on
/// how wide the tab strip got, and every accelerator the shell advertises is a real key.**
///
/// facett-app's `TabView` draws its headers in a plain `ui.horizontal`: it neither wraps
/// nor scrolls. With 20 tabs — each now wearing a chrome title bar, which widened every
/// header — the strip needs roughly 1300pt while the workspace pane gets 80% of the
/// window, so on any window narrower than ~1650pt the LAST headers lay out past the
/// pane's right edge, are clipped, and drop out of the AccessKit tree: unreachable by
/// mouse AND by robot. (`robot_remote.rs` widens its headless canvas to 2400pt, which
/// hides that from the tests without fixing it for a human. The real fix is upstream —
/// facett patch #1 in `tabbed.rs`'s module docs.)
///
/// The shell must therefore carry a width-independent route to every tab, and it does:
/// the **Go** menu, whose menubar has three groups and always fits. This asserts it:
///
/// * every shipped tab has exactly one `go:<slug>` command, and firing it lands that tab;
/// * the advertised accelerators are UNIQUE and are REAL keys — the shell used to promise
///   `Ctrl+1`…`Ctrl+20` (there are ten digits) and bind NOTHING at all, because
///   `facett_menu::Command::shortcut` is a display hint no key handler ever read;
/// * the first ten are now genuinely BOUND: a real `Ctrl+<n>` key event switches the tab.
#[test]
fn robot_every_tab_is_reachable_without_the_tab_strip() {
    let scene = dwarves_scene(&labels());

    // (1) one `go:` command per shipped tab, in strip order — no tab is menu-orphaned.
    let go = scene
        .menubar
        .iter()
        .find(|m| m.name == "go")
        .expect("the Go menu exists");
    let ids: Vec<String> = go.items.iter().map(|i| i.name.clone()).collect();
    let expected: Vec<String> = tab_keys().iter().map(|k| format!("go.{k}")).collect();
    assert_eq!(
        ids, expected,
        "one Go entry per shipped tab, in strip order"
    );
    for k in tab_keys() {
        assert_eq!(
            scene
                .menu_item(&format!("go.{k}"))
                .and_then(|i| i.action.clone()),
            Some(Action::Command(format!("go:{k}"))),
            "Go · {k} routes the shared `go:{k}` command"
        );
    }

    // (2) the advertised accelerators are unique AND real keys (`Ctrl+0`…`Ctrl+9` only).
    let mut seen: Vec<(String, String)> = Vec::new();
    for item in &go.items {
        let Some(keys) = item.shortcut.as_deref() else {
            continue;
        };
        assert!(
            matches!(
                keys,
                "Ctrl+1"
                    | "Ctrl+2"
                    | "Ctrl+3"
                    | "Ctrl+4"
                    | "Ctrl+5"
                    | "Ctrl+6"
                    | "Ctrl+7"
                    | "Ctrl+8"
                    | "Ctrl+9"
            ),
            "`{keys}` (advertised for `{}`) is a key that exists AND is ours — `Ctrl+0` \
             belongs to egui's zoom reset, so it is deliberately not offered",
            item.name
        );
        if let Some((other, _)) = seen.iter().find(|(_, k)| k == keys) {
            panic!(
                "the Go menu advertises `{keys}` for BOTH `{other}` and `{}`",
                item.name
            );
        }
        seen.push((item.name.clone(), keys.to_string()));
    }
    assert_eq!(
        seen.len(),
        tab_keys().len().min(9),
        "Ctrl+1..Ctrl+9 carry an accelerator and nothing past them does"
    );

    // (3) firing the command lands the tab — the width-independent path, over the SAME
    // seam the menu click routes into.
    let mut app = offline_app();
    let mut fired_ok = true;
    for (key, label) in DWARVES_TABS.iter() {
        Facet::update_json(&mut app, &json!({ "set_tab": key }).to_string());
        fired_ok &= app.active_tab() == *label;
        assert!(
            fired_ok,
            "tab `{label}` is reachable by id, without touching the strip"
        );
    }

    // (4) the first ten accelerators are genuinely BOUND: a real Ctrl+<n> key event
    // switches the tab. RED-when-broken the moment the binding is dropped and the menu
    // goes back to advertising keys nothing reads.
    let app = Rc::new(RefCell::new(offline_app()));
    let a = app.clone();
    let mut h = Harness::builder()
        .with_size(egui::vec2(2400.0, 1000.0))
        .build_ui(move |ui| Facet::ui(&mut *a.borrow_mut(), ui));
    // `step`, not `run`: several panes (the pipeline DAG among them) request a repaint
    // every frame, so `Harness::run` — which spins until the UI stops asking — hits its
    // 4-step ceiling and panics. One frame per key event is what a key needs anyway.
    h.step();
    let mut bound_ok = true;
    for (i, key) in [
        (0usize, egui::Key::Num1),
        (2, egui::Key::Num3),
        (8, egui::Key::Num9),
    ] {
        h.input_mut().events.push(egui::Event::Key {
            key,
            physical_key: None,
            pressed: true,
            repeat: false,
            modifiers: egui::Modifiers::COMMAND,
        });
        h.step();
        let want = DWARVES_TABS[i].1;
        let got = app.borrow().active_tab().to_string();
        bound_ok &= got == want;
        assert_eq!(
            got,
            want,
            "Ctrl+{} actually selects tab {i} ({want})",
            i + 1
        );
    }

    matrix(
        "scene_every_tab_reachable_without_the_strip",
        fired_ok && bound_ok,
        &format!(
            "all {} shipped tabs are reachable by a path that does not depend on the tab strip's width — one `go:<slug>` command each in the Go menu (whose menubar always fits), with UNIQUE, REAL, genuinely BOUND `Ctrl+1`..`Ctrl+9` accelerators for the first nine (`Ctrl+0` is egui's zoom reset, so it is not claimed); the shell previously advertised `Ctrl+1`..`Ctrl+20` and bound none of them, while facett-app's TabView strip neither wraps nor scrolls so the last headers fall out of the AccessKit tree on a normal window",
            DWARVES_TABS.len(),
        ),
    );
}

// ── 10. the 🔍 Lenses pane: the three Mimir lenses, and where they belong ──────

/// ROBOT SCENARIO — the three Mimir lenses' RESULTS, asserted through the pane over a
/// deterministic fixture (no server, no warehouse).
///
/// Every assertion is a number the engine computed:
/// * **bloat** — a generic instantiated 3× for 60000 bytes taxes 40000, and only that
///   NAMED generic counts as actionable while the bigger drop-glue group does not.
/// * **zombie** — `korp/spark41` is named never-built, and `knut/grpc` is NOT flagged
///   because knut declares no build space (the false-positive guard: an earlier draft
///   called every optional feature a zombie, which would have been 30-of-30 noise).
/// * **risk** — the three inputs ride WITH the score, and Tornhill's citation rides in
///   the data so no renderer can drop it.
///
/// RED-when-broken: collapse the tax formula, mark glue actionable, call an optional
/// feature never-built, or drop the inputs/citation from the risk output.
#[test]
fn robot_mimir_lenses_report_computed_results_with_their_evidence() {
    use dwarves_gui::mimir_lens::LensPane;

    let mut pane = LensPane::fixture();
    assert_eq!(
        pane.state_json()["scanned"],
        false,
        "opens honestly un-scanned"
    );
    pane.update_json("\"scan\"");
    let s = pane.state_json();

    // ── A: generic bloat tax
    let b = &s["bloat"];
    let bloat_ok = b["artifacts"] == json!(1)
        && b["total_tax_bytes"] == json!(130_000)
        && b["actionable_tax_bytes"] == json!(40_000)
        && b["scip_available"] == json!(false);
    assert!(bloat_ok, "the tax splits actionable from glue: {b}");
    assert_eq!(
        b["worst_artifacts"][0]["top_named"][0]["symbol"],
        "znippy::parse_stream"
    );

    // ── B: zombie features, INCLUDING the false-positive guard
    let z = &s["zombie"];
    let worst: Vec<Value> = serde_json::from_value(z["worst"].clone()).unwrap();
    let named: Vec<String> = worst
        .iter()
        .map(|w| {
            format!(
                "{}/{}",
                w["repo"].as_str().unwrap(),
                w["feature"].as_str().unwrap()
            )
        })
        .collect();
    let unknown: Vec<String> = serde_json::from_value(z["build_space_unknown"].clone()).unwrap();
    let zombie_ok = named.contains(&"korp/spark41".to_string())
        && !named.iter().any(|n| n.starts_with("knut/"))
        && unknown.contains(&"knut".to_string());
    assert!(
        zombie_ok,
        "spark41 flagged, knut/grpc NOT (no build space ⇒ unknown): {named:?}"
    );

    // ── C: change risk — inputs beside the score, citation in the data
    let r = &s["risk"];
    let u = &r["worst"][0];
    let attr = r["model_attribution"]
        .as_str()
        .unwrap_or_default()
        .to_string();
    let risk_ok = u["downstream_callers"] == json!(2)
        && u["churn_90d"] == json!(37)
        && u["explain"]
            .as_str()
            .unwrap_or_default()
            .contains("2 callers × 37 commits")
        && attr.contains("Tornhill")
        && attr.contains("CodeScene")
        && attr.contains("Yamaguchi");
    assert!(
        risk_ok,
        "the three inputs and the citation ride with the score: {r}"
    );

    // The L5a staging decision is observable, not just a doc comment.
    let rpcs: Vec<String> = serde_json::from_value(s["proposed_rpcs"].clone()).unwrap();
    let belongs_ok = rpcs.contains(&"Mimir.GenericCost".to_string())
        && rpcs.contains(&"Mimir.ZombieFeatures".to_string())
        && rpcs.contains(&"Mimir.ChangeRisk".to_string())
        && rpcs.contains(&"Mimir.SimilarCode".to_string());
    assert!(
        belongs_ok,
        "the nornir RPCs the query half should become are carried: {rpcs:?}"
    );

    matrix(
        "mimir_lenses_report_computed_results_with_their_evidence",
        bloat_ok && zombie_ok && risk_ok && belongs_ok,
        &format!(
            "the three Mimir lenses compute over a deterministic fixture: the generic bloat tax \
             splits {} actionable bytes (one NAMED generic, 3 instantiations) from {} total \
             (the rest compiler drop glue, correctly NOT actionable); the zombie lens flags \
             korp/spark41 never-built while REFUSING to flag knut/grpc because knut declares no \
             build space ({:?} unevaluable, reported as unknown rather than clean); and change \
             risk carries its three inputs ({} callers, {} commits/90d, coverage) and Tornhill's \
             CodeScene attribution IN THE DATA beside the score. The {} nornir RPCs the query \
             half should become (L5a) are observable in the pane's own state.",
            b["actionable_tax_bytes"],
            b["total_tax_bytes"],
            unknown,
            u["downstream_callers"],
            u["churn_90d"],
            rpcs.len(),
        ),
    );
}

// ── the SHIPPED shell opens About at startup (#31, wave-plan §6c) ─────────────

/// ROBOT SCENARIO — **About is OPEN on the first frame of the SHIPPED shell** (task
/// #31; `.nornir/dwarves-ui-wave-plan.md` §6c), and it dismisses over the seam.
///
/// The builder and the wiring already existed: `TabbedView::with_about_at_startup`
/// (`tabbed.rs`) and the `.with_about_at_startup(about::identity())` chained in
/// `build_native_app`. What did NOT exist was a guard on the SHIPPED path. The
/// in-crate cell `robot_release_pane_and_about_at_startup` builds its own
/// `TabbedView::new(...).with_about_at_startup(...)`, so it proves the BUILDER works
/// and says nothing about whether the shell the user launches chains it — delete that
/// one line from `build_native_app` and every existing test stays green while the
/// shipped UI silently stops greeting anybody.
///
/// This drives `build_native_app` itself — the exact builder `main` runs, offline-safe
/// — and asserts the startup posture on the state seam.
///
/// RED-when-broken: change `build_native_app`'s `.with_about_at_startup(…)` to plain
/// `.with_about(…)` and `about.open` is `false` on the first frame. PROVEN by doing
/// exactly that on 2026-08-25; reverted.
#[test]
fn robot_about_opens_at_startup_in_the_shipped_shell() {
    let mut app = offline_app();

    // The FIRST frame's posture, read before anything is driven.
    let st = Facet::state_json(&app);
    let opened = st["about"]["open"] == true;
    assert!(
        opened,
        "the shipped shell must open the About card on its first frame (#31): {}",
        st["about"]
    );

    // It is dwarves' OWN identity card, not an empty modal: the shared `facett-about`
    // oracle carries the wordmark and the embedded forge logo.
    let identity_ok = st["about"]["wordmark"] == "dwarves" && st["about"]["has_logo"] == true;
    assert!(
        identity_ok,
        "the startup card is dwarves' identity: {}",
        st["about"]
    );

    // …and it is DISMISSABLE — a modal that opens at startup and cannot be closed is a
    // worse bug than one that never opens. The ℹ button re-opens it.
    Facet::update_json(&mut app, r#"{"about": false}"#);
    let closed = Facet::state_json(&app)["about"]["open"] == false;
    assert!(closed, "About dismisses over the seam");
    Facet::update_json(&mut app, r#"{"about": true}"#);
    let reopened = Facet::state_json(&app)["about"]["open"] == true;
    assert!(reopened, "…and re-opens (the ℹ button's path)");

    matrix(
        "about_opens_at_startup",
        opened && identity_ok && closed && reopened,
        "the SHIPPED shell (dwarves_gui::build_native_app — the exact builder `main` runs) \
         has the shared facett-about card OPEN on its first frame carrying dwarves' own \
         Identity (wordmark + embedded forge logo), and it dismisses and re-opens over the \
         state seam. Guards the `.with_about_at_startup(about::identity())` wiring itself, \
         which the in-crate builder cell could not: that one constructs its own TabbedView",
    );
}