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

nordisk/nornir

src/viz/robot_remote.rs

//! **`robot-remote`** — MOUNT nornir's viz as a WIRE-REMOTE robot-drive target.
//!
//! This closes the LAST mile of the North-Star full-system-autotest fleet: edda's
//! fleet harness already drives an egui member's mounted
//! [`CmdJsonServer`](nornir_robotui::cmd_json::CmdJsonServer) over a byte stream, and
//! four sibling apps (dwarves-demo, holger-ui, korp, ordning-gui) already MOUNT one.
//! nornir's viz — the fleet's 5th and final member, closing the last 28 `@fat` nodes
//! (one per [`Tab`](crate::viz::Tab)) — mounts it here.
//!
//! ## What this is (REUSE, no second drive path)
//! * The command vocabulary is the ONE robotui `Action`/`Assert`/`Target`/`Step` set
//!   carried by `cmd_json` — the SAME a co-located [`RobotSession`] uses. Nothing new.
//! * The host is a [`RobotSession`] over the viz's OWN `egui_kittest` shell — the SAME
//!   harness shape `tests/robot_viz.rs` builds (a one-line [`Observable`] delegate to
//!   the viz's existing `state_json()`), REUSE not a twin. The observable oracle IS
//!   [`UrdrThreadsApp::state_json`] (no twinned oracle).
//! * The transport is an [`IoChannel`] over a real `AF_UNIX` [`UnixListener`] — the
//!   Unix twin of the fleet's TCP wire; edda's reusable `UnixEguiWire` dials the exact
//!   socket [`serve_robot_remote`] binds (`<dir>/nornir.sock`).
//!
//! ## Concurrency (ROOT LAW #0)
//! The server is plain BLOCKING I/O pumped single-threaded — it spawns NOTHING. The
//! mount runs on the process's own thread (the CLI hands `main` over to it); a driver
//! is served sequentially via [`CmdJsonServer::serve`] until it hangs up (EOF).
//!
//! ## Scope
//! Feature-gated behind `robot-remote` and reached ONLY via the `nornir-ui`
//! / `nornir-solo` `--robot-remote <socket>` CLI flag, so the SHIPPED default binary never mounts an
//! always-on socket.

use std::io::{self, BufReader};
use std::os::unix::net::UnixListener;
use std::path::Path;

use nornir_robotui::cmd_json::{CmdJsonServer, IoChannel};
use nornir_robotui::egui::Vec2;
use nornir_robotui::egui_kittest::Harness;
use nornir_robotui::{Observable, RobotSession};

use crate::viz::UrdrThreadsApp;

/// The viz shell wrapped as an [`Observable`] — the robot reads exactly what the
/// shell renders through the SAME `state_json()` the in-process reference test
/// (`tests/robot_viz.rs`) uses (no twinned oracle). The whole `UrdrThreadsApp` is
/// mounted, so a driver can read/act on every one of the 28 tabs, not one pane.
struct VizApp(UrdrThreadsApp);

impl Observable for VizApp {
    fn state_json(&self) -> serde_json::Value {
        self.0.state_json()
    }
}

