tests/support/mcp_seed.rs
//! Temp-warehouse seeder for the MCP tool harness (`tests/mcp_tools.rs`).
//!
//! Builds a self-contained workspace on disk whose layout lets the real
//! `nornir-mcp` binary discover it via `NORNIR_CONFIG` (`load_explicit`
//! derives `workspace_root = config.parent().parent().parent()`), then
//! seeds every warehouse table the embedded MCP tools read from (at the
//! per-workspace path `Loaded::warehouse_root()` resolves to —
//! `<HOME>/.nornir/workspaces/default/builds`) with a
//! small, KNOWN dataset so each tool can be asserted against exact values:
//!
//! - a real cargo workspace (the 10-repo holger-like diamond from
//! [`genworkspace`]) → `repos_list`, the dependency Mímir tools,
//! `guard_*`, `release_gate_*`, `docs_*`, `search`;
//! - a syn knowledge scan of the `holger` repo → `knowledge_*`;
//! - a synthetic DWARF facts snapshot for `holger` → `dwarf_*`;
//! - a `doc_exports` row for `holger` → `docs_history`;
//! - a `bench_history.jsonl` for `holger` → `bench_history`;
//! - a built Tantivy index over the workspace → `search`.
//!
//! The warehouse is opened, seeded, and **closed** before the MCP server is
//! spawned: skade-katalog's catalog.redb is single-writer, so the test must
//! release it before `nornir-mcp` opens it embedded.
#![allow(dead_code)]
use std::path::{Path, PathBuf};
use tempfile::TempDir;
#[path = "genworkspace.rs"]
mod genworkspace;
/// What `shop.rs` expects beside it (the same three helpers the lib's own
/// reader fixture gives it, `src/code_room/reader/fixture.rs`).
#[cfg(feature = "code-room")]
mod common {
use std::path::Path;
use mimir_ingest::{EventLog, RepoSpec, Specs};
/// A scratch repo with `n` commits that each touch a `.rs` file.
pub fn scratch(dir: &Path, n: usize) {
nornir::gitio::init(dir).unwrap();
for i in 0..n {
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(dir.join("src/lib.rs"), format!("// commit {i}\n")).unwrap();
nornir::gitio::commit_all(dir, &format!("c{i}")).unwrap();
}
}
pub fn repo_spec(tmp: &Path, name: &str) -> RepoSpec {
RepoSpec {
name: name.into(),
source: tmp.join(format!("src-{name}")).to_string_lossy().into_owned(),
branch: "main".into(),
worktree: tmp.join(format!("clone-{name}")),
initial_offset: None,
}
}
pub async fn open_log(tmp: &Path) -> EventLog {
let wh = skade::open(tmp.join("lake")).await.unwrap();
EventLog::open(wh, Specs::mimir().unwrap()).await.unwrap()
}
}
/// The `shop` room of mimir-room's tests — ONE source: the same file, included
/// here as the lib's reader fixture includes it. Repo `shop` (checkout → total,
/// charge → audit → libc_write, a `cart` module with a type and its impl) and
/// repo `web` (render → shop's checkout).
#[cfg(feature = "code-room")]
#[path = "../../crates/mimir-room/tests/room/shop.rs"]
mod shop;
/// The two repos the seeded code room holds (read by the `knowledge_*` tools).
pub const ROOM_REPO: &str = "shop";
pub const ROOM_DEPENDENT: &str = "web";
use genworkspace::{GenSpec, GeneratedWorkspace, Shape, gen_workspace};
use nornir::introspect::artifact::Symbol;
use nornir::introspect::persist::DwarfFacts;
use nornir::warehouse::iceberg::IcebergWarehouse;
/// A seeded workspace ready for the MCP harness. Hold it for the test's
/// lifetime (drop cleans the tempdir).
pub struct Seeded {
pub r#gen: GeneratedWorkspace,
/// Path the spawned `nornir-mcp` must be pointed at via `NORNIR_CONFIG`.
pub config_path: PathBuf,
/// Path the spawned `nornir-mcp` must be pointed at via `NORNIR_WORKSPACE`
/// (the dependency-Mímir descriptor).
pub descriptor_path: PathBuf,
/// Workspace root = where the repo dirs + `.nornir/` live.
pub root: PathBuf,
/// The repo whose knowledge/dwarf/bench/docs data was seeded.
pub seeded_repo: String,
/// A guard-forbidden path created so `guard_*` has a real target.
pub forbidden_rel: String,
/// The funnel-store root the MCP resolves — home-derived
/// `<root>/.nornir/funnel` (the child runs with `HOME=<root>`).
pub funnel_root: PathBuf,
_keep: TempDir,
}
/// Known item the knowledge scan of `holger` is guaranteed to contain: every
/// generated crate defines `pub fn metric()` and `pub const SELF_VALUE`.
pub const KNOWLEDGE_SYMBOL_HIT: &str = "metric";
pub const KNOWLEDGE_CALLEE_HIT: &str = "metric";
/// Seeded `test_results` run ids for `holger` (read by `test_history`): an
/// older all-green run and a newer run carrying a failure.
pub const TEST_RUN_GREEN: &str = "run-green-0001";
pub const TEST_RUN_RED: &str = "run-red-0002";
/// Known DWARF facts seeded for `holger`.
pub const DWARF_SYMBOL_HIT: &str = "holger::engine::run";
pub const DWARF_CALLEE_HIT: &str = "holger::engine::step";
pub const DWARF_FILE_HIT: &str = "engine.rs";
/// Build + seed everything. Panics on any failure (test-only helper).
pub fn seed() -> Seeded {
let keep = tempfile::tempdir().expect("outer tempdir");
// genworkspace builds its OWN tempdir; we reparent the important paths
// into a layout `load_explicit` accepts. Simpler: generate, then place a
// config 3 levels deep *inside* the generated root so workspace_root ==
// that root (where the repo dirs already live).
let r#gen = gen_workspace(&GenSpec::new(Shape::NamedHolgerTree, 10));
let root = r#gen.root.clone();
let seeded_repo = "holger".to_string();
// ── nornir.toml at <root>/_cfg/_rel/nornir.toml so workspace_root == root.
let cfg_dir = root.join("_cfg").join("_rel");
std::fs::create_dir_all(&cfg_dir).unwrap();
let forbidden_rel = "holger/LOCKED.txt".to_string();
std::fs::write(root.join(&forbidden_rel), b"sealed\n").unwrap();
let config_path = cfg_dir.join("nornir.toml");
std::fs::write(&config_path, nornir_toml(&r#gen.repo_names, &forbidden_rel)).unwrap();
// ── bench_history.jsonl for the seeded repo (read by `bench_history` and
// the release gates / docs).
let hist = root.join(&seeded_repo).join("bench_history.jsonl");
std::fs::write(&hist, bench_history_jsonl()).unwrap();
// `docs_install_hooks` runs `git config --local core.hooksPath` in the repo
// checkout, which requires a real git repo — gen_workspace only writes files, so
// `git init` the seeded repo here (no commit needed; hooksPath is a local config).
let _ = std::process::Command::new("git")
.args(["init", "-q"])
.arg(root.join(&seeded_repo))
.status();
// ── warehouse seeding (open → seed → close before the MCP spawns).
//
// The path MUST equal what the spawned `nornir-mcp` resolves via
// `Loaded::warehouse_root()`: with the home-derived default (empty
// `[storage].local_path`) that is `<HOME>/.nornir/workspaces/<name>/builds`
// (per-workspace layout, 2026-06-12). The child runs with `HOME=<root>` and
// its config sits at `<root>/_cfg/_rel/nornir.toml` — no `workspace_<name>`
// path component → workspace name `default`. So both sides agree on
// `<root>/.nornir/workspaces/default/builds`. (Pre-2026-06-12 this was
// `<root>/.nornir/warehouse`; seeding there left every warehouse tool
// reading an empty table — the silent drift this comment guards against.)
let warehouse_root = root
.join(".nornir")
.join("workspaces")
.join("default")
.join("builds");
std::fs::create_dir_all(&warehouse_root).unwrap();
{
let wh = IcebergWarehouse::open(&warehouse_root).expect("open temp warehouse");
// (No `symbol_facts` seeding: the `syn` scan that wrote that table went
// with Mímir v2. The knowledge tools read the code room, seeded below.)
// DWARF: a synthetic facts snapshot (no binary needed).
let facts = synthetic_dwarf_facts();
let cache = root
.join(".nornir")
.join("cache")
.join("dwarf")
.join(&seeded_repo);
nornir::introspect::persist::snapshot_facts(
&wh,
"synthetic",
&seeded_repo,
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
"main",
&facts,
&cache,
)
.expect("snapshot dwarf facts");
// Test results: two runs for `holger` so `test_history` returns a real
// newest-first summary — an OLDER all-green run, then a NEWER run with a
// failure (so the newest summary reads red). Injected here, asserted by
// the harness (LAW: inject values, assert results).
{
use nornir::warehouse::test_results::{TestResultRow, append_test_results, status};
let green = vec![
TestResultRow::unit(
TEST_RUN_GREEN,
&seeded_repo,
&seeded_repo,
"engine::ok",
status::PASS,
5.0,
1_000,
"",
),
TestResultRow::unit(
TEST_RUN_GREEN,
&seeded_repo,
&seeded_repo,
"engine::also_ok",
status::PASS,
6.0,
1_000,
"",
),
];
let red = vec![
TestResultRow::unit(
TEST_RUN_RED,
&seeded_repo,
&seeded_repo,
"engine::ok",
status::PASS,
5.0,
2_000,
"",
),
TestResultRow::unit(
TEST_RUN_RED,
&seeded_repo,
&seeded_repo,
"engine::regressed",
status::FAIL,
3.0,
2_000,
"assert_eq left != right",
),
];
wh.block_on(append_test_results(&wh, &green))
.expect("seed green test run");
wh.block_on(append_test_results(&wh, &red))
.expect("seed red test run");
}
// Bench runs: the `bench_history` tool reads the warehouse `bench_runs`
// table in embedded mode (NOT the legacy on-disk bench_history.jsonl —
// `Ops.RunBench` only writes the warehouse), so seed the runs HERE too or the
// tool reads an empty table. Two runs (machine "synthetic", a "throughput"
// result) mirror `bench_history_jsonl` and give `no_regression` a baseline;
// the harness asserts the JSON contains "throughput" and "synthetic".
{
use nornir::bench::{BenchResult, BenchRun};
let run = |date: &str, ops: f64| {
let mut metrics = serde_json::Map::new();
metrics.insert("ops_sec".into(), serde_json::json!(ops));
BenchRun {
// the seed is where the run is made, so it mints the id;
// it did not look at the box
id: BenchRun::new_id(),
machine_facts: None,
date: date.into(),
timestamp: Some(format!("{date}T10:00:00Z")),
version: "0.1.0".into(),
machine: "synthetic".into(),
cores: 1,
results: vec![BenchResult {
name: "throughput".into(),
metrics,
source: Default::default(),
}],
tests: vec![],
}
};
wh.block_on(wh.append_bench_run_async(&seeded_repo, &run("2024-01-01", 1000.0)))
.expect("seed bench run a");
wh.block_on(wh.append_bench_run_async(&seeded_repo, &run("2024-01-02", 1010.0)))
.expect("seed bench run b");
}
// Docs: one historized export row.
nornir::docs::record_doc_export(
&wh,
"synthetic",
&seeded_repo,
"README",
"0.1.0",
"md",
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
b"# holger\n\nseeded doc export\n",
)
.expect("record doc export");
// close (drop) the warehouse → release the redb write lock.
}
// ── the code room: the lake the embedded MCP opens under its warehouse
// root, written by mimir-ingest's own consumer (scripted dumps, the `shop`
// fixture) and closed again before the MCP spawns — a lake has one owner.
#[cfg(feature = "code-room")]
{
let base = nornir::code_room::CodeRoom::new("seed", &warehouse_root).layout().base().to_path_buf();
std::fs::create_dir_all(&base).unwrap();
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().expect("runtime for the room seed");
drop(rt.block_on(shop::shop_log(&base)));
}
// ── Tantivy index over the workspace so `search` returns real hits.
{
let idx = nornir::index::Index::open(&root).expect("open index");
idx.build().expect("build index");
}
// Funnel root the MCP resolves: home-derived `<HOME>/.nornir/funnel`. The
// MCP child runs with `HOME=<root>`, so it lands here — distinct from the
// `<root>/.nornir/warehouse` bench/knowledge warehouse (no path env-var).
let funnel_root = root.join(".nornir").join("funnel");
std::fs::create_dir_all(&funnel_root).unwrap();
Seeded {
descriptor_path: r#gen.descriptor_path.clone(),
r#gen,
config_path,
root,
seeded_repo,
forbidden_rel,
funnel_root,
_keep: keep,
}
}
fn sym(name: &str, file: &str, line: u32) -> Symbol {
Symbol {
name: name.to_string(),
name_demangled: name.to_string(),
name_mangled: format!("_ZN{}", name.replace("::", "")),
file: file.to_string(),
line: Some(line),
size_bytes: Some(64),
krate: "holger".to_string(),
}
}
fn synthetic_dwarf_facts() -> DwarfFacts {
DwarfFacts {
symbols: vec![
sym(DWARF_SYMBOL_HIT, "holger/src/engine.rs", 10),
sym(DWARF_CALLEE_HIT, "holger/src/engine.rs", 22),
sym("holger::engine::finish", "holger/src/engine.rs", 40),
],
}
}
/// Two bench runs so `no_regression` has a prior baseline to compare against.
fn bench_history_jsonl() -> String {
let a = r#"{"date":"2024-01-01","version":"0.1.0","machine":"synthetic","cores":1,"results":[{"name":"throughput","ops_sec":1000.0},{"name":"holger","holger_ops_sec":1000.0,"nexus_ops_sec":500.0}]}"#;
let b = r#"{"date":"2024-01-02","version":"0.1.0","machine":"synthetic","cores":1,"results":[{"name":"throughput","ops_sec":1010.0},{"name":"holger","holger_ops_sec":1010.0,"nexus_ops_sec":500.0}]}"#;
format!("{a}\n{b}\n")
}
/// A `nornir.toml` with home-derived storage (no `[storage].local_path`), which
/// declares each generated repo and forbids one real path so `guard_*` has
/// something to operate on. The MCP child runs with `HOME=<root>`, so the
/// warehouse resolves to the seeded `<root>/.nornir/warehouse`.
fn nornir_toml(repos: &[String], forbidden_rel: &str) -> String {
let mut s = String::new();
s.push_str("[guard]\n");
s.push_str(&format!("forbidden = [\"{forbidden_rel}\"]\n\n"));
// No `[storage].local_path`: the warehouse + funnel are home-derived. The
// MCP child runs with `HOME=<root>`, so the warehouse lands at the
// seeded `<root>/.nornir/warehouse` and the funnel at the DISTINCT
// `<root>/.nornir/funnel` — no shared-dir redb lock collision.
s.push_str("[storage]\nkind = \"local\"\n\n");
for r in repos {
s.push_str(&format!("[repo.{r}]\n"));
// Enable the gates the harness asserts on the seeded repo.
if r == "holger" {
s.push_str("[repo.holger.gates]\n");
s.push_str("no_path_patches = true\n");
s.push_str("nexus_floor = true\n");
s.push_str("no_regression = true\n");
s.push_str("max_regression_pct = 50.0\n\n");
} else {
s.push_str("[repo.");
s.push_str(r);
s.push_str(".gates]\nno_path_patches = true\n\n");
}
}
s
}
/// Convenience for callers that only need a path under the workspace.
pub fn join(root: &Path, rel: &str) -> PathBuf {
root.join(rel)
}