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

//! **ROBOT-UI: the thin appliance — the tab finds the constellation, and the live
//! medium's tunnel is a real choice on it.**
//!
//! Two claims, both on the constellation's REAL declaration (no fixture invents a
//! product here), driven headless through the live widget tree:
//!
//! 1. **The tab finds the constellation from a plausible launch cwd.** `default_root`
//!    was `cwd.parent()` — an arithmetic guess — and answered `/home` when the ui was
//!    launched from `$HOME` on 2026-08-27, so the ⚒ roster held one repo and `sök`
//!    could not find gunnar at all. The red half is asserted here too: the old guess's
//!    answer (`/home`) is read for a roster and must NOT hold gunnar, so this test says
//!    what it protects rather than merely passing.
//!
//! 2. **`[[build.iso.options]]` is a choice, not a decoration.** The row has been a
//!    field on `IsoBuild` and been VALIDATED since wave 2, and `SettingsTree::of_iso`
//!    has always drawn a checkbox for it — while nothing on the ISO assemble leg
//!    applied it: the payload was staged from the untouched variant set, so an iso
//!    toggle moved a checkbox and left the shipped binary byte-identical. The applier
//!    is `dwarves::assemble_cell::resolve_cell_options` (the one resolver both cell
//!    kinds route through), and the pane's own toggle line is fed to it VERBATIM here —
//!    law B at the option axis: the click and the prompt's `--opt wireguard=off` reach
//!    the same build space.
//!
//! The RED for claim 2 is in the test: the same real cell with its `options` emptied —
//! the world before this declaration — is asserted to draw no such node and to REFUSE
//! the flag by name. Without that, a green here could not tell an applied toggle from
//! an accepted-and-ignored one.
//!
//! No build is run: composing this medium is a kernel compile plus a musl link. What
//! is compared is the build space both doors resolve, which is what the artifact then
//! comes out of.

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

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

use dwarves::build_thing::ConstellationBuilds;
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)
    }
}

/// The live appliance medium and the row it packs, as gunnar spells them.
const REPO: &str = "gunnar";
const ROW: &str = "gunnar-server";
const ISO: &str = "iso:gunnar-appliance-iso";
/// The recipe's own word for the toggle — the label the operator reads.
const WG: &str = "WireGuard-klient";

/// The constellation as the ui would find it, plus the home the walk stops at.
///
/// `DWARVES_CONSTELLATION` wins when set (a lane may point this elsewhere); otherwise
/// the launch site is `$HOME` on purpose — that is the cwd that broke the roster on
/// 2026-08-27, and the only launch site whose correct answer needs the child-directory
/// leg of `root_above`.
fn constellation() -> Option<(std::path::PathBuf, std::path::PathBuf)> {
    let home = std::path::PathBuf::from(std::env::var_os("HOME")?);
    // `default_root` reads `$DWARVES_REPO_ROOT` before the cwd, so the launch site is
    // set rather than `chdir`-ed: an integration binary's tests share one process and a
    // cwd change would reach the others.
    if std::env::var_os("DWARVES_CONSTELLATION").is_none() {
        std::env::set_var("DWARVES_REPO_ROOT", &home);
    }
    let root = ConstellationBuilds::default_root();
    Some((root, home))
}

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(1600.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);
    robot
}

/// The settings the pane carries on one row's one column, as `/rows` renders them.
fn settings_on(
    robot: &RobotSession<'_, SceneApp>,
    row: &str,
    column: &str,
) -> Vec<serde_json::Value> {
    robot
        .state_at("/rows")
        .and_then(|r| r.as_array().cloned())
        .unwrap_or_default()
        .iter()
        .find(|r| r["name"] == json!(row))
        .and_then(|r| r["targets"].as_array().cloned())
        .unwrap_or_default()
        .iter()
        .find(|t| t["name"] == json!(column))
        .and_then(|t| t["settings"].as_array().cloned())
        .unwrap_or_default()
}

