tests/viz_demo.rs
//! Robot-verified proof for **demo mode** — the hands-free guided tour
//! (`nornir viz --demo`). THE_BIG_PLAN LAW #1 (feed real input, assert real
//! output) + LAW #6 (state is observable): the demo is NOT a static assert — we
//! drive the real `DemoDriver` script through the real control channel
//! (`control::write_command` → `poll_control_channel` via a `draw_ui` tick →
//! `state_json()`), and assert the tour (1) walks ≥7 DISTINCT views and (2) emits
//! a state TRANSITION per beat (`state_json` changes), so the motion is proven as
//! DATA, not pixels.
//!
//! FC-7: the schedule is time-INJECTED — we feed the driver synthetic elapsed-ms,
//! so the whole tour is deterministic with no wall clock.
#![cfg(feature = "viz")]
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::sync::Mutex;
use nornir::viz::UrdrThreadsApp;
use nornir::viz::control;
use nornir::viz::demo::DemoDriver;
/// `$NORNIR_VIZ_CMD` is process-global; serialize so a parallel test can't clobber
/// this one's channel file.
static CHANNEL_LOCK: Mutex<()> = Mutex::new(());
fn build_app() -> UrdrThreadsApp {
let warehouse_root =
std::env::temp_dir().join(format!("nornir-viz-demo-{}", std::process::id()));
let _ = std::fs::create_dir_all(&warehouse_root);
UrdrThreadsApp::with_repos(
warehouse_root,
"nornir".to_string(),
PathBuf::new(),
vec!["nornir".to_string()],
)
}
/// One headless frame so `poll_control_channel` runs (it's at the top of
/// `draw_ui`), exactly as the live window would each frame.
fn tick(app: &mut UrdrThreadsApp) {
let ctx = egui::Context::default();
// egui 0.36 debug-asserts on a FullOutput dropped with unapplied texture
// deltas; nothing is uploaded headless, so they are cleared (2026-10-03).
ctx.run_ui(egui::RawInput::default(), |ui| app.draw_ui(ui)).textures_delta.clear();
}
/// A compact fingerprint of the on-screen state, so we can assert a TRANSITION
/// per beat (the tab, the active palette, and the funnel/test slices the tour
/// touches — a beat that isn't a tab switch still moves one of these).
fn fingerprint(app: &UrdrThreadsApp) -> String {
let s = app.state_json();
format!(
"tab={}|palette={}|funnel={}|test={}",
s["tab"], s["palette"], s["funnel"], s["test"]
)
}
#[test]
fn demo_tour_robot_drives_every_view_through_the_control_channel() {
let _guard = CHANNEL_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let cmd_file =
std::env::temp_dir().join(format!("nornir-viz-cmd-demo-{}.json", std::process::id()));
// SAFETY: single-threaded test, serialized by CHANNEL_LOCK.
unsafe { std::env::set_var("NORNIR_VIZ_CMD", &cmd_file) };
let _ = std::fs::remove_file(&cmd_file);
let mut app = build_app();
let mut driver = DemoDriver::tour(1000);
let total_beats = driver.len();
assert!(
total_beats >= 10,
"the tour has a real script, got {total_beats} beats"
);
// The script itself walks every view the demo brief names.
let scripted_tabs = driver.tabs();
for want in [
"Architecture",
"Metro",
"Knowledge",
"Funnel",
"WarehouseDeck",
"Release",
"Security",
] {
assert!(
scripted_tabs.iter().any(|t| t == want),
"tour scripts {want}: {scripted_tabs:?}"
);
}
// ── DRIVE the tour, beat by beat, through the REAL control channel ─────────
let mut visited_tabs: BTreeSet<String> = BTreeSet::new();
let mut fingerprints: Vec<String> = Vec::new();
let mut transitions = 0usize;
let mut last_fp = fingerprint(&app);
visited_tabs.insert(app.state_json()["tab"].as_str().unwrap_or("").to_string());
// Walk the injected clock past the end of the script; pull each due beat.
let end = driver.duration_ms() + 10;
for ms in (0..=end).step_by(50) {
while let Some(step) = driver.due(ms) {
// viz.click WRITE-half: drop the beat on the channel (what the live
// demo_emit does each frame).
control::write_command(&step.cmd).expect("write demo command");
assert!(cmd_file.exists(), "the beat was written to the channel");
// APPLY: one viz frame consumes + applies it (consume-once).
tick(&mut app);
assert!(
!cmd_file.exists(),
"the viz consumed the command (consume-once)"
);
// OBSERVE: the tour produced an observable state transition.
let fp = fingerprint(&app);
if fp != last_fp {
transitions += 1;
}
last_fp = fp.clone();
fingerprints.push(fp);
visited_tabs.insert(app.state_json()["tab"].as_str().unwrap_or("").to_string());
}
}
// (1) ≥7 distinct VIEWS walked (robot-verified, not just scripted).
assert!(
visited_tabs.len() >= 7,
"the demo robot-drove ≥7 distinct views, got {}: {visited_tabs:?}",
visited_tabs.len()
);
for want in [
"Architecture",
"Metro",
"Knowledge",
"Funnel",
"WarehouseDeck",
"Release",
"Security",
] {
assert!(
visited_tabs.contains(want),
"the live tour landed on {want}: {visited_tabs:?}"
);
}
// (2) MOTION as DATA: most beats changed the observable state. (Two beats are
// pickers that may already match — e.g. re-selecting the home tab — so we
// require the large majority, not literally every beat.)
assert!(
transitions >= total_beats - 2,
"≥{} of {total_beats} beats produced a state transition, got {transitions}",
total_beats - 2
);
// (3) The drive went through the REAL apply path: the tour's last palette beat
// re-skinned the whole viz (observable in state), and the action trail records
// real clicks — proving apply_command ran, not a bypass.
let state = app.state_json();
assert_eq!(
state["palette"].as_str(),
Some("nordic-aurora"),
"the tour's last palette re-skin took effect (whole-viz glow)"
);
let trail: Vec<String> = state["action_log"]["recent"]
.as_array()
.unwrap()
.iter()
.filter_map(|e| e.as_str().map(String::from))
.collect();
assert!(
trail.iter().any(|l| l.contains("viz.click")),
"the tour's clicks are in the action trail: {trail:?}"
);
let _ = std::fs::remove_file(&cmd_file);
}