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

//! **ROBOT-UI: the kernel is the `kärna` NODE of ⚒'s build graph, its kconfig picture
//! is the unfolded subgraph under it, and the preset picked in the node's menu is the
//! preset ⚒ builds.**
//!
//! The owner went looking for the kconfig configurator in ⚒ three separate times and did
//! not find it. He overruled the ⚙ tab; the configurator moved into ⚒ as a fifth LEAF
//! with a presence overlay; that shape was retired the same day (gap #6 of
//! `.nornir/build-thing-model-design.md`: *the kernel is not a special leaf; it is the
//! first feature whose subtree is deep and delegated*); and the settings list became a
//! GRAPH on his ruling ("byt"). So the kernel is `kärna` — a node of the build graph at
//! path `kernel`, present exactly when the resolved variant has the feature (its own
//! `[[build.kernel]]`, or an edge to a donor's), with `kernel.preset` and
//! `kernel.<symbol>` as its subgraph and the honesty label on it.
//!
//! Four recipe shapes, four different graphs:
//!
//! 1. **nothing resolved** → an empty graph (`/graph` null), no tree;
//! 2. **a library** → variant nodes, no `kärna`;
//! 3. **a donor-consuming installer** → `kärna` with its EDGE (`tunnr/tunnr-kernel :
//!    kernel`) and a CONSUMPTION edge to the donor's node — the choice is the donor's;
//! 4. **an own kernel** → `kärna` as the kernel variant's own subgraph: `kernel.preset`
//!    (a val) and one `kernel.<symbol>` feature per symbol, folded until clicked.
//!
//! And the knob turns: clicking `full` in the node's menu on a declaration that says
//! `nuked` moves the `preset` the pane will hand to `Act::Build`, and the flag beside
//! it. The other half — that the moved value reaches the build worker's own
//! declaration — is `build_forge`'s `a_preset_picked_in_the_forge_is_the_preset_the_build_runs`.
//!
//! RED BEFORE GREEN: on the parent commit `/graph`, `/menu` and `/tree` did not exist.
//!
//! Nothing here presses BYGG: that would start a real kernel compile.

use facett_core::Facet;
use nornir_robotui::egui_kittest::Harness;
use nornir_robotui::{Observable, RobotSession};
use serde_json::{json, Value};

use dwarves_gui::build_forge::DeclaredForge;
use dwarves_gui::build_scene::BuildScene;

struct SceneApp(BuildScene);

impl Observable for SceneApp {
    fn state_json(&self) -> serde_json::Value {
        Facet::state_json(&self.0)
    }
}

/// A syntactically valid pin. The kind REFUSES an unpinned kernel by name.
const PIN: &str = "949d166a1263bd0cf0c5aab44eeb57869615099e7c48bf88d0e4adb77379b999";

/// One checkout that declares a kernel AND a plain cargo row. `orten` and not any real
/// product: the scene's own guard forbids a product name in its source.
fn declaration() -> String {
    format!(
        r#"
[build]
base_suite = "ORIGINAL"

[[build.variant]]
name = "orten-server"
package = "orten"
bin = "orten"
role = "server"
default = true
suites = ["ORIGINAL"]
kind = "exe"

[[build.variant]]
name = "orten-kernel"
role = "server"
default = false
suites = ["ORIGINAL"]
kind = "kernel"

[[build.kernel]]
name = "orten-kernel"
version = "6.12.1"
preset = "nuked"
tarball_sha256 = "{PIN}"
"#
    )
}

