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

nordisk/nornir

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