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

nordisk/nornir

tests/runtime_register_served.rs

//! Inject-assert: a workspace **registered at runtime is served WITHOUT a restart**.
//!
//! The bug: `Workspaces.Register` upserted the registry row + built the warehouse
//! but never added a served `WorkspaceCtx` to `AppState.workspaces` (a map fixed at
//! boot). So every workspace-routed RPC (`Index.Stats`, Knowledge, Viz, …) failed
//! with `NotFound: workspace `X` is not served` until the server restarted and the
//! boot loop re-read `reg.list()` into the map. The fix opens + inserts the served
//! handle in `register` (before the eager build), so register-then-query works at
//! once. This test is RED before that fix (NotFound) and GREEN after.
//!
//! Real input: a hermetic monitored workspace whose members are local `file://`
//! git crates (no network). Real output: an `Index.Stats` reply (the workspace is
//! served — the index is reachable, empty is fine), NOT a `NotFound` status. The
//! one process is never restarted between register and the routed query.
//!
//! Run: cargo test --features "viz,server" --test runtime_register_served -- --test-threads=1
#![cfg(all(feature = "viz", feature = "server"))]

use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::time::{Duration, Instant};

use uuid::Uuid;

fn free_port() -> u16 {
    TcpListener::bind("127.0.0.1:0")
        .unwrap()
        .local_addr()
        .unwrap()
        .port()
}

/// A minimal committed Rust-crate fixture repo a pure-gix `file://` clone can fetch.
fn fixture_crate(dir: &Path, name: &str) {
    std::fs::create_dir_all(dir.join("src")).unwrap();
    std::fs::write(
        dir.join("Cargo.toml"),
        format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n"),
    )
    .unwrap();
    std::fs::write(
        dir.join("src").join("lib.rs"),
        format!("//! {name} fixture crate\npub fn {name}_answer() -> i32 {{ 42 }}\n"),
    )
    .unwrap();
    nornir::gitio::init(dir).unwrap();
    nornir::gitio::commit_all(dir, "init fixture").unwrap();
}

fn file_url(dir: &Path) -> String {
    format!("file://{}", dir.display())
}

/// An ephemeral `nornir-server` with a FRESH EMPTY registry (no workspaces served).
struct Server {
    child: Child,
    root: PathBuf,
    endpoint: String,
    token: String,
}
impl Drop for Server {
    fn drop(&mut self) {
        let _ = self.child.kill();
        let _ = self.child.wait();
        let _ = std::fs::remove_dir_all(&self.root);
    }
}

fn spawn_empty_server() -> Server {
    let token = "runtime-register-served-token-0123456789";
    let port = free_port();
    let endpoint = format!("http://127.0.0.1:{port}");
    let root = std::env::temp_dir().join(format!(
        "nornir-rt-register-{}-{}",
        std::process::id(),
        Uuid::new_v4()
    ));
    let _ = std::fs::remove_dir_all(&root);
    let server_home = root.join("home");
    // Empty registry dir — the server starts serving 0 workspaces.
    std::fs::create_dir_all(server_home.join(".nornir").join("workspaces")).unwrap();

    let child = Command::new(env!("CARGO_BIN_EXE_nornir-server"))
        .env("NORNIR_SERVER_ADDR", format!("127.0.0.1:{port}"))
        .env("NORNIR_SERVER_TOKEN", token)
        .env("HOME", &server_home)
        // Long poll so the SWEEP loop never races our register (we assert the
        // register itself serves the workspace, not a later poll tick).
        .env("NORNIR_POLL", "3600s")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("spawn nornir-server");

    let srv = Server {
        child,
        root,
        endpoint: endpoint.clone(),
        token: token.to_string(),
    };
    let deadline = Instant::now() + Duration::from_secs(45);
    loop {
        if nornir::viz::remote::list_workspaces(&endpoint, token).is_ok() {
            break;
        }
        assert!(Instant::now() < deadline, "empty server never came up");
        std::thread::sleep(Duration::from_millis(200));
    }
    srv
}