/// Build a [`CmdJsonServer`] over a [`RobotSession`] driving the WHOLE nornir viz
/// through its real `egui_kittest` harness — the host side a driver dials. Built
/// offline-safe (a throwaway warehouse root, no server endpoint, empty repo list) —
/// the SAME headless build the in-crate robot tests use, so mounting the server dials
/// no network.
fn robot_session() -> CmdJsonServer<RobotSession<'static, VizApp>> {
    // Skip the durable action-log sink so its drain thread doesn't hold the catalog's
    // exclusive lock for the app's lifetime (mirrors `tests/robot_viz.rs::robot`). We
    // are the whole process here — a dedicated robot-drive mount — so setting a global
    // is safe (single-threaded; no GUI thread races it).
    // 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-robot-remote-{}", std::process::id()));
    let _ = std::fs::create_dir_all(&warehouse_root);

    let mut app = UrdrThreadsApp::with_repos(
        warehouse_root,
        "nornir".to_string(),
        std::path::PathBuf::new(),
        vec!["nornir".to_string()],
    );
    // Age `started_at` past the opaque intro splash so the tab bar is hit-testable for
    // a driver. No data is seeded: a robot-drive mount should serve the app as it
    // really starts.
    //
    // ⚠ This line used to be `app.inject_test_results_for_test(Vec::new())`, called
    // purely for the same splash side-effect. `ce553b9` ("viz: cut the
    // test/bench/coverage/leaderboard DISPLAY tabs") removed that injector along with
    // the 🧪 Test tab and left the call behind — a hard `E0599` that sat on `main`
    // undetected because `robot-remote` is in no default feature set and no CI
    // command built it, so the whole module had not been compiled since. Measured
    // 2026-08-01 on `origin/main` (eab671a). It is now built by the
    // `--features solo,robot-remote` CI step that `tests/solo_embeds_server.rs`
    // requires.
    app.age_past_splash_for_test();

    // A generously sized canvas so the whole tab bar + the active pane lay out and are
    // hit-testable (mirrors the `robot_viz` kittest harness).
    let harness = Harness::builder()
        .with_size(Vec2::new(1400.0, 900.0))
        .build_ui_state(|ui, a: &mut VizApp| a.0.draw_ui(ui), VizApp(app));
    let mut session = RobotSession::new(harness, "nornir");
    session.run(4); // settle layout + first paint so every tab is in the AccessKit tree
    CmdJsonServer::new(session)
}

/// MOUNT the `CmdJsonServer` on a Unix socket at `path` and serve drivers on THIS (the
/// process's own) blocking thread — NO `thread::spawn` (ROOT LAW #0). One driver at a
/// time, sequentially: the fleet opens a fresh `CmdJsonClient` per tab drive, so
/// [`serve`](CmdJsonServer::serve) blocks answering that driver until it hangs up
/// (EOF), then the next connection is accepted. A per-connection I/O error is logged,
/// not fatal. Never returns except on an `accept` error.
pub fn serve_robot_remote(path: &Path) -> io::Result<()> {
    let _ = std::fs::remove_file(path); // clear a stale socket so the re-bind succeeds
    let listener = UnixListener::bind(path)?;
    let mut server = robot_session();
    eprintln!(
        "nornir viz: robot-remote server listening on {}",
        path.display()
    );
    loop {
        let (stream, _) = listener.accept()?;
        match stream.try_clone() {
            Ok(rd) => {
                let mut chan = IoChannel::new(BufReader::new(rd), stream);
                if let Err(e) = server.serve(&mut chan) {
                    eprintln!("nornir viz: robot-remote connection ended: {e}");
                }
            }
            Err(e) => eprintln!("nornir viz: robot-remote stream clone failed: {e}"),
        }
    }
}

// ── the REAL-SOCKET proof — nornir's viz driven wire-remote end-to-end ────────────
#[cfg(test)]
mod tests {
    use super::*;
    use nornir_robotui::cmd_json::{CmdJsonClient, CmdRequest, CmdResponse};
    use nornir_robotui::{Action, Assert, Target};
    use serde_json::json;
    use std::os::unix::net::UnixStream;

