dwarves-gui/tests/robot_code_road.rs
//! **ROBOT-UI: ⚒ reads the CODE over wasm, not the table** — gap #9 of
//! `.nornir/build-thing-model-design.md`, the ⚒ half.
//!
//! Two claims, driven through the real widgets over a staged constellation:
//!
//! 1. **the same recipe through the wasm and through the table is the same matrix**
//! (law B) — `orten` resolved over its `<repo>-build-thing` cdylib lands with `how`
//! naming the module (`kod: orten-build-thing @ <key> (wasm…)`) and its rows equal
//! to the rows the table road renders for the identical declaration; while the wasm
//! is being built the console shows the cargo lines and `how` says the table is
//! answering *until* it lands;
//! 2. **a repo with no recipe crate is refused by name**, and the table is the fallback
//! the refusal names — on `how`, on screen, never a silent second path.
//!
//! RED BEFORE GREEN: on the parent commit `how` was `hela receptet` for every recipe
//! and nothing in ⚒ ever loaded a wasm module; the `/how` assertions below fail on it.
//!
//! The wasm build is real — INSIDE the toolchain container, off the registry `nornir-build-thing` — and
//! its module is cached under `$CARGO_TARGET_DIR/dwarves-build-thing-wasm`, shared with
//! `crates/dwarves/tests/code_road.rs` — whichever runs first pays the build.
#![cfg(not(target_arch = "wasm32"))]
use std::time::{Duration, Instant};
use facett_core::Facet;
use nornir_robotui::egui_kittest::Harness;
use nornir_robotui::{Observable, RobotSession};
use dwarves::build_thing::code::testing;
use dwarves_gui::build_forge::DeclaredForge;
use dwarves_gui::build_scene::BuildScene;
struct SceneApp(BuildScene);
impl Observable for SceneApp {
fn state_json(&self) -> serde_json::Value {
Facet::state_json(&self.0)
}
}
fn session(root: &std::path::Path) -> RobotSession<'_, SceneApp> {
let scene = BuildScene::new(Box::new(DeclaredForge::at(root.to_path_buf())));
let harness = Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(1400.0, 1000.0))
.build_ui_state(
|ui, app: &mut SceneApp| Facet::ui(&mut app.0, ui),
SceneApp(scene),
);
let mut robot = RobotSession::new(harness, "dwarves-gui");
robot.run(2);
robot
}
fn resolve(robot: &mut RobotSession<'_, SceneApp>, name: &str) {
robot
.type_into("artefakt", name)
.expect("the artefakt field is reachable");
robot
.click_label("sök")
.expect("the sök button is reachable");
robot.run(2);
}
fn how(robot: &RobotSession<'_, SceneApp>) -> String {
robot.state()["how"]
.as_str()
.unwrap_or_default()
.to_string()
}
/// The rows as the pane renders them — names, badges, columns and their settings —
/// which is what law B is about.
fn rows(robot: &RobotSession<'_, SceneApp>) -> serde_json::Value {
robot.state()["rows"].clone()
}
/// Ten minutes for a cold wasm32 build; a cached one lands on the first frame.
const DEADLINE: Duration = Duration::from_secs(600);
#[test]
fn the_matrix_through_the_wasm_is_the_matrix_through_the_table_and_says_which() {
// ── THE TABLE ROAD, as the reference: the identical declaration, no crate ──
let table_dir = tempfile::tempdir().unwrap();
testing::stage_orten(table_dir.path(), None);
let mut table = session(table_dir.path());
resolve(&mut table, "orten");
let table_how = how(&table);
assert!(
table_how.contains("kodvägen VÄGRAD") && table_how.contains("`orten-build-thing`"),
"no crate ⇒ refused BY NAME on `how`: {table_how:?}"
);
assert!(
table_how.contains("toml"),
"the fallback is named: {table_how:?}"
);
let table_rows = rows(&table);
assert_eq!(table_rows.as_array().map(Vec::len), Some(2), "{table_rows}");
// ── THE CODE ROAD ─────────────────────────────────────────────────────────
let code_dir = tempfile::tempdir().unwrap();
let repo = testing::stage_orten(code_dir.path(), Some(true));
testing::lock_fixture(&repo).expect("the fixture's lockfile (host scaffolding, not the build)");
let mut robot = session(code_dir.path());
resolve(&mut robot, "orten");
let first = how(&robot);
assert!(
first.starts_with("hela receptet · "),
"the first half of `how` is still the resolve's own: {first:?}"
);
let building = first.contains("tills `orten-build-thing` byggt till wasm");
let cached = first.contains("kod: orten-build-thing @");
assert!(
building || cached,
"the code road is either in flight or answered from the cache: {first:?}"
);
if building {
// The table answers meanwhile — and SAYS so — and the console shows cargo.
assert_eq!(
rows(&robot),
table_rows,
"the table's rows while the wasm builds"
);
let lines = robot.state()["lines"].clone();
assert!(
lines.as_array().is_some_and(|l| l.iter().any(|x| x
.as_str()
.is_some_and(|s| s.starts_with("── kod · cargo build")))),
"the wasm build is a build like any other, on the console: {lines}"
);
}
// Wait for the landing — the scene polls the forge once a frame.
let until = Instant::now() + DEADLINE;
while !how(&robot).contains("kod: orten-build-thing @") {
// A refusal is terminal and NAMED — the container road never falls back to host
// cargo, so a box whose toolchain image lacks wasm32 reads its own fix here.
let h = how(&robot);
if h.contains("kodvägen VÄGRAD") {
// The toolchain image on this box carries no std for wasm32: the refusal must
// NAME the fix, and the table answers with the refusal on it. Any other
// refusal is red.
assert!(
h.contains("rustup target add wasm32-unknown-unknown"),
"the code road was refused for another reason than the image's wasm32 std: {h}"
);
assert_eq!(
rows(&robot),
table_rows,
"the table answers, and says so, when the container refuses"
);
eprintln!("SKIP law B on screen — the toolchain image lacks wasm32 std; the refusal names the fix:\n{h}");
return;
}
assert!(
Instant::now() < until,
"the wasm never landed; how = {:?}",
how(&robot)
);
std::thread::sleep(Duration::from_millis(150));
robot.run(1);
}
let landed = how(&robot);
assert!(landed.contains("(wasm"), "{landed:?}");
assert!(
!landed.contains('⚠'),
"law B: no drift between code and table: {landed:?}"
);
// LAW B, ON SCREEN: the rows the code answers are the rows the table declares.
assert_eq!(
rows(&robot),
table_rows,
"the same matrix through the wasm and the table"
);
// A row is still pickable after the swap — every existing click survives.
robot
.click_label("orten-server")
.expect("the row is clickable after the swap");
robot.run(2);
assert_eq!(robot.state()["row"].as_str(), Some("orten-server"));
}