#[test]
fn the_build_thing_tab_finds_gunnar_from_a_launch_cwd_of_home() {
    let Some((root, home)) = constellation() else {
        eprintln!("SKIP: no $HOME to launch from");
        return;
    };
    let gunnar_decl = home
        .join("git")
        .join(REPO)
        .join(".nornir")
        .join("build-variants.toml");
    if !gunnar_decl.is_file() {
        eprintln!("SKIP: {} is not on this disk", gunnar_decl.display());
        return;
    }

    // ── THE RED: the arithmetic guess, asked the same question ────────────────
    // `cwd.parent()` of `$HOME` is `/home`, and a roster read there is the 2026-08-27
    // failure verbatim: it finds SOMETHING (so an empty-roster assertion would not
    // catch it) and it does not find gunnar.
    let old_guess = home
        .parent()
        .map(std::path::PathBuf::from)
        .expect("$HOME has a parent");
    let by_guess = ConstellationBuilds::discover(&old_guess);
    assert!(
        !by_guess.repos.iter().any(|r| r.repo == REPO),
        "the OLD guess ({}) found gunnar, so this test cannot tell the fix from the bug: {:?}",
        old_guess.display(),
        by_guess
            .repos
            .iter()
            .map(|r| r.repo.clone())
            .collect::<Vec<_>>(),
    );

    // ── THE GREEN: found by evidence ─────────────────────────────────────────
    let builds = ConstellationBuilds::discover(&root);
    let names: Vec<String> = builds.repos.iter().map(|r| r.repo.clone()).collect();
    assert!(
        names.iter().any(|n| n == REPO),
        "the roster the ⚒ tab serves from root {} does not hold gunnar: {names:?}",
        root.display(),
    );
    eprintln!(
        "root {} · {} repo(s) · gunnar found",
        root.display(),
        names.len()
    );

    // ── and the TAB itself: sök gunnar, and its variants are on screen ────────
    let mut robot = session(&root);
    robot
        .type_into("artefakt", REPO)
        .expect("the artefakt field");
    robot.click_label("sök").expect("sök");
    robot.run(3);
    let rows: Vec<String> = robot
        .state_at("/rows")
        .and_then(|r| r.as_array().cloned())
        .unwrap_or_default()
        .iter()
        .filter_map(|r| r["name"].as_str().map(str::to_string))
        .collect();
    assert!(
        rows.iter().any(|r| r == ROW),
        "the ⚒ matrix offers no `{ROW}` row: {rows:?}"
    );
    assert!(
        rows.iter().any(|r| r == "gunnar-appliance-iso"),
        "the ⚒ matrix offers no appliance-iso variant: {rows:?}"
    );
    // The row is a real widget, and the iso column on it is reachable.
    robot
        .click_label(ROW)
        .expect("the appliance row is a real widget");
    robot.run(2);
    robot
        .click_label(ISO)
        .unwrap_or_else(|e| panic!("the `{ISO}` column is not clickable: {e}"));
    robot.run(3);
    assert_eq!(
        robot.state_at("/target"),
        Some(json!(ISO)),
        "the iso column is not the subject"
    );
}

