dwarves-gui/tests/robot_kernel_scene.rs
//! **ROBOT-UI: the kernel feature scene — a preset picked through a real widget, a
//! symbol clicked OFF through a real widget, and a spin that is CAMERA-ONLY.**
//!
//! `d0f559d` gave the `kernel` KIND a cell to click in the ⚒ matrix. It showed the
//! preset, the version, the resolved base — four words about a machine with dozens of
//! parts. Design §10 asks for the parts: *"add a rotating 3d graph over the feature
//! select … like 'remove this part and then no need for this dependency'."*
//!
//! This file drives `kernel_scene::KernelScene` through its LIVE widgets — never
//! `update_json` — the way `robot_kernel_kind.rs` and `robot_build_scene_ui.rs` do. It
//! is its own test binary so the process-wide `kernel_graph_build_calls()` counter
//! reflects only this scenario (the isolation `robot_funnel_dag_3d` uses).
//!
//! # Every green here carries a red counterweight
//!
//! A claim that cannot fail is not a test, so each step below is paired with the input
//! that would have made it fail, in the same run:
//!
//! | the claim | its counterweight |
//! |---|---|
//! | picking `nuked` changes the graph | `full` in the same session draws ZERO nodes and says why — so "the pick reaches the graph" is not a pane that always shows the same cloud |
//! | clicking `CONFIG_NET_CORE` off turns `CONFIG_NET` amber | clicking `CONFIG_VIRTIO_MENU` off turns NOTHING amber — `CONFIG_VIRTIO` keeps three other children, so the cascade is about the relation and not about every click |
//! | dropping `CONFIG_NLS` is REFUSED by name | the two clicks above were ACCEPTED, so "refused" is a statement about the boot contract and not a pane that refuses everything |
//! | `build_count` stays 1 across 24 spin frames + two cascades | changing the preset bumps it to 2 in the same session, so `== 1` is not a constant |
//!
//! # What I watched fail, before any of it was green
//!
//! Six sabotages, each run against this scene's own model:
//!
//! 1. dropped the underscore boundary from the name-parent rule → `CONFIG_NET` "parented"
//! `CONFIG_NETDEVICES` and a fabricated edge entered the cascade;
//! 2. removed the backward (orphan) arm of the cascade → clicking `CONFIG_NET_CORE` off
//! left `CONFIG_NET` `locked`, i.e. the pane could not answer §10's one question;
//! 3. removed the refusal → dropping `CONFIG_NLS` came back `clicked_off`, silently
//! taking the boot contract's charset tables with it;
//! 4. made a cascade re-run the layout → `build_count` went 2 → 3 on a click, the exact
//! "the graph jumps under the pointer that just clicked it" defect;
//! 5. treated `# CONFIG_X is not set` as "not a node" → `nuked` collapsed from 48 nodes
//! to 32 and its sixteen named absences — its entire content — vanished from the
//! picture;
//! 6. computed the symbol universe from `base.config` alone → `nuked` and `normal` both
//! reported 57 undecided and the "smaller machine" claim became a tautology.
//!
//! …and three more at THIS layer, two of which were not sabotage at all but real defects
//! this scenario found on its first run, in a pane whose own unit tests were already
//! green — which is the entire argument for driving the widgets instead of the seam:
//!
//! 7. the symbol list was laid out at x≈1989 in a 1500 px window, because the canvas
//! sized itself from an unbounded parent's `available_size()`. The pane's model was
//! perfect and half its controls were off the edge of the world;
//! 8. once the width was fixed, the list came back at x≈1585 — a `ScrollArea` inherits
//! the enclosing layout, and this one is enclosed by `horizontal_top`, so forty-eight
//! symbols laid out LEFT TO RIGHT in one row. Both were reported by
//! `RobotSession::click_label`'s visibility gate, verbatim: *"the user cannot see it,
//! and a click at its centre lands off-surface"*;
//! 9. replaced the preset picker's `selectable_label` with a plain `label` → step 2 came
//! back `normal` where it wanted `nuked`, proving these gestures go through real
//! widgets and not through a JSON seam that would not have noticed.
//!
//! # The number that is NOT what was asked for, and why
//!
//! The brief for this scene asked the robot to assert that `nuked` has FEWER nodes than
//! `normal`. **It does not, and it cannot: measured 2026-08-23, `normal` was 16 nodes and
//! `nuked` 48 — and by 2026-09-11 `normal` was 33, because the boot contract in edda's
//! `base.config` grew (PROC_FS and the ten the live nuked kernel died without, SYSFS, SMP,
//! NR_CPUS …) while this file still said 16. The counts below are therefore DERIVED from
//! the model the pane renders (`KernelFeatureModel::derive`, whose numbers edda pins in
//! `kernel_features::the_lowered_model_matches_the_measured_pane_numbers`), so this robot
//! asserts the PANE draws the MODEL and cannot rot the next time the contract moves.** A preset's fragment is a DELTA over a make target inside the kernel tree
//! (`defconfig` for `normal`, `tinyconfig` for `nuked`), and `nuked`'s whole content is
//! stated as ABSENCE — sixteen `# CONFIG_X is not set` vetoes, each of which is a line.
//! Counting those as "not nodes" is sabotage 5 above: it makes the number smaller by
//! deleting the picture. Asserting `nuked < normal` would simply be red.
//!
//! So this file asserts the honest neighbour, which measures the same thing and CAN
//! fail: **`nuked` leaves fewer of the kind's committed symbol universe undecided** —
//! (25 of 73 versus `normal`'s 57 of 73 when first measured; read off the model now).
//! That is "the smaller, more completely stated
//! machine" as a number derived from committed text, and step 2 asserts the node-count
//! direction explicitly (nuked > normal) so nobody reads the absence of the requested claim as
//! an oversight.
//!
//! Nothing here presses a build button: there is none. This pane compiles nothing.
#![cfg(not(target_arch = "wasm32"))]
use facett_core::Facet;
use nornir_robotui::egui_kittest::Harness;
use nornir_robotui::{Observable, RobotSession};
use dwarves_gui::kernel_scene::{kernel_graph_build_calls, KernelScene};
use nornir_build_thing::kernel::HardeningProfile;
use nornir_build_thing::kernel_features::KernelFeatureModel;
/// The MODEL's own numbers for a preset — what the pane must draw. Derived the way the
/// pane derives them (`KernelFeatureModel::derive`, the pane's default `Vanilla`
/// profile), so this file asserts "the pane renders the model" and not a copy of the
/// model's numbers: the copy said `normal` = 16 from 2026-08-23 until 2026-09-11, while
/// edda's `base.config` took it to 33 and edda's own test re-pinned each move.
struct ModelNumbers {
nodes: u64,
edges: u64,
isolated: u64,
enabled: u64,
vetoed: u64,
contract_closure_edges: u64,
}
fn model_numbers(preset: Preset) -> ModelNumbers {
let m = KernelFeatureModel::derive(preset, HardeningProfile::Vanilla);
ModelNumbers {
nodes: m.symbols.len() as u64,
edges: m.edges.len() as u64,
isolated: m.symbols.iter().filter(|s| s.isolated()).count() as u64,
enabled: m.symbols.iter().filter(|s| s.enabled).count() as u64,
vetoed: m.symbols.iter().filter(|s| !s.enabled).count() as u64,
contract_closure_edges: m
.edges
.iter()
.filter(|e| e.2 && m.symbols[e.1].contract)
.count() as u64,
}
}
/// The scene as the robot's subject — the same `Facet::state_json` a shell would read,
/// after real gestures. No twinned oracle.
struct SceneApp(KernelScene);
impl Observable for SceneApp {
fn state_json(&self) -> serde_json::Value {
Facet::state_json(&self.0)
}
}
fn session() -> RobotSession<'static, SceneApp> {
let harness = Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(1500.0, 1100.0))
.build_ui_state(
|ui, app: &mut SceneApp| Facet::ui(&mut app.0, ui),
SceneApp(KernelScene::new()),
);
let mut robot = RobotSession::new(harness, "dwarves-gui");
robot.run(2);
robot
}
/// A `usize` out of the pane's own state at `pointer`, or a panic naming the pointer —
/// a missing field is a defect, not a `0`.
fn num(robot: &RobotSession<'_, SceneApp>, pointer: &str) -> u64 {
robot
.state_at(pointer)
.and_then(|v| v.as_u64())
.unwrap_or_else(|| panic!("no number at {pointer:?}"))
}
/// The state token of one symbol, read out of the pane's `symbols` array by NAME. The
/// fixture's ordering is not this test's business.
fn state_of(robot: &RobotSession<'_, SceneApp>, name: &str) -> String {
robot
.state_at("/symbols")
.and_then(|v| v.as_array().cloned())
.unwrap_or_default()
.iter()
.find(|s| s["name"].as_str() == Some(name))
.unwrap_or_else(|| panic!("no symbol {name:?} in the pick"))["state"]
.as_str()
.unwrap_or_default()
.to_string()
}
#[test]
fn a_robot_picks_a_preset_clicks_a_symbol_off_and_the_spin_is_camera_only() {
let mut robot = session();
// ── Step 1 — the pane opens on the DEFAULT preset, with the measured contract ──
// `normal` is what the appliances ship today: exactly the options of the measured
// boot contract (`base.config`), the derivable edges between them, and the symbols
// whose dependency is real but does not nest by name and are therefore drawn as
// ISOLATED rather than invented. The numbers are the MODEL's, read off the same
// derivation the pane runs — see `model_numbers`.
assert_eq!(robot.state_at("/preset"), Some(serde_json::json!("normal")));
assert_eq!(
robot.state_at("/base"),
Some(serde_json::json!("defconfig"))
);
assert_eq!(
robot.state_at("/profile"),
Some(serde_json::json!("vanilla")),
"the derivation below assumes the pane's default profile"
);
let normal = model_numbers(Preset::Normal);
let normal_nodes = num(&robot, "/graph/nodes");
let normal_undecided = num(&robot, "/graph/undecided");
assert_eq!(
normal_nodes, normal.nodes,
"the boot contract's own symbol count"
);
assert!(
normal_nodes >= 16,
"the contract cannot have SHRUNK below its 2026-08-23 sixteen: {normal_nodes}"
);
assert_eq!(num(&robot, "/graph/edges"), normal.edges);
assert_eq!(
num(&robot, "/graph/isolated"),
normal.isolated,
"and it SAYS how many have no edge"
);
assert!(
num(&robot, "/graph/isolated") > 0,
"the contract has symbols with no name-parent; a zero here is a pane that stopped counting"
);
assert_eq!(
num(&robot, "/graph/vetoed"),
0,
"the default preset states no absences"
);
assert_eq!(normal.vetoed, 0, "…and that is the model's claim too");
assert_eq!(num(&robot, "/graph/build_count"), 1, "one layout so far");
assert_eq!(
robot.state_at("/graph/empty_reason"),
Some(serde_json::Value::Null),
"there is a graph here, so there is no reason to print instead of one"
);
// The words are attached in 3D — asserted as TEXT, not a count.
let words: Vec<String> = robot
.state_at("/graph/label_texts")
.and_then(|v| v.as_array().cloned())
.unwrap_or_default()
.iter()
.map(|v| v.as_str().unwrap_or_default().to_string())
.collect();
assert!(
words.contains(&"CONFIG_VIRTIO_NET".to_string()),
"{words:?}"
);
// ── Step 2 — pick `nuked` through the REAL widget ─────────────────────────────
robot
.click_label("nuked")
.expect("the preset picker is a real, clickable widget");
robot.run(2);
assert_eq!(robot.state_at("/preset"), Some(serde_json::json!("nuked")));
assert_eq!(
robot.state_at("/base"),
Some(serde_json::json!("tinyconfig"))
);
let nuked = model_numbers(Preset::Nuked);
let nuked_nodes = num(&robot, "/graph/nodes");
assert_eq!(nuked_nodes, nuked.nodes);
assert_eq!(num(&robot, "/graph/enabled"), nuked.enabled);
assert_eq!(
num(&robot, "/graph/vetoed"),
nuked.vetoed,
"the smallest preset's content IS its named absences"
);
assert!(
nuked.vetoed >= 16,
"`nuked.config` stated sixteen absences on 2026-08-23 and has only grown (IPv6 by name on 2026-08-29): {}",
nuked.vetoed
);
// Only some of the closure relations the deps fragment claims to close actually
// nest by name. The pane draws exactly those and no more.
assert_eq!(
num(&robot, "/graph/contract_closure_edges"),
nuked.contract_closure_edges
);
assert!(
nuked.contract_closure_edges > 0,
"the closure fragment records at least one contract parent"
);
// THE SIZE CLAIM, stated in the direction that is true (see the header): `nuked`
// draws MORE nodes than `normal` and leaves LESS of the universe undecided.
let nuked_undecided = num(&robot, "/graph/undecided");
assert!(
nuked_nodes > normal_nodes,
"nuked draws MORE nodes ({nuked_nodes}) than normal ({normal_nodes}) — absence \
takes a line too"
);
assert!(
nuked_undecided < normal_undecided,
"…and yet decides MORE of the committed universe: {nuked_undecided} undecided vs \
normal's {normal_undecided} (of {})",
num(&robot, "/graph/universe")
);
// ── Step 3 — click a symbol OFF, and read the cascade ────────────────────────
// `CONFIG_NET_CORE` is the only derivable child of `CONFIG_NET`, and neither is in
// the boot contract. Dropping the child leaves the parent with nothing to be for —
// §10's "remove this part and then no need for this dependency", in one gesture.
assert_eq!(
state_of(&robot, "CONFIG_NET"),
"locked",
"held by its one child"
);
assert_eq!(state_of(&robot, "CONFIG_NET_CORE"), "kept");
assert_eq!(
num(&robot, "/states/cascade_dropped"),
0,
"nothing is amber yet"
);
robot
.click_label("CONFIG_NET_CORE")
.expect("every symbol is a real, clickable widget beside the graph");
robot.run(2);
assert_eq!(state_of(&robot, "CONFIG_NET_CORE"), "clicked_off");
assert_eq!(
state_of(&robot, "CONFIG_NET"),
"cascade_dropped",
"nothing else needs it any more — it goes too"
);
assert_eq!(num(&robot, "/states/cascade_dropped"), 1);
// COUNTERWEIGHT — a click that cascades NOTHING. `CONFIG_VIRTIO_MENU` is one of
// `CONFIG_VIRTIO`'s four children; dropping it leaves three, so the parent stays
// locked and the amber count does not move. Without this, "clicking turns things
// amber" would be indistinguishable from "clicking turns everything amber".
robot.click_label("CONFIG_VIRTIO_MENU").expect("clickable");
robot.run(2);
assert_eq!(state_of(&robot, "CONFIG_VIRTIO_MENU"), "clicked_off");
assert_eq!(
state_of(&robot, "CONFIG_VIRTIO"),
"locked",
"three kept children still hold it — the graph names them"
);
assert_eq!(
num(&robot, "/states/cascade_dropped"),
1,
"the second drop cascaded nothing, so the first amber meant something"
);
let held: Vec<String> = robot
.state_at("/symbols")
.and_then(|v| v.as_array().cloned())
.unwrap_or_default()
.iter()
.find(|s| s["name"].as_str() == Some("CONFIG_VIRTIO"))
.map(|s| {
s["held_by"]
.as_array()
.cloned()
.unwrap_or_default()
.iter()
.map(|v| v.as_str().unwrap_or_default().to_string())
.collect()
})
.unwrap_or_default();
assert!(
held.contains(&"CONFIG_VIRTIO_BLK".to_string()),
"a locked symbol NAMES its holder: {held:?}"
);
// ── Step 4 — the REFUSAL. A drop that reaches the boot contract is refused BY
// NAME, and does not take. `CONFIG_NLS` is the closure parent of both charset
// tables in `base.config`; the FAT-fs charset lesson of 2026-08-13 is exactly what
// dropping it would undo.
let dropped_before = robot
.state_at("/clicked_off")
.and_then(|v| v.as_array().map(Vec::len))
.unwrap_or_default();
robot.click_label("CONFIG_NLS").expect("clickable");
robot.run(2);
assert_eq!(state_of(&robot, "CONFIG_NLS"), "refused");
assert_eq!(
robot.state_at("/refused_symbol"),
Some(serde_json::json!("CONFIG_NLS"))
);
let breaks: Vec<String> = robot
.state_at("/refused_breaks")
.and_then(|v| v.as_array().cloned())
.unwrap_or_default()
.iter()
.map(|v| v.as_str().unwrap_or_default().to_string())
.collect();
assert!(
breaks.contains(&"CONFIG_NLS_CODEPAGE_437".to_string())
&& breaks.contains(&"CONFIG_NLS_ISO8859_1".to_string()),
"a refusal NAMES what it would break, never a silent no: {breaks:?}"
);
assert_eq!(
robot
.state_at("/clicked_off")
.and_then(|v| v.as_array().map(Vec::len)),
Some(dropped_before),
"the refused drop did not take — the pane said no and meant it"
);
// ── Step 5 — ROTATION IS CAMERA-ONLY ─────────────────────────────────────────
// Two cascades and a refusal have happened since the layout was built. If any of
// them had relaid it out, this counter would already have moved.
assert_eq!(
num(&robot, "/graph/build_count"),
2,
"one layout for `normal`, one for `nuked` — a cascade is a REPAINT"
);
assert!(
robot.state_at("/graph/rotate") == Some(serde_json::json!(true)),
"it spins by default"
);
let angle0 = robot
.state_at("/graph/rotate_angle")
.and_then(|v| v.as_f64())
.expect("angle");
let calls0 = kernel_graph_build_calls();
const FRAMES: usize = 24;
robot.run(FRAMES);
let angle1 = robot
.state_at("/graph/rotate_angle")
.and_then(|v| v.as_f64())
.expect("angle");
assert_eq!(
kernel_graph_build_calls() - calls0,
0,
"{FRAMES} spin frames rebuilt the layout — rotation must RE-PROJECT a cache"
);
assert_eq!(
num(&robot, "/graph/build_count"),
2,
"…and the per-pane counter agrees"
);
assert!(
(angle1 - angle0).abs() > 1e-3,
"the camera azimuth advanced over {FRAMES} frames (Δ={:.5} rad) — a frozen \
camera would pass the rebuild check for the wrong reason",
(angle1 - angle0).abs()
);
// COUNTERWEIGHT — a GENUINE change DOES rebuild, exactly once. `build_count == 2`
// above is a measurement, not a constant.
robot
.click_label("normal")
.expect("the picker is still there");
robot.run(2);
assert_eq!(robot.state_at("/preset"), Some(serde_json::json!("normal")));
assert_eq!(
num(&robot, "/graph/build_count"),
3,
"a new pick rebuilds ONCE"
);
let rebuilds_for_one_pick = kernel_graph_build_calls() - calls0;
assert_eq!(
rebuilds_for_one_pick, 1,
"and the process-wide counter saw exactly that one build"
);
assert_eq!(
robot
.state_at("/clicked_off")
.and_then(|v| v.as_array().map(Vec::len)),
Some(0),
"a new pick clears clicks that named symbols the new pick may not have"
);
// ── Step 6 — NEVER-EMPTY-RETURN. `full` contributes no committed fragment, so the
// pane draws no nodes — and must say WHY rather than leave a gap where a graph was.
robot.click_label("full").expect("clickable");
robot.run(2);
assert_eq!(num(&robot, "/graph/nodes"), 0);
let reason = robot
.state_at("/graph/empty_reason")
.and_then(|v| v.as_str().map(str::to_string))
.unwrap_or_default();
assert!(
reason.contains("defconfig") && reason.contains("not enumerable"),
"absence is a SENTENCE, not a gap: {reason:?}"
);
let ok = nuked_nodes == nuked.nodes
&& normal_nodes == normal.nodes
&& nuked_undecided < normal_undecided
&& rebuilds_for_one_pick == 1;
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"a_robot_picks_a_preset_clicks_a_symbol_off_and_the_spin_is_camera_only",
ok,
&format!(
"the kernel feature scene renders the SELECTED preset's committed kconfig symbol \
set as a rotating 3D graph over the shared camera: picking `nuked` through a real \
widget lowers {} symbols ({} enabled + {} named absences) against `normal`'s {} — \
the model's own numbers, not a copy — clicking CONFIG_NET_CORE off turns its \
only-parent CONFIG_NET amber while clicking CONFIG_VIRTIO_MENU off cascades \
nothing, dropping CONFIG_NLS is REFUSED by name (it would break the boot \
contract's two charset tables) and does not take, and across 24 spin frames plus \
two cascades the layout build counter stays put — rising by exactly 1 when the \
preset genuinely changes; `full` draws zero nodes and prints why",
nuked.nodes, nuked.enabled, nuked.vetoed, normal.nodes
),
);
assert!(ok);
}
// ── the RUN report and the decision it produces (design §25 gaps 1 and 2) ─────
use dwarves_gui::kernel_scene::RunReport;
use nornir_build_thing::kernel::Preset;
/// ★ RED-FIRST 2026-08-23. Gap 1: the pane drew the DECLARED graph and nothing
/// else, while 150 measured drop candidates sat in a file on disk. A measurement
/// that cannot reach the person deciding is not a measurement, it is a log.
///
/// The join is the same one edda's `KernelRun::unused_configs` performs, brought
/// over as DATA so the pane needs a report and not a live kernel build.
#[test]
fn a_run_report_reaches_the_graph_and_zero_hits_are_not_absence() {
// `<function> <object> <CONFIG>` exactly as the container writes it, and a
// `-` row for an object no Makefile claims.
let mapping = "\
vfs_statx fs/stat.o CONFIG_VT\n\
vfs_fstatat fs/stat.o CONFIG_VT\n\
snd_pcm_open sound/core/pcm.o CONFIG_SOUND\n\
mystery vmlinux.o -\n";
let executed = vec!["vfs_statx".to_string()];
let r = RunReport::from_mapping("prof2 run", mapping, &executed);
assert_eq!(
r.hits.get("CONFIG_VT"),
Some(&1),
"one of its functions ran"
);
assert_eq!(
r.hits.get("CONFIG_SOUND"),
Some(&0),
"measured, and never entered"
);
assert!(
!r.hits.contains_key("-"),
"an unclaimed object is not a symbol a person can click"
);
assert_eq!(r.never_touched(), vec!["CONFIG_SOUND"]);
// Onto the graph. `nuked` vetoes both VT and SOUND, so both are in the pick.
let mut s = KernelScene::new();
s.set_preset(Preset::Nuked);
// RED: with no report there are NO drop candidates — absence of evidence is
// never painted as evidence of absence.
assert!(
s.drop_candidates().is_empty(),
"a candidate appeared with nothing measured"
);
assert!(s.run_report().is_none());
s.set_report(Some(r));
assert_eq!(s.run_report().map(|r| r.source.as_str()), Some("prof2 run"));
assert!(
s.drop_candidates().contains(&"CONFIG_SOUND"),
"the measured zero-hit symbol is not offered: {:?}",
s.drop_candidates()
);
assert!(
!s.drop_candidates().contains(&"CONFIG_VT"),
"a symbol whose function RAN was offered as a drop candidate"
);
// …and the honest remainder is reported, so a two-name list is not read as
// "everything else is used".
assert!(
s.unmeasured() > 0,
"the report covers 2 of this preset's symbols; the rest are unmeasured"
);
// Dropping the report returns the pane to the declared-only view.
s.set_report(None);
assert!(s.drop_candidates().is_empty());
}
/// ★ RED-FIRST. Gap 2: clicking a symbol off changed some colours and produced
/// NOTHING — no fragment, no declaration, nothing to build. A decision that
/// cannot leave the pane is not a decision.
#[test]
fn clicking_a_symbol_off_produces_a_fragment_someone_can_build() {
let mut s = KernelScene::new();
s.set_preset(Preset::Nuked);
// With nothing clicked, the fragment says so rather than pretending.
let empty = s.decision_fragment();
assert!(empty.contains("nothing is dropped"), "{empty}");
assert!(
empty.contains("evidence: NONE"),
"no report loaded, and it SAYS so:\n{empty}"
);
// Click a real symbol of this preset off.
let target = s
.symbols()
.iter()
.find(|sym| !sym.contract && sym.name != "CONFIG_EXPERT")
.map(|sym| sym.name.clone())
.expect("the nuked preset declares symbols");
s.toggle(&target);
assert!(s.clicked_off().contains(&target), "the click did not take");
let frag = s.decision_fragment();
// The DROP IS A VETO, not an omission: the build's canary catches a veto
// creeping back in and cannot catch a line nobody wrote.
let veto = format!("# {target} is not set");
assert!(
frag.contains(&veto),
"the fragment must VETO {target}, not omit it:\n{frag}"
);
assert!(
frag.contains("operator's choice"),
"unmeasured drops say so:\n{frag}"
);
assert!(frag.contains("preset = nuked"), "{frag}");
// With a report loaded, the SAME click is justified by the measurement.
let mapping = format!("some_fn drivers/x.o {target}\n");
s.set_report(Some(RunReport::from_mapping("a run", &mapping, &[])));
s.toggle(&target);
s.toggle(&target);
let frag = s.decision_fragment();
assert!(
frag.contains("measured ZERO hits"),
"the evidence must ride the file:\n{frag}"
);
// And a click AGAINST the measurement is written down as such rather than
// quietly rendered like any other line.
let mapping = format!("some_fn drivers/x.o {target}\n");
s.set_report(Some(RunReport::from_mapping(
"a run",
&mapping,
&["some_fn".to_string()],
)));
let frag = s.decision_fragment();
assert!(
frag.contains("AGAINST the measurement") || frag.contains("WARNING"),
"dropping something the RUN saw executing must be flagged:\n{frag}"
);
}
// ── the buttons actually fire (design §5 / §25 gap 4) ────────────────────────
use dwarves_gui::kernel_scene::{KernelJobKind, KernelJobProgress, KernelJobRequest, KernelJobs};
use std::sync::{Arc, Mutex};
/// A host that records what it was asked for and hands back a scripted
/// progress, so the buttons are testable without starting a kernel compile.
#[derive(Default)]
struct FakeJobs {
seen: Arc<Mutex<Vec<KernelJobRequest>>>,
refuse: Option<String>,
feed: Vec<KernelJobProgress>,
}
impl KernelJobs for FakeJobs {
fn start(&mut self, req: &KernelJobRequest) -> Result<(), String> {
self.seen.lock().unwrap().push(req.clone());
match &self.refuse {
Some(w) => Err(w.clone()),
None => Ok(()),
}
}
fn progress(&mut self) -> Option<KernelJobProgress> {
if self.feed.is_empty() {
None
} else {
Some(self.feed.remove(0))
}
}
}
/// ★ RED-FIRST 2026-08-23. Gap 4: the pane had no buttons at all — §5 specified
/// Minify / Build / Run as dispatched jobs and the pane was a viewer, so
/// everything it showed was something a person then had to go and do by hand.
#[test]
fn a_click_dispatches_a_job_carrying_the_decision() {
let mut s = KernelScene::new();
s.set_preset(Preset::Nuked);
// RED: unwired, a dispatch REFUSES by name rather than doing nothing
// quietly. A missing button and a broken one must not look alike.
assert!(!s.can_dispatch());
let e = s
.dispatch(KernelJobKind::Build)
.expect_err("an unwired pane cannot build");
assert!(e.contains("not wired"), "{e}");
assert_eq!(s.job_error(), Some(e.as_str()));
let seen = Arc::new(Mutex::new(Vec::new()));
s.wire_jobs(
"tunnr-kernel",
Box::new(FakeJobs {
seen: Arc::clone(&seen),
..FakeJobs::default()
}),
);
assert!(s.can_dispatch());
// A plain BYGG carries the pick and NO fragment — nothing was clicked off,
// so there is no decision to carry and inventing one would be a lie.
s.dispatch(KernelJobKind::Build).expect("wired");
{
let g = seen.lock().unwrap();
assert_eq!(g.len(), 1);
assert_eq!(g[0].kind, KernelJobKind::Build);
assert_eq!(g[0].variant, "tunnr-kernel");
assert_eq!(g[0].preset, "nuked");
assert!(
g[0].fragment.is_empty(),
"a fragment appeared with nothing clicked off"
);
}
// Click a symbol off; the NEXT dispatch carries the decision as kconfig.
let target = s
.symbols()
.iter()
.find(|sym| !sym.contract && sym.name != "CONFIG_EXPERT")
.map(|sym| sym.name.clone())
.expect("symbols");
s.toggle(&target);
s.dispatch(KernelJobKind::Minify).expect("wired");
{
let g = seen.lock().unwrap();
assert_eq!(g.len(), 2);
assert_eq!(g[1].kind, KernelJobKind::Minify);
assert!(
g[1].fragment.contains(&format!("# {target} is not set")),
"the click did not reach the job:\n{}",
g[1].fragment
);
}
// A refusal from the host is kept and shown verbatim, not swallowed.
let mut s2 = KernelScene::new();
s2.wire_jobs(
"tunnr-kernel",
Box::new(FakeJobs {
refuse: Some("no build space under this root".into()),
..FakeJobs::default()
}),
);
let e = s2
.dispatch(KernelJobKind::Run)
.expect_err("the host said no");
assert_eq!(e, "no build space under this root");
assert_eq!(s2.job_error(), Some(e.as_str()));
assert!(
s2.job().is_none(),
"a refused dispatch must not look like a running job"
);
}
/// ★ The progress bar is COMPOSED, not invented: `make` emits no percentage, so
/// a phase with no honest fraction says so instead of drawing a bar that means
/// nothing, and the compile phase is marked ESTIMATED because its total comes
/// from the previous build's object count.
#[test]
fn progress_is_composed_and_says_where_it_is_guessing() {
// A phase with no honest number.
let p = KernelJobProgress::from_line("tar -xf /work/linux-6.12.104.tar.xz", Some(4000), 0);
assert_eq!(p.phase, "unpack");
assert!(
p.fraction.is_none(),
"unpack cannot report a percentage honestly"
);
assert!(!p.estimated);
// The compile phase: a fraction, and it SAYS it is an estimate.
let p = KernelJobProgress::from_line(" CC init/main.o", Some(4000), 1000);
assert_eq!(p.phase, "compile");
assert_eq!(p.fraction, Some(0.25));
assert!(
p.estimated,
"a total taken from the PREVIOUS build must be labelled"
);
// With no previous build there is no total, so there is no fraction —
// rather than a made-up denominator.
let p = KernelJobProgress::from_line(" CC init/main.o", None, 1000);
assert_eq!(p.phase, "compile");
assert!(
p.fraction.is_none(),
"a first build has nothing to estimate against"
);
// The refusals the kernel kind actually emits are phases of their own, so a
// canary refusal does not read as a compile that stalled.
let p = KernelJobProgress::from_line(
"KERNEL-CONFIG-REFUSED: CONFIG_TUN=y did not survive olddefconfig",
Some(4000),
10,
);
assert_eq!(p.phase, "canary");
assert!(p.fraction.is_none());
// And the pane surfaces whatever the host reports.
let mut s = KernelScene::new();
s.wire_jobs(
"tunnr-kernel",
Box::new(FakeJobs {
feed: vec![KernelJobProgress {
phase: "compile".into(),
fraction: Some(0.5),
estimated: true,
current: "CC fs/stat.o".into(),
done: None,
}],
..FakeJobs::default()
}),
);
s.dispatch(KernelJobKind::Build).expect("wired");
s.poll_job();
let j = s.job().expect("a running job");
assert_eq!(j.phase, "compile");
assert!(j.estimated);
}
// ── the pane inside a HORIZONTAL SPLIT, under a parent that lies ───────────────
/// ROBOT SCENARIO — **the symbol list stays on-surface when this pane is not at x≈0.**
///
/// The canvas has always been bounded by the viewport rather than by
/// `available_size()`, because an unbounded parent `Ui` reports far more width than the
/// window has and the graph would eat all of it. But it bounded a **width** against the
/// window's **width**:
///
/// ```text
/// let screen = ui.ctx().viewport_rect().size();
/// canvas.x = (avail.x.min(screen.x) - LIST_W - 18.0).max(240.0);
/// ```
///
/// That is only correct for a pane whose left edge is at x≈0. A pane starting at `x = L`
/// and taking `screen.x` of width overruns the right edge by exactly `L`. As a whole tab
/// this pane started at 8 px and the error was invisible — which is how it survived. As
/// the ⚒ tab's `build-thing-kernel` LEAF it sits in a horizontal split
/// (`build-thing-choose`, settings │ kernel) and starts hundreds of pixels in, so
/// mounting the five leaves turns the latent error live. Reported to this lane by the
/// facett lane that had just fixed the identical shape in the tab strip (`d272f1a0`).
///
/// The drive: mount the pane in a child `Ui` offset 620 px to the right and claiming
/// **2600 px** of width inside a 1400 px viewport — a parent that lies exactly the way a
/// split child's parent lies — and then click a real symbol. `RobotSession::click_label`
/// gates on visibility, so an off-surface list fails with its own words: *"the user
/// cannot see it, and a click at its centre lands off-surface"*.
///
/// RED WHEN REVERTED: put `avail.x.min(screen.x)` back and the list lands past 1400 px.
#[test]
fn the_symbol_list_is_clickable_when_the_pane_is_a_leaf_of_a_split() {
/// How far in a split child's left edge sits — wider than the whole error budget of
/// the old formula, and about what `build-thing-choose` gives the kernel half.
const LEFT: f32 = 620.0;
/// What the lying parent claims. `scene::render` builds every pane's child `Ui` with
/// `UiBuilder::max_rect(alloc)`, and an ancestor built from an unbounded `Ui` can
/// hand down a `max_rect` far past the viewport — this is that, made explicit.
const CLAIMED: f32 = 2600.0;
const VIEWPORT: f32 = 1400.0;
let harness = Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(VIEWPORT, 900.0))
.build_ui_state(
|ui, app: &mut SceneApp| {
let origin = ui.max_rect().min + nornir_robotui::egui::vec2(LEFT, 0.0);
let liar = nornir_robotui::egui::Rect::from_min_size(
origin,
nornir_robotui::egui::vec2(CLAIMED, 1400.0),
);
ui.scope_builder(
nornir_robotui::egui::UiBuilder::new().max_rect(liar),
|ui| {
Facet::ui(&mut app.0, ui);
},
);
},
SceneApp(KernelScene::new()),
);
let mut robot = RobotSession::new(harness, "dwarves-gui");
robot.run(2);
// A real symbol of the DEFAULT preset (`normal`'s sixteen boot-contract options),
// clicked through the real widget. This is the whole assertion: the gate refuses a
// widget the user cannot see, and before the edge fix the list laid out past 1400 px.
robot
.click_label("CONFIG_VIRTIO_NET")
.expect("the symbol list is on-surface when the pane starts 620px in");
robot.run(2);
// …and the click landed IN THE MODEL, not just on the pixels. `CONFIG_VIRTIO_NET` is
// a measured boot-contract symbol, so the honest answer is a REFUSAL naming it —
// which is a stronger proof than a silent accept: the pane both saw the gesture and
// said why it will not obey.
assert_eq!(
state_of(&robot, "CONFIG_VIRTIO_NET"),
"refused",
"the click reached the model, not just the pixels"
);
assert_eq!(
robot.state_at("/refused_symbol"),
Some(serde_json::json!("CONFIG_VIRTIO_NET")),
"…and the refusal names the symbol the pointer actually hit"
);
}