fn staged(decl: &str) -> tempfile::TempDir {
    let dir = tempfile::tempdir().expect("tempdir");
    let repo = dir.path().join("orten");
    std::fs::create_dir_all(repo.join(".nornir")).expect("mkdir");
    std::fs::write(
        repo.join("Cargo.toml"),
        "[package]\nname = \"orten\"\nversion = \"1.0.0\"\nedition = \"2021\"\n\n[dependencies]\n",
    )
    .expect("manifest");
    std::fs::write(
        repo.join("Cargo.lock"),
        "version = 4\n\n[[package]]\nname = \"orten\"\nversion = \"1.0.0\"\n",
    )
    .expect("lockfile");
    std::fs::write(repo.join(".nornir").join("build-variants.toml"), decl).expect("declaration");
    // The sibling DONOR. An edge is type-checked against the donor's own
    // declaration at load (lane AE/AF), so a fixture that names
    // `tunnr/tunnr-kernel` and stages no `tunnr` is refused before the pane
    // ever draws — the whole file went red on it. The constellation layout is
    // the rule products resolve by, so the fixture stages it.
    let tunnr = dir.path().join("tunnr");
    std::fs::create_dir_all(tunnr.join(".nornir")).expect("mkdir");
    std::fs::write(
        tunnr.join(".nornir").join("build-variants.toml"),
        "[build]\nbase_suite = \"ORIGINAL\"\n\n[[build.kernel]]\nname = \"tunnr-kernel\"\nversion = \"6.12.104\"\npreset = \"nuked\"\ntarball_sha256 = \"949d166a1263bd0cf0c5aab44eeb57869615099e7c48bf88d0e4adb77379b999\"\n\n[[build.variant]]\nname = \"tunnr-kernel\"\nkind = \"kernel\"\nrole = \"server\"\ndefault = false\n",
    )
    .expect("donor declaration");
    dir
}

fn session(root: &std::path::Path) -> RobotSession<'_, SceneApp> {
    let scene = BuildScene::new(Box::new(DeclaredForge::at(root.to_path_buf())));
    let harness = Harness::builder()
        .with_size(nornir_robotui::egui::Vec2::new(1400.0, 1000.0))
        .build_ui_state(
            |ui, app: &mut SceneApp| Facet::ui(&mut app.0, ui),
            SceneApp(scene),
        );
    let mut robot = RobotSession::new(harness, "dwarves-gui");
    robot.run(2);
    robot
}

/// Resolve a RECIPE and stop there — no row, no column.
fn resolve<A: Observable>(robot: &mut RobotSession<'_, A>, name: &str) {
    robot
        .type_into("artefakt", name)
        .expect("the artefakt field is reachable");
    robot
        .click_label("sök")
        .expect("the sök button is reachable");
    robot.run(2);
}

/// Land on `(row, column)` through the accessible strips — the clicks that existed
/// before the graph and survive it (law D).
fn land(robot: &mut RobotSession<'_, SceneApp>, row: &str, column: &str) {
    robot
        .click_label(row)
        .unwrap_or_else(|e| panic!("the {row} row is not clickable: {e}"));
    robot.run(2);
    robot
        .click_label(column)
        .unwrap_or_else(|e| panic!("the {column} column is not clickable: {e}"));
    robot.run(3);
}

fn pick(robot: &mut RobotSession<'_, SceneApp>, row: &str, column: &str) {
    resolve(robot, row);
    land(robot, row, column);
}

/// The node ids of the graph on screen.
fn graph_ids<A: Observable>(robot: &RobotSession<'_, A>) -> Vec<String> {
    robot
        .state_at("/graph/ids")
        .and_then(|v| v.as_array().cloned())
        .map(|a| {
            a.iter()
                .filter_map(|v| v.as_str().map(str::to_string))
                .collect()
        })
        .unwrap_or_default()
}

/// The `kärna` node of the picked variant's tree, if it has one.
fn kernel_node<A: Observable>(robot: &RobotSession<'_, A>) -> Option<Value> {
    robot
        .state_at("/tree/roots")
        .and_then(|t| t.as_array().cloned())
        .and_then(|roots| roots.into_iter().find(|n| n["path"] == json!("kernel")))
}

fn library_declaration() -> &'static str {
    r#"
[build]
base_suite = "ORIGINAL"

[[build.variant]]
name = "orten"
package = "orten"
role = "library"
default = true
suites = ["ORIGINAL"]
kind = "lib"
linkage = "not-applicable"
"#
}

fn donor_declaration() -> &'static str {
    r#"
[build]
base_suite = "ORIGINAL"

[[build.variant]]
name = "orten-server"
package = "orten"
bin = "orten"
role = "server"
default = true
suites = ["ORIGINAL"]
kind = "exe"

