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"
);
}