/// Write a monitored-workspace descriptor (server-readable local path) with two
/// `file://`-cloneable members. Returns the descriptor path the register RPC points
/// at. Crates live under `dir` so the running test + server share the filesystem.
fn write_descriptor(dir: &Path, ws_name: &str) -> PathBuf {
    let src_alpha = dir.join("src_alpha");
    let src_beta = dir.join("src_beta");
    fixture_crate(&src_alpha, "alpha");
    fixture_crate(&src_beta, "beta");
    let desc = dir.join("nornir-workspace.toml");
    std::fs::write(
        &desc,
        format!(
            "[workspace]\nname = \"{ws_name}\"\n\n\
             [repos.alpha]\ngit = \"{}\"\n\n\
             [repos.beta]\ngit = \"{}\"\n",
            file_url(&src_alpha),
            file_url(&src_beta),
        ),
    )
    .unwrap();
    desc
}

#[test]
fn register_at_runtime_serves_immediately_no_restart() {
    let srv = spawn_empty_server();
    let ws = "rtserved";

    // Fixtures + descriptor under the server's root (shared fs, server-readable).
    let fixtures = srv.root.join("fixtures");
    std::fs::create_dir_all(&fixtures).unwrap();
    let desc = write_descriptor(&fixtures, ws);

    // ── RED-before-fix precondition: a workspace-routed RPC for `ws` BEFORE
    //    register is NotFound (`is not served`). This proves the probe genuinely
    //    distinguishes served vs not-served (not a vacuous green).
    let before = nornir::viz::remote::index_stats(&srv.endpoint, &srv.token, ws);
    let before_err = before.expect_err("an unregistered workspace must NOT be served");
    let before_msg = format!("{before_err:#}");
    assert!(
        before_msg.contains("is not served") || before_msg.contains("NotFound"),
        "pre-register routed RPC should be a not-served NotFound, got: {before_msg}"
    );

    // ── Register the workspace AT RUNTIME (eager, monitored, local descriptor). ──
    let (name, mode, members) = nornir::viz::remote::register_workspace(
        &srv.endpoint,
        &srv.token,
        ws,
        &desc.to_string_lossy(),
        "monitored",
        "3600s",
    )
    .expect("runtime register must succeed");
    assert_eq!(name, ws, "registered the workspace we asked for");
    assert_eq!(mode, "monitored");
    assert_eq!(
        members, 2,
        "eager register cloned both members from the descriptor"
    );

    // ── THE FIX: the workspace must be SERVED the instant register returns — a
    //    workspace-routed RPC succeeds with NO server restart. Before the fix this
    //    was `NotFound: workspace `rtserved` is not served`. ──
    let after = nornir::viz::remote::index_stats(&srv.endpoint, &srv.token, ws);
    let (total, by_corpus) = after.unwrap_or_else(|e| {
        panic!(
            "register-then-query must be SERVED without a restart, but the routed \
             Index.Stats RPC failed: {e:#}"
        )
    });
    // The served index exists (empty is fine — the point is it routed, not NotFound).
    let _ = (total, by_corpus);

    // And the registry lists it as registered (sanity — the row was upserted too).
    let listed =
        nornir::viz::remote::list_workspaces(&srv.endpoint, &srv.token).expect("list workspaces");
    assert!(
        listed.iter().any(|w| w == ws),
        "the runtime-registered workspace is listed: {listed:?}"
    );

    // ── Bonus: Workspaces.Remove drops the served handle too — a routed RPC after
    //    remove is NotFound again (the lock is released; no restart needed). ──
    nornir::viz::remote::remove_workspace(&srv.endpoint, &srv.token, ws, false)
        .expect("remove must succeed");
    let after_remove = nornir::viz::remote::index_stats(&srv.endpoint, &srv.token, ws);
    let rm_err = after_remove.expect_err("a removed workspace must no longer be served");
    let rm_msg = format!("{rm_err:#}");
    assert!(
        rm_msg.contains("is not served") || rm_msg.contains("NotFound"),
        "post-remove routed RPC should be not-served again, got: {rm_msg}"
    );
}