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

nordisk/nornir

tests/viz_law4b_screenshot.rs

//! **LAW4b** inject-assert test: the viz app's self-screenshot control channel.
//!
//! Tests the complete `VizCommand::Screenshot` → `apply_command` → `state_json`
//! path WITHOUT requiring a real GPU or a running display.  The test drives
//! the headless `UrdrThreadsApp` purely via its public API (inject-assert LAW):
//!
//! 1. Write a real `VizCommand::Screenshot { out_path: Some(...) }` via the
//!    control channel file.
//! 2. Call `apply_command` (the same path the running viz runs each frame).
//! 3. Assert `state_json["screenshot"]["pending"] == true` — the command was
//!    consumed and the screenshot state machine is armed.
//! 4. Assert `state_json["screenshot"]["last_path"] == null` — no image has
//!    arrived yet (that requires a real GPU frame).
//!
//! No GPU, no eframe::run, no window — pure control+state_json path.
//!
//! Run:
//!   cargo test --features viz --test viz_law4b_screenshot
#![cfg(feature = "viz")]

use std::path::PathBuf;

use nornir::viz::{
    UrdrThreadsApp,
    control::{ScreenshotRequest, VizCommand},
};

/// Build a minimal headless app (no display, no warehouse data needed).
fn headless_app() -> UrdrThreadsApp {
    // Skip the durable action-log warehouse drain so the test doesn't need an
    // Iceberg catalog lock.
    // TODO: Audit that the environment access only happens in single-threaded code.
    unsafe { std::env::set_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG", "1") };
    let warehouse_root = std::env::temp_dir().join(format!(
        "nornir-law4b-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.subsec_nanos())
            .unwrap_or(0),
    ));
    let _ = std::fs::create_dir_all(&warehouse_root);
    UrdrThreadsApp::with_repos(warehouse_root, "nornir".to_string(), PathBuf::new(), vec![])
}

/// LAW4b — inject a `VizCommand::Screenshot` via `apply_command`, then assert
/// `state_json["screenshot"]["pending"] == true` and `["last_path"] == null`.
///
/// This is the inject-assert proof that the screenshot command wires through the
/// control+state_json contract without a GPU: the command sets `pending = true`,
/// which `state_json` exposes, so a robot-driver can poll for the screenshot
/// arriving in a live session.
#[test]
fn screenshot_command_sets_pending_and_state_json_reflects_it() {
    let mut app = headless_app();

    // Pre-condition: no screenshot pending.
    let before = app.state_json();
    assert_eq!(
        before["screenshot"]["pending"],
        serde_json::json!(false),
        "screenshot.pending is false before any command"
    );
    assert_eq!(
        before["screenshot"]["last_path"],
        serde_json::json!(null),
        "screenshot.last_path is null before any screenshot"
    );

    // Inject a Screenshot command via apply_command (same path as the live
    // control channel).
    let cmd = VizCommand {
        screenshot: Some(ScreenshotRequest {
            out_path: Some("/tmp/nornir_law4b_test.png".into()),
        }),
        ..Default::default()
    };
    app.apply_command(&cmd);

    // Assert: state_json["screenshot"]["pending"] == true.
    let after = app.state_json();
    assert_eq!(
        after["screenshot"]["pending"],
        serde_json::json!(true),
        "apply_command(Screenshot) must set screenshot.pending = true in state_json"
    );
    // last_path is still null — the image hasn't arrived (no GPU frame yet).
    assert_eq!(
        after["screenshot"]["last_path"],
        serde_json::json!(null),
        "screenshot.last_path remains null until Event::Screenshot is consumed"
    );
}

/// LAW4b — `VizCommand::Screenshot { out_path: None }` (warehouse-only mode)
/// also sets `pending = true` with no out_path.
#[test]
fn screenshot_command_warehouse_only_also_sets_pending() {
    let mut app = headless_app();

    let cmd = VizCommand {
        screenshot: Some(ScreenshotRequest { out_path: None }),
        ..Default::default()
    };
    app.apply_command(&cmd);

    let state = app.state_json();
    assert_eq!(
        state["screenshot"]["pending"],
        serde_json::json!(true),
        "warehouse-only Screenshot command also sets pending = true"
    );
}

/// LAW4b — the control channel round-trip: `write_command` + `take_pending`
/// deserializes `ScreenshotRequest` correctly.
#[test]
fn screenshot_request_roundtrips_through_control_channel() {
    use nornir::viz::control::{take_pending, write_command};

    // Use a unique tmp file so parallel test runs don't conflict.
    let tmp = format!("/tmp/nornir_law4b_ctrl_{}.json", std::process::id());
    // TODO: Audit that the environment access only happens in single-threaded code.
    unsafe { std::env::set_var("NORNIR_VIZ_CMD", &tmp) };
    let _ = std::fs::remove_file(&tmp);

    // Nothing pending yet.
    assert!(take_pending().is_none(), "channel starts empty");

    // Write a Screenshot command.
    let cmd = VizCommand {
        screenshot: Some(ScreenshotRequest {
            out_path: Some("/tmp/roundtrip.png".into()),
        }),
        ..Default::default()
    };
    write_command(&cmd).expect("write_command succeeds");

    // Read it back (consumes the file).
    let got = take_pending()
        .expect("a command is pending")
        .expect("deserializes without error");

    assert_eq!(
        got.screenshot,
        Some(ScreenshotRequest {
            out_path: Some("/tmp/roundtrip.png".into())
        }),
        "ScreenshotRequest round-trips through the control channel"
    );
    // Consume-once: the file was removed.
    assert!(
        take_pending().is_none(),
        "consume-once: second take is None"
    );

    let _ = std::fs::remove_file(&tmp);
}

/// LAW4b — `request_screenshot` public method arms the state machine directly
/// (without going through the control-channel file), used by the keyboard shortcut.
#[test]
fn request_screenshot_pub_method_arms_pending() {
    let mut app = headless_app();

    app.request_screenshot(Some("/tmp/direct.png".into()));

    let state = app.state_json();
    assert_eq!(
        state["screenshot"]["pending"],
        serde_json::json!(true),
        "request_screenshot() sets screenshot.pending = true in state_json"
    );
    assert_eq!(
        state["screenshot"]["last_path"],
        serde_json::json!(null),
        "last_path stays null until an egui Event::Screenshot arrives"
    );
}