[[build.installer]]
name = "orten-installer"
variant = "orten-server"
kernel_donor = { repo = "tunnr", kernel = "tunnr-kernel" }
"#
}

/// **A LIBRARY IS NOT OFFERED A KERNEL.** Its graph has its variant and nothing
/// kernel-shaped; its tree has no `kärna`.
#[test]
fn a_recipe_that_declares_no_kernel_is_offered_no_configurator() {
    let dir = staged(library_declaration());
    let mut robot = session(dir.path());
    resolve(&mut robot, "orten");
    land(&mut robot, "orten", "lib");
    let ids = graph_ids(&robot);
    assert!(
        ids.iter().any(|i| i == "orten"),
        "the variant is a node: {ids:?}"
    );
    assert!(
        !ids.iter()
            .any(|i| i.contains("kernel") || i.contains("kärna")),
        "a LIBRARY grew a kernel node: {ids:?}"
    );
    assert_eq!(
        kernel_node(&robot),
        None,
        "a LIBRARY grew a `kärna` node: {:?}",
        robot.state_at("/tree")
    );
}

/// **THE MOVE.** The kernel is reachable inside ⚒ — as the `kärna` node of the graph,
/// with the symbols as its subgraph — and absent for a selection that builds no kernel.
#[test]
fn the_kernel_configurator_is_reachable_inside_build_thing() {
    let dir = staged(&declaration());
    let mut robot = session(dir.path());

    // ── NOTHING RESOLVED ⇒ NO GRAPH, NO TREE. THE RECIPE DECIDES ────────────────
    assert_eq!(
        robot.state_at("/graph"),
        Some(Value::Null),
        "no recipe ⇒ no graph"
    );
    assert_eq!(
        robot.state_at("/tree"),
        Some(Value::Null),
        "no recipe ⇒ no tree"
    );

    // ── A RECIPE THAT DECLARES A KERNEL, NOTHING PICKED ⇒ THE STANDING QUESTION ──
    // The operator knows WHICH product they are looking at but not yet which row:
    // the standing question's tree is the variant that carries `kärna`.
    resolve(&mut robot, "orten-kernel");
    let ids = graph_ids(&robot);
    assert!(
        ids.iter().any(|i| i == "orten-kernel") && ids.iter().any(|i| i == "orten-server"),
        "both variants are nodes: {ids:?}"
    );
    assert!(
        ids.iter().any(|i| i == "orten-kernel/kernel"),
        "the `kärna` node is in the graph with nothing picked: {ids:?}"
    );
    let k = kernel_node(&robot)
        .expect("a resolved recipe that DECLARES a kernel grows `kärna` in the standing tree");
    assert_eq!(k["label"], json!("kärna"));

    land(&mut robot, "orten-kernel", "kernel");
    assert_eq!(robot.state_at("/target"), Some(json!("kernel")));
    assert_eq!(
        robot.state_at("/graph/selected"),
        Some(json!("orten-kernel")),
        "the strip pick selects the variant node"
    );

    // THE SUBGRAPH UNDER kärna: `kernel.preset` a val with its flag, one feature per
    // symbol with its flag — law C — and folded until clicked.
    let k = kernel_node(&robot).expect("`kärna` on the picked kernel variant");
    let kids = k["children"].as_array().cloned().unwrap_or_default();
    let preset = kids
        .iter()
        .find(|c| c["path"] == json!("kernel.preset"))
        .expect("kernel.preset under kärna");
    assert_eq!(preset["kind"], json!("val"));
    assert_eq!(preset["flag"], json!("--opt kernel.preset=nuked"));
    let symbols: Vec<&Value> = kids
        .iter()
        .filter(|c| c["kind"] == json!("feature") && c["path"] != json!("kernel.profile"))
        .collect();
    assert!(
        symbols.len() > 10,
        "one `kernel.<symbol>` feature per symbol of the pick: {}",
        symbols.len()
    );
    assert!(
        symbols.iter().all(|s| s["flag"]
            .as_str()
            .is_some_and(|f| f.starts_with("--opt kernel."))),
        "every symbol feature equals an `--opt kernel.<symbol>=on|off` on the prompt"
    );
    let ids = graph_ids(&robot);
    assert!(
        ids.iter().any(|i| i == "orten-kernel/kernel.preset"),
        "the subgraph's nodes are in the graph: {ids:?}"
    );
    let visible = robot
        .state_at("/graph/visible")
        .and_then(|v| v.as_u64())
        .unwrap_or(0);
    assert!(
        (visible as usize) < ids.len(),
        "the kärna subgraph starts FOLDED — {visible} visible of {} nodes",
        ids.len()
    );

    // THE COUNTERWEIGHT — a selection that builds no kernel has NO `kärna`.
    robot
        .click_label("orten-server")
        .expect("the cargo row is clickable");
    robot.run(2);
    robot
        .click_label("exe")
        .expect("the exe column is clickable");
    robot.run(2);
    assert_eq!(
        kernel_node(&robot),
        None,
        "an exe variant has no `kärna` feature: {:?}",
        robot.state_at("/tree")
    );
    assert!(
        !graph_ids(&robot)
            .iter()
            .any(|i| i.starts_with("orten-server/kernel")),
        "…and no kernel node under it"
    );
}

