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

//! **ROBOT-UI: the `kernel` KIND is a clickable cell in the ⚒ matrix — and the
//! sugar spelling is not invisible.**
//!
//! edda's `kernel` KIND landed on main 2026-08-22 (`[[build.kernel]]`: a pinned
//! kernel.org tarball compiled inside a toolchain container, presets
//! full/normal/slim/nuked). Three real kernels have been built through it. None
//! of that is worth anything to an operator if the declaration has no cell to
//! click, and the ONLY thing dwarves knew about the kind was
//! `role_of_kind() => Role::Server` in the CLI.
//!
//! This file drives the REAL widgets (never `update_json`), the way
//! `robot_build_scene_ui.rs` does, and holds two claims apart:
//!
//! 1. a `kind = "kernel"` variant row resolves, is clickable, and opens NO pane —
//!    a bare `bzImage` boots nothing without a medium or a rootfs around it, so
//!    `PaneKind::for_artifact(Kernel)` is `None` ON PURPOSE. The counterweight is
//!    the `exe` row in the same fixture opening the Exec pane, so "no pane" is a
//!    statement about this kind rather than a pane that is always null.
//!
//! 2. **the two spellings are the same declaration.** edda calls
//!    `[[build.kernel]]` "permanent sugar" for `kind = "kernel"`, exactly like
//!    `[[build.iso]]` / `[[build.installer]]`. A repo that writes only the sugar —
//!    which is what the design document tells appliances to write — used to
//!    produce NO ROW AT ALL in this pane, because `menu_for` walked `set.variants`
//!    and the sugar lands in `set.kernels`. Declared, listed by the CLI, and
//!    unreachable in the matrix: the same defect as the 2026-08-16 installer
//!    column, which is recorded in `build_forge.rs` a few lines away.
//!
//! 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 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)
    }
}

/// Stage a checkout on disk. `declaration` is the whole
/// `.nornir/build-variants.toml`, so each arm below states its own spelling and
/// nothing is shared but the crate skeleton.
///
/// The names are deliberately not any product's: the scene's own guard forbids a
/// product name in its source, and a fixture that spelled a real appliance would
/// make the claims here unfalsifiable.
fn staged(declaration: &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"),
        declaration,
    )
    .expect("declaration");
    dir
}

/// A syntactically valid pin. The kind REFUSES an unpinned kernel by name, so a
/// fixture without this parses into a declaration the pane would be right to
/// reject — and then this file would be testing the refusal, not the cell.
const PIN: &str = "8b8bb37e4ddbe4bc9b62931d29e0a58fda706a58bbebeba549e2274db34bb122";

/// Both spellings, side by side: one cargo row, one kernel row written the long
/// way, and the `[[build.kernel]]` payload the row resolves to at build time.
fn field_spelling() -> 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.4"
tarball_sha256 = "{PIN}"
preset = "slim"
"#
    )
}

/// The SUGAR alone — no `kind = "kernel"` variant row. This is what the design
/// document tells an appliance to write, and what produced nothing at all.
fn sugar_spelling() -> 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.kernel]]
name = "orten-kernel"
version = "6.12.4"
tarball_sha256 = "{PIN}"
preset = "nuked"
"#
    )
}

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
}

/// The column strip on one row, by name, read out of the pane's own state.
fn strip(robot: &RobotSession<'_, SceneApp>, row: usize) -> Vec<String> {
    robot
        .state_at(&format!("/rows/{row}/targets"))
        .and_then(|v| v.as_array().cloned())
        .unwrap_or_default()
        .iter()
        .map(|t| t["name"].as_str().unwrap_or_default().to_string())
        .collect()
}

/// Which row index carries `name` — the fixture's order is not this test's
/// business, and asserting on a hardcoded index is how a test starts passing for
/// the wrong reason.
fn row_index(robot: &RobotSession<'_, SceneApp>, name: &str) -> usize {
    robot
        .state_at("/rows")
        .and_then(|v| v.as_array().cloned())
        .unwrap_or_default()
        .iter()
        .position(|r| r["name"].as_str() == Some(name))
        .unwrap_or_else(|| panic!("no row named {name:?} in the matrix"))
}

