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_five_leaf_body.rs

//! **robot_five_leaf_body** — the ⚒ Build Thing tab's body is a DECLARED tree of
//! leaves, rendered by the SHIPPED shell; the operator can DRAG every divider inside
//! it, FOLD every leaf, and MAXIMISE the graph.
//!
//! The name is from the five-leaf days and is kept so the lane that named it still
//! finds it. There are THREE leaves now (2026-08-26, the owner: "byt"): the settings
//! renderer is a GRAPH, so recipes · matrix · settings · kernel · console collapsed
//! to **graph | menu | console** — the kernel a node of the graph (gap #6), the matrix
//! the graph's variants, the settings the selected node's menu. `tests/robot_kernel_in_build.rs`
//! drives the four recipe shapes of the graph; this file drives the leaves, their two
//! dividers, the folds, and the maximise.
//!
//! ```text
//! build-thing ── Split(H)      graph | side
//!                               └─ side ── Split(V)   menu / console
//! ```
//!
//! | claim | RED when |
//! |---|---|
//! | the SHIPPED shell binds all three declared leaves as foldable, chromed, distinctly named panes, and renders no kernel / matrix / settings leaf | a leaf is dropped, or a retired pane comes back |
//! | every divider inside the tab is a real handle a pointer drags, both children moving oppositely with their sum conserved | a split loses `resizable`, or a handle is chrome only |
//! | folding any leaf drops only its own slot | fold reaches the wrong pane |
//! | MAXIMISING the graph gives it the whole tab body, its siblings zero extent by `maximize`, and restore returns the ratios | the graph cannot be enlarged, or restore forgets the drag |
//!
//! The canvas stays at 1400×900 for the reason the previous revision recorded: the tab
//! strip is not in the loop (active tab set through `NavState`), so this suite stays
//! independent of strip-width defects.

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

use dwarves_gui::build_thing_scene::{self, CONSOLE, GRAPH, LEAVES, MENU, TAB};
use dwarves_gui::tabbed::{dwarves_scene, TabbedView, BODY};
use dwarves_gui::{build_native_app, tab_keys, tab_labels};

use facett_app::scene::{self, Content, Layout, NavState, Pane, SceneHost};
use facett_core::Facet;
use serde_json::{json, Value};

use egui_kittest::Harness;

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

/// The EXACT shipped native shell, offline-safe (dead endpoint, nonexistent ledger) —
/// the same builder `main` runs, so what is asserted is what ships.
fn offline_app() -> TabbedView {
    build_native_app(
        "http://127.0.0.1:0".into(),
        PathBuf::from("/nonexistent/dwarves-gui/robot-five-leaf"),
    )
}

/// A host that draws one queryable label per slot — mirrors `robot_scene.rs`'s
/// `TestHost`, so the render below exercises the SAME `dwarves_scene()` value the app
/// ships without dragging every real pane's I/O into a geometry test.
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 })
    }
}

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

/// A rendered pane's allotted rect `(x, y, w, h)` — the apportionment its parent handed
/// it this frame, which is the geometry oracle (`rect` can lag a frame).
fn 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))
}

fn extent(state: &Value, pane: &str, along_x: bool) -> f32 {
    let (_, _, w, h) = alloc(state, pane);
    if along_x {
        w
    } else {
        h
    }
}

/// The centre of a split pane's first drag handle.
fn sep_center(state: &Value, pane: &str) -> egui::Pos2 {
    let s = &state["panes"][pane]["separators"][0];
    let f = |i: usize| {
        s.get(i).and_then(Value::as_f64).unwrap_or_else(|| {
            panic!(
                "pane `{pane}` exposes a drag handle: {}",
                state["panes"][pane]
            )
        }) as f32
    };
    egui::pos2(f(0) + f(2) / 2.0, f(1) + f(3) / 2.0)
}

/// Every SPLIT in the declared ⚒ body, as `(name, [child names])`, in tree order.
fn declared_splits() -> Vec<(String, Vec<String>)> {
    fn walk(p: &Pane, out: &mut Vec<(String, Vec<String>)>) {
        if let Content::Children(kids) = &p.content {
            if matches!(p.layout, Layout::Split { .. }) {
                out.push((
                    p.name.clone(),
                    kids.iter().map(|k| k.name.clone()).collect(),
                ));
            }
            for k in kids {
                walk(k, out);
            }
        }
    }
    let mut out = Vec::new();
    walk(&build_thing_scene::body(), &mut out);
    out
}

