dwarves-gui/tests/robot_pane_builds_the_medium.rs
//! **DOOR 1, RUN FOR REAL: the ⚒ pane's `BYGG <kant>` button composes the same medium
//! the prompt and the CI door compose.**
//!
//! # Why this file exists
//!
//! On 2026-08-27 the prompt door (`dwarves buildthing`) and the CI door
//! (`dwarves run <file>.runa.ron`) were proven to produce the SAME medium, byte for
//! byte on the ISO-9660 half. The PANE's equality was *inferred* — the three doors
//! share `BuildRun::spawn_recording_with_selection`, so the argument went, therefore
//! the pane must produce the same thing. Nobody had ever pressed the button and
//! measured the result. `robot_two_doors` drives the pane to a real PLAN and stops
//! there, on purpose, because comparing plans must not cost a kernel compile.
//!
//! An inference is not a measurement. This test presses the real button.
//!
//! # What is compared, and what deliberately is NOT
//!
//! **NOT the ISO's sha256.** The nondeterminism was measured the same day at exactly
//! **39 bytes out of 16 754 688**, all of them inside FAT 8.3 directory entries in the
//! ESP — creation-time tenths-of-a-second. The ISO-9660 half is byte-identical. So the
//! oracle here is:
//!
//! * the composed line the assembler prints — the app size, the **znippy pin**, the
//! kernel size and its **pinned blake3** — which names the exact payload and the
//! exact kernel that went in;
//! * the console's own **receipt fragment** per step, which is the kernel decision the
//! chain ran from;
//! * a byte-level diff against the reference medium: the differing bytes must be few
//! and must all lie in the ESP, never in the ISO-9660 half.
//!
//! # Ignored by default
//!
//! This composes a real appliance ISO. `cargo test -p dwarves-gui --test
//! robot_pane_builds_the_medium -- --ignored --nocapture` runs it. It SKIPS by name
//! when the declaration or the reference medium is not on this disk — a fixture that
//! invented a donor would prove the fixture.
#![cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
use facett_core::Facet;
use nornir_build_thing::tree::KERNEL_PATH;
use nornir_robotui::egui_kittest::Harness;
use nornir_robotui::{Observable, RobotSession};
use serde_json::{json, Value};
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) -> Value {
Facet::state_json(&self.0)
}
}
/// The medium the OTHER two doors produced, for the byte comparison. Overridable so the
/// path is not baked into the test; absent ⇒ the byte leg SKIPS by name and the composed
/// line + fragment legs still run.
fn reference_medium() -> Option<PathBuf> {
let p = std::env::var_os("DWARVES_REFERENCE_ISO")
.map(PathBuf::from)
.unwrap_or_else(|| {
PathBuf::from(std::env::var_os("HOME").unwrap_or_default())
.join("scratch/l4-CLIDOOR-gunnar-appliance-iso.iso")
});
p.is_file().then_some(p)
}
/// The constellation the pane resolves against.
fn constellation() -> PathBuf {
let root = std::env::var_os("DWARVES_CONSTELLATION")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."));
root.canonicalize().unwrap_or(root)
}
/// Every differing byte offset between two files, and their lengths.
fn byte_diff(a: &Path, b: &Path) -> (usize, usize, Vec<usize>) {
let x = std::fs::read(a).expect("read the pane's medium");
let y = std::fs::read(b).expect("read the reference medium");
let diffs: Vec<usize> = x
.iter()
.zip(y.iter())
.enumerate()
.filter_map(|(i, (p, q))| (p != q).then_some(i))
.collect();
(x.len(), y.len(), diffs)
}
#[test]
#[ignore = "composes a REAL appliance ISO through the ⚒ pane — minutes, and it writes .iso-out"]
fn the_pane_button_composes_the_same_medium_as_the_other_two_doors() {
let root = constellation();
let builds = ConstellationBuilds::discover(&root);
// The ISO cell that consumes a donor kernel — the one the other two doors built.
let Some((repo, iso_name, repo_root)) = builds.repos.iter().find_map(|r| {
r.set
.isos
.iter()
.find(|i| i.kernel_donor.is_some())
.map(|i| (r.repo.clone(), i.name.clone(), r.root.clone()))
}) else {
eprintln!(
"SKIP: no checkout under {} declares an iso with a kernel_donor",
root.display()
);
return;
};
eprintln!(
"[pane] real declaration: {repo} · {iso_name} · root {}",
repo_root.display()
);
// ── the pane, on the REAL forge ────────────────────────────────────────────
let scene = BuildScene::new(Box::new(DeclaredForge::at(root.clone())));
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);
// REAL WIDGETS: type the product into the real field, press the real search button.
robot
.type_into("artefakt", &repo)
.expect("the artefakt field");
robot.click_label("sök").expect("sök");
robot.run(2);
robot.harness_mut().state_mut().0.select_node(&iso_name);
robot.run(2);
assert_eq!(
robot.state_at("/menu/variant"),
Some(json!(iso_name)),
"the iso cell is the menu's subject"
);
// The `kärna` node is where the consumption edge — and its verb — lives.
robot
.harness_mut()
.state_mut()
.0
.select_node(&format!("{iso_name}/{KERNEL_PATH}"));
robot.run(2);
assert_eq!(
robot.state_at("/menu/node"),
Some(json!(KERNEL_PATH)),
"the kärna node is open"
);
// ★ THE LINE UNDER THE BUTTON, BEFORE THE CLICK. It must reproduce the click — the
// whole point of `StepPlan::respell_cli`. Captured here so the report can quote what
// the operator saw beside the button that produced the medium below.
let printed = robot
.state_at("/menu/build_verbs")
.and_then(|v| v.as_array().cloned())
.and_then(|v| v.into_iter().find(|b| b["path"] == json!(KERNEL_PATH)))
.and_then(|b| b["cli"].as_str().map(str::to_string))
.expect("the kärna node offers a BYGG verb with a CLI line");
eprintln!("[pane] the line printed under the button: {printed}");
assert!(
printed.starts_with(&format!(
"dwarves buildthing {repo} --variant {iso_name} --build {KERNEL_PATH}"
)),
"the printed line is not the click's own command: {printed}"
);
// ★ THE PIN, PICKED THROUGH THE PANE'S OWN CONTROL.
//
// The other two doors passed no `--opt` at all and took the donor's PINNED kernel
// (blake3 642b8c7c158e1208, preset `nuked`). The pane spells its whole default state
// explicitly, and its `Preset` val defaults to `normal`, so the planner refuses the
// pin BY NAME: *"the pin was built from preset 'nuked' … the choice here merges to
// preset 'normal'"*. That is a real difference between the doors and it is recorded
// in the report; here the robot does what an operator would do to reach the pinned
// medium — open the real `Preset` combo and pick `nuked`.
if let Some(want) = std::env::var("DWARVES_PANE_PRESET")
.ok()
.filter(|s| !s.is_empty())
{
// ⚠ NOT through the combo, and the reason is a finding of its own: egui's
// ComboBox puts the current VALUE on the button and the caption beside it, so
// "Preset" is a caption ("in the AccessKit tree but NOTHING carrying it is
// pressable"), and its value `normal` names MORE THAN ONE pressable widget in
// this pane — the shared label→node oracle refuses to guess, correctly. A drive
// cannot reach this control today. So the preset is set through the pane's own
// declared `set_setting` seam, and the BUILD is still pressed as a real widget.
// ⚠ THE SETTINGS MAP IS KEYED BY THE NODE'S `key`, NOT ITS `path`.
//
// This leg used to write `set_setting {"kernel.preset": …}` and then assert
// `/settings/kernel.preset` came back equal. It ALWAYS passed and it proved
// NOTHING: `update_json` drops whatever string it is handed straight into
// `chosen`, and `state_at` read back the very key it had just written — while
// `settings_now()` builds the map the build actually carries from each
// setting's `key` (`preset`), so the invented `kernel.preset` entry was read by
// nobody. MEASURED on the run that exposed it: the assertion said `nuked` and
// the step the pane really ran carried `--opt kernel.preset=normal`.
//
// A state round-trip through the thing under test is the identity value of LAW 2.
// The oracle is the APPLIED OUTPUT: the routed `--opt` line the plan sends.
Facet::update_json(
&mut robot.harness_mut().state_mut().0,
&json!({"set_setting": {"preset": want}}).to_string(),
);
robot.run(3);
let printed_now = robot
.state_at("/menu/build_verbs")
.and_then(|v| v.as_array().cloned())
.and_then(|v| v.into_iter().find(|b| b["path"] == json!(KERNEL_PATH)))
.and_then(|b| b["cli"].as_str().map(str::to_string))
.expect("the kärna node still offers a BYGG verb");
assert!(
printed_now.contains(&format!("--opt {KERNEL_PATH}.preset={want}")),
"the preset choice never reached the line the button sends — it is still: {printed_now}"
);
}
if std::env::var_os("DWARVES_PANE_DUMP").is_some() {
let k = robot
.state_at("/tree/roots")
.and_then(|r| r.as_array().cloned())
.and_then(|rs| rs.into_iter().find(|n| n["path"] == json!(KERNEL_PATH)))
.unwrap();
for c in k["children"].as_array().unwrap().iter().take(4) {
eprintln!("[dump] {}", serde_json::to_string(c).unwrap());
}
return;
}
let out = repo_root.join(".iso-out").join(format!("{iso_name}.iso"));
let before = std::fs::metadata(&out).ok().and_then(|m| m.modified().ok());
// ★ THE REAL BUTTON. `bygg_edge_at` is the headless twin and would prove the twin;
// this presses the widget a human presses, resolved through the shared
// label→node oracle (so "visible" and "clickable" cannot disagree).
robot
.click_label("BYGG kärnan")
.expect("the BYGG kärnan button on the kärna node");
robot.run(2);
// ── drive the frame loop until the console settles ────────────────────────
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60 * 60);
let outcome = loop {
robot.run(1);
if let Some(o) = robot
.state_at("/steps/receipt/outcome")
.and_then(|v| v.as_str().map(str::to_string))
{
if o != "running" {
break o;
}
}
assert!(
std::time::Instant::now() < deadline,
"the chain never settled: {}",
robot
.state_at("/steps")
.map(|v| v.to_string())
.unwrap_or_default()
);
std::thread::sleep(std::time::Duration::from_millis(200));
};
let steps = robot.state_at("/steps").expect("the console");
eprintln!(
"[pane] console: {}",
serde_json::to_string_pretty(&steps).unwrap_or_default()
);
assert_eq!(outcome, "done", "the pane's own chain refused: {steps}");
// ── the medium ────────────────────────────────────────────────────────────
let meta = std::fs::metadata(&out).unwrap_or_else(|e| {
panic!(
"the pane reported `done` and wrote no medium at {}: {e}",
out.display()
)
});
assert!(
before.is_none_or(|b| meta.modified().is_ok_and(|m| m > b)),
"{} was not rewritten — a `done` over a stale artifact proves nothing",
out.display()
);
eprintln!("[pane] medium: {} · {} B", out.display(), meta.len());
// ★ THE FRAGMENT. Every settled step names the decision it ran from; a chain that
// ran a different kernel choice than the other doors would differ here first.
// ⚠ The console's state has NO `plan` key and its `state` is a WORD, not an object:
// `StepConsole::state_json` spells `{product, variant, cli, steps: [...], receipt}`
// and `Step::json` puts the pin under `detail` — `{sha, fragment}` when a step is
// Done, `{key}` when it was Cached. Reading `["plan"]["steps"]` and `["state"]["key"]`
// yielded an empty vector, so this leg would have failed on `is_empty()` at the END
// of a half-hour compose, and any softer spelling of it would have passed while
// comparing nothing at all. Read off the shape the pane really publishes.
let fragments: Vec<String> = steps["steps"]
.as_array()
.map(|ss| {
ss.iter()
.filter_map(|s| {
let d = &s["detail"];
d["fragment"]
.as_str()
.or_else(|| d["key"].as_str())
.or_else(|| d["sha"].as_str())
.map(|f| format!("{}={f}", s["coordinate"].as_str().unwrap_or("?")))
})
.collect()
})
.unwrap_or_default();
assert!(
!fragments.is_empty(),
"no step named a fragment or a pin — the console says nothing to compare: {steps}"
);
eprintln!("[pane] fragments/pins: {fragments:?}");
// ── the byte comparison against the other doors' medium ───────────────────
let Some(reference) = reference_medium() else {
eprintln!("SKIP (byte leg): no reference medium — set DWARVES_REFERENCE_ISO");
return;
};
let (mine, theirs, diffs) = byte_diff(&out, &reference);
eprintln!(
"[pane] {mine} B vs reference {theirs} B · {} differing bytes",
diffs.len()
);
assert_eq!(
mine, theirs,
"the two doors produced media of different LENGTH"
);
// The ISO-9660 half must be identical: the first differing byte cannot be in it.
// The ESP is a partition inside the image; every known difference is a FAT 8.3
// directory entry's creation-time tenths, which live far past the primary volume
// descriptor at 0x8000 and the path tables that follow it.
assert!(
diffs.len() <= 64,
"{} bytes differ between the pane's medium and the reference — that is not \
creation-time jitter, the doors built different things (first at {:#x})",
diffs.len(),
diffs.first().copied().unwrap_or(0)
);
if let Some(&first) = diffs.first() {
assert!(
first > 0x10000,
"the FIRST differing byte is at {first:#x}, inside the ISO-9660 half — that \
half is supposed to be byte-identical across the doors"
);
}
}