#[test]
fn the_live_mediums_tunnel_is_a_toggle_and_the_click_is_the_prompts_flag() {
    let Some((root, home)) = constellation() else {
        eprintln!("SKIP: no $HOME to launch from");
        return;
    };
    if !home
        .join("git")
        .join(REPO)
        .join(".nornir")
        .join("build-variants.toml")
        .is_file()
    {
        eprintln!("SKIP: gunnar is not on this disk");
        return;
    }
    let builds = ConstellationBuilds::discover(&root);
    let Some(repo) = builds.repo(REPO) else {
        eprintln!("SKIP: root {} does not hold gunnar", root.display());
        return;
    };
    let set = repo.set.clone();
    let iso = set
        .isos
        .iter()
        .find(|i| i.name == "gunnar-appliance-iso")
        .cloned()
        .expect("gunnar declares the appliance iso");
    assert_eq!(
        iso.from_variant.as_deref(),
        Some(ROW),
        "the iso packs the server row"
    );

    // ── THE RED, first: the same cell with its options emptied ────────────────
    // The world before the declaration. The tree draws no such node, and the flag is
    // refused BY NAME rather than accepted and applied to nothing.
    let bare = nornir_build_thing::iso::IsoBuild {
        options: Vec::new(),
        ..iso.clone()
    };
    let bare_tree = nornir_build_thing::tree::SettingsTree::of_iso(&bare, None);
    assert!(
        bare_tree.get("wireguard").is_none(),
        "an option-less iso already offers a `wireguard` node — the red is not red"
    );
    let e = dwarves::assemble_cell::resolve_cell_options(
        "iso 'gunnar-appliance-iso'",
        &set,
        &bare_tree,
        &bare.options,
        &bare.params,
        bare.from_variant.as_deref(),
        &["wireguard=off".to_string()],
    )
    .expect_err("an undeclared toggle was accepted");
    let e = format!("{e:#}");
    assert!(
        e.contains("wireguard"),
        "the refusal of an undeclared toggle does not name it: {e}"
    );

    // ── THE PANE: the toggle is a real, ticked checkbox on the ISO column ─────
    let mut robot = session(&root);
    robot
        .type_into("artefakt", REPO)
        .expect("the artefakt field");
    robot.click_label("sök").expect("sök");
    robot.run(3);
    robot.click_label(ROW).expect("the appliance row");
    robot.run(2);
    robot.click_label(ISO).expect("the iso column");
    robot.run(3);
    let settings = settings_on(&robot, ROW, ISO);
    let wg = settings
        .iter()
        .find(|s| s["key"] == json!("wireguard"))
        .unwrap_or_else(|| panic!("the iso column carries no `wireguard` setting: {settings:?}"));
    assert_eq!(
        wg["option"]["label"],
        json!(WG),
        "the label is not the recipe's own word"
    );
    assert_eq!(
        wg["option"]["feature"],
        json!("wg-client"),
        "the toggle decides the wrong feature"
    );
    assert!(
        robot.label_visible(WG),
        "the tunnel opt-out has no control on the iso column"
    );
    assert_eq!(
        robot.field_checked(&Target::Label(WG.into())),
        Some(true),
        "an opt-out must ship unless somebody opts out"
    );
    assert_eq!(
        robot.state_at("/feature_delta"),
        Some(json!("features: +wg-client"))
    );

    // ── the CLICK ────────────────────────────────────────────────────────────
    robot
        .click_label(WG)
        .expect("the wireguard checkbox is clickable");
    robot.run(3);
    assert_eq!(robot.field_checked(&Target::Label(WG.into())), Some(false));
    assert_eq!(
        robot.state_at("/feature_delta"),
        Some(json!("features: \u{2212}wg-client")),
        "the delta does not follow the toggle"
    );

    // ── LAW B: that click, spelled as the prompt spells it, reaches the same
    //    build space through the CLI door's own resolver ─────────────────────
    // The TREE is the third argument since lane AF: it is what tells a feature
    // from a val from a donor's pair, so the door hands it the iso's own tree.
    let iso_tree = nornir_build_thing::tree::SettingsTree::of_set(&set, &iso.name)
        .expect("the iso declares a settings tree");
    let (thin, resolved) = dwarves::assemble_cell::resolve_cell_options(
        "iso 'gunnar-appliance-iso'",
        &set,
        &iso_tree,
        &iso.options,
        &iso.params,
        iso.from_variant.as_deref(),
        &["wireguard=off".to_string()],
    )
    .expect("the declared toggle resolves");
    let before = set
        .variants
        .iter()
        .find(|v| v.name == ROW)
        .expect("the row")
        .features
        .clone();
    let after = thin
        .variants
        .iter()
        .find(|v| v.name == ROW)
        .expect("the row")
        .features
        .clone();
    assert!(
        before.iter().any(|f| f == "wg-client"),
        "the declared row does not carry wg-client, so subtracting it proves nothing: {before:?}"
    );
    assert!(
        !after.iter().any(|f| f == "wg-client"),
        "`--opt wireguard=off` left wg-client in the build space the payload is staged from: {after:?}"
    );
    assert_eq!(
        after.len(),
        before.len() - 1,
        "the toggle changed more than the one feature it decides: {before:?} → {after:?}"
    );
    assert_eq!(
        resolved.feature_delta(),
        "features: \u{2212}wg-client",
        "the CLI door's delta"
    );
    // And the symbol families the medium must now be read back for — the ISO leg's
    // `refuse_forbidden_symbols` subject, declared rather than assumed.
    let forbidden: Vec<String> = resolved
        .forbidden_symbols
        .iter()
        .flat_map(|(_, f)| f.clone())
        .collect();
    assert!(
        forbidden.iter().any(|f| f == "wg_appliance_core") && forbidden.iter().any(|f| f == "boringtun"),
        "the off toggle forbids no symbol family, so the read-back would measure nothing: {forbidden:?}"
    );
}