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