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

nordisk/nornir

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