#[test]
fn a_robot_clicks_a_kernel_row_and_gets_a_cell_that_offers_no_run() {
    let dir = staged(&field_spelling());
    let mut robot = session(dir.path());

    // RED — nothing is resolved before the search.
    assert_eq!(
        robot
            .state_at("/rows")
            .and_then(|v| v.as_array().map(Vec::len)),
        Some(0)
    );
    assert_eq!(robot.state_at("/target"), Some(serde_json::Value::Null));
    assert_eq!(robot.state_at("/pane"), Some(serde_json::Value::Null));

    // Step 1 — the real field, the real button.
    robot
        .type_into("artefakt", "orten-kernel")
        .expect("the artefakt field is reachable");
    robot
        .click_label("sök")
        .expect("the sök button is reachable");
    robot.run(2);

    // Step 2 — the kernel is a ROW, and `kernel` is a COLUMN of the matrix.
    let k = row_index(&robot, "orten-kernel");
    let e = row_index(&robot, "orten-server");
    assert!(
        strip(&robot, k).contains(&"kernel".to_string()),
        "{:?}",
        strip(&robot, k)
    );
    assert_eq!(
        strip(&robot, k),
        strip(&robot, e),
        "the strip is the MATRIX: every row carries every column, declared or greyed"
    );
    let kernel_col = strip(&robot, k)
        .iter()
        .position(|c| c == "kernel")
        .expect("the column");
    assert_eq!(
        robot.state_at(&format!("/rows/{k}/targets/{kernel_col}/declared")),
        Some(serde_json::json!(true)),
        "the kernel row declares its own kernel cell"
    );
    assert_eq!(
        robot.state_at(&format!("/rows/{e}/targets/{kernel_col}/declared")),
        Some(serde_json::json!(false)),
        "…and the cargo row carries the same cell GREYED — that pair is what makes \
         `declared` mean something"
    );

    // Step 3 — CLICK the row, then the column, through the live widget tree.
    robot
        .click_label("orten-kernel")
        .expect("the kernel row is a real, clickable widget");
    robot.run(2);
    assert_eq!(
        robot.state_at("/row"),
        Some(serde_json::json!("orten-kernel"))
    );
    robot
        .click_label("kernel")
        .expect("the kernel column is a real, clickable widget");
    robot.run(2);
    assert_eq!(robot.state_at("/target"), Some(serde_json::json!("kernel")));
    assert_eq!(
        robot.state_at(&format!("/rows/{k}/targets/{kernel_col}/kind")),
        Some(serde_json::json!("kernel"))
    );

    // THE CLAIM: a built kernel offers NO run pane. A bare bzImage boots nothing
    // without a medium or a rootfs around it, so an "▶ run" here would be a
    // button that cannot work.
    assert_eq!(
        robot.state_at("/pane"),
        Some(serde_json::json!("none")),
        "a bzImage is not a runnable artifact and must not be offered as one"
    );
    // …and it IS buildable: the cell has no unmet declared input.
    assert_eq!(robot.state_at("/can_build"), Some(serde_json::json!(true)));

    // Step 4 — THE COUNTERWEIGHT. The same gestures on the cargo row DO open a
    // pane, so step 3's absence is about this KIND and not about a pane that is
    // always null in this fixture.
    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!(robot.state_at("/target"), Some(serde_json::json!("exe")));
    assert_eq!(
        robot.state_at("/pane"),
        Some(serde_json::json!("exec")),
        "the counterweight is dead: this fixture opens no pane for anything"
    );
}

#[test]
fn the_sugar_spelling_is_the_same_declaration_and_gets_the_same_row() {
    let dir = staged(&sugar_spelling());
    let mut robot = session(dir.path());

    robot.type_into("artefakt", "orten-kernel").expect("field");
    robot.click_label("sök").expect("button");
    robot.run(2);

    // ★ RED-FIRST 2026-08-22. Before the fix this found NO ROW: `menu_for`
    // walked `set.variants`, and a `[[build.kernel]]` table lands in
    // `set.kernels`. The declaration was valid, the CLI listed it, and the
    // matrix had nowhere to click — the 2026-08-16 installer defect again.
    let rows: Vec<String> = robot
        .state_at("/rows")
        .and_then(|v| v.as_array().cloned())
        .unwrap_or_default()
        .iter()
        .map(|r| r["name"].as_str().unwrap_or_default().to_string())
        .collect();
    assert!(
        rows.contains(&"orten-kernel".to_string()),
        "a `[[build.kernel]]` declaration has no row in the matrix — rows: {rows:?}"
    );

    let k = row_index(&robot, "orten-kernel");
    let kernel_col = strip(&robot, k)
        .iter()
        .position(|c| c == "kernel")
        .expect("the column");
    assert_eq!(
        robot.state_at(&format!("/rows/{k}/targets/{kernel_col}/declared")),
        Some(serde_json::json!(true)),
        "the sugar row must DECLARE its cell, not carry it greyed like a bystander"
    );

    // It clicks like any other cell, and answers the same way.
    robot
        .click_label("orten-kernel")
        .expect("the sugar row is clickable");
    robot.run(2);
    robot.click_label("kernel").expect("its cell is clickable");
    robot.run(2);
    assert_eq!(
        robot.state_at("/row"),
        Some(serde_json::json!("orten-kernel"))
    );
    assert_eq!(robot.state_at("/target"), Some(serde_json::json!("kernel")));
    assert_eq!(robot.state_at("/pane"), Some(serde_json::json!("none")));

    // The DECLARED preset reaches the operator. `nuked` and `slim` build kernels
    // that differ by 86 KiB and by what an appliance can do, so a pane that
    // showed the kind and hid the preset would be showing the half that does not
    // decide anything.
    let settings = robot
        .state_at(&format!("/rows/{k}/targets/{kernel_col}/settings"))
        .and_then(|v| v.as_array().cloned())
        .unwrap_or_default();
    let shown: Vec<(String, String)> = settings
        .iter()
        .map(|s| {
            (
                s["key"].as_str().unwrap_or_default().to_string(),
                s["value"].as_str().unwrap_or_default().to_string(),
            )
        })
        .collect();
    assert!(
        shown.iter().any(|(n, v)| n == "kind" && v == "kernel"),
        "the cell must say what it is: {shown:?}"
    );
    assert!(
        shown.iter().any(|(n, v)| n == "preset" && v == "nuked"),
        "the DECLARED preset must reach the operator: {shown:?}"
    );
    assert!(
        shown.iter().any(|(n, v)| n == "version" && v == "6.12.4"),
        "…and which kernel it is: {shown:?}"
    );
}