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

//! **ROBOT-UI: `BYGG <kant>` exists exactly on nodes with a consumption edge and
//! NOWHERE else, and clicking it produces the CLI line the console shows** (wave 3,
//! gap #4: `[BYGG kärnan]` ≡ `--build kernel`).
//!
//! | claim | RED when |
//! |---|---|
//! | a LIBRARY's menu bears no `BYGG <kant>` | a verb leaks onto a variant with no edge |
//! | an installer with a donor bears exactly ONE — `BYGG kärnan` — with its CLI line | the verb is missing, doubled, or spells a different line |
//! | clicking it plans the DAG donor-first in the step console, and the console's line IS the menu's | the two lines drift (law B/C across the doors) |
//! | the click reaches the CHAIN PLANNER, and an unresolvable donor is refused by that planner's name — never by a missing runner's | the wave-3 stub returns, or a silent no-op, or fake progress |
//! | an EDGELESS iso (korp's shape) bears NONE, and the refusal names whose decision the edge is | a verb to nowhere, or a refusal that hides the decision |
//!
//! Fixtures are staged (`orten`, never a product name); the last test reads the
//! constellation's own edgeless iso when one is on disk and SKIPS, saying so, otherwise.
//! RED BEFORE GREEN: on the parent commit `/menu/build_verbs`, `/steps` and
//! `/edge_build_receipt` did not exist, and the kärna node's flag column read
//! "gap #4 (kedjan finns inte än)".

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

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

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

struct SceneApp(BuildScene);

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

const LIBRARY: &str = r#"
[build]
base_suite = "ORIGINAL"

[[build.variant]]
name = "orten"
package = "orten"
role = "library"
default = true
suites = ["ORIGINAL"]
kind = "lib"
"#;

/// An installer that consumes a donor kernel — the `kärna` node with its edge.
const DONOR: &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" }

[[build.installer.options]]
name = "dhcp"
label = "DHCP"
feature = "dhcp"
"#;

/// korp's shape: a live iso that names NO kernel edge — the medium boots the host's
/// kernel, and whether it should be pinned to one is the product owner's decision.
const EDGELESS_ISO: &str = r#"
[build]
base_suite = "ORIGINAL"

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

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

[[build.iso]]
name = "orten-appliance-iso"
from_variant = "orten-server"
boot_params = ["console=ttyS0", "orten.entry=orten-server"]
"#;

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("lock");
    std::fs::write(repo.join(".nornir/build-variants.toml"), decl).expect("declaration");
    dir
}

