tests/eager_register.rs
//! Inject-assert: **register is EAGER by default** (LAW 1).
//!
//! Registering a monitored workspace must CLONE its members *at once* — members
//! populate immediately, NOT only after a poll tick. (The warehouse BUILD that
//! followed was the v1 populate, removed with Mímir v2.) The opt-out (`lazy`)
//! still records with empty members.
//!
//! These tests drive the exact pipeline the eager register path runs across the
//! CLI / server RPC / MCP tool: `Workspace::new` (seed members from the
//! descriptor) → `upsert` → (eager) `monitor::fetch_workspace` (clone members).
//! The lazy variant stops after `upsert`.
//!
//! Real input: two fixture git crates cloned over `file://`. Real output: the
//! registry row's per-member `last_seen_sha` (populated ⇒ cloned).
use std::path::{Path, PathBuf};
use nornir::monitor;
use nornir::registry::{Mode, Registry, Workspace};
/// Make a minimal committed Rust-crate fixture repo at `dir` and return a
/// `file://` URL a pure-gix clone can fetch. Two crates so the workspace has
/// ≥2 members (LAW 1) and the cross-repo dep graph has something to chew on.
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())
}
/// Build the descriptor + registry for a monitored workspace with two
/// `file://`-cloneable members. Returns (registry, server-root, descriptor-path).
fn setup(tmp: &Path) -> (Registry, PathBuf, PathBuf) {
// Two source crates the members clone FROM.
let src_alpha = tmp.join("src_alpha");
let src_beta = tmp.join("src_beta");
fixture_crate(&src_alpha, "alpha");
fixture_crate(&src_beta, "beta");
// The workspace descriptor (git = file:// so a pure-gix clone works offline).
let desc = tmp.join("nornir-workspace.toml");
std::fs::write(
&desc,
format!(
"[workspace]\nname = \"eagertest\"\n\n\
[repos.alpha]\ngit = \"{}\"\n\n\
[repos.beta]\ngit = \"{}\"\n",
file_url(&src_alpha),
file_url(&src_beta),
),
)
.unwrap();
// The server root where <name>/{git,builds} land.
let root = tmp.join("root");
std::fs::create_dir_all(&root).unwrap();
let reg = Registry::open(&root).unwrap();
(reg, root, desc)
}
#[test]
fn register_is_eager_by_default_members_populate_at_once_no_fake_snapshot() {
let tmp = tempfile::tempdir().unwrap();
let (reg, root, desc) = setup(tmp.path());
// ── The eager register path (default): record → fetch → build, inline. ──
let ws = Workspace::new(
"eagertest".into(),
desc.to_string_lossy().into_owned(),
Mode::Monitored,
"60s".into(),
None,
);
// The descriptor seeded ≥2 members, but none are cloned yet (lazy state).
assert_eq!(ws.members.len(), 2, "descriptor seeds both members");
assert!(
ws.members.iter().all(|m| m.last_seen_sha.is_empty()),
"no member is cloned at the record step — eager fetch must do it"
);
reg.upsert(&ws).unwrap();
// EAGER: clone members AT ONCE (what register now runs). The warehouse build
// that followed was the v1 populate, removed with Mímir v2.
monitor::fetch_workspace(®, &root, "eagertest").expect("eager fetch clones members");
// ── Assert members populated IMMEDIATELY (non-empty), not after a poll. ──
let after = reg.get("eagertest").unwrap().unwrap();
assert_eq!(after.members.len(), 2, "still two members");
for m in &after.members {
assert!(
!m.last_seen_sha.is_empty(),
"member `{}` must be cloned at once (has a HEAD sha) — register is eager",
m.name
);
assert_eq!(m.sync_state, "ok", "member `{}` cloned cleanly", m.name);
}
// Both member checkouts physically exist under <root>/<name>/git/<member>/.
for member in ["alpha", "beta"] {
assert!(
root.join("eagertest")
.join("git")
.join(member)
.join(".git")
.exists(),
"member `{member}` checkout materialized at once"
);
}
// ── No v1 build ran, so no snapshot is recorded — never a fake one. ──
assert!(
after.current_snapshot.trim().is_empty(),
"no warehouse build exists until the Mímir v2 consumer loop lands"
);
}
#[test]
fn lazy_opt_out_records_with_empty_members_no_snapshot() {
let tmp = tempfile::tempdir().unwrap();
let (reg, root, desc) = setup(tmp.path());
// ── The `--lazy` / `eager:false` opt-out: record ONLY. ──
let ws = Workspace::new(
"eagertest".into(),
desc.to_string_lossy().into_owned(),
Mode::Monitored,
"60s".into(),
None,
);
reg.upsert(&ws).unwrap();
// Lazy = stop here: NO fetch_workspace, NO republish.
let after = reg.get("eagertest").unwrap().unwrap();
// The descriptor still seeds the member *list*, but nothing is cloned and
// no warehouse is built — population is deferred to the poll loop.
assert!(
after.members.iter().all(|m| m.last_seen_sha.is_empty()),
"lazy register clones nothing — every member has an empty HEAD sha"
);
assert!(
after.current_snapshot.trim().is_empty(),
"lazy register builds no warehouse snapshot"
);
assert!(
!root
.join("eagertest")
.join("git")
.join("alpha")
.join(".git")
.exists(),
"lazy register materializes no member checkout"
);
}