dwarves-gui/tests/robot_gunnar_installer.rs
//! **ROBOT-UI: the ONE click, driven — `gunnar` → `installer:gunnar-installer` → ▶ KÖR.**
//!
//! Everything else in this tree tests the ⚒ scene against a STAGED constellation, which
//! is right for a scene that must know no product by name. This file is the other half:
//! the operator's actual gesture, on the REAL checkouts this box holds, ending in a REAL
//! QEMU/OVMF/KVM install whose serial says `INSTALLER: INSTALL-OK` and whose framebuffer
//! reaches the pane as a texture-sized `screen`.
//!
//! It drives the scene through its REAL widgets, resolved through the live AccessKit
//! tree — `type_into("artefakt", …)`, `click_label("sök")`, the row, the column, `BYGG`,
//! `▶ KÖR`. It never calls `update_json`, and it never opens a window: the whole drive is
//! off-screen `egui_kittest` (robot-UI first).
//!
//! `#[ignore]` because it BUILDS and BOOTS: it wants QEMU + OVMF + `/dev/kvm` + a
//! readable kernel, it masters an ISO, and it takes minutes. Run it deliberately:
//!
//! ```text
//! cargo test -p dwarves-gui --features vm-boot --test robot_gunnar_installer \
//! -- --ignored --nocapture
//! ```
//!
//! gunnar is the DEFAULT product, not the only one: see [`named`] for the three env
//! knobs that point the same gesture at another repo's `[[build.installer]]`.
//!
//! # WHAT THIS PROVES, and the stand-in it used to prove instead
//!
//! **`▶ KÖR` boots the medium `BYGG` composed.** Every leg is asserted against the
//! product, by name:
//!
//! ```text
//! BYGG ok=true, a named artifact, that file on disk before ▶ is ever pressed
//! ▶ KÖR the console names THAT file as the medium and never says STAND-IN
//! install korp-boot reads a cmdline carrying korp.entry=<p> the product, off the ISO
//! INSTALLER: INSTALL-OK the install itself
//! disk the installed ESP's korp/cmdline carries APPLIED OUTPUT, read off
//! korp.entry=<p> the bytes the installer wrote
//! reboot a SECOND boot off that disk reaches korp-start's the owner's "verifiera att
//! `exec argv: ["<p>"` — the last line before fexecve det går att boota om"
//! screen the winning framebuffer DIFFERS from every other the boot logo, per product
//! product's, byte for byte
//! ```
//!
//! ## The stand-in this replaced, and why the frame gate is what it is
//!
//! Until 2026-08-16 `BootRun::boot_installer` compiled its own UEFI installer stub
//! (`korp-installer-efistub`, carrying an EMBEDDED korp `app.znippy`), mastered its own
//! ISO into `/tmp/dwarves-installer-<cell>-<pid>/`, and booted THAT. The medium at
//! `<repo>/.iso-out/<cell>.iso` was never opened.
//!
//! MEASURED that day, and from a run, not from the code: driving holger, `BYGG` went
//! `FEL` and produced **no artifact at all**, and ▶ still reached
//! `INSTALLER: INSTALL-OK`. gunnar's and holger's captured frames were **md5-identical,
//! all six each** — `c9573786…`, `7d4764e9…`, `76ec6119…`, `0e45ee0e…`, `0ecccb27…`,
//! `32b65f91…` — because it was one image every time.
//!
//! That is why [`the_frame_is_this_products_own`] exists and why it compares against
//! OTHER products' frames rather than measuring ink: an ink count, a coverage ratio and
//! "did we get a frame" were all green on the stand-in. Only "this is not the same
//! picture holger booted" could ever have caught it.
//!
//! Every refusal is reported BY CODE where the launcher speaks one (`dwarves-launch-N`):
//! a robot keys on the code, not the sentence.
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::{Duration, Instant};
use facett_core::Facet;
use nornir_robotui::egui_kittest::Harness;
use nornir_robotui::{Observable, RobotSession};
use dwarves_gui::build_forge::DeclaredForge;
use dwarves_gui::build_scene::BuildScene;
/// The checkout the operator types.
const REPO: &str = "gunnar";
/// The row the installer variant hangs off (`[[build.installer]].variant`).
const ROW: &str = "gunnar-server";
/// The column label the forge renders for a `[[build.installer]]` variant.
const COLUMN: &str = "installer:gunnar-installer";
/// **The same drive, pointed at another product's installer.**
///
/// The gesture is not gunnar's — it is the operator's, and every declared
/// `[[build.installer]]` renders the same three widgets. Copying this file per
/// product would be 325 lines of twin (LAW 5) that drift the first time the pane
/// changes, so the three names it types are overridable and default to gunnar's:
///
/// ```text
/// DWARVES_ROBOT_REPO=korp \
/// DWARVES_ROBOT_ROW=korp-server \
/// DWARVES_ROBOT_COLUMN=installer:korp-installer \
/// cargo test -p dwarves-gui --features vm-boot --test robot_gunnar_installer \
/// -- --ignored --nocapture
/// ```
///
/// A fourth knob, `DWARVES_ROBOT_PRODUCT`, names the `korp.entry=` the installed disk
/// must start; it defaults to the repo name, which is what all three products declare.
///
/// Nothing else is parameterised. The assertions — a landed column pick, an armed BYGG,
/// the composed medium named on the console, `korp.entry=<product>` on the booted AND on
/// the installed cmdline, `INSTALL-OK`, the reboot's handoff line, a frame no other
/// product drew — are product-independent CLAIMS about a product-specific artifact, and
/// a knob that could weaken one of them would be a knob that turns the proof off.
fn named(var: &str, fallback: &'static str) -> String {
std::env::var(var)
.ok()
.filter(|v| !v.trim().is_empty())
.unwrap_or_else(|| fallback.to_string())
}
/// The scene as the robot's subject — the SAME `Facet::state_json` the shell exposes.
struct SceneApp(BuildScene);
impl Observable for SceneApp {
fn state_json(&self) -> serde_json::Value {
Facet::state_json(&self.0)
}
}
/// **Where the real checkouts are.** `ConstellationBuilds::default_root()` is the
/// PARENT of the current directory, and a `cargo test` runs in the PACKAGE root
/// (`…/dwarves/dwarves-gui`) — so its default lands inside dwarves and discovers
/// nothing at all. Named here rather than worked around: `DWARVES_CONSTELLATION` is the
/// same override the shipped binary reads, and the fallback climbs the two levels a
/// test's cwd costs.
fn constellation_root() -> std::path::PathBuf {
if let Some(r) = std::env::var_os("DWARVES_CONSTELLATION") {
return std::path::PathBuf::from(r);
}
let here = std::env::current_dir().expect("cwd");
let up = here
.parent()
.and_then(|p| p.parent())
.map(std::path::Path::to_path_buf);
up.unwrap_or(here)
}
/// Mount the scene over the REAL constellation, off-screen.
fn session() -> RobotSession<'static, SceneApp> {
let root = constellation_root();
eprintln!("[robot] constellation : {}", root.display());
let forge = DeclaredForge::at(root);
eprintln!("[robot] boot engine : {}", forge.boot_engine());
eprintln!(
"[robot] declared repos: {}",
forge.declared_repos().join(" ")
);
let scene = BuildScene::new(Box::new(forge));
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
}
/// Pump real frames until `done` or the deadline. Returns whether it landed.
fn pump(
robot: &mut RobotSession<'_, SceneApp>,
secs: u64,
what: &str,
done: impl Fn(&RobotSession<'_, SceneApp>) -> bool,
) -> bool {
let deadline = Instant::now() + Duration::from_secs(secs);
let mut last = String::new();
while Instant::now() < deadline {
robot.run(1);
if done(robot) {
return true;
}
let phase = robot
.state_at("/phase")
.map(|v| v.to_string())
.unwrap_or_else(|| "null".to_string());
if phase != last {
eprintln!("[robot] {what}: phase {phase}");
last = phase;
}
std::thread::sleep(Duration::from_millis(40));
}
false
}
/// One captured frame as a P6 PPM — a raw framebuffer needs no encoder to be proof.
fn ppm_of(s: &dwarves_gui::build_scene::Screen) -> Vec<u8> {
let mut ppm = format!("P6\n{} {}\n255\n", s.width, s.height).into_bytes();
ppm.extend(s.rgba.chunks_exact(4).flat_map(|p| [p[0], p[1], p[2]]));
ppm
}
/// Every line the pane is showing, for a report that names what the guest said.
fn lines(robot: &RobotSession<'_, SceneApp>) -> Vec<String> {
robot
.state_at("/lines")
.and_then(|v| serde_json::from_value::<Vec<String>>(v).ok())
.unwrap_or_default()
}
/// **The winning frame is THIS product's own** — the guard the stand-in could never
/// have survived, and the only one of the graphical assertions that could not.
///
/// Every drive records its inkiest frame's blake3 under `<tmp>/dwarves-boot-frames/`,
/// keyed by product. This one then reads every OTHER product's recorded hash and
/// refuses to match any of them.
///
/// It is deliberately a CROSS-RUN guard on files rather than an in-process one: the
/// three products are driven as three separate `cargo test` invocations (three env
/// knobs, one file — see [`named`]), so nothing in a single process ever sees two
/// products' screens. A hash a previous run left behind is the only witness there is.
///
/// A first-ever run has nothing to compare against and says so instead of passing
/// quietly — "no other product has been driven on this box" is a different sentence
/// from "the frames differ", and reporting the second when the first is true is how a
/// guard goes blind.
fn the_frame_is_this_products_own(product: &str, ppm: &[u8]) {
let dir = std::env::temp_dir().join("dwarves-boot-frames");
let _ = std::fs::create_dir_all(&dir);
let mine = blake3::hash(ppm).to_hex().to_string();
eprintln!("[robot] frame hash : {product} blake3={mine}");
let mut others: Vec<(String, String)> = Vec::new();
if let Ok(entries) = std::fs::read_dir(&dir) {
for e in entries.flatten() {
let name = e.file_name().to_string_lossy().to_string();
let Some(other) = name.strip_suffix(".blake3") else {
continue;
};
if other == product {
continue;
}
let Ok(theirs) = std::fs::read_to_string(e.path()) else {
continue;
};
others.push((other.to_string(), theirs.trim().to_string()));
}
}
for (who, hash) in &others {
eprintln!("[robot] frame vs : {who} blake3={hash}");
}
let verdict = frame_verdict(product, &mine, &others);
// Recorded BEFORE the verdict is applied: a run that fails this guard must still
// leave its hash behind, or the next run has nothing to compare against and the
// guard quietly disarms itself exactly when it has just fired.
std::fs::write(dir.join(format!("{product}.blake3")), &mine).expect("record the frame hash");
match verdict {
Ok(0) => eprintln!(
"[robot] frame vs : NOTHING — no other product has been driven on this box \
yet, so the cross-product frame guard compared 0 screens. Drive a second \
product to arm it; {} now holds this run's hash.",
dir.display(),
),
Ok(n) => eprintln!("[robot] frame vs : {n} other product screen(s), all different"),
Err(why) => panic!("{why}"),
}
}
/// The pure half of [`the_frame_is_this_products_own`]: `Ok(n)` when `mine` matches none
/// of the `others`, having compared `n` of them; `Err(why)` naming the product whose
/// screen this one is byte-for-byte.
///
/// Split out so the guard itself can be seen RED — see
/// [`the_frame_guard_fires_on_two_products_with_one_screen`]. A guard that has only ever
/// been observed passing is a guard nobody has tested.
fn frame_verdict(product: &str, mine: &str, others: &[(String, String)]) -> Result<usize, String> {
for (who, theirs) in others {
if mine == theirs {
return Err(format!(
"the boot screen {product} drew is byte-for-byte the one {who} drew \
(blake3 {mine}). That is what a stand-in looks like: one medium booted for \
every product, or one logo baked for every product. Neither is {product} \
installing."
));
}
}
Ok(others.len())
}
/// **RED, seen.** The exact shape 2026-08-16 measured — two products, one screen.
///
/// gunnar's and holger's captured frames were md5-identical, all six each, because ▶
/// booted the same korp-shaped stand-in for both. Those are the values below.
#[test]
fn the_frame_guard_fires_on_two_products_with_one_screen() {
let one_screen = "32b65f91ac759010ecfcbd4d7ce88b11".to_string();
let others = vec![("holger".to_string(), one_screen.clone())];
let err = frame_verdict("gunnar", &one_screen, &others)
.expect_err("two products showing ONE screen must not pass");
assert!(err.contains("gunnar") && err.contains("holger"), "{err}");
assert!(err.contains("byte-for-byte"), "{err}");
// …and it passes only when they genuinely differ, having actually compared one.
assert_eq!(frame_verdict("gunnar", "5c2bd9e6", &others), Ok(1));
// A first-ever run compares nothing, and the count says so rather than the verdict.
assert_eq!(frame_verdict("gunnar", "5c2bd9e6", &[]), Ok(0));
}
/// **The ssh-forward host port, read off the pane's own console line.** The live-ISO
/// leg prints `[dwarves] ssh forward: host 127.0.0.1:<port> -> guest 2222` the moment
/// the guest starts booting (`dwarves_gui::boot_run::ssh_forward_notice`); this pulls
/// `<port>` out of that line so the drive can open a socket to it.
///
/// `None` when NO such line is present. A boot that never announced a forward has no
/// address to serve through, and saying that is a different fact from guessing
/// [`LIVE_SSH_HOST_PORT`] — a phantom port would turn "the guest never came up" into a
/// connection refused against the wrong thing. Split out pure so it can be seen RED
/// without QEMU — see [`the_ssh_forward_port_parser_refuses_a_console_without_the_line`].
fn parse_ssh_forward_port(lines: &[String]) -> Option<u16> {
const MARK: &str = "[dwarves] ssh forward: host 127.0.0.1:";
for l in lines {
let Some(idx) = l.find(MARK) else { continue };
let rest = &l[idx + MARK.len()..];
let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if let Ok(p) = digits.parse::<u16>() {
return Some(p);
}
}
None
}
/// **The listener over the tunnel is THIS product's own SSH server.** gunnar's
/// appliance sshd answers `SSH-2.0-gunnar_<ver>` (its own forge test,
/// `gunnar_appliance_iso`, asserts the same prefix over a forwarded port); a foreign
/// sshd, or a bare TCP echo, or nothing at all, is not the product serving.
///
/// `Ok(())` only when the banner names the product; `Err(why)` quotes what actually
/// answered. Product-parametric so the same guard serves korp/holger, and split out
/// pure so it can be seen RED against a wrong banner without QEMU — see
/// [`the_serving_banner_helper_passes_only_the_products_own_sshd`].
fn banner_names_the_product(banner: &str, product: &str) -> Result<(), String> {
let want = format!("SSH-2.0-{product}");
if banner.starts_with(&want) {
Ok(())
} else {
Err(format!(
"the listener on the ssh forward is not {product}'s own SSH server — it \
answered {banner:?}, not an {want} banner. Nothing is SERVING through the tunnel."
))
}
}
/// **RED, seen without QEMU.** The parser reads the port the live leg announces, and a
/// console that never announced a forward yields `None` — not a phantom port.
#[test]
fn the_ssh_forward_port_parser_refuses_a_console_without_the_line() {
// The honest line, base port and rebound port both — the port is the load-bearing
// field, read out of exactly what `ssh_forward_notice` emits.
assert_eq!(
parse_ssh_forward_port(&[
"[dwarves] booting gunnar-appliance-iso".to_string(),
"[dwarves] ssh forward: host 127.0.0.1:10022 -> guest 2222".to_string(),
]),
Some(10022),
);
assert_eq!(
parse_ssh_forward_port(&[
"[dwarves] ssh forward: host 127.0.0.1:10024 -> guest 2222 (10022 was taken)"
.to_string(),
]),
Some(10024),
"a rebound forward's port is still read, past the ` (… was taken)` suffix",
);
// The failing shape: a boot with no forward line. `None`, never a guessed 10022.
assert_eq!(
parse_ssh_forward_port(&["[dwarves] booting, no forward announced here".to_string()]),
None,
);
assert_eq!(parse_ssh_forward_port(&[]), None);
}
/// **RED, seen without QEMU.** Only the product's own banner passes; OpenSSH, an empty
/// read, anything else is refused BY what it said.
#[test]
fn the_serving_banner_helper_passes_only_the_products_own_sshd() {
banner_names_the_product("SSH-2.0-gunnar_0.1.0", "gunnar").expect("gunnar's own banner passes");
// A foreign sshd on the forward is NOT gunnar serving — the exact miss the whole
// proof exists to catch: a listener answered, but not the product's.
let err = banner_names_the_product("SSH-2.0-OpenSSH_9.6p1", "gunnar")
.expect_err("a foreign sshd must not pass as gunnar serving");
assert!(
err.contains("SSH-2.0-gunnar") && err.contains("OpenSSH"),
"{err}"
);
assert!(err.contains("SERVING"), "{err}");
// No banner at all — a connect that read zero bytes — is refused too.
assert!(
banner_names_the_product("", "gunnar").is_err(),
"an empty banner must not pass"
);
// Product-parametric: the same guard serves another product's own banner.
banner_names_the_product("SSH-2.0-holger_2.0", "holger").expect("holger's own banner passes");
}
/// One connect-and-read of the SSH identification string off `127.0.0.1:<port>`.
/// `Some(line)` once the guest's sshd answers, `None` while it is not yet up — the
/// drive retries between pumped frames. Mirrors gunnar's own forge-test probe: read the
/// banner, write a probe line back so the guest's protocol loop does not stall.
fn read_ssh_banner(port: u16) -> Option<String> {
let mut s = TcpStream::connect(("127.0.0.1", port)).ok()?;
s.set_read_timeout(Some(Duration::from_secs(3))).ok();
let mut buf = [0u8; 256];
match s.read(&mut buf) {
Ok(n) if n > 0 => {
let line = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let _ = s.write_all(b"SSH-2.0-probe\r\n");
Some(line)
}
_ => None,
}
}
/// The launcher's coded refusal, if the pane is showing one.
fn refusal(robot: &RobotSession<'_, SceneApp>) -> Option<String> {
let hay = format!(
"{}\n{}\n{}",
robot
.state_at("/phase")
.map(|v| v.to_string())
.unwrap_or_default(),
robot
.state_at("/note")
.map(|v| v.to_string())
.unwrap_or_default(),
lines(robot).join("\n"),
);
hay.split(|c: char| !(c.is_ascii_alphanumeric() || c == '-'))
.find(|w| w.starts_with("dwarves-launch-"))
.map(str::to_string)
}
#[test]
#[ignore = "builds and BOOTS: needs QEMU + OVMF + /dev/kvm, takes minutes"]
fn a_robot_clicks_gunnar_installer_and_watches_it_install_in_the_pane() {
let repo = named("DWARVES_ROBOT_REPO", REPO);
let row = named("DWARVES_ROBOT_ROW", ROW);
let column = named("DWARVES_ROBOT_COLUMN", COLUMN);
// The product the installed disk starts — `korp.entry=` in the cell's declaration,
// which for all three products is the repo's own name. Overridable for a repo whose
// checkout and product disagree, never derived from anything the guest said (a
// marker read out of the serial and then asserted against that same serial is the
// identity value with extra steps).
let product = named("DWARVES_ROBOT_PRODUCT", "gunnar");
let product = if std::env::var_os("DWARVES_ROBOT_PRODUCT").is_some() {
product
} else {
repo.clone()
};
let (repo, row, column, product) = (
repo.as_str(),
row.as_str(),
column.as_str(),
product.as_str(),
);
eprintln!("[robot] driving : {repo} → {row} → {column} (product {product})");
let mut robot = session();
// RED — nothing is resolved before anything is typed.
assert_eq!(
robot
.state_at("/rows")
.and_then(|v| v.as_array().map(Vec::len)),
Some(0)
);
assert_eq!(robot.state_at("/can_build"), Some(serde_json::json!(false)));
// 1 — TYPE the artifact name into the real field, then CLICK the real search button.
robot
.type_into("artefakt", repo)
.expect("the artefakt field is reachable");
robot
.click_label("sök")
.expect("the sök button is reachable");
robot.run(2);
let recipe = robot.state_at("/recipe");
assert_ne!(
recipe,
Some(serde_json::Value::Null),
"the search resolved no recipe for {repo}: {:?}",
robot.state_at("/error"),
);
eprintln!("[robot] recipe : {recipe:?}");
// 2 — CLICK the row the installer chains off.
robot
.click_label(row)
.unwrap_or_else(|e| panic!("the {row} row is not clickable: {e}"));
robot.run(2);
assert_eq!(robot.state_at("/row"), Some(serde_json::json!(row)));
// The column strip this row actually renders — printed BEFORE the click, so a
// missing column names what WAS there instead of only what was not.
let columns: Vec<String> = robot
.state_at("/rows")
.and_then(|v| v.as_array().cloned())
.unwrap_or_default()
.iter()
.find(|r| r["name"] == serde_json::json!(row))
.and_then(|r| r["targets"].as_array().cloned())
.unwrap_or_default()
.iter()
.map(|t| {
format!(
"{}{}",
t["name"].as_str().unwrap_or("?"),
if t["declared"] == serde_json::json!(true) {
""
} else {
"(grey)"
}
)
})
.collect();
eprintln!("[robot] columns : {}", columns.join(" "));
// 3 — CLICK the installer column. A column the recipe does not declare is disabled, so
// a landed pick is itself the proof that the repo DECLARES this installer.
robot
.click_label(column)
.unwrap_or_else(|e| panic!("the {column} column is not clickable: {e}"));
robot.run(2);
assert_eq!(robot.state_at("/target"), Some(serde_json::json!(column)));
assert_eq!(
robot.state_at("/pane"),
Some(serde_json::json!("console")),
"an installer variant did not open the Console pane",
);
assert_eq!(
robot.state_at("/can_build"),
Some(serde_json::json!(true)),
"BYGG is not armed"
);
// 3b — TYPE the cell's declared per-site parameters into their real inputs.
//
// The pane renders `[[build.*.params]]` as editable settings whose accessible
// name IS the parameter name, so the robot types into them exactly as an
// operator would. gunnar declares none and this loop does nothing; holger
// declares four, one of them REQUIRED (`holger_ron`), and without a value BYGG
// refuses BY NAME — which is the declaration doing its job, not a blocker to be
// worked around. `@<path>` reads the value out of a file, the same door
// `--param` speaks.
//
// Semicolon-separated because a value may contain `=` (an `@path` may not, but
// a text parameter can) and because a shell-quoted env var is the one channel
// a drive has.
for pair in named("DWARVES_ROBOT_PARAMS", "").split(';') {
let Some((key, value)) = pair.split_once('=') else {
continue;
};
let (key, value) = (key.trim(), value.trim());
if key.is_empty() {
continue;
}
robot.type_into(key, value).unwrap_or_else(|e| {
panic!("the declared parameter input '{key}' is not reachable: {e}")
});
eprintln!("[robot] param : {key}={value}");
}
robot.run(2);
// 4 — CLICK BYGG, and wait for the build to settle. The pane below only exists once
// the build produced an outcome, so this is the gate on the ▶ button existing.
robot
.click_label("BYGG")
.expect("the BYGG button is reachable");
let built = pump(&mut robot, 900, "bygg", |r| {
r.state_at("/outcome").is_some_and(|v| !v.is_null())
});
// `state_json` carries the phase LABEL ("FEL"); the reason lives in the phase itself,
// and a failure whose reason a driver cannot print is a failure nobody can act on.
if let Some(dwarves_gui::build_scene::Phase::Failed(why)) = robot.app().0.report().phase.clone()
{
eprintln!("[robot] BYGG failed : {why}");
}
eprintln!("[robot] build phase : {:?}", robot.state_at("/phase"));
eprintln!("[robot] outcome : {:?}", robot.state_at("/outcome"));
eprintln!("[robot] note : {:?}", robot.state_at("/note"));
assert!(
built,
"the build never settled — no outcome, so no pane and no ▶"
);
// **THE BUILD MUST HAVE SUCCEEDED, and it is asserted HERE — before ▶.**
//
// "An outcome exists" was the whole gate until 2026-08-16, and a FAILED build
// produces an outcome too. MEASURED that day, driving holger: BYGG went `FEL`
// with `required parameter 'holger_ron' … was not supplied`, `ok=false`,
// `artifact=""` — and the test went GREEN, INSTALL-OK and all, because ▶ KÖR
// does not boot what BYGG built. It re-masters through `boot_installer`, which
// fell back to a korp-appliance-lean SCAFFOLD; the serial said
// `INSTALLER: INSTALL-OK`, the pane got a full-bleed splash, and every
// assertion below passed over a product that had not been built at all.
//
// So the three facts that separate "this product installed" from "something
// installed" are checked before the pane is ever asked to boot: the build says
// it succeeded, it NAMES an artifact, and that artifact is on disk. A drive
// that cannot get past this line is telling the truth about the product.
let outcome = robot
.state_at("/outcome")
.unwrap_or(serde_json::Value::Null);
assert_eq!(
outcome["ok"],
serde_json::json!(true),
"BYGG did not succeed, so anything ▶ boots afterwards is not this product: {:?}",
robot.state_at("/phase"),
);
let artifact = outcome["artifact"].as_str().unwrap_or_default().to_string();
assert!(
!artifact.is_empty(),
"the build reported ok with NO artifact — there is nothing for ▶ to boot",
);
assert!(
std::path::Path::new(&artifact).is_file(),
"the build named {artifact} and no such file is on disk",
);
eprintln!("[robot] medium : {artifact}");
// 5 — CLICK ▶ KÖR. It boots THAT medium: a blank target at the cell's declared size,
// the ISO on `-cdrom` AND on read-only virtio (the appliance kernel has no
// ATA/AHCI, so the PID-1's CD001 probe would find no `sr0`), gating on INSTALL-OK,
// then a SECOND boot off the disk it just wrote.
robot
.click_label("▶ KÖR")
.expect("the console pane's ▶ KÖR button is reachable");
robot.run(2);
assert_eq!(
robot.state_at("/live"),
Some(serde_json::json!(true)),
"▶ KÖR did not bring a subject up{}",
refusal(&robot)
.map(|c| format!(" — refused {c}"))
.unwrap_or_default(),
);
// 6 — WATCH it install, KEEPING THE FRAME WITH THE MOST INK ON IT.
//
// The last frame is the wrong frame. An install is ~1.4 s and the pane reboots
// straight into the installed disk afterwards, so by the time the serial says
// INSTALL-OK the framebuffer has already gone black for the second boot's firmware
// — MEASURED 2026-08-16: 1280×800 captured, 16 non-zero bytes in the whole image,
// which is a PPM header and nothing else. A black frame is a captured frame, so
// "did we get a frame" is not the question. The question is whether the guest DREW
// anything, and the answer is kept as the run goes: every frame is weighed and the
// inkiest one survives.
let mut best: Option<dwarves_gui::build_scene::Screen> = None;
let mut best_ink = 0usize;
let mut seen: std::collections::BTreeSet<(usize, usize, usize)> =
std::collections::BTreeSet::new();
let deadline = Instant::now() + Duration::from_secs(900);
let mut installed = false;
// korp-start's last line before `fexecve` replaces the process
// (`korp-installer/src/bin/korp-start.rs`, argv[0] = the payload's entry). Reaching
// it off the INSTALLED disk is the whole reboot claim: firmware → korp-boot →
// vmlinuz → korp-start → payload verified → this product handed control.
// **`INSTALLER: `-prefixed on purpose.** korp-start emits every line through the
// installer's serial sink, so the real handoff reads
// `INSTALLER: exec argv: ["gunnar", "serve", …]`. The pane's OWN narration also
// names the gate it is waiting for — `[dwarves] … gate `exec argv: ["gunnar"` …` —
// and it is printed BEFORE the second boot starts. MEASURED 2026-08-16: matching
// the bare form latched on that narration, so a run whose second boot never
// happened at all reported a reboot. The prefix is what separates what the GUEST
// said from what the pane said about it.
let handoff = format!("INSTALLER: exec argv: [\"{product}");
let mut rebooted: Option<Instant> = None;
let mut settled: Option<Instant> = None;
while Instant::now() < deadline {
robot.run(1);
if let Some(s) = robot.app().0.report().screen.clone() {
let ink = s.rgba.iter().filter(|b| **b != 0).count();
// EVERY distinct frame is kept, not only the winner: "the guest drew
// something" is not the same claim as "the guest drew THE SPLASH", and the
// only way to tell them apart is to look at the frames.
if seen.insert((s.width, s.height, ink)) {
let out = std::env::temp_dir()
.join(format!("dwarves-{repo}-frame-{:02}.ppm", seen.len()));
let _ = std::fs::write(&out, ppm_of(&s));
eprintln!(
"[robot] frame : {}×{} ink {ink} → {}",
s.width,
s.height,
out.display(),
);
}
if ink > best_ink {
best_ink = ink;
best = Some(s);
}
}
if !installed && lines(&robot).iter().any(|l| l.contains("INSTALL-OK")) {
eprintln!("[robot] INSTALL-OK : latched");
installed = true;
}
if rebooted.is_none() && lines(&robot).iter().any(|l| l.contains(&handoff)) {
eprintln!("[robot] reboot handoff: latched — {handoff}\"…]");
rebooted = Some(Instant::now());
}
// **The run ends when the SECOND boot has spoken, not three seconds after
// INSTALL-OK.**
//
// MEASURED 2026-08-16, the first run on the real medium: the old three-second
// grace stopped the watch while the installed disk was still in its firmware,
// and the drive reported on an install whose reboot it had not waited for. An
// install that is never booted is bytes on a disk.
//
// The pane's own terminal phase is the other exit: `KLAR`/`FEL` means the whole
// two-phase console resolved, and a frame grace after it lands the final screen.
if rebooted.is_some_and(|t| t.elapsed() > Duration::from_secs(12)) {
break;
}
let live = robot.state_at("/live") == Some(serde_json::json!(true));
match (live, settled) {
(false, None) => settled = Some(Instant::now()),
(false, Some(t)) if t.elapsed() > Duration::from_secs(10) => break,
(true, _) => settled = None,
_ => {}
}
// The OTHER exit: the pane's boot has ended (`/live` went false), plus a grace
// for the last frames to land.
//
// It reads `/live` and not `/phase`. MEASURED 2026-08-16: `/phase` is the
// BUILD's phase — `build_forge::poll` derives it from `(run, outcome)` alone —
// so it says KLAR the moment BYGG succeeds and keeps saying it for the whole
// boot. Watching it turned this loop into a ten-second window that started at
// BYGG: holger's guest reaches INSTALL-OK at 1.9 s and won it, gunnar's lost it
// once the install guest's graceful shutdown was added, and the drive then
// reported "never came back up" about a boot it had stopped watching.
std::thread::sleep(Duration::from_millis(10));
}
for l in lines(&robot) {
eprintln!("[serial] {l}");
}
eprintln!("[robot] phase : {:?}", robot.state_at("/phase"));
eprintln!("[robot] refusal code : {:?}", refusal(&robot));
// The guest's REAL screen, written out of the SAME `Report` the pane draws its
// texture from — a P6 PPM, because a raw framebuffer needs no encoder to be proof.
if let Some(s) = &best {
let out = std::env::temp_dir().join(format!("dwarves-{repo}-installer-screen.ppm"));
match std::fs::write(&out, ppm_of(s)) {
Ok(()) => eprintln!(
"[robot] gfx frame : {} ({}×{}, {best_ink} non-black bytes)",
out.display(),
s.width,
s.height,
),
Err(e) => eprintln!("[robot] gfx frame FAILED: {e}"),
}
}
let serial = lines(&robot).join("\n");
// ── TEAR THE GUEST DOWN, BEFORE ANY ASSERTION ───────────────────────────────
//
// Ordered here on purpose. An assertion that fires unwinds past everything below
// it, so a teardown at the end of the test only ever runs on a PASSING run —
// MEASURED 2026-08-16: four failing drives left four live guests, one each. It
// also releases the disk before the readback opens it.
//
// MEASURED 2026-08-16: three drives left three live `qemu-system-x86_64`
// processes behind, one per run, each still holding its 64 GiB target image. The
// reason is not a leak in the pane — it is that tunnr spawns QEMU in its OWN
// process group (so a killed harness cannot orphan a half-torn-down guest), and a
// `cargo test` that has finished asserting simply exits while the reboot worker is
// still lingering on the running guest for framebuffer capture. Nothing ever told
// it to stop.
//
// So the drive presses the pane's own ✕ STOPP — the operator's gesture, not a
// `pkill` — and pumps long enough for the cancel to travel: cancel → the console
// pump's kill flag → RebootConsole::kill → the reboot worker's 50 ms poll → the
// guest. A run that cannot reach the button says so rather than leaving a VM
// behind quietly.
if robot.state_at("/live") == Some(serde_json::json!(true)) {
match robot.click_label("✕ STOPP") {
Ok(()) => {
let stopped = pump(&mut robot, 25, "stopp", |r| {
r.state_at("/live") == Some(serde_json::json!(false))
});
eprintln!("[robot] stopp : live=false after cancel: {stopped}");
}
Err(e) => eprintln!("[robot] stopp : the ✕ STOPP button is not reachable: {e}"),
}
}
// The worker kills the guest ~50 ms after it sees the flag; a short settle so the
// process is reaped before this one exits and the group outlives it.
std::thread::sleep(Duration::from_secs(2));
// ── THE MEDIUM ──────────────────────────────────────────────────────────────
// The pane announces which file it is about to boot, before it boots it, and says
// STAND-IN when there is none. Both halves are asserted: a console that merely
// FAILED to say STAND-IN could be a console that says nothing.
assert!(
serial.contains(&format!("medium: {artifact}")),
"the console never named {artifact} as the medium it booted:\n{serial}",
);
assert!(
!serial.contains("STAND-IN"),
"▶ fell back to the korp-installer efistub SCAFFOLD — whatever installed, it was \
not {product}:\n{serial}",
);
// ── THE INSTALL, NAMING THE PRODUCT ─────────────────────────────────────────
// korp-boot echoes the cmdline it read off the medium's own ESP. `korp.entry=<p>`
// is what compose_installer_row packed the payload under and what korp-start will
// resolve — so a medium carrying another product's entry cannot pass here.
assert!(
serial.contains(&format!("korp.entry={product}")),
"the booted medium's cmdline never named korp.entry={product} — this is not \
{product}'s medium:\n{serial}",
);
assert!(
installed,
"no INSTALLER: INSTALL-OK on the serial{}",
refusal(&robot)
.map(|c| format!(" — refused {c}"))
.unwrap_or_default(),
);
// ── THE DISK, READ BACK ─────────────────────────────────────────────────────
// **Applied output.** Everything above is the guest talking. This is the bytes the
// installer actually wrote: the GPT entry array at LBA 2, then `korp/cmdline` out
// of the ESP's FAT, through `korp_installer::kvmproof` — the same range readers
// holger's and gunnar's own crown proofs use.
let disk = dwarves_gui::boot_run::TunnrBootLauncher::installer_disk_path(&cell_of(column));
assert!(
disk.is_file(),
"the installer's target disk is not at {}",
disk.display()
);
let mbr = korp_installer::kvmproof::read_at(&disk, 510, 2);
assert_eq!(
(mbr[0], mbr[1]),
(0x55, 0xAA),
"no protective MBR on {} — nothing partitioned this disk",
disk.display(),
);
let (esp_first, esp_last) = korp_installer::kvmproof::gpt_entry(&disk, 0);
let installed_cmdline =
korp_installer::kvmproof::read_fat_file(&disk, esp_first, esp_last, "korp/cmdline")
.map(|b| String::from_utf8_lossy(&b).to_string())
.unwrap_or_else(|| {
panic!("the installed ESP (p1 LBA {esp_first}..{esp_last}) carries no korp/cmdline")
});
eprintln!("[robot] installed cmd : {installed_cmdline}");
assert!(
installed_cmdline.contains(&format!("korp.entry={product}")),
"the INSTALLED disk's cmdline does not start {product}: {installed_cmdline}",
);
// ── THE REBOOT ──────────────────────────────────────────────────────────────
// The owner: "du skall verifiera att det går att boota om". A machine that installed
// and cannot boot is bytes on a disk, so the second boot is asserted on ITS OWN
// marker — korp-start's handoff line, which carries argv[0] and therefore names
// this product and no other.
assert!(
rebooted.is_some(),
"the machine installed but never came back up: no `{handoff}\"…]` off the \
installed disk. phase {:?}\n{serial}",
robot.state_at("/phase"),
);
// What the handoff line does NOT prove is that the app then RAN — it is printed
// immediately BEFORE `fexecve`. So the two ways that exec dies are refused by name,
// the same pair gunnar's and holger's own first-boot proofs refuse
// (`isoproof::FirstBootProof::refuse_serial`). A payload that cannot be executed
// (a host-glibc binary with a PT_INTERP no initramfs can resolve, say) says
// `INSTALLER: FAILED exec` right after the handoff, and a machine that boots and
// then dies is not a machine that boots.
for refuse in ["FAILED exec", "Kernel panic"] {
assert!(
!serial.contains(refuse),
"the reboot reached the handoff and then died with {refuse:?}:\n{serial}",
);
}
// **And the product SPOKE.** Everything korp-start prints is prefixed `INSTALLER: `;
// after `fexecve` the process IS the app and its stdout is unprefixed. So a line
// after the handoff that is neither korp-start's protocol nor a kernel `[ n.nnn]`
// stamp can only have come from the installed product itself. That is the difference
// between "the loader was about to exec" and "the thing on this disk is running",
// and it is product-neutral: it names no marker any one product owns.
let spoke: Vec<&str> = serial
.split('\n')
.skip_while(|l| !l.contains(&handoff))
.skip(1)
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with("INSTALLER:") && !l.starts_with('['))
.collect();
eprintln!(
"[robot] app said : {} line(s), first: {:?}",
spoke.len(),
spoke.first()
);
assert!(
!spoke.is_empty(),
"korp-start handed off to {product} and nothing but korp-start and the kernel ever \
spoke again — the payload was reached but never ran:\n{serial}",
);
// ── THE SCREEN ──────────────────────────────────────────────────────────────
let s = best
.as_ref()
.expect("the graphics console never produced a frame for the pane");
eprintln!(
"[robot] winning frame : {}×{} {best_ink}/{} bytes inked ({:.0}%)",
s.width,
s.height,
s.rgba.len(),
best_ink as f64 / s.rgba.len() as f64 * 100.0,
);
the_frame_is_this_products_own(product, &ppm_of(s));
}
/// The launch cell's name, out of the column label the operator clicked
/// (`installer:gunnar-installer` → `gunnar-installer`). The pane names its columns
/// `<kind>:<cell>`, so this is the one place the drive has to undo that.
fn cell_of(column: &str) -> String {
column
.split_once(':')
.map_or_else(|| column.to_string(), |(_, c)| c.to_string())
}
/// **The ONE gesture, to a LIVE iso-kind boot** — the drive both iso-kind proofs share.
///
/// Resolves the checkout, picks the row and the iso column, presses BYGG and asserts
/// the composed medium exists on disk BEFORE ▶ (the exact gate the installer drive
/// learned 2026-08-16: a FAILED build has an outcome too), then presses ▶ KÖR and
/// asserts the pane brought a live subject up. Returns the artifact the console will
/// name as the medium.
///
/// Written once because the gesture is identical whether the watch that follows proves
/// the boot chain (the frame + handoff proof) or the product SERVING (the ssh-banner
/// proof) — two claims about the same live guest, not two gestures (LAW 5).
///
/// # THE ANCHOR IS PART OF THE GESTURE, not of one proof's assertions
///
/// It used to be set inside the serving proof only, and that is the whole reason the
/// boot proof was RED on main: an appliance ISO composed with no
/// `KORP_ROOT_SIGNING_KEY` stages no `root.pub`/`app.sig`, `korp.require_signed`
/// defaults CLOSED, and `korp-start` refuses the payload it just verified the blake3
/// of — `signature-over-root: NOT-ENROLLED`. Two proofs of one medium cannot compose
/// it two ways, so the enrolment moved to the ONE place that composes it.
fn drive_iso_variant_to_live(
robot: &mut RobotSession<'_, SceneApp>,
repo: &str,
row: &str,
column: &str,
) -> String {
// 0 — DEV-SIGNED, or nothing this drive composes can boot. See the doc above.
// Named on the console so it is never a silent default, and so a run that
// brought its OWN key says which one composed the medium.
if std::env::var_os("KORP_ROOT_SIGNING_KEY").is_none() {
std::env::set_var("KORP_ROOT_SIGNING_KEY", "dev");
}
eprintln!(
"[robot] signing key : KORP_ROOT_SIGNING_KEY={:?} — the appliance is fail-closed, \
so an UNENROLLED medium refuses its own payload at korp-start",
std::env::var("KORP_ROOT_SIGNING_KEY").ok(),
);
// 1 — resolve the checkout, pick the row, pick the iso column.
robot
.type_into("artefakt", repo)
.expect("the artefakt field is reachable");
robot
.click_label("sök")
.expect("the sök button is reachable");
robot.run(2);
assert_ne!(
robot.state_at("/recipe"),
Some(serde_json::Value::Null),
"the search resolved no recipe for {repo}: {:?}",
robot.state_at("/error"),
);
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(2);
assert_eq!(robot.state_at("/target"), Some(serde_json::json!(column)));
assert_eq!(
robot.state_at("/can_build"),
Some(serde_json::json!(true)),
"BYGG is not armed"
);
// 2 — BYGG, and the composed medium MUST exist before ▶ is ever pressed.
robot
.click_label("BYGG")
.expect("the BYGG button is reachable");
let built = pump(robot, 900, "bygg", |r| {
r.state_at("/outcome").is_some_and(|v| !v.is_null())
});
if let Some(dwarves_gui::build_scene::Phase::Failed(why)) = robot.app().0.report().phase.clone()
{
eprintln!("[robot] BYGG failed : {why}");
}
assert!(
built,
"the build never settled — no outcome, so no pane and no ▶"
);
let outcome = robot
.state_at("/outcome")
.unwrap_or(serde_json::Value::Null);
assert_eq!(
outcome["ok"],
serde_json::json!(true),
"BYGG did not succeed, so anything ▶ boots afterwards is not this product: {:?}",
robot.state_at("/phase"),
);
let artifact = outcome["artifact"].as_str().unwrap_or_default().to_string();
assert!(
!artifact.is_empty(),
"the build reported ok with NO artifact — nothing to boot"
);
assert!(
std::path::Path::new(&artifact).is_file(),
"the build named {artifact} and no such file is on disk",
);
eprintln!("[robot] medium : {artifact}");
// 3 — ▶ KÖR. The live leg boots THE composed file off -cdrom, GPU framebuffer on.
robot
.click_label("▶ KÖR")
.expect("the console pane's ▶ KÖR button is reachable");
robot.run(2);
assert_eq!(
robot.state_at("/live"),
Some(serde_json::json!(true)),
"▶ KÖR did not bring a subject up{}",
refusal(robot)
.map(|c| format!(" — refused {c}"))
.unwrap_or_default(),
);
artifact
}
/// **ROBOT-UI: BUILT is not done, ANSWERED is done — `gunnar` → `iso:gunnar-appliance-iso`
/// → ▶ KÖR → the appliance SERVES over the ssh forward.**
///
/// The sibling proof [`a_robot_clicks_gunnar_appliance_iso_and_watches_it_boot_in_the_pane`]
/// ends at korp-start's exec handoff and the framebuffer — that a boot HAPPENED. This
/// one carries the house law one step further (the doctrine
/// `dwarves/crates/dwarves/tests/variant_drive.rs` states verbatim: "BUILT is not done.
/// ANSWERED is done"): it drives the SAME gesture, then opens a TCP socket to the
/// live-ISO ssh forward and asserts the guest returns gunnar's OWN SSH banner —
/// `SSH-2.0-gunnar` — i.e. the product is answering a real request through the tunnel.
/// The gate is the CODE on the wire, never a rendered string.
///
/// The chain proven: BYGG composes the dev-signed medium → ▶ KÖR boots it live off
/// `-cdrom`, GPU framebuffer on, its user netdev carrying `hostfwd=tcp:127.0.0.1:<port>-:2222`
/// → the console announces the forward's host address → the drive reads `<port>` off
/// that line and connects → gunnar's sshd, bound in the guest on `0.0.0.0:2222`,
/// answers `SSH-2.0-gunnar_<ver>`. A boot that reaches the framebuffer but whose sshd
/// never binds (a payload the root-of-trust refused, a selftest that fail-closed) draws
/// a frame and then serves nothing — which is exactly the gap a banner assertion, and
/// only a banner assertion, closes.
///
/// # DEV-SIGNED IS REQUIRED
///
/// The appliance is fail-closed: `require_signed` defaults CLOSED, so an UNSIGNED iso
/// refuses its own payload at selftest and sshd never binds. The SHARED driver
/// [`drive_iso_variant_to_live`] sets `KORP_ROOT_SIGNING_KEY=dev` if the operator did
/// not — it is the composition's input, so it sits with the composition rather than in
/// one of the two proofs — so signature-over-root=PASS and gunnar reaches serving.
/// Prefer to pass it explicitly on the run line (below) so it is never a silent default.
///
/// # RUN IT (on a box with QEMU + OVMF + /dev/kvm — NOT dwarf, which has no qemu)
///
/// ```bash
/// KORP_ROOT_SIGNING_KEY=dev \
/// cargo test -p dwarves-gui --features vm-boot --test robot_gunnar_installer \
/// a_robot_builds_gunnar_and_the_appliance_serves -- --ignored --nocapture
/// ```
///
/// The same env knobs as the boot proof re-point it at another product
/// (`DWARVES_ROBOT_ISO_REPO` / `_ROW` / `_COLUMN`, plus `DWARVES_ROBOT_PRODUCT` for the
/// banner prefix — gunnar answers `SSH-2.0-gunnar`).
#[test]
#[ignore = "builds and BOOTS and SERVES: needs QEMU + OVMF + /dev/kvm and gunnar's sshd answering over the forward, takes minutes"]
fn a_robot_builds_gunnar_and_the_appliance_serves() {
// The dev-signing default used to be set HERE, which is why the sibling boot proof
// was RED: the enrolment belongs to the composition, not to one proof's asserts, so
// it lives in `drive_iso_variant_to_live` now — one writer, both drives.
let repo = named("DWARVES_ROBOT_ISO_REPO", REPO);
let row = named("DWARVES_ROBOT_ISO_ROW", ROW);
let column = named("DWARVES_ROBOT_ISO_COLUMN", "iso:gunnar-appliance-iso");
let product = if std::env::var_os("DWARVES_ROBOT_PRODUCT").is_some() {
named("DWARVES_ROBOT_PRODUCT", "gunnar")
} else {
repo.clone()
};
let (repo, row, column, product) = (
repo.as_str(),
row.as_str(),
column.as_str(),
product.as_str(),
);
eprintln!("[robot] driving : {repo} → {row} → {column} (product {product})");
let mut robot = session();
// 1–3 — the ONE gesture, to a live boot (shared with the boot proof).
let artifact = drive_iso_variant_to_live(&mut robot, repo, row, column);
// 4 — READ THE FORWARD PORT OFF THE CONSOLE. The live leg announces
// `[dwarves] ssh forward: host 127.0.0.1:<port> -> guest 2222` the moment the
// guest starts booting; the address is read off the pane, never guessed. A boot
// that never announced a forward has no address to serve through and fails here.
let mut port: Option<u16> = None;
let deadline = Instant::now() + Duration::from_secs(60);
while Instant::now() < deadline && port.is_none() {
robot.run(1);
port = parse_ssh_forward_port(&lines(&robot));
std::thread::sleep(Duration::from_millis(40));
}
let port = port.unwrap_or_else(|| {
panic!(
"the live leg never announced its ssh forward on the console — no \
`[dwarves] ssh forward: host 127.0.0.1:<port>` line, so there is no address \
for the product to serve through:\n{}",
lines(&robot).join("\n"),
)
});
eprintln!(
"[robot] ssh forward : host 127.0.0.1:{port} -> guest {}",
dwarves_gui::boot_run::LIVE_SSH_GUEST_PORT,
);
// 5 — SERVE: connect to the forward and read the guest's SSH banner. The guest is
// still booting on the pane's console pump, so frames are pumped between connect
// attempts — sshd binds only once the payload was verified and exec'd, which is
// the whole serving claim. This is a BOUND LISTENER answering on the wire, not a
// marker in a log (a marker can be printed by code that then dies).
let mut best_ink = 0usize;
let mut banner: Option<String> = None;
let deadline = Instant::now() + Duration::from_secs(180);
while Instant::now() < deadline && banner.is_none() {
robot.run(1);
if let Some(s) = robot.app().0.report().screen.clone() {
best_ink = best_ink.max(s.rgba.iter().filter(|b| **b != 0).count());
}
banner = read_ssh_banner(port);
if banner.is_none() {
std::thread::sleep(Duration::from_millis(200));
}
}
for l in lines(&robot) {
eprintln!("[serial] {l}");
}
let serial = lines(&robot).join("\n");
eprintln!("[robot] inkiest frame : {best_ink} non-black bytes");
// Tear the guest down BEFORE asserting (same reasoning as the sibling drives: a
// firing assertion unwinds past a teardown placed after it, leaving a live guest).
if robot.state_at("/live") == Some(serde_json::json!(true)) {
match robot.click_label("✕ STOPP") {
Ok(()) => {
let stopped = pump(&mut robot, 25, "stopp", |r| {
r.state_at("/live") == Some(serde_json::json!(false))
});
eprintln!("[robot] stopp : live=false after cancel: {stopped}");
}
Err(e) => eprintln!("[robot] stopp : the ✕ STOPP button is not reachable: {e}"),
}
}
std::thread::sleep(Duration::from_secs(2));
// ── THE MEDIUM: the console named THE composed file, and no stand-in, no refusal ──
assert!(
serial.contains(&format!("medium: {artifact}")),
"the console never named {artifact} as the medium it booted:\n{serial}",
);
assert!(
!serial.contains("STAND-IN"),
"▶ fell back to the stand-in — whatever booted, it was not {product}:\n{serial}",
);
assert_eq!(
refusal(&robot),
None,
"a composed iso-kind variant was refused before it could serve:\n{serial}",
);
// ── ANSWERED: the product is SERVING through the tunnel ──────────────────────
let banner = banner.unwrap_or_else(|| {
panic!(
"no SSH banner on 127.0.0.1:{port} within 180 s — {product} BUILT and BOOTED \
but never ANSWERED: sshd never bound (a fail-closed selftest, or an unsigned \
payload the root-of-trust refused, draws a frame and serves nothing). Set \
KORP_ROOT_SIGNING_KEY=dev.\n{serial}"
)
});
eprintln!("[robot] banner : {banner:?}");
banner_names_the_product(&banner, product).unwrap_or_else(|e| panic!("{e}\n{serial}"));
eprintln!("[robot] SERVED : {product} answered {banner:?} over 127.0.0.1:{port} — ANSWERED is done");
}
/// **ROBOT-UI: the composed iso-kind variant — `gunnar` → `iso:gunnar-appliance-iso` →
/// ▶ KÖR.** Gap #7 of the dwarves-ui audit, driven end to end through the real widgets.
///
/// REPRODUCED 2026-08-20 before the fix: BYGG composed a real 32 MB self-contained
/// El Torito ESP ISO (app.znippy pinned, kernel blake3'd, ESP mastered) and ▶ KÖR
/// answered `dwarves-launch-5 … no real appliance wired for launch cell
/// 'gunnar-appliance-iso'`, offering the korp-installer efistub STAND-IN. So the drive
/// asserts, in order: BYGG ok + a named artifact on disk, the console naming THAT file
/// as the medium (and never STAND-IN, never a `dwarves-launch-*` refusal), and the
/// serial reaching korp-start's exec-argv handoff — `INSTALLER: exec argv: ["<entry>"`,
/// the last line before `fexecve` — which proves firmware → korp-boot → vmlinuz →
/// korp-start → payload verified, off THE COMPOSED MEDIUM, run-from-RAM (no target
/// disk: this is the live leg, nothing is installed).
///
/// The same three env knobs as the installer drive re-point it
/// (`DWARVES_ROBOT_ISO_REPO` / `_ROW` / `_COLUMN`, plus `DWARVES_ROBOT_PRODUCT` for the
/// `korp.entry=` — gunnar declares `korp.entry=gunnar`).
///
/// No cross-product frame guard here on purpose: the live splash and the installed
/// splash are the same product logo, so recording this run under the product's key
/// would fight the installer drive's record instead of guarding anything.
#[test]
#[ignore = "builds and BOOTS: needs QEMU + OVMF + /dev/kvm, takes minutes"]
fn a_robot_clicks_gunnar_appliance_iso_and_watches_it_boot_in_the_pane() {
let repo = named("DWARVES_ROBOT_ISO_REPO", REPO);
let row = named("DWARVES_ROBOT_ISO_ROW", ROW);
let column = named("DWARVES_ROBOT_ISO_COLUMN", "iso:gunnar-appliance-iso");
let product = if std::env::var_os("DWARVES_ROBOT_PRODUCT").is_some() {
named("DWARVES_ROBOT_PRODUCT", "gunnar")
} else {
repo.clone()
};
let (repo, row, column, product) = (
repo.as_str(),
row.as_str(),
column.as_str(),
product.as_str(),
);
eprintln!("[robot] driving : {repo} → {row} → {column} (product {product})");
let mut robot = session();
// 1–3 — the ONE gesture, to a live boot: resolve → row → iso column → BYGG (medium
// on disk before ▶) → ▶ KÖR → a live subject. Shared with the serving proof.
let artifact = drive_iso_variant_to_live(&mut robot, repo, row, column);
// 4 — watch until korp-start hands off to the product (the boot's own PASS gate),
// keeping the inkiest frame as the honest screen report.
let handoff = format!("INSTALLER: exec argv: [\"{product}");
let deadline = Instant::now() + Duration::from_secs(300);
let mut best_ink = 0usize;
let mut best: Option<dwarves_gui::build_scene::Screen> = None;
let mut handed_off = false;
let mut settled: Option<Instant> = None;
while Instant::now() < deadline {
robot.run(1);
if let Some(s) = robot.app().0.report().screen.clone() {
let ink = s.rgba.iter().filter(|b| **b != 0).count();
if ink > best_ink {
best_ink = ink;
best = Some(s);
}
}
if !handed_off && lines(&robot).iter().any(|l| l.contains(&handoff)) {
eprintln!("[robot] live handoff : latched — {handoff}\"…]");
handed_off = true;
}
// Exit once the handoff has spoken and a frame grace has passed, or once the
// pane's run ended (live went false) plus the same grace.
let live = robot.state_at("/live") == Some(serde_json::json!(true));
match (handed_off || !live, settled) {
(true, None) => settled = Some(Instant::now()),
(true, Some(t)) if t.elapsed() > Duration::from_secs(10) => break,
(false, _) => settled = None,
_ => {}
}
std::thread::sleep(Duration::from_millis(10));
}
for l in lines(&robot) {
eprintln!("[serial] {l}");
}
let serial = lines(&robot).join("\n");
// Tear the guest down BEFORE any assertion (same reasoning as the installer drive).
if robot.state_at("/live") == Some(serde_json::json!(true)) {
match robot.click_label("✕ STOPP") {
Ok(()) => {
let stopped = pump(&mut robot, 25, "stopp", |r| {
r.state_at("/live") == Some(serde_json::json!(false))
});
eprintln!("[robot] stopp : live=false after cancel: {stopped}");
}
Err(e) => eprintln!("[robot] stopp : the ✕ STOPP button is not reachable: {e}"),
}
}
std::thread::sleep(Duration::from_secs(2));
// ── THE MEDIUM: the console names THE composed file, and no stand-in, no refusal ──
assert!(
serial.contains(&format!("medium: {artifact}")),
"the console never named {artifact} as the medium it booted:\n{serial}",
);
assert!(
!serial.contains("STAND-IN"),
"▶ fell back to the stand-in — whatever booted, it was not {product}:\n{serial}",
);
assert_eq!(
refusal(&robot),
None,
"a composed iso-kind variant was refused — gap #7 is back:\n{serial}",
);
// ── THE BOOT: the composed chain ran, off this product's own medium ──
assert!(
serial.contains(&format!("korp.entry={product}")),
"the booted medium's cmdline never named korp.entry={product} — this is not \
{product}'s medium:\n{serial}",
);
assert!(
handed_off,
"the medium booted but korp-start never handed off: no `{handoff}\"…]` on the \
serial. phase {:?}\n{serial}",
robot.state_at("/phase"),
);
for refuse in ["FAILED exec", "Kernel panic"] {
assert!(
!serial.contains(refuse),
"the boot reached the handoff and then died with {refuse:?}:\n{serial}",
);
}
// ── THE SCREEN, reported honestly (captured or not — the gate is the serial) ──
match &best {
Some(s) => {
let out = std::env::temp_dir().join(format!("dwarves-{repo}-appliance-iso-screen.ppm"));
let _ = std::fs::write(&out, ppm_of(s));
eprintln!(
"[robot] gfx frame : {} ({}×{}, {best_ink} non-black bytes)",
out.display(),
s.width,
s.height,
);
}
None => eprintln!("[robot] gfx frame : none captured before the run settled"),
}
}