dwarves-gui/tests/robot_build_scene.rs
//! **The scene's guards.** Every one of them is watched failing here, in the same
//! test, against the input that makes it fail — a guard that has only ever been seen
//! green is a guard whose subject you are guessing at.
//!
//! The headline guard is [`the_scene_source_names_no_product`]: the scene's own source,
//! tokenised, may not contain a single product name. A product name in that file means
//! somebody put an `if` back, and the whole design is that there are no such `if`s.
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use dwarves_gui::build_scene::{
Act, Axis, BuildScene, Forge, Menu, Outcome, Phase, Proof, Query, Report, Row, Setting, Target,
};
use facett_core::Facet;
use nornir_build_thing::contract::{Coordinate, PaneKind, Receipt, RECEIPT_VERSION};
use nornir_build_thing::receipt_proof::ReceiptVerdict;
use nornir_build_thing::variant::ArtifactKind;
use nornir_robotui::RobotSession;
// ── the product-name guard ────────────────────────────────────────────────────
/// **Every product this constellation builds**, plus the three framework repos the
/// scene links against (`nornir`, `facett`, `edda`) and the forge's own name.
///
/// The framework three are deliberately IN the list rather than exempted wholesale:
/// they are allowed to appear in a `use` line and nowhere else, which is what
/// [`scan`] enforces. That way "the scene imports the contract crate" stays legal and
/// "the scene mentions the contract crate halfway down a render function" does not.
const PRODUCTS: [&str; 24] = [
"bifrost",
"draupnir",
"dwarves",
"edda",
"facett",
"garmr",
"gunnar",
"heimdal",
"heimdall",
"holger",
"jera",
"knut",
"korp",
"mimir",
"modgunn",
"njord",
"nornir",
"ordning",
"saga",
"skade",
"skidbladnir",
"tunnr",
"znippy",
"weston",
];
/// The scene, as it ships.
const SCENE: &str = include_str!("../src/build_scene.rs");
/// Every product name in `src`, in order of appearance.
///
/// The rule, stated so it can be argued with:
///
/// * everything from the file's own `#[cfg(test)]` line onward is not read. The subject
/// is the SHIPPED scene; the scene's unit tests have to model a REAL build report, and
/// a fixture with an invented variant name is a fixture that proves nothing about the
/// thing it stands in for. This is the one exemption that is neither a declaration nor
/// the shipped surface, and it is a TAIL exemption — the line above `#[cfg(test)]` is
/// still the scene;
/// * lines whose first non-space characters are `use ` are removed next. That is the
/// one place a dependency crate is named, and it is a declaration, not a decision;
/// * what is left is split on every character that is not a letter or a digit — so `_`
/// and `-` both split. `korp_server`, `korp-server` and `"korp"` all yield the token
/// `korp`, and `nornir_build_thing` outside a `use` line yields `nornir`;
/// * a token matches if it EQUALS a product name, case-insensitively.
///
/// Comments are NOT exempt, deliberately: `RED 2` below pins `"// korp."` as a finding,
/// because prose that reasons about one product by name is how a special case gets
/// argued back in.
fn scan(src: &str) -> Vec<String> {
// The TAIL exemption. `position` on the trimmed line, so an indented `#[cfg(test)]`
// inside a nested module does not cut the file short at the wrong place.
let shipped = src
.lines()
.position(|l| l.trim_end() == "#[cfg(test)]")
.map_or(usize::MAX, |i| i);
let body: String = src
.lines()
.take(shipped)
.filter(|l| !l.trim_start().starts_with("use "))
.collect::<Vec<_>>()
.join("\n");
body.split(|c: char| !c.is_ascii_alphanumeric())
.map(str::to_ascii_lowercase)
.filter(|t| PRODUCTS.contains(&t.as_str()))
.collect()
}
/// **The measure of done.** Zero product names in the scene — and the check watched
/// failing on four separate inputs first, because a scanner that finds nothing is
/// exactly what a broken scanner looks like.
#[test]
fn the_scene_source_names_no_product() {
// RED 1 — the shape this whole design exists to prevent: an `if` over a name.
assert_eq!(
scan("if row == \"korp\" { draw_the_special_pane() }"),
vec!["korp"],
"the scanner cannot see a product name in an if, so it can never report red"
);
// RED 2 — the separators. A product name is usually half of a longer name.
for spelling in [
"\"korp-server\"",
"let korp_server = 1;",
"// korp.",
"(korp)",
] {
assert_eq!(
scan(spelling),
vec!["korp"],
"the scanner missed the product name in {spelling:?}"
);
}
// RED 3 — the `use`-line exemption is exactly one line wide. Naming the contract
// crate anywhere else is a finding.
assert!(
scan("use nornir_build_thing::contract::PaneKind;").is_empty(),
"an import is a declaration, not a decision, and must stay legal"
);
assert_eq!(
scan(" let k = nornir_build_thing::variant::ArtifactKind::Exe;"),
vec!["nornir"],
"the exemption leaked past the use line, so the scanner is blind below it"
);
// RED 4 — the `#[cfg(test)]` TAIL exemption, in both directions at once. The scene's
// own unit tests may model a real report (`gunnar-appliance-iso`, `korp-falkordb`);
// the line ABOVE the tail may not.
assert_eq!(
scan("let _ = \"korp\";\n#[cfg(test)]\nmod t { fn f() { let _ = \"holger\"; } }"),
vec!["korp"],
"the tail exemption swallowed the line above `#[cfg(test)]` (or does not exempt \
the tail at all) — it is a tail, not a file-wide amnesty"
);
// RED 5 — the real file, plus one line, injected ABOVE the tail so the exemption
// cannot be what makes the guard quiet. This is the guard pointed at the actual
// subject, so a green below cannot be an accident of the file being unreadable.
let with_a_name = SCENE.replacen(
"\n#[cfg(test)]",
"\nfn special() { let _ = \"holger-ui\"; }\n#[cfg(test)]",
1,
);
assert_ne!(
with_a_name, SCENE,
"the injection point `#[cfg(test)]` is gone from the scene"
);
assert_eq!(
scan(&with_a_name),
vec!["holger"],
"the guard does not fire on the REAL scene source plus one product name"
);
assert!(!SCENE.is_empty(), "the scene source did not load");
assert!(
SCENE.lines().any(|l| l.trim_end() == "#[cfg(test)]"),
"the tail exemption is aimed at a line the scene does not have — it would be \
exempting nothing, or everything"
);
// GREEN — and it means something, because the four above were red.
let found = scan(SCENE);
assert!(
found.is_empty(),
"the scene names {} product(s): {found:?} — that is an `if` somebody put back",
found.len()
);
}
// ── the kind-dispatch guard ───────────────────────────────────────────────────
/// The ⚒ tree declaration, held to the same rule: it names the five leaves and their
/// shape, and a kind reaching into it would be the same defect one file later.
const TREE: &str = include_str!("../src/build_thing_scene.rs");
/// Every `ArtifactKind::` path in the SHIPPED half of `src`, with its line — the
/// scanner behind [`the_scene_dispatches_no_pane_on_a_kind`].
///
/// The same tail rule as [`scan`]: everything from the file's own top-level
/// `#[cfg(test)]` line onward is not read, and a line whose first non-space characters
/// are `use ` is a declaration, not a decision. ONE deliberate difference: comment
/// lines (`//`, `///`, `//!`) are exempt here and are NOT in the product guard. The
/// product guard reads prose because prose reasoning about one product is how a
/// special case gets argued back in; this guard hunts EXECUTABLE dispatch, and the
/// module doc's opening job is precisely to STATE the one legal mapping
/// (`ArtifactKind::Exe -> PaneKind::Exec`, …) so a reader knows where it lives — a
/// `match` in a comment compiles to nothing.
fn kind_dispatch(src: &str) -> Vec<String> {
let shipped = src
.lines()
.position(|l| l.trim_end() == "#[cfg(test)]")
.map_or(usize::MAX, |i| i);
src.lines()
.take(shipped)
.enumerate()
.filter(|(_, l)| {
let t = l.trim_start();
!t.starts_with("use ") && !t.starts_with("//")
})
.filter(|(_, l)| l.contains("ArtifactKind::"))
.map(|(i, l)| format!("line {}: {}", i + 1, l.trim()))
.collect()
}
/// **The scene dispatches no pane on a KIND.** `ArtifactKind::` must not appear in the
/// shipped half of `build_scene.rs` or `build_thing_scene.rs` — the only kind→pane
/// mapping the design allows is the declared `PaneKind` switch in edda
/// (`PaneKind::for_artifact`, reached through `Target::pane()`), which the scene calls
/// and never re-implements.
///
/// # Why this guard exists as a SIBLING of the product-name guard
///
/// [`the_scene_source_names_no_product`] scans 24 PRODUCT names, and that left it
/// blind to exactly one shape of special case: UI keyed on a KIND. That is how the
/// kernel configurator slipped past it — `kernel` is a kind, not a product, so a pane
/// wired to `ArtifactKind::Kernel` would read as clean to the product guard while
/// being the same `if` in different clothes. `d253db6` is the record: the kernel leaf
/// was offered to fifteen recipes that build no kernel, and no guard went red. The
/// doctrine (edda's `PaneKind` doc) says the scene "renders what a recipe declares" —
/// a KIND branch is a product branch one abstraction up, and this pins the class.
///
/// The scene is clean today (measured 2026-08-26: the only `ArtifactKind` in shipped
/// code is the `Row.kind` FIELD TYPE, which is data riding through, not a decision) —
/// this guard keeps it that way.
#[test]
fn the_scene_dispatches_no_pane_on_a_kind() {
// RED 1 — the shape this exists to catch: a pane picked by matching a kind.
let dispatch = "match t.kind { ArtifactKind::Kernel => self.draw_kernel_pane(ui), _ => {} }";
assert_eq!(
kind_dispatch(dispatch),
vec![format!("line 1: {dispatch}")],
"the scanner cannot see a kind-keyed match, so it can never report red"
);
// RED 2 — an `if` and a bare variant expression are the same decision.
for spelling in [
"if t.kind == ArtifactKind::Kernel { special(ui); }",
"let want = ArtifactKind::Iso;",
] {
assert_eq!(
kind_dispatch(spelling).len(),
1,
"the scanner missed the kind path in {spelling:?}"
);
}
// The two exemptions, exactly as wide as stated.
assert!(
kind_dispatch("use nornir_build_thing::variant::ArtifactKind;").is_empty(),
"an import is a declaration, not a decision, and must stay legal"
);
assert!(
kind_dispatch("//! ArtifactKind::Exe -> PaneKind::Exec argv + stdout").is_empty(),
"the module doc STATES the declared switch so a reader can find it — prose \
compiles to nothing, and forbidding it would strip the map from the file"
);
// RED 3 — the tail rule, both directions at once: the scene's unit tests may build
// a `Row` with a real kind, the line above the tail may not.
assert_eq!(
kind_dispatch(
"let k = ArtifactKind::Vm;
#[cfg(test)]
mod t { const K: ArtifactKind = ArtifactKind::Iso; }"
),
vec!["line 1: let k = ArtifactKind::Vm;".to_string()],
"the tail exemption swallowed the line above `#[cfg(test)]`, or exempts nothing"
);
// RED 4 — the real file plus one dispatch, injected ABOVE the tail, so a green on
// the real file cannot be an accident of the file being unreadable.
let with_a_kind = SCENE.replacen(
"\n#[cfg(test)]",
"\nfn special(t: &Target) -> bool { t.kind == ArtifactKind::Kernel }\n#[cfg(test)]",
1,
);
assert_ne!(
with_a_kind, SCENE,
"the injection point `#[cfg(test)]` is gone from the scene"
);
assert_eq!(
kind_dispatch(&with_a_kind).len(),
1,
"the guard does not fire on the REAL scene source plus one kind dispatch"
);
// GREEN — and it means something, because the four above were red.
for (name, src) in [("build_scene.rs", SCENE), ("build_thing_scene.rs", TREE)] {
let found = kind_dispatch(src);
assert!(
found.is_empty(),
"{name} dispatches on a KIND in its shipped half: {found:?} — the kernel \
pane went through the product-name guard exactly this way, because a kind \
is not a product name; the one legal mapping is edda's PaneKind switch"
);
}
}
// ── a forge that knows nothing, for a scene that knows nothing ────────────────
/// The rows the fake declares. Deliberately not any product's names: a fixture that
/// spelled a real atom would make the guard above pass for the wrong reason.
const ALPHA: &str = "alpha";
const BETA: &str = "beta";
/// A forge that records what it was asked and answers from a script.
#[derive(Default)]
struct Fake {
/// Every [`Act`] the scene sent, in order.
acts: Arc<Mutex<Vec<Act>>>,
/// What [`Forge::poll`] returns.
report: Arc<Mutex<Report>>,
/// Queries that must fail, and why.
refuse: Option<String>,
}
impl Fake {
fn new() -> Self {
Self::default()
}
/// A row whose single column produces `kind`.
fn row(name: &str, kind: ArtifactKind, target: &str) -> Row {
Row {
name: name.to_string(),
badges: vec!["ORIGINAL".to_string()],
targets: vec![
Target {
name: target.to_string(),
kind,
pane: None,
declared: true,
variant: None,
settings: vec![
Setting::open("gpu", "false"),
Setting::fixed("base", "scratch", "declared by the recipe"),
],
},
Target {
name: "rpm".to_string(),
kind: ArtifactKind::Exe,
pane: None,
declared: false,
variant: None,
settings: vec![],
},
],
}
}
}
impl Forge for Fake {
/// No manifests behind this fake, so no picture — which is a different answer
/// from a failed one and says so. The axis edge itself is measured in
/// `robot_build_scene_ui.rs`, against a real crate graph on disk.
fn axis(&mut self, _pick: &dwarves_gui::build_scene::AxisPick) -> Axis {
Axis::Absent
}
fn resolve(&mut self, query: &Query) -> Result<Menu, String> {
if let Some(why) = &self.refuse {
return Err(why.clone());
}
let how = match query {
Query::Artifact(_) => "löst ur namnet",
Query::At(_) => "angiven koordinat",
};
Ok(Menu {
coordinate: Coordinate::parse("a-build-thing:0.3.1:a91f4c2e").unwrap(),
how: how.to_string(),
rows: vec![
Fake::row(ALPHA, ArtifactKind::Exe, "exe"),
Fake::row(BETA, ArtifactKind::Container, "container"),
],
})
}
fn act(&mut self, act: Act) {
self.acts.lock().unwrap().push(act);
}
fn poll(&mut self) -> Report {
self.report.lock().unwrap().clone()
}
}
/// The landed-record sentence, in `BuildRecordReceipt::line`'s own shape.
const WAREHOUSE_LANDED: &str =
"build records: 1 provenance + 3 build_info row(s) landed in `nordisk` at \
http://127.0.0.1:7878";
/// A finished build, so the pane below has something to act on.
fn built() -> Report {
Report {
phase: Some(Phase::Done),
elapsed_ms: 12_400,
outcome: Some(Outcome {
ok: true,
artifact: "target/release/alpha".to_string(),
size_bytes: Some(38_200_000),
sha: Some("7c1a2b3c4d5e".to_string()),
receipt: Some(Receipt {
receipt_version: RECEIPT_VERSION,
build_thing: Coordinate::parse("a-build-thing:0.3.1:a91f4c2e").unwrap(),
atom: ALPHA.to_string(),
target: "exe".to_string(),
settings: BTreeMap::from([("gpu".to_string(), "false".to_string())]),
inputs: BTreeMap::new(),
}),
// The receipt says `gpu = false`; the binary agrees. Both halves are
// present here so the pane's `kvitto` and `bevis` rows have something
// to render, and so `proven` in the robot JSON is exercised as `true`
// by a case that could also be `false`.
proof: Some(Proof {
verdict: ReceiptVerdict::Proven,
lines: vec!["gpu = false and the binary links no GPU stack ✓".to_string()],
}),
elapsed_ms: 12_400,
ports: vec![5901],
note: None,
// A fixture-filled per-site value, as the forge spells it — the
// pane must say so beside the receipt, never silently.
applied: vec![
"hub_conf ← fixture wg-hub (wg-hub pub=k endpoint=10.0.2.2:1 key=/s/k.key)"
.to_string(),
],
// The build's provenance record LANDED. Worded exactly as
// `BuildRecordReceipt::line` words it, so this fixture cannot drift
// into a sentence the real writer never produces.
warehouse_line: WAREHOUSE_LANDED.to_string(),
warehouse_red: false,
warehouse_landed: true,
features: None,
}),
live: false,
lines: vec!["USAGE: …".to_string()],
screen: None,
note: None,
menu: None,
}
}
/// Wire a scene onto a fake, keeping the fake's two shared slots.
fn scene() -> (BuildScene, Arc<Mutex<Vec<Act>>>, Arc<Mutex<Report>>) {
let f = Fake::new();
let (acts, report) = (f.acts.clone(), f.report.clone());
(BuildScene::new(Box::new(f)), acts, report)
}
/// Drive one message through the headless seam.
fn drive(s: &mut BuildScene, msg: &str) {
s.update_json(msg);
}
// ── the flow ──────────────────────────────────────────────────────────────────
/// Steps 1→5 in order, and the two refusals in the middle of them.
///
/// Both directions are asserted, so the "can build" oracle is watched red (a row with
/// no column) before it is trusted green (a row and a column).
#[test]
fn the_flow_needs_one_row_and_one_column_before_it_can_build() {
let (mut s, acts, _r) = scene();
// RED — nothing typed.
drive(&mut s, r#"{"resolve":true}"#);
assert!(
s.state_json()["error"].as_str().is_some(),
"an empty artifact field resolved to something"
);
assert!(s.state_json()["rows"].as_array().unwrap().is_empty());
// Step 1+2 — a name, and the matrix.
drive(&mut s, &format!(r#"{{"artifact":"{ALPHA}"}}"#));
drive(&mut s, r#"{"resolve":true}"#);
let st = s.state_json();
assert_eq!(st["recipe"], "a-build-thing:0.3.1:a91f4c2e");
assert_eq!(
st["rows"].as_array().unwrap().len(),
2,
"the whole home matrix"
);
// The typed name matched a row, so that row is picked — a name matching itself.
assert_eq!(st["row"], ALPHA);
// RED — a row alone cannot build, and pressing BYGG must reach nothing.
assert_eq!(st["target"], serde_json::Value::Null);
assert_eq!(
st["can_build"], false,
"a row with no column armed the build"
);
drive(&mut s, r#"{"build":true}"#);
assert!(
acts.lock().unwrap().is_empty(),
"BYGG fired with no column picked"
);
// RED — a column the recipe does not declare cannot be picked.
drive(&mut s, r#"{"pick_target":"rpm"}"#);
assert_eq!(
s.state_json()["target"],
serde_json::Value::Null,
"an undeclared column was selectable"
);
// Step 4+5.
drive(&mut s, r#"{"pick_target":"exe"}"#);
let st = s.state_json();
assert_eq!(st["target"], "exe");
assert_eq!(
st["can_build"], true,
"a row AND a column still would not build"
);
drive(&mut s, r#"{"build":true}"#);
let sent = acts.lock().unwrap();
assert_eq!(sent.len(), 1, "BYGG did not reach the forge exactly once");
match &sent[0] {
Act::Build {
row,
target,
settings,
} => {
assert_eq!(row, ALPHA);
assert_eq!(target, "exe");
// Every declared setting travels, including the one the recipe fixed —
// a setting missing here is a setting missing from the receipt.
assert_eq!(settings.get("gpu").map(String::as_str), Some("false"));
assert_eq!(settings.get("base").map(String::as_str), Some("scratch"));
}
other => panic!("BYGG sent {other:?} instead of a build"),
}
}
/// Step 3 is single-selection, and it invalidates everything downstream.
///
/// Watched red by asserting the SAME expressions before the second pick, when they
/// are all populated: without the clear in `pick_row` the three assertions below
/// would still see the previous row's column, outcome and pane.
#[test]
fn picking_a_second_row_clears_the_column_the_outcome_and_the_pane() {
let (mut s, _acts, report) = scene();
drive(&mut s, &format!(r#"{{"artifact":"{ALPHA}"}}"#));
drive(&mut s, r#"{"resolve":true}"#);
drive(&mut s, r#"{"pick_target":"exe"}"#);
*report.lock().unwrap() = built();
drive(&mut s, r#"{"poll":true}"#);
// RED — the state the assertions below must NOT find after the second pick.
let st = s.state_json();
assert_eq!(st["target"], "exe");
assert_eq!(st["pane"], "exec");
assert!(
!st["outcome"].is_null(),
"the fixture never produced an outcome"
);
drive(&mut s, &format!(r#"{{"pick_row":"{BETA}"}}"#));
let st = s.state_json();
assert_eq!(st["row"], BETA, "the second row was not picked");
assert!(
st["target"].is_null(),
"the previous row's column survived the pick"
);
assert!(
st["pane"].is_null(),
"a pane is open for a row nobody is looking at"
);
assert!(
st["outcome"].is_null(),
"the previous row's outcome survived the pick"
);
assert_eq!(st["can_build"], false);
}
/// **The whole switch**, asserted through the scene rather than through the contract:
/// what the operator sees for each kind is what [`PaneKind::for_artifact`] says.
///
/// Red by construction — the four expectations differ, so a switch collapsed to any
/// single arm fails at least three of them.
#[test]
fn every_artifact_kind_opens_the_pane_the_design_names() {
let cases = [
(ArtifactKind::Exe, "exec"),
(ArtifactKind::Container, "log"),
(ArtifactKind::Service, "log"),
(ArtifactKind::Iso, "console"),
(ArtifactKind::Vm, "console"),
(ArtifactKind::Lib, "none"),
(ArtifactKind::Wasm, "none"),
];
// The expectations are not all the same value, so no constant answer passes.
let distinct: std::collections::BTreeSet<&str> = cases.iter().map(|(_, p)| *p).collect();
assert_eq!(distinct.len(), 4, "the case table lost its discrimination");
for (kind, want) in cases {
let mut f = Fake::new();
f.report = Arc::new(Mutex::new(built()));
let mut s = BuildScene::new(Box::new(FixedKind { kind, inner: f }));
drive(&mut s, r#"{"artifact":"x"}"#);
drive(&mut s, r#"{"resolve":true}"#);
drive(&mut s, r#"{"pick_row":"only"}"#);
drive(&mut s, r#"{"pick_target":"the-target"}"#);
assert_eq!(
s.state_json()["pane"],
want,
"{kind:?} opened the wrong pane"
);
}
}
/// A forge whose one row's one column produces exactly the kind under test.
struct FixedKind {
kind: ArtifactKind,
inner: Fake,
}
impl Forge for FixedKind {
fn axis(&mut self, pick: &dwarves_gui::build_scene::AxisPick) -> Axis {
self.inner.axis(pick)
}
fn resolve(&mut self, _query: &Query) -> Result<Menu, String> {
Ok(Menu {
coordinate: Coordinate::parse("a-build-thing:0.1.0").unwrap(),
how: "test".to_string(),
rows: vec![Row {
name: "only".to_string(),
badges: vec![],
targets: vec![Target {
name: "the-target".to_string(),
kind: self.kind,
pane: None,
declared: true,
variant: None,
settings: vec![],
}],
}],
})
}
fn act(&mut self, act: Act) {
self.inner.act(act);
}
fn poll(&mut self) -> Report {
self.inner.poll()
}
}
/// A recipe may override the pane its kind would get — and the override is the only
/// thing that can move it, so the default is still the interface.
#[test]
fn a_recipe_can_override_the_pane_its_kind_would_get() {
let t = Target {
name: "t".to_string(),
kind: ArtifactKind::Exe,
pane: None,
declared: true,
variant: None,
settings: vec![],
};
// RED — the default, so the override below cannot pass by coincidence.
assert_eq!(t.pane(), PaneKind::Exec);
let overridden = Target {
pane: Some(PaneKind::None),
..t
};
assert_eq!(
overridden.pane(),
PaneKind::None,
"the recipe's override was ignored"
);
}
/// **Two entry points, one state.** The scene downstream of the resolve cannot tell
/// which door was used, because there is only one thing to tell.
#[test]
fn both_entry_points_land_in_the_same_state() {
let by_name = {
let (mut s, _a, _r) = scene();
drive(&mut s, &format!(r#"{{"artifact":"{ALPHA}"}}"#));
drive(&mut s, r#"{"resolve":true}"#);
drive(&mut s, r#"{"pick_target":"exe"}"#);
s.state_json()
};
let by_coordinate = {
let (mut s, _a, _r) = scene();
drive(&mut s, r#"{"coordinate":"a-build-thing:0.3.1:a91f4c2e"}"#);
drive(&mut s, r#"{"resolve_coordinate":true}"#);
drive(&mut s, &format!(r#"{{"pick_row":"{ALPHA}"}}"#));
drive(&mut s, r#"{"pick_target":"exe"}"#);
s.state_json()
};
for key in ["recipe", "rows", "row", "target", "pane", "can_build"] {
assert_eq!(
by_name[key], by_coordinate[key],
"the two doors disagree about {key}"
);
}
// …and `how` is the ONE thing that differs, which is why it is shown.
assert_ne!(by_name["how"], by_coordinate["how"]);
// RED — a coordinate that cannot resolve to exactly one recipe is refused, not
// defaulted. `Coordinate::parse` owns that rule; the scene must surface it.
let (mut s, _a, _r) = scene();
drive(&mut s, r#"{"coordinate":"a-build-thing"}"#);
drive(&mut s, r#"{"resolve_coordinate":true}"#);
let st = s.state_json();
assert!(
st["error"]
.as_str()
.is_some_and(|e| e.contains("a-build-thing")),
"a versionless coordinate resolved to something: {:?}",
st["error"]
);
assert!(st["recipe"].is_null());
}
/// A pane verb before there is anything to act on reaches the forge not at all.
///
/// Watched red the only honest way: the same verb, after a build, DOES reach it.
#[test]
fn a_pane_verb_reaches_the_forge_only_after_a_build() {
let (mut s, acts, report) = scene();
drive(&mut s, &format!(r#"{{"artifact":"{ALPHA}"}}"#));
drive(&mut s, r#"{"resolve":true}"#);
drive(&mut s, r#"{"pick_target":"exe"}"#);
drive(&mut s, r#"{"argv":"--help"}"#);
drive(&mut s, r#"{"run":true}"#);
drive(&mut s, r#"{"start":true}"#);
drive(&mut s, r#"{"stop":true}"#);
drive(&mut s, r#"{"reboot":true}"#);
assert!(
acts.lock().unwrap().is_empty(),
"a pane verb ran against a build that never happened"
);
*report.lock().unwrap() = built();
drive(&mut s, r#"{"poll":true}"#);
drive(&mut s, r#"{"run":true}"#);
let sent = acts.lock().unwrap();
assert_eq!(
sent.len(),
1,
"the same verb did not reach the forge after a build"
);
assert_eq!(sent[0], Act::Run("--help".to_string()));
}
/// **A Console pane shows BOTH consoles at once**, and the graphical one says it has no
/// frame rather than drawing a placeholder that could be read as the guest's screen.
///
/// The two-at-once rule is the refinement of 2026-08-06: during an install the serial
/// says what is HAPPENING and the graphical says what the USER SEES, and a toggle forces
/// a choice between them in the one situation that needs both. Watched red against the
/// two panes that are NOT consoles, which list none — a constant `["serial","graphical"]`
/// would fail there.
#[test]
fn a_console_pane_shows_both_consoles_and_an_absent_frame_says_so() {
let (mut s, _a, report) = scene();
drive(&mut s, &format!(r#"{{"artifact":"{ALPHA}"}}"#));
drive(&mut s, r#"{"resolve":true}"#);
// RED — before a column is picked there is no pane, so there are no consoles.
assert_eq!(s.state_json()["consoles"], serde_json::json!([]));
// RED — an Exec pane is not a console pane, so it lists none either.
drive(&mut s, r#"{"pick_target":"exe"}"#);
*report.lock().unwrap() = built();
drive(&mut s, r#"{"poll":true}"#);
let st = s.state_json();
assert_eq!(st["pane"], "exec");
assert_eq!(
st["consoles"],
serde_json::json!([]),
"an exe pane grew a console"
);
// A Console pane lists BOTH, in the order the design names them.
let mut s = console_scene(&report);
drive(&mut s, r#"{"poll":true}"#);
let st = s.state_json();
assert_eq!(st["pane"], "console");
assert_eq!(
st["consoles"],
serde_json::json!(["serial", "graphical"]),
"the console pane is showing one console, so it is still a toggle"
);
// The graphical half is honest when the forge has produced no frame.
assert!(
st["screen"].is_null(),
"a frame appeared out of a forge that has none"
);
// …and the serial half is up at the same time, with the lines the forge has.
assert_eq!(
st["line_count"], 1,
"the serial half is empty while the frame is absent"
);
// RED — with a frame, the same field reports its real dimensions, and the serial
// lines are STILL there: both halves are live together, not one instead of the other.
let mut r = built();
r.screen = Some(dwarves_gui::build_scene::Screen {
width: 4,
height: 2,
rgba: vec![0; 4 * 2 * 4],
});
r.lines = vec!["installer stage 1".to_string(), "partitioning".to_string()];
*report.lock().unwrap() = r;
drive(&mut s, r#"{"poll":true}"#);
let st = s.state_json();
assert_eq!(st["screen"]["width"], 4);
assert_eq!(st["screen"]["bytes"], 32);
assert_eq!(
st["line_count"], 2,
"the serial half went dark when a frame arrived"
);
assert_eq!(st["consoles"], serde_json::json!(["serial", "graphical"]));
// There is no message that can turn one of them off — a toggle would have one.
drive(&mut s, r#"{"console":"graphical"}"#);
assert_eq!(
s.state_json()["consoles"],
serde_json::json!(["serial", "graphical"]),
"a console selector survived, so the pane can still be put into one mode"
);
}
/// A scene sitting on a picked Iso column — the Console pane's subject.
fn console_scene(report: &Arc<Mutex<Report>>) -> BuildScene {
let mut f = Fake::new();
f.report = report.clone();
let mut s = BuildScene::new(Box::new(FixedKind {
kind: ArtifactKind::Iso,
inner: f,
}));
drive(&mut s, r#"{"artifact":"x"}"#);
drive(&mut s, r#"{"resolve":true}"#);
drive(&mut s, r#"{"pick_row":"only"}"#);
drive(&mut s, r#"{"pick_target":"the-target"}"#);
s
}
/// A resolve that fails is shown verbatim, and leaves no half-built matrix behind.
#[test]
fn a_failed_resolve_is_shown_and_clears_the_matrix() {
let (mut s, _a, _r) = scene();
drive(&mut s, &format!(r#"{{"artifact":"{ALPHA}"}}"#));
drive(&mut s, r#"{"resolve":true}"#);
assert_eq!(s.state_json()["rows"].as_array().unwrap().len(), 2);
let mut f = Fake::new();
f.refuse = Some("inget recept deklarerar den artefakten".to_string());
let mut s = BuildScene::new(Box::new(f));
drive(&mut s, &format!(r#"{{"artifact":"{ALPHA}"}}"#));
drive(&mut s, r#"{"resolve":true}"#);
let st = s.state_json();
assert_eq!(st["error"], "inget recept deklarerar den artefakten");
assert!(st["rows"].as_array().unwrap().is_empty());
assert!(st["recipe"].is_null());
assert_eq!(st["can_build"], false);
}
/// The receipt is rendered as the sketches show it: the recipe, then every resolved
/// setting. A build with no receipt SAYS there is none.
#[test]
fn the_receipt_line_names_the_recipe_and_every_resolved_setting() {
let r = built().outcome.unwrap().receipt.unwrap();
let line = dwarves_gui::build_scene::receipt_line(&r);
assert!(line.starts_with("a-build-thing:0.3.1 a91f4c2e"), "{line}");
assert!(
line.contains("gpu=false"),
"a resolved setting is missing from {line}"
);
// A REAL recipe hash is a full sha256, and the line shows it short: 64
// characters would push every resolved setting off the row, which is the part
// an operator reads. The record is untouched — only the render shortens.
let mut long = r.clone();
long.build_thing.hash =
Some("e484a4c6dfcbeaa18d1bac7915750420c817bc23dd322b7da954a818b904d78f".to_string());
let line = dwarves_gui::build_scene::receipt_line(&long);
assert!(
line.contains("e484a4c6…"),
"the hash was not shortened: {line}"
);
assert!(
!line.contains("904d78f"),
"the whole 64-character digest is on the line: {line}"
);
assert!(line.contains("gpu=false"), "the settings fell off: {line}");
assert_eq!(
long.build_thing.hash.as_deref().map(str::len),
Some(64),
"the render mutated the record"
);
// RED — an outcome without a receipt is reported as such, never as an empty one.
let (mut s, _a, report) = scene();
drive(&mut s, &format!(r#"{{"artifact":"{ALPHA}"}}"#));
drive(&mut s, r#"{"resolve":true}"#);
drive(&mut s, r#"{"pick_target":"exe"}"#);
let mut b = built();
b.outcome.as_mut().unwrap().receipt = None;
*report.lock().unwrap() = b;
drive(&mut s, r#"{"poll":true}"#);
assert_eq!(s.state_json()["outcome"]["has_receipt"], false);
// …and the APPLIED per-site record still says where a value came from: the
// fixture line is its own row, independent of the receipt.
let applied = s.state_json()["outcome"]["applied"].clone();
assert_eq!(
applied,
serde_json::json!([
"hub_conf ← fixture wg-hub (wg-hub pub=k endpoint=10.0.2.2:1 key=/s/k.key)"
]),
"the fixture-filled value is named in the result: {applied}"
);
}
/// **Only `Proven` passes, and the pane says which of the other five it is.**
///
/// This is the whole reason [`Proof`] carries the verdict rather than a boolean.
/// `NotMeasured` is a stripped binary — gunnar's `[profile.release]` sets
/// `strip = true`, so it is the ordinary case for a shipped artifact, not an
/// exotic one — and folding it into a pass would make the third level green for
/// ever on precisely the artifacts that ship.
///
/// Watched red by asserting the false side over every non-passing variant, not
/// by asserting the true side over one.
#[test]
fn every_verdict_except_proven_reads_as_not_proven_and_has_its_own_word() {
use ReceiptVerdict::{
Disproven, MalformedReceipt, NoReceipt, NotMeasured, NothingProvable, Proven,
};
let of = |v| Proof {
verdict: v,
lines: vec![],
};
assert!(of(Proven).holds(), "the one passing verdict does not pass");
for v in [
NotMeasured,
NoReceipt,
MalformedReceipt,
NothingProvable,
Disproven,
] {
assert!(
!of(v).holds(),
"{v:?} reads as proven — an unchecked claim is not a verified one"
);
}
// Six distinct words: an operator has to be able to tell "the binary
// contradicts its receipt" from "nobody could read the binary".
let words: std::collections::BTreeSet<&str> = [
Proven,
Disproven,
NoReceipt,
MalformedReceipt,
NothingProvable,
NotMeasured,
]
.into_iter()
.map(|v| of(v).label())
.collect();
assert_eq!(words.len(), 6, "two verdicts share a word: {words:?}");
// And the robot JSON carries it, with `proven` false on a build that finished
// green. A green build whose receipt is unproven is exactly the state that
// must be visible.
let (mut s, _a, report) = scene();
drive(&mut s, &format!(r#"{{"artifact":"{ALPHA}"}}"#));
drive(&mut s, r#"{"resolve":true}"#);
drive(&mut s, r#"{"pick_target":"exe"}"#);
let mut b = built();
b.outcome.as_mut().unwrap().proof = Some(of(NotMeasured));
*report.lock().unwrap() = b;
drive(&mut s, r#"{"poll":true}"#);
let st = s.state_json();
assert_eq!(st["outcome"]["ok"], true, "the build itself failed");
assert_eq!(st["outcome"]["proven"], false);
assert_eq!(st["outcome"]["proof"], of(NotMeasured).label());
// The passing case, so the assertion above is not passing because the field
// is always false.
let mut b = built();
b.outcome.as_mut().unwrap().proof = Some(of(Proven));
*report.lock().unwrap() = b;
drive(&mut s, r#"{"poll":true}"#);
assert_eq!(s.state_json()["outcome"]["proven"], true);
}
// ── the pane must ASK to be drawn again ───────────────────────────────────────
/// **The pane asks egui for the next frame while the work is somewhere else.**
///
/// egui draws on demand. Every slow thing this pane shows — the build, the boot, the
/// guest's serial and framebuffer, the dependency graph — happens off the ui thread
/// and arrives through [`Forge::poll`]/[`Forge::axis`], so unless the pane asks for
/// another frame there is nothing to make one happen.
///
/// **It did not ask, and it shipped that way.** MEASURED 2026-08-16 driving the real
/// `dwarves-ui-gpu` binary on a real X server: BYGG on `gunnar /
/// installer:gunnar-installer` left the window frozen at `BYGGER 0.3s` for the whole
/// 237-second build. Moving the mouse — any event at all — repainted it and the
/// finished run appeared at once. The build was never broken; nobody had asked for a
/// frame.
///
/// **No `egui_kittest` test in this tree could ever have caught it**, which is why
/// this one is written against the raw [`egui::Context`] instead. `Harness::run()`
/// pumps frames unconditionally, so a harness that always redraws is structurally
/// blind to a pane that never asks to be redrawn (LAW 2: the guard was green about
/// something else).
///
/// The oracle is the APPLIED output — `ViewportOutput::repaint_delay`, the number
/// eframe's event loop actually waits on — not a call count and not a state
/// round-trip. `Duration::MAX` is egui's "do not wake up".
///
/// RED-WHEN-BROKEN in both directions, and both are needed: drop the
/// `request_repaint_after` and the working case goes `MAX`; ask unconditionally
/// (a plain `request_repaint()` every frame) and the settled case stops being `MAX`
/// and the pane spins the CPU forever on an idle screen.
#[test]
fn the_pane_asks_for_frames_while_work_is_in_flight() {
/// The delay eframe would sleep for once the pane has SETTLED.
///
/// Settled, and not the first pass, because egui itself asks for an immediate
/// second pass on a fresh [`egui::Context`] — fonts, layout, the tessellator all
/// want one — so pass 1 reports `0ns` for every input and would make this whole
/// test green about egui's warm-up instead of about the pane. Measured 2026-08-16
/// while writing it: the idle case came back `0ns` on the first pass and
/// `Duration::MAX` from the third onward.
fn repaint_delay(s: &mut BuildScene) -> std::time::Duration {
let ctx = egui::Context::default();
let mut delay = std::time::Duration::ZERO;
for _ in 0..4 {
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| s.ui(ui));
// egui 0.36 debug-asserts on a FullOutput dropped with unapplied texture
// deltas; nothing is uploaded headless, so they are cleared (2026-10-03).
out.textures_delta.clear();
delay = out
.viewport_output
.get(&egui::ViewportId::ROOT)
.expect("the root viewport is always in the output")
.repaint_delay;
}
delay
}
let (mut s, _acts, report) = scene();
drive(&mut s, &format!(r#"{{"artifact":"{ALPHA}"}}"#));
drive(&mut s, r#"{"resolve":true}"#);
drive(&mut s, r#"{"pick_target":"exe"}"#);
// RED FIRST — a settled pane must NOT ask, or the fix is "repaint forever" and
// the assertion below would pass on a pane that burns a core doing nothing.
*report.lock().unwrap() = built();
assert_eq!(
repaint_delay(&mut s),
std::time::Duration::MAX,
"a finished build still asks egui to wake up — this pane never sleeps",
);
// A build in flight: the pane must ask, and soon enough to read as a timer.
*report.lock().unwrap() = Report {
phase: Some(Phase::Building),
elapsed_ms: 300,
live: true,
..Report::default()
};
let d = repaint_delay(&mut s);
assert!(
d <= std::time::Duration::from_millis(250),
"a build in flight asked for the next frame in {d:?} — the operator watches a \
still picture until they happen to move the mouse",
);
// A live boot with no `Phase::Building` — the console leg, which is the one that
// streams serial lines and framebuffers and therefore needs it most.
*report.lock().unwrap() = Report {
phase: Some(Phase::Done),
live: true,
lines: vec!["INSTALLER: INSTALL-OK".to_string()],
..Report::default()
};
let d = repaint_delay(&mut s);
assert!(
d <= std::time::Duration::from_millis(250),
"a LIVE subject asked for the next frame in {d:?} — the serial and the \
graphical console would only advance when something else woke the window",
);
}
// ── where the build record landed ─────────────────────────────────────────────
/// The pane, wrapped so the robot can read its observable state.
struct SceneApp(BuildScene);
impl nornir_robotui::Observable for SceneApp {
fn state_json(&self) -> serde_json::Value {
Facet::state_json(&self.0)
}
}
/// A `Report` whose outcome is `built()`'s with the three warehouse fields set,
/// so each of the three landings is the SAME build in every other respect. Two
/// states that differ in more than the thing under test cannot tell you which
/// difference the pane reacted to.
fn landed_as(line: &str, red: bool, landed: bool) -> Report {
let mut r = built();
let o = r.outcome.as_mut().expect("built() has an outcome");
o.warehouse_line = line.to_string();
o.warehouse_red = red;
o.warehouse_landed = landed;
r
}
/// Drive the real pane to a finished build and hand back a live robot over it.
fn drawn(report: Report) -> RobotSession<'static, SceneApp> {
let f = Fake::new();
*f.report.lock().unwrap() = report;
let mut scene = BuildScene::new(Box::new(f));
// Steps 1→4 through the headless seam; step 5's OUTCOME is already in the
// fake's report, which is what puts the result head on screen.
scene.update_json(r#"{"artifact":"alpha"}"#);
scene.update_json(r#"{"resolve":true}"#);
scene.update_json(&format!(r#"{{"pick_row":"{ALPHA}"}}"#));
scene.update_json(r#"{"pick_target":"exe"}"#);
let harness = nornir_robotui::egui_kittest::Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(1400.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(3);
robot
}
/// The three sentences, as the record writer words them. Kept here so the three
/// states are compared against each OTHER as well as against the screen.
const NOT_SUBMITTED: &str = "build records: NOT SUBMITTED — no nornir server configured \
(set NORNIR_SERVER); this build filed no provenance";
const ZERO_ROWS: &str = "⚠ build records: ZERO ROWS LANDED at http://127.0.0.1:7878 \
(build_provenance=0, build_info=0/3 offered) — dwarves-buildthing-10";
/// **The register row is LAID OUT on the real pane, and it says which of the three
/// landings this build had.**
///
/// ── WHAT WAS WRONG ────────────────────────────────────────────────────────────
///
/// The ⚒ pane never read the build's warehouse receipt at all: `Outcome::from_report`
/// took the report and dropped it, and `grep warehouse` over `build_scene.rs` and
/// `build_forge.rs` answered with NOTHING. So a build that filed its provenance and a
/// build that filed nowhere rendered identically — which is the four-times-empty
/// records disease seen from the producing end. The receipt type's own doc says a
/// submit "must never be invisible"; on this pane it was.
///
/// ── WHY THIS GUARD IS TWO-SIDED ───────────────────────────────────────────────
///
/// Each state asserts its own sentence is on screen **with area** AND that the other
/// two are not. A pane that drew a constant string, or drew the row unconditionally
/// from stale state, passes the first half and fails the second. Asserting only "the
/// register row exists" would be green against a pane that always says the same thing,
/// which is precisely the failure being fixed.
#[test]
fn where_the_build_record_landed_is_drawn_on_the_real_pane() {
let states: [(&str, bool, bool); 3] = [
(WAREHOUSE_LANDED, false, true),
(ZERO_ROWS, true, false),
(NOT_SUBMITTED, false, false),
];
let all: Vec<&str> = states.iter().map(|(l, _, _)| *l).collect();
for (line, red, landed) in states {
let robot = drawn(landed_as(line, red, landed));
// MINE — laid out, with area. A widget with a zero rect is not on screen.
let rect = robot.label_visibility(line).unwrap_or_else(|e| {
panic!(
"the register row is not on the pane for state (red={red}, landed={landed}): \
{e}\nthe pane says: {}",
robot.state()
)
});
assert!(
rect.area() > 0.0,
"the register row is laid out at zero area for (red={red}, landed={landed}): {rect:?}"
);
// NOT THE OTHERS — the row is a report, not a decoration.
for other in all.iter().filter(|o| **o != line) {
assert!(
robot.label_visibility(other).is_err(),
"the pane drew a landing it did not have: showing {other:?} while the \
build's own landing was {line:?}"
);
}
// And the same three facts are readable headlessly, so a driver need not
// scrape pixels to know where a build filed.
let o = robot
.state_at("/outcome")
.expect("the outcome is observable");
assert_eq!(
o["warehouse_line"], line,
"the observable line disagrees with the screen"
);
assert_eq!(o["warehouse_red"], red);
assert_eq!(o["warehouse_landed"], landed);
}
}