fn session(forge: DeclaredForge) -> RobotSession<'static, SceneApp> {
    let scene = BuildScene::new(Box::new(forge));
    let harness = Harness::builder()
        .with_size(nornir_robotui::egui::Vec2::new(2400.0, 1600.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
}

fn resolve(robot: &mut RobotSession<'_, SceneApp>, name: &str) {
    robot
        .type_into("artefakt", name)
        .expect("the artefakt field");
    robot.click_label("sök").expect("sök");
    robot.run(2);
}

/// Every accessible label on screen that spells a `BYGG <kant>` verb — `BYGG` alone
/// is the variant's own build button and is NOT one of these.
fn bygg_edge_labels(robot: &mut RobotSession<'_, SceneApp>) -> Vec<String> {
    atom_labels(&*robot.harness_mut())
        .into_iter()
        .filter(|l| l.starts_with("BYGG ") && l.len() > "BYGG ".len())
        .collect()
}

fn build_verbs(robot: &RobotSession<'_, SceneApp>) -> Vec<Value> {
    robot
        .state_at("/menu/build_verbs")
        .and_then(|v| v.as_array().cloned())
        .unwrap_or_default()
}

/// **A library: no edge, no `BYGG <kant>`.** The plain BYGG is still there.
#[test]
fn a_library_bears_no_edge_verb() {
    let dir = staged(LIBRARY);
    let mut robot = session(DeclaredForge::at(dir.path().to_path_buf()));
    resolve(&mut robot, "orten");
    robot.click_label("orten").expect("the row");
    robot.run(2);
    robot.click_label("lib").expect("the column");
    robot.run(3);
    assert!(robot.label_visible("BYGG"), "the variant's own BYGG stays");
    assert_eq!(
        bygg_edge_labels(&mut robot),
        Vec::<String>::new(),
        "a library bears no BYGG <kant>"
    );
    assert!(build_verbs(&robot).is_empty());
    assert_eq!(
        robot.state_at("/menu/no_edge_why"),
        Some(Value::Null),
        "a lib is not a kind that could consume a kernel — no note either"
    );
    // The headless twin refuses too, by name.
    robot
        .harness_mut()
        .state_mut()
        .0
        .update_json(r#"{"bygg_edge":"orten"}"#);
    robot.run(2);
    assert_eq!(
        robot.state_at("/steps"),
        Some(Value::Null),
        "nothing planned"
    );
}

/// **An installer with a donor: exactly `BYGG kärnan`, and its line is the CLI's.**
/// Clicking plans donor-first, the console's line is the menu's, and with no runner
/// every step is refused BY NAME with the clock at zero.
#[test]
fn the_karna_node_bears_bygg_karnan_and_the_click_is_the_cli_line() {
    let dir = staged(DONOR);
    let mut robot = session(DeclaredForge::at(dir.path().to_path_buf()));
    resolve(&mut robot, "orten-installer");
    robot
        .harness_mut()
        .state_mut()
        .0
        .select_node("orten-installer");
    robot.run(3);
    assert_eq!(
        robot.state_at("/menu/variant"),
        Some(json!("orten-installer"))
    );

    // Exactly one verb, on the node with the edge.
    let labels = bygg_edge_labels(&mut robot);
    assert_eq!(
        labels,
        vec!["BYGG kärnan".to_string()],
        "one edge ⇒ one verb: {labels:?}"
    );
    let verbs = build_verbs(&robot);
    assert_eq!(verbs.len(), 1, "{verbs:?}");
    let v = &verbs[0];
    assert_eq!(v["path"], json!("kernel"));
    assert_eq!(v["coordinate"], json!("tunnr/tunnr-kernel"));
    assert_eq!(v["typ"], json!("kernel"));
    let cli = v["cli"]
        .as_str()
        .expect("the CLI line beside the verb")
        .to_string();
    assert!(
        cli.starts_with("dwarves buildthing orten --variant orten-installer --build kernel"),
        "the line the menu shows is the prompt's: {cli}"
    );
    assert!(
        robot.label_visible(&cli),
        "the CLI line is ON the menu, not only in the state"
    );
    // Law C: the kärna node's flag column reads the same `--build kernel`.
    let k = robot
        .state_at("/tree/roots")
        .and_then(|r| r.as_array().cloned())
        .and_then(|roots| roots.into_iter().find(|n| n["path"] == json!("kernel")))
        .expect("the kärna node");
    assert_eq!(k["flag"], json!("--build kernel"), "{k}");
    // Nothing planned before the click — the console is empty.
    assert_eq!(robot.state_at("/steps"), Some(Value::Null));

    robot
        .click_label("BYGG kärnan")
        .expect("the verb is a button");
    robot.run(3);

    let steps = robot
        .state_at("/steps")
        .expect("the step console has a plan");
    assert_eq!(
        steps["cli"],
        json!(cli),
        "the console's line IS the menu's line"
    );
    let rows = steps["steps"].as_array().expect("steps");
    assert_eq!(rows.len(), 2, "donor + consumer: {rows:?}");
    assert_eq!(rows[0]["role"], json!("donator"));
    assert_eq!(rows[0]["coordinate"], json!("tunnr/tunnr-kernel"));
    assert_eq!(rows[0]["typ"], json!("kernel"));
    assert_eq!(rows[1]["role"], json!("konsument"));
    assert_eq!(
        rows[1]["coordinate"],
        json!("orten/orten-installer"),
        "the consumer wears its COORDINATE — the key the chain runner registers it under"
    );
    // The honest expectation, with its source, on every step.
    for r in rows {
        assert!(
            r["expect"]["cold"].as_str().is_some_and(|s| !s.is_empty()),
            "{r}"
        );
        assert!(
            r["expect"]["cached"]
                .as_str()
                .is_some_and(|s| !s.is_empty()),
            "{r}"
        );
        assert!(
            r["expect"]["source"]
                .as_str()
                .is_some_and(|s| s.contains("förväntan")),
            "an expectation names itself: {r}"
        );
    }
    // The runner IS wired now (wave 3, W4): what refuses this click is the PLANNER,
    // because the staged constellation has no `tunnr` checkout to build the donor in.
    // A refusal naming the missing RUNNER here would mean the stub came back.
    assert_eq!(rows[0]["state"], json!("refused"));
    let line = rows[0]["line"].as_str().unwrap();
    assert!(
        !line.contains(MISSING_RUNNER),
        "the shipped forge wires the chain runner — a `{MISSING_RUNNER}` refusal is the wave-3 stub returning: {line}"
    );
    assert!(
        line.contains("tunnr"),
        "the refusal names the donor it could not resolve, so the repair is readable off the row: {line}"
    );
    assert!(
        robot.label_visible(line),
        "the refusal is on screen, verbatim"
    );
    assert_eq!(rows[1]["state"], json!("refused"));
    assert!(rows[1]["line"]
        .as_str()
        .unwrap()
        .contains("donatorn vägrades"));
    assert_eq!(
        steps["elapsed_ms"],
        json!(0),
        "no fake progress: the clock never started"
    );
    let receipt = robot
        .state_at("/edge_build_receipt")
        .expect("the receipt beside the other two");
    assert_eq!(receipt["verb"], json!("BYGG kärnan"));
    assert_eq!(receipt["cli"], json!(cli));
    assert!(
        receipt["outcome"]
            .as_str()
            .unwrap()
            .starts_with("refused: "),
        "{receipt}"
    );

    // The headless twin plans the same line.
    robot
        .harness_mut()
        .state_mut()
        .0
        .update_json(r#"{"bygg_edge":"kernel"}"#);
    robot.run(2);
    assert_eq!(robot.state_at("/steps/cli"), Some(json!(cli)));
}

/// **An edgeless iso — korp's shape: no verb, and the refusal names whose decision the
/// edge is.**
#[test]
fn an_edgeless_iso_bears_no_verb_and_names_whose_decision_the_edge_is() {
    let dir = staged(EDGELESS_ISO);
    let mut robot = session(DeclaredForge::at(dir.path().to_path_buf()));
    resolve(&mut robot, "orten-appliance-iso");
    robot
        .harness_mut()
        .state_mut()
        .0
        .select_node("orten-appliance-iso");
    robot.run(3);
    assert_eq!(
        robot.state_at("/menu/variant"),
        Some(json!("orten-appliance-iso"))
    );
    assert_eq!(robot.state_at("/menu/typ"), Some(json!("iso")));
    assert_eq!(
        bygg_edge_labels(&mut robot),
        Vec::<String>::new(),
        "no edge ⇒ no BYGG <kant>"
    );
    assert!(build_verbs(&robot).is_empty());
    let why = robot
        .state_at("/menu/no_edge_why")
        .and_then(|v| v.as_str().map(str::to_string))
        .expect("the refusal is named");
    assert!(
        why.contains("orten-appliance-iso")
            && why.contains("produktägarens beslut")
            && why.contains("kernel_donor"),
        "{why}"
    );
    assert!(robot.label_visible(&why), "the refusal is on the menu");
}

/// **The constellation's own edgeless iso**, when one is on disk. SKIPS, saying so.
#[test]
fn the_constellations_edgeless_iso_bears_no_verb() {
    let forge = DeclaredForge::discover();
    let builds = dwarves::build_thing::ConstellationBuilds::discover(
        &dwarves::build_thing::ConstellationBuilds::default_root(),
    );
    let Some((repo, iso)) = builds.repos.iter().find_map(|r| {
        r.set
            .isos
            .iter()
            .find(|i| i.kernel_donor.is_none() && i.edges.is_empty())
            .map(|i| (r.repo.clone(), i.name.clone()))
    }) else {
        eprintln!("SKIP: no checkout on this disk declares an edgeless [[build.iso]]");
        return;
    };
    eprintln!("live edgeless iso: {repo} · {iso}");
    let mut robot = session(forge);
    resolve(&mut robot, &repo);
    robot.harness_mut().state_mut().0.select_node(&iso);
    robot.run(3);
    assert_eq!(
        robot.state_at("/menu/variant"),
        Some(json!(iso)),
        "the iso is the menu's subject"
    );
    assert_eq!(
        bygg_edge_labels(&mut robot),
        Vec::<String>::new(),
        "{repo}'s `{iso}` names no edge ⇒ no BYGG <kant>"
    );
    let why = robot
        .state_at("/menu/no_edge_why")
        .and_then(|v| v.as_str().map(str::to_string))
        .expect("the refusal is named");
    assert!(
        why.contains("produktägarens beslut"),
        "the edge is the product owner's decision, and the refusal says so: {why}"
    );
}

/// ★ **THE LINE PRINTED UNDER THE BUTTON REPRODUCES THE CLICK, INCLUDING THE CELL'S OWN
/// `--opt`** — the CALL SITE, driven through the real widgets.
///
/// # Why this exists next to a unit test that looks like it already covers it
///
/// `build_steps::tests::the_printed_line_reproduces_the_click_including_the_cells_own_opt`
/// asserts the rule on a `StepPlan` it constructs itself. It cannot see the bug, because
/// the bug was never in `StepPlan` — it was in `BuildScene::push_build_verb`, which
/// spelled its own line from `Self::scoped_flags(n)` (the flags UNDER the clicked edge
/// node) and never asked the plan at all. A helper can be perfect while the one call
/// site that matters passes it something else; that is the same shape as the
/// hard-coded `0` in `nornir jobs --limit`, found the same day.
///
/// So this reads the line off `/menu/build_verbs` — the state the pane RENDERS — after
/// a real drive, and asserts the property the operator depends on.
///
/// # The oracle
///
/// `dhcp` is declared on the INSTALLER, so `--opt dhcp=…` rides the consumer's line and
/// is not under the `kärna` node at all. The old spelling therefore printed a command
/// starting at `--opt kernel.preset=…` while the button sent `dhcp` too. Both polarities
/// are driven: the declared default (`on`), and the operator's own untick (`off`), which
/// is the case that actually changes what is compiled in and the one a pasted line would
/// silently undo.
///
/// Order is asserted, not just membership: `routed_opts` puts the CONSUMER's line first
/// and the kernel grammar applies options IN ORDER, so `dhcp` leading is part of the
/// claim, not an accident of the fixture.
///
/// RED on the parent of this lane's `push_build_verb` change: the line began
/// `… --build kernel --opt kernel.preset=normal …` with no `dhcp` word anywhere.
#[test]
fn the_printed_line_carries_the_cells_own_opt_not_only_the_edge_subtrees() {
    let dir = staged(DONOR);
    let mut robot = session(DeclaredForge::at(dir.path().to_path_buf()));
    resolve(&mut robot, "orten-installer");
    robot
        .harness_mut()
        .state_mut()
        .0
        .select_node("orten-installer");
    robot.run(3);

    // The declared default: DHCP is on, and the printed line has to say so.
    robot.harness_mut().state_mut().0.select_node("kernel");
    robot.run(2);
    let on = build_verbs(&robot);
    let on = on
        .first()
        .and_then(|v| v["cli"].as_str())
        .expect("the BYGG kärnan line")
        .to_string();
    assert!(
        on.contains("--opt dhcp=on"),
        "`dhcp` is the INSTALLER's own option — it rides the consumer's line, not the \
         `kärna` subtree — and the command printed under the button dropped it: {on}"
    );
    assert!(
        on.starts_with("dwarves buildthing orten --variant orten-installer --build kernel --opt dhcp=on --opt kernel."),
        "the consumer's own line leads, then the donor's, because the kernel grammar \
         applies options IN ORDER: {on}"
    );

    // ★ The operator's own untick, through the REAL checkbox. This is the polarity that
    // changes the artifact: a pasted line without it builds the capability back in.
    robot
        .harness_mut()
        .state_mut()
        .0
        .select_node("orten-installer");
    robot.run(2);
    robot
        .click_label("DHCP")
        .expect("the DHCP checkbox is a real widget");
    robot.run(2);
    assert_eq!(
        robot.state_at("/settings/dhcp"),
        Some(json!("false")),
        "the untick did not land in the settings BYGG carries"
    );

    robot.harness_mut().state_mut().0.select_node("kernel");
    robot.run(2);
    let off = build_verbs(&robot);
    let off = off
        .first()
        .and_then(|v| v["cli"].as_str())
        .expect("the BYGG kärnan line")
        .to_string();
    assert!(
        off.contains("--opt dhcp=off"),
        "the operator unticked DHCP and the printed command still does not say so — \
         anyone who pastes it builds the capability back in: {off}"
    );
    assert!(
        !off.contains("--opt dhcp=on"),
        "both polarities on one line: {off}"
    );
    // The line the menu SHOWS is the line the state carries — not only a JSON field.
    assert!(
        robot.label_visible(&off),
        "the corrected line is on the menu, not only in the state"
    );
}