/// Render the shipped scene with the ⚒ tab active — the ONE shape the tab has.
fn render_hammer() -> (Harness<'static>, Rc<RefCell<NavState>>, Rc<RefCell<Value>>) {
    let scene = dwarves_scene(&tab_labels());
    let idx = tab_keys()
        .iter()
        .position(|k| k == TAB)
        .expect("⚒ is 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();
    (h, nav, state)
}

// ── 0. no retired leaf — the kernel, the matrix and the settings are the graph and the menu ──

/// ROBOT SCENARIO — **the shipped shell renders NO kernel, matrix or settings leaf in
/// ⚒, in any state.** Three leaves, nothing marked absent, all three slots filled.
///
/// RED WHEN REVERTED: declare a retired leaf again in `build_thing_scene::body` and
/// the roster names it.
#[test]
fn robot_the_shipped_shell_has_only_the_three_leaves_in_the_hammer_tab() {
    let app = offline_app();
    let st = Facet::state_json(&app);
    let panes: Vec<String> = st["panes"]
        .as_array()
        .expect("the shell reports its pane roster")
        .iter()
        .map(|v| v.as_str().unwrap_or_default().to_string())
        .collect();
    for retired in [
        "build-thing-kernel",
        "build-thing-matrix",
        "build-thing-settings",
        "build-thing-recipes",
    ] {
        assert!(
            !panes.iter().any(|p| p == retired),
            "a retired LEAF `{retired}` is bound in the shipped shell: {panes:?}"
        );
    }
    let absent: Vec<String> = st["absent"]
        .as_array()
        .map(|a| {
            a.iter()
                .map(|v| v.as_str().unwrap_or_default().to_string())
                .collect()
        })
        .unwrap_or_default();
    assert!(
        absent.is_empty(),
        "nothing in the shipped shell is gated by runtime absence any more: {absent:?}"
    );

    let (_h, _nav, state) = render_hammer();
    let render = state.borrow();
    for leaf in LEAVES {
        assert!(
            render["slots"].get(leaf).is_some(),
            "`{leaf}` fills its slot: {}",
            render["slots"]
        );
    }
    matrix(
        "hammer_three_leaves_no_retired_pane",
        true,
        "the SHIPPED dwarves shell (build_native_app, offline-safe) binds three ⚒ leaves (graph, menu, console) and no kernel/matrix/settings/recipes pane, marks nothing absent, and a 1400x900 render fills all three slots — the kernel is a node of the build graph, the matrix its variants, the settings the selected node's menu",
    );
}

// ── 1. every declared leaf is addressable ─────────────────────────────────────

/// ROBOT SCENARIO — **all three ⚒ leaves are DECLARED, foldable, chromed panes of the
/// shipped shell with distinct names.**
#[test]
fn robot_all_declared_leaves_are_addressable_and_distinct() {
    let app = offline_app();
    let st = Facet::state_json(&app);
    let panes: Vec<String> = st["panes"]
        .as_array()
        .expect("the shell reports its pane roster")
        .iter()
        .map(|v| v.as_str().unwrap_or_default().to_string())
        .collect();

    let missing: Vec<&str> = LEAVES
        .iter()
        .copied()
        .filter(|l| !panes.iter().any(|p| p == l))
        .collect();
    assert!(missing.is_empty(), "the ⚒ body DECLARES three leaves and the shipped shell must bind every one; missing {missing:?} of {panes:?}");

    let dressed = LEAVES.iter().all(|leaf| {
        st["foldable"].as_array().unwrap().iter().any(|n| n == leaf)
            && st["chrome"].as_array().unwrap().iter().any(|n| n == leaf)
            && st["folded"].get(*leaf).is_some()
    });
    assert!(
        dressed,
        "every declared ⚒ leaf is a foldable, chromed, addressable scene pane: {st}"
    );

    let mut sorted = panes.clone();
    sorted.sort();
    sorted.dedup();
    assert_eq!(
        sorted.len(),
        panes.len(),
        "duplicate pane name in the shipped shell: {panes:?}"
    );

    matrix(
        "hammer_three_leaves_addressable",
        true,
        &format!("the ⚒ tab declares THREE named leaves ({}) — each a foldable, chromed, distinctly named scene pane of the shipped shell", LEAVES.join(", ")),
    );
}

// ── 2. every divider really drags ─────────────────────────────────────────────

/// ROBOT SCENARIO — **every divider inside the ⚒ tab is a real draggable handle.**
#[test]
fn robot_every_divider_inside_the_hammer_tab_really_drags() {
    let splits = declared_splits();
    assert_eq!(
        splits.len(),
        2,
        "the ⚒ body declares two dividers (graph|side, menu|console): {splits:?}"
    );

    let st = Facet::state_json(&offline_app());
    let draggable: Vec<String> = st["draggable_splits"]
        .as_array()
        .expect("the shell reports its draggable splits")
        .iter()
        .map(|v| v.as_str().unwrap_or_default().to_string())
        .collect();
    for (name, _) in &splits {
        assert!(
            draggable.contains(name),
            "`{name}` is a DRAGGABLE divider of the shipped shell: {draggable:?}"
        );
    }

    let mut dragged = 0usize;
    for (name, kids) in &splits {
        assert_eq!(
            kids.len(),
            2,
            "`{name}` divides exactly two children: {kids:?}"
        );
        let (mut h, _nav, state) = render_hammer();
        let (sw, sh) = {
            let s = &state.borrow()["panes"][name.as_str()]["separators"][0];
            (
                s[2].as_f64().expect("a handle rect") as f32,
                s[3].as_f64().unwrap() as f32,
            )
        };
        let along_x = sw < sh;
        let d = 90.0_f32;
        let (a, b) = (&kids[0], &kids[1]);
        let (a0, b0) = (
            extent(&state.borrow(), a, along_x),
            extent(&state.borrow(), b, along_x),
        );
        let span0 = a0 + b0;
        assert!(
            a0 > 20.0 && b0 > 20.0,
            "`{name}` has two LIVE children: {a}={a0} {b}={b0}"
        );
        let from = sep_center(&state.borrow(), name);
        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 (a1, b1) = (
            extent(&state.borrow(), a, along_x),
            extent(&state.borrow(), b, along_x),
        );
        assert!(
            (a1 - a0 - d).abs() < 26.0,
            "dragging `{name}`'s divider {d}pt grew `{a}` by ~{d}: {a0} -> {a1}"
        );
        assert!(
            (b0 - b1 - d).abs() < 26.0,
            "…and shrank `{b}` by the same ~{d}: {b0} -> {b1}"
        );
        assert!(
            ((a1 + b1) - span0).abs() < 8.0,
            "`{name}` conserves its extent: {span0} -> {}",
            a1 + b1
        );
        dragged += 1;
    }
    assert_eq!(dragged, 2, "both ⚒ dividers were driven");

    matrix(
        "hammer_dividers_drag",
        true,
        "every divider inside the ⚒ tab is a real handle a pointer drags: both grow one child ~90pt, shrink its sibling the same, and conserve the sum",
    );
}

// ── 3. the leaves fold independently, and the roster is data ──────────────────

/// ROBOT SCENARIO — **folding one ⚒ leaf drops ONLY its own slot**; the shell reports
/// the declared leaf roster as data so a driver never has to hard-code it.
#[test]
fn robot_hammer_leaves_fold_independently_and_the_roster_is_data() {
    let st = Facet::state_json(&offline_app());
    let reported: Vec<String> = st["body_leaves"][TAB]
        .as_array()
        .unwrap_or_else(|| panic!("the shell reports the ⚒ leaf roster as data: {st}"))
        .iter()
        .map(|v| v.as_str().unwrap_or_default().to_string())
        .collect();
    assert_eq!(
        reported,
        LEAVES.to_vec(),
        "the shell's reported ⚒ leaf roster is the declared one, in reading order"
    );
    let n = |k: &str| st[k].as_array().map(Vec::len).unwrap_or(0);
    assert_eq!(
        n("split_bodies") + n("leaf_bodies") + n("single_pane_bodies"),
        dwarves_gui::DWARVES_TABS.len(),
        "every shipped tab is decomposed, leaved, or honestly single-pane: {st}"
    );

    let (mut h, nav, state) = render_hammer();
    for leaf in LEAVES {
        nav.borrow_mut().set_fold(leaf, true);
        h.run();
        let gone = state.borrow()["slots"].get(leaf).is_none();
        let others = LEAVES
            .iter()
            .filter(|l| **l != leaf)
            .all(|s| state.borrow()["slots"].get(*s).is_some());
        assert!(
            gone && others,
            "folding `{leaf}` drops ONLY its own slot — its siblings keep drawing: {}",
            state.borrow()["slots"]
        );
        nav.borrow_mut().set_fold(leaf, false);
        h.run();
    }

    matrix(
        "hammer_leaves_fold_independently",
        true,
        &format!("the fold vocabulary reaches inside the ⚒ tab: folding any of {GRAPH}/{MENU}/{CONSOLE} drops exactly its own slot and leaves its siblings drawing, and the shell publishes the declared roster under body_leaves"),
    );
}

// ── 4. the graph is ENLARGEABLE: maximise / restore ───────────────────────────

/// ROBOT SCENARIO — **maximising the graph leaf gives it the whole tab body; its
/// siblings are allotted zero extent BY `maximize`; restore returns the ratios —
/// including a divider the operator had dragged.**
///
/// The owner, 2026-08-26: the graph pane MUST be enlargeable, and the kconfig subgraph
/// must be readable at full pane size. The mechanism is facett-scene's own
/// (`NavState::set_maximized` / `toggle_maximize`, `hidden_by: "maximize"`), driven
/// headless the way folds are.
///
/// RED WHEN REVERTED: make the graph leaf a fixed-size child, or let `toggle_maximize`
/// forget the ratios, and the alloc / restore assertions below fail.
#[test]
fn robot_the_graph_leaf_maximises_to_the_tab_body_and_restores_the_ratios() {
    let (mut h, nav, state) = render_hammer();
    // Drag the graph|side divider first, so restore has a NON-default ratio to return to.
    let (from_w, from) = {
        let s = state.borrow();
        (extent(&s, GRAPH, true), sep_center(&s, TAB))
    };
    let to = egui::pos2(from.x + 120.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 dragged_w = extent(&state.borrow(), GRAPH, true);
    assert!(
        (dragged_w - from_w - 120.0).abs() < 26.0,
        "the drag took: {from_w} -> {dragged_w}"
    );

    // MAXIMISE the graph.
    nav.borrow_mut().set_maximized(GRAPH);
    h.run();
    h.run();
    {
        let s = state.borrow();
        let (bx, by, bw, bh) = alloc(&s, TAB);
        let (gx, gy, gw, gh) = alloc(&s, GRAPH);
        assert!(
            (gw - bw).abs() < 2.0
                && (gh - bh).abs() < 40.0
                && (gx - bx).abs() < 2.0
                && gy >= by - 1.0,
            "the maximised graph is allotted the tab body: body {:?} graph {:?}",
            (bx, by, bw, bh),
            (gx, gy, gw, gh)
        );
        // The siblings live under the `side` split; the scene records the hidden pane
        // at the level the maximise cut — the split itself — and its children draw
        // nothing (no slot filled, or zero extent).
        let side = format!("{TAB}-side");
        assert_eq!(
            s["panes"][&side]["hidden_by"],
            json!("maximize"),
            "`{side}` is hidden BY the maximise: {}",
            s["panes"][&side]
        );
        for sib in [MENU, CONSOLE] {
            let hidden = s["panes"][sib]["hidden_by"] == json!("maximize");
            let w = s["panes"][sib]["alloc"][2].as_f64().unwrap_or(0.0);
            let hh = s["panes"][sib]["alloc"][3].as_f64().unwrap_or(0.0);
            assert!(
                hidden || w == 0.0 || hh == 0.0 || s["slots"].get(sib).is_none(),
                "`{sib}` is allotted nothing while the graph is maximised: {}",
                s["panes"][sib]
            );
        }
        assert!(
            s["slots"].get(GRAPH).is_some(),
            "the maximised graph still fills its slot"
        );
    }

    // RESTORE — the dragged ratio comes back, not the default.
    nav.borrow_mut().toggle_maximize(GRAPH);
    h.run();
    h.run();
    {
        let s = state.borrow();
        let w = extent(&s, GRAPH, true);
        assert!(
            (w - dragged_w).abs() < 4.0,
            "restore returns the DRAGGED ratio: {dragged_w} -> {w}"
        );
        for sib in [MENU, CONSOLE] {
            assert!(
                extent(&s, sib, false) > 20.0,
                "`{sib}` is back after restore"
            );
        }
    }

    matrix(
        "hammer_graph_maximises_and_restores",
        true,
        "the ⚒ graph leaf maximises through facett-scene's NavState (alloc == the tab body, menu and console hidden_by=maximize at zero extent) and restore returns the graph|side ratio the operator had dragged to, not the default",
    );
}