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