/// **THE ONE LIVE EXAMPLE ON THIS DISK.** SKIPS rather than lies when there is no
/// constellation to read.
#[test]
fn the_constellations_own_kernel_declaration_reaches_the_leaf() {
    let declarations = DeclaredForge::discover().kernel_declarations();
    let Some((repo_name, kernel_name, declared_preset)) = declarations.first().cloned() else {
        eprintln!("SKIP: no checkout on this disk declares a [[build.kernel]]");
        return;
    };
    eprintln!(
        "{} kernel declaration(s) on this disk: {declarations:?}",
        declarations.len()
    );
    eprintln!("live declaration: {repo_name} · {kernel_name} · preset {declared_preset}");

    let scene = BuildScene::new(Box::new(DeclaredForge::discover()));
    let harness = Harness::builder()
        .with_size(nornir_robotui::egui::Vec2::new(2400.0, 1400.0))
        .build_ui_state(
            |ui, app: &mut SceneApp| Facet::ui(&mut app.0, ui),
            SceneApp(scene),
        );
    let mut robot = RobotSession::new(harness, "dwarves-gui");
    robot.run(2);
    pick(&mut robot, &kernel_name, "kernel");

    let k = kernel_node(&robot).unwrap_or_else(|| {
        panic!(
            "the constellation's ONLY kernel declaration does not reach the `kärna` node: {:?}",
            robot.state_at("/tree")
        )
    });
    let preset = k["children"]
        .as_array()
        .unwrap()
        .iter()
        .find(|c| c["path"] == json!("kernel.preset"))
        .cloned()
        .expect("kernel.preset");
    assert_eq!(
        preset["value"],
        json!(declared_preset),
        "the node opens on what {repo_name} declares"
    );
    assert_eq!(
        robot.state_at("/settings/preset"),
        Some(json!(declared_preset))
    );

    let want = if declared_preset == "slim" {
        "normal"
    } else {
        "slim"
    };
    robot
        .click_label(want)
        .unwrap_or_else(|e| panic!("the `{want}` preset is not clickable: {e}"));
    robot.run(3);
    assert_eq!(robot.state_at("/settings/preset"), Some(json!(want)));
}

