Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

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);
}