tests/viz_robot_loop.rs
//! Inject-and-assert proof for the **robot-UI-tester loop** (viz-introspection /
//! robot-UI-tester epic) — THE_BIG_PLAN LAW #1: feed real input, assert real
//! output, never just "didn't panic".
//!
//! The loop is: an agent writes a command to the viz control channel
//! (`$NORNIR_VIZ_CMD`) via `viz.click`, the running viz polls + applies it next
//! frame, and the result shows up in `state_json()` (which `viz.state` reads from
//! `$NORNIR_VIZ_STATE`). Here we drive that loop **through the real file
//! channel** — `control::write_command` (what the `viz.click` MCP tool calls) →
//! `poll_control_channel` (what the viz runs each frame, here via a `draw_ui`
//! tick) → `state_json()` (what `viz.state` returns) — and assert the Test tab
//! became active AND its rendered rows are present in the returned state.
#![cfg(feature = "viz")]
use std::path::PathBuf;
use std::sync::Mutex;
use nornir::viz::UrdrThreadsApp;
use nornir::viz::control::{self, VizCommand};
/// `$NORNIR_VIZ_CMD` (and `$NORNIR_VIZ_ACTIONLOG`/`$NORNIR_VIZ_STATE`) are
/// process-global; these two tests each set their own channel file, so serialize
/// them to keep one from clobbering the other's env while it runs.
static CHANNEL_LOCK: Mutex<()> = Mutex::new(());
fn build_app() -> UrdrThreadsApp {
let warehouse_root =
std::env::temp_dir().join(format!("nornir-viz-robot-{}", 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 invoked 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();
}
#[test]
fn robot_loop_clicks_a_tab_through_the_control_channel() {
let _guard = CHANNEL_LOCK.lock().unwrap_or_else(|p| p.into_inner());
// Isolate this test's control channel from any real viz / other tests.
let cmd_file =
std::env::temp_dir().join(format!("nornir-viz-cmd-robot-{}.json", std::process::id()));
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::set_var("NORNIR_VIZ_CMD", &cmd_file) };
let _ = std::fs::remove_file(&cmd_file);
let mut app = build_app();
// Start somewhere that is NOT the Knowledge tab so the click is observable.
app.apply_command(&VizCommand {
tab: Some("Timeline".into()),
..Default::default()
});
assert_eq!(
app.state_json()["tab"].as_str(),
Some("Timeline"),
"precondition: on Timeline"
);
// ── DRIVE: the `viz.click` write-half — drop a command on the channel ─────
control::write_command(&VizCommand {
tab: Some("Knowledge".into()),
..Default::default()
})
.expect("write viz command");
assert!(cmd_file.exists(), "command file written by viz.click");
// ── APPLY: one viz frame consumes + applies the command (consume-once) ────
tick(&mut app);
assert!(
!cmd_file.exists(),
"the viz consumed (removed) the command file"
);
// ── OBSERVE: the `viz.state` read-half — the clicked tab is now active ─────
let state = app.state_json();
assert_eq!(
state["tab"].as_str(),
Some("Knowledge"),
"click switched to the Knowledge tab"
);
let tabs: Vec<&str> = state["all_tabs"]
.as_array()
.unwrap()
.iter()
.filter_map(|t| t.as_str())
.collect();
assert!(tabs.contains(&"Knowledge"), "Knowledge present in all_tabs");
// The drive is auditable: the click landed in the action trail (same log as a
// human click), proving the command went through apply_command, not a bypass.
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 tab → Knowledge")),
"the viz.click drive is in the action trail: {trail:?}"
);
let _ = std::fs::remove_file(&cmd_file);
}
#[test]
fn robot_loop_switches_workspace_via_command() {
let _guard = CHANNEL_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let cmd_file =
std::env::temp_dir().join(format!("nornir-viz-cmd-ws-{}.json", std::process::id()));
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::set_var("NORNIR_VIZ_CMD", &cmd_file) };
let _ = std::fs::remove_file(&cmd_file);
let mut app = build_app();
assert_eq!(
app.state_json()["workspace_picker"]["selected"].as_str(),
Some("nornir")
);
// A workspace the local app doesn't have still gets selected (the picker
// honours an explicit name); the timeline load just errors, which is fine —
// we're asserting the *control channel applied the selection*.
control::write_command(&VizCommand {
workspace: Some("korp".into()),
..Default::default()
})
.expect("write viz command");
tick(&mut app);
let state = app.state_json();
assert_eq!(
state["workspace_picker"]["selected"].as_str(),
Some("korp"),
"viz.click switched the selected workspace"
);
let _ = std::fs::remove_file(&cmd_file);
}