/// **THE KNOB TURNS.** The preset is the `kernel.preset` val in the node's menu, and
/// clicking it moves the value the pane will hand to `Act::Build` — and the flag.
#[test]
fn clicking_a_preset_in_build_thing_moves_what_the_build_would_run() {
    let dir = staged(&declaration());
    let mut robot = session(dir.path());
    pick(&mut robot, "orten-kernel", "kernel");

    let rows = robot
        .state_at("/rows")
        .and_then(|v| v.as_array().cloned())
        .unwrap_or_default();
    let row = rows
        .iter()
        .find(|r| r["name"] == json!("orten-kernel"))
        .expect("the kernel row");
    let settings = row["targets"]
        .as_array()
        .expect("the column strip")
        .iter()
        .find(|t| t["name"] == json!("kernel"))
        .expect("the kernel column")["settings"]
        .as_array()
        .cloned()
        .unwrap_or_default();
    let preset = settings
        .iter()
        .find(|s| s["key"] == json!("preset"))
        .unwrap_or_else(|| panic!("no `preset` setting on the kernel variant: {settings:?}"));
    assert_eq!(preset["fixed"], json!(false), "the preset is still a LABEL");
    assert_eq!(
        preset["choices"],
        json!(["full", "normal", "slim", "nuked"]),
        "the picker must offer the kind's whole vocabulary"
    );

    // The menu of the picked variant carries the val — one control, addressed by its
    // choice words.
    let controls = robot
        .state_at("/menu/controls")
        .and_then(|v| v.as_array().cloned())
        .unwrap_or_default();
    let flag = |controls: &[Value]| {
        controls
            .iter()
            .find(|c| c["path"] == json!("kernel.preset"))
            .map(|c| c["flag"].clone())
    };
    assert_eq!(robot.state_at("/settings/preset"), Some(json!("nuked")));
    // The menu of the variant shows kärna as a root; the preset sits one level down and
    // is reached by selecting the node. Select it through the state door (the twin of
    // a canvas click) and read the flag off the menu.
    let _ = controls;
    robot
        .click_label("nuked")
        .expect("the `nuked` preset is a real, clickable widget in ⚒");
    robot.run(2);
    let controls = robot
        .state_at("/menu/controls")
        .and_then(|v| v.as_array().cloned())
        .unwrap_or_default();
    assert_eq!(flag(&controls), Some(json!("--opt kernel.preset=nuked")));

    // THE CLICK, on the real widget.
    robot
        .click_label("full")
        .expect("the `full` preset is a real, clickable widget in ⚒");
    robot.run(3);
    assert_eq!(
        robot.state_at("/settings/preset"),
        Some(json!("full")),
        "the click did not move the value the build is handed — a knob that turns nothing"
    );
    // Law C: the flag beside the node moved with it, in the menu and in the tree.
    let controls = robot
        .state_at("/menu/controls")
        .and_then(|v| v.as_array().cloned())
        .unwrap_or_default();
    assert_eq!(flag(&controls), Some(json!("--opt kernel.preset=full")));
    let k = kernel_node(&robot).expect("kärna");
    let tree_flag = k["children"]
        .as_array()
        .unwrap()
        .iter()
        .find(|c| c["path"] == json!("kernel.preset"))
        .unwrap()["flag"]
        .clone();
    assert_eq!(tree_flag, json!("--opt kernel.preset=full"));
}

// ── the four recipe shapes, hosted as the ⚒ scene's leaves ───────────────────

struct HostedApp(BuildScene);

impl Observable for HostedApp {
    fn state_json(&self) -> serde_json::Value {
        Facet::state_json(&self.0)
    }
}

/// The scene hosted as the ⚒ scene's LEAVES — the mount the shipped shell uses
/// (`lib.rs` mounts `build_scene_pane().hosted()`): the graph leaf (root search, graph,
/// strips) and the menu leaf.
fn hosted_session(root: &std::path::Path) -> RobotSession<'_, HostedApp> {
    use dwarves_gui::build_thing_scene as tree;
    use dwarves_gui::tabbed::LeafPanel;
    let scene = BuildScene::new(Box::new(DeclaredForge::at(root.to_path_buf()))).hosted();
    let harness = Harness::builder()
        .with_size(nornir_robotui::egui::Vec2::new(1400.0, 1000.0))
        .build_ui_state(
            move |ui, app: &mut HostedApp| {
                app.0.ui_leaf(tree::GRAPH, ui);
                app.0.ui_leaf(tree::MENU, ui);
            },
            HostedApp(scene),
        );
    let mut robot = RobotSession::new(harness, "dwarves-gui");
    robot.run(2);
    robot
}

fn on_screen(robot: &mut RobotSession<'_, HostedApp>) -> String {
    nornir_robotui::atom_labels(robot.harness_mut()).join("\n")
}