    /// The whole point: a driver drives the REAL nornir viz shell OVER A REAL UNIX
    /// SOCKET (no duplex, no container — an actual `AF_UNIX` stream). Single-threaded,
    /// lockstep (ROOT LAW #0): connect into the listener backlog, accept, then pump the
    /// host's `serve_one` between the client's `send` and `recv` — the app's own I/O
    /// task without a thread.
    ///
    /// RED-WHEN-BROKEN: a real tab-switch CLICK sent over the socket must move the
    /// REMOTE shell's active tab and the wired-back `state_json` must reflect it; a
    /// broken mount, a wrong socket, a dropped command, or a swallowed click all fail
    /// this. A stale assertion must surface as `Asserted { pass: false }` with a reason.
    #[test]
    fn drives_the_nornir_viz_over_a_real_unix_socket() {
        let dir = tempfile::tempdir().unwrap();
        let sock = dir.path().join("nornir.sock");

        // Bind → connect (into the backlog) → accept — all single-threaded, no spawn.
        let listener = UnixListener::bind(&sock).unwrap();
        let client_stream = UnixStream::connect(&sock).unwrap();
        let (server_stream, _) = listener.accept().unwrap();

        let mut server = robot_session();
        let mut server_chan = IoChannel::new(
            BufReader::new(server_stream.try_clone().unwrap()),
            server_stream,
        );
        let mut client = CmdJsonClient::new(IoChannel::new(
            BufReader::new(client_stream.try_clone().unwrap()),
            client_stream,
        ));

        // A lockstep pump: send → host serves one → recv (stands in for the host's own
        // I/O task without any thread).
        macro_rules! call {
            ($req:expr) => {{
                client.send(&$req).expect("send over the socket");
                assert!(
                    server.serve_one(&mut server_chan).expect("host serves one"),
                    "host served a request over the socket",
                );
                client.recv().expect("recv over the socket")
            }};
        }

        // Handshake travels the real socket.
        assert!(
            matches!(call!(CmdRequest::Ping), CmdResponse::Pong),
            "ping/pong over a real AF_UNIX socket",
        );

        // The mounted shell opens on the 🧪 Test tab (the splash-dismiss inject selects
        // it) — read the REMOTE active tab back over the wire.
        match call!(CmdRequest::ReadStateAt {
            pointer: "/tab".into()
        }) {
            CmdResponse::StateAt { value } => {
                assert_eq!(
                    value,
                    Some(json!("Test")),
                    "remote viz opens on the Test tab"
                )
            }
            other => panic!("want StateAt, got {other:?}"),
        }

        // locate-by-testid: the 🧬 nornir tab resolves to a real node id remotely.
        match call!(CmdRequest::Locate {
            target: Target::label("🧬 nornir")
        }) {
            CmdResponse::Located { id } => {
                assert!(id.is_some(), "the 🧬 nornir tab is located over the wire")
            }
            other => panic!("want Located, got {other:?}"),
        }

        // Drive a REAL tab-switch CLICK over the socket (🧪 Test → 🧬 nornir).
        match call!(CmdRequest::Act {
            action: Action::ClickLabel("🧬 nornir".into())
        }) {
            CmdResponse::Acted {
                ok: true,
                error: None,
            } => {}
            other => panic!("the tab click did not land over the wire: {other:?}"),
        }

        // The wired-back state proves the REMOTE nornir viz actually changed.
        match call!(CmdRequest::ReadStateAt {
            pointer: "/tab".into()
        }) {
            CmdResponse::StateAt { value } => assert_eq!(
                value,
                Some(json!("Nornir")),
                "the click moved the REMOTE nornir active tab over the socket",
            ),
            other => panic!("want StateAt, got {other:?}"),
        }

        // RED-WHEN-BROKEN: a stale assertion must surface as Asserted{pass:false} with a
        // reason — not a silent pass, not a dropped connection.
        match call!(CmdRequest::Assert {
            assert: Assert::State {
                pointer: "/tab".into(),
                expected: json!("Test")
            },
        }) {
            CmdResponse::Asserted {
                pass: false,
                reason: Some(r),
            } => {
                assert!(!r.is_empty(), "the failure reason travels the wire")
            }
            other => panic!("a stale assertion must fail over the wire: {other:?}"),
        }
        // …and the TRUE post-click state passes over the same wire.
        match call!(CmdRequest::Assert {
            assert: Assert::State {
                pointer: "/tab".into(),
                expected: json!("Nornir")
            },
        }) {
            CmdResponse::Asserted {
                pass: true,
                reason: None,
            } => {}
            other => panic!("the true state must pass over the wire: {other:?}"),
        }

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "nornir",
            "wire_remote_socket_drivable",
            true,
            "nornir's viz MOUNTS a cmd_json::CmdJsonServer over its egui shell on a real \
             AF_UNIX socket; a CmdJsonClient pings, locates + CLICKS the 🧬 nornir tab, and \
             the wired-back state_json shows the REMOTE active tab changed Test→Nornir — the \
             5th and final full-system-autotest fleet member driven wire-remote (red-when-broken)",
        );
    }
}