/// **Four recipe states, four different SHAPES of the graph.**
///
/// The rule (owner, 2026-08-26): *"kernel val skall dyka upp om man väljer gunnar och
/// den har en sådan feature i sig, men en annan build thing, säg ett library, skall ju
/// inte ha de i sig"* — the `kärna` node is present exactly when the resolved variant
/// has the feature.
///
/// RED WHEN REVERTED: drop the donor edge from the lowering and state 3 loses its
/// node; make the node unconditional and states 1–2 grow one.
#[test]
fn the_four_recipe_states_wear_four_different_shapes() {
    // ── STATE 1: nothing resolved ⇒ no graph, no words about kernels at all ──────
    let lib_dir = staged(library_declaration());
    let mut robot = hosted_session(lib_dir.path());
    assert_eq!(
        robot.state_at("/graph"),
        Some(Value::Null),
        "no recipe ⇒ no graph"
    );
    let text = on_screen(&mut robot);
    assert!(
        !text.contains("kärna"),
        "kernel words with nothing resolved:\n{text}"
    );
    assert!(
        !robot.label_visible("nuked"),
        "a preset strip with nothing resolved points at no declaration"
    );

    // ── STATE 2: a library ⇒ its variant, NO kärna ──────────────────────────────
    resolve(&mut robot, "orten");
    assert_eq!(
        robot.state_at("/recipe").map(|v| v.is_string()),
        Some(true),
        "the library recipe really resolved"
    );
    let ids = graph_ids(&robot);
    assert!(
        ids.iter().any(|i| i == "orten"),
        "the library is a node: {ids:?}"
    );
    assert!(
        !ids.iter().any(|i| i.contains("kernel")),
        "a library grew a kernel node: {ids:?}"
    );
    assert_eq!(kernel_node(&robot), None);
    assert!(!robot.label_visible("nuked"));

    // ── STATE 3: a DONOR-consuming installer ⇒ kärna with its EDGE, the donor a node ──
    let donor_dir = staged(donor_declaration());
    let mut robot = hosted_session(donor_dir.path());
    resolve(&mut robot, "orten");
    let ids = graph_ids(&robot);
    assert!(
        ids.iter().any(|i| i == "orten-installer/kernel"),
        "the installer's `kärna` node: {ids:?}"
    );
    assert!(
        ids.iter().any(|i| i == "tunnr/tunnr-kernel"),
        "the donor's kernel is a node the installer CONSUMES: {ids:?}"
    );
    let k = kernel_node(&robot).expect("a recipe whose installer binds a kernel_donor HAS a kernel answer — the node must exist to say it");
    assert_eq!(
        k["edge"],
        json!("tunnr/tunnr-kernel : kernel"),
        "the edge, typed: {k}"
    );
    let text = on_screen(&mut robot);
    assert!(
        text.contains("kärna") && text.contains("tunnr/tunnr-kernel"),
        "the node must NAME the donor on screen:\n{text}"
    );

    // ── STATE 4: an own-kernel recipe ⇒ kärna as the variant's own subgraph ─────
    let kern_dir = staged(&declaration());
    let mut robot = hosted_session(kern_dir.path());
    resolve(&mut robot, "orten-kernel");
    let ids = graph_ids(&robot);
    assert!(
        ids.iter().any(|i| i == "orten-kernel/kernel"),
        "the kernel variant's `kärna` node: {ids:?}"
    );
    assert!(
        ids.iter().any(|i| i == "orten-kernel/kernel.preset"),
        "kernel.preset under it: {ids:?}"
    );
    assert!(
        ids.iter()
            .filter(|i| i.starts_with("orten-kernel/kernel."))
            .count()
            > 10,
        "the symbols under it: {ids:?}"
    );
    let k = kernel_node(&robot).expect("`kärna`");
    assert_eq!(k["edge"], Value::Null);
    let text = on_screen(&mut robot);
    assert!(
        text.contains("kärna"),
        "the node is named on screen:\n{text}"
    );
    assert!(
        text.contains("eddas incheckade fragment"),
        "the honesty label wears the unfolded kernel subgraph:\n{text}"
    );
}