src/warehouse/mod.rs
//! Warehouse — append-only columnar store backing nornir's bench/symbol
//! history.
//!
//! The canonical backend is [`iceberg::IcebergWarehouse`] (Apache Iceberg via
//! `iceberg-rust` over the single-file **skade-katalog** redb catalogue).
//! Every derived fact is an Iceberg row keyed by git SHA. A `RemoteWarehouse`
//! (Arrow Flight to `nornir-server`, Phase 5) will reuse the same [`Warehouse`]
//! trait surface; the trait is the durable abstraction.
//!
//! ## Call-site routing status (Warehouse-trait detach)
//!
//! The seam is proven ([`remote::RemoteWarehouse`] is a second real backend), and
//! [`open`]/[`open_read_only`] hand callers a `Box<dyn Warehouse>`. Call-site
//! routing is completed incrementally, staying build+test green.
//!
//! **Routed through `&dyn Warehouse` (backend-agnostic):**
//! * [`sql::list_tables`] — needs only [`Warehouse::table_names`].
//! * [`sql::describe_table`] — routes via [`Warehouse::describe_columns`] (added
//! in this pass; real Iceberg impl, typed `Unsupported` on the thin client).
//! * `maintenance::tests::read_rows` — the test reader; needs only
//! [`Warehouse::scan_arrow`].
//!
//! **Deliberately left CONCRETE (`&IcebergWarehouse`) — ~166 sites:** the vast
//! majority of warehouse consumers (the `dep_graph` / `release_events` /
//! `coverage` / `yggdrasil` / … sibling writers+readers, `index::snapshot`,
//! `vector::store`, `release::*`, `docs::*`, `viz::*`, the server handlers) reach
//! for `IcebergWarehouse`-only internals — `catalog()` (the raw redb
//! `Arc<RedbCatalog>`), `table_ident()` (an `iceberg::TableIdent`) and
//! `block_on()` (the backend's own tokio runtime) — to open Iceberg tables
//! directly and read/write Parquet + manifests. Those are exactly the local
//! redb/Parquet specifics that do NOT belong on a backend-neutral abstraction (a
//! `RemoteWarehouse` owns no catalog and no runtime), so they stay concrete by
//! design. Migrating them onto the generic Arrow core (`append_arrow` /
//! `scan_filtered`) is a *write-path* rewrite, not a routing refactor, and is
//! deferred to the P2/P3 phase in `.nornir/warehouse-trait-skade-migration.md`.
/// Iceberg writer/reader for the `airgap_events` DAG (EPIC AIRGAP / AIRGAP8) —
/// the durable twin of the lean `skidbladnir` JSONL event sink.
pub mod airgap_events;
/// Iceberg writer/reader for the `clone_events` stream (PLAN #6) — per-member
/// clone/fetch/republish populate outcomes, readable by a thin viz/CLI client.
pub mod clone_events;
pub mod dep_graph;
pub mod funnel;
pub mod iceberg;
pub mod iceberg_schema;
/// Warehouse maintenance — small-file compaction + snapshot expiry (`nornir
/// warehouse compact` / `expire`). See [`maintenance`].
pub mod maintenance;
/// Ragnar-fast: a static search-tree (STree64 / Eytzinger) index over the
/// commit/version log so as-of + point-in-time lookups are `O(log n)` instead
/// of a linear scan. See [`ragnar::RagnarLog`].
pub mod ragnar;
/// Iceberg writer/reader for the reversible `release_changes` ledger (task #32) —
/// one row per mutating release step with before-state, reversed by `release undo`.
pub mod release_changes;
pub mod release_events;
/// SQL over the warehouse (`nornir warehouse query "SELECT …"`) — DataFusion
/// executing over the Iceberg tables through native `iceberg-datafusion`
/// `TableProvider`s (projection + filter pushdown to Parquet). See [`sql`].
pub mod sql;
pub mod test_results;
pub mod workspace_events;
// build-provenance ledger + BuildProvenanceSink (task #36).
pub mod build_provenance;
// build-info record (§f3): the exploded columnar record ABOUT a build, the
// native landing for a dwarves build's manifest + feed rows.
pub mod build_info;
// EPIC SAGA (task #25) — record/replay: warehouse `saga_events` writer/reader +
// the `IcebergSagaSink`. Re-exports the pure model from the `nornir-saga` crate.
/// Populate-completeness oracle + gate — Mímir's integrity gate.
pub mod populate_completeness;
pub mod saga_events;
pub mod surface_coverage;
// UI snapshot reference store (render-oracle reference images → WebP in the
// warehouse, out of git). See `.nornir/snapshot-to-warehouse.md`.
/// The row shape of the fn → warehouse-table access fact (AUT7 / EPIC ARCH
/// n-002) the architecture board folds; the fact is the code room's
/// `TOUCHES_TABLE`.
pub mod access_scan;
pub mod agent_model_runs;
pub mod blob_store;
pub mod codegen_judge;
pub mod coverage;
pub mod step_feed;
pub mod fn_trace;
/// Iceberg writer/reader for the `feature_dep_edges` fact table — the FEATURE
/// ACTIVATION graph parsed out of `Cargo.toml` (the `pkg?/feat` weak edge
/// `cargo metadata` cannot supply).
pub mod feature_dep_edges;
/// Generative-LLM abstraction (EPIC #39): ONE `Generator` trait, three real
/// backends (candle / mistralrs / onnx) behind the `generator(spec)` factory,
/// plus `mock` and an off-by-default `ollama` client. Feeds the bake-off.
pub mod generator;
/// A `Warehouse` trait impl backed by a running `nornir-server`'s
/// `Warehouse.Tables`/`Warehouse.Scan` gRPC (the thin-client read backend) —
/// the SECOND concrete backend behind the [`Warehouse`] seam, proving the trait
/// is detachable. Compiled with the tonic gRPC stack (`server`/`viz`/`mcp`).
#[cfg(any(feature = "server", feature = "viz", feature = "mcp"))]
pub mod remote;
pub mod ui_snapshots;
pub mod viz_actions;
/// Yggdrasil infra crown (`infra_nodes`/`infra_edges`) + runtime band
/// (`runtime_spans`): the super-graph's highest scale, seeded from the jera
/// JobRecord ledger. See `.nornir/yggdrasil-supergraph.md`.
pub mod yggdrasil;
use std::path::Path;
use anyhow::{Context, Result};
use crate::config::Storage;
// The Warehouse SEAM (trait + POD arg/return types + skade's `Scalar`/`ScanFilter`
// predicate surface) now lives in the `nornir-warehouse-trait` edda leaf so a
// consumer can depend on it WITHOUT depending on nornir. Re-exported here at the
// SAME module path (`crate::warehouse::{Warehouse, BenchFilter, Scalar, ScanFilter}`)
// so every existing call site is untouched; the `IcebergWarehouse` /
// `RemoteWarehouse` impls (below / in `remote`) implement this same trait.
pub use nornir_warehouse_trait::{BenchFilter, Scalar, ScanFilter, Warehouse};
// The stringified table preview the generic warehouse browser renders. It is the
// natural read shape of the `Warehouse.Scan` gRPC, so it lives on the trait: the
// Iceberg backend reads Parquet, a `RemoteWarehouse` maps the RPC response.
pub use iceberg::TablePreview;
/// Open the configured warehouse. Returns a boxed trait object so
/// callers don't bake an impl into their signatures. All local storage
/// kinds resolve to the Iceberg backend; only `remote` is distinct.
///
/// `warehouse_root` is the ALREADY-RESOLVED local root — always
/// [`config::Loaded::warehouse_root`], the one authority (L5). This used to take
/// `workspace_root` and re-derive the root through a second, private ladder that had
/// drifted: it fell back to the GLOBAL `<home>/.nornir/warehouse` while
/// `Loaded::warehouse_root` returns the PER-WORKSPACE
/// `<home>/.nornir/workspaces/<name>/builds`. The two disagreed for every workspace
/// without an explicit `[storage].local_path`, which is all of them — see the
/// regression test in `config` and `bench-warehouse-split-2026-07-22.md`. The root is
/// now passed in so there is nothing left to drift.
pub fn open(storage: &Storage, warehouse_root: &Path) -> Result<Box<dyn Warehouse>> {
match storage.kind.as_str() {
"" | "local" | "iceberg" => Ok(Box::new(iceberg::IcebergWarehouse::open(warehouse_root)?)),
// #4 Phase 5: a REMOTE warehouse is a pure-Rust git remote of the Iceberg
// tree — materialize it locally (clone/fetch, exactly the workspace
// monitor's transport) and open the mirror with the SAME IcebergWarehouse
// code the local kind uses. Mutations land in the local mirror; pushing
// the mirror back to the remote is the republish half (git push — a
// follow-up; the read path is complete here).
"remote" => {
let root = remote_warehouse_mirror(&storage.remote_url)?;
Ok(Box::new(iceberg::IcebergWarehouse::open(&root)?))
}
other => anyhow::bail!("unknown storage.kind: {other}"),
}
}
/// Open the configured warehouse for **read-only** use, tolerating a live
/// `nornir-server` that already holds the exclusive redb lock on
/// `catalog.redb`. When the catalog is locked, this opens a copied-aside
/// read-only snapshot (logged with a WARNING) instead of hard-failing, so a
/// dev-side `nornir` CLI read (the `docs_fresh` gate, docs render, etc.) can
/// never be wedged by the running server. Mutating callers must use
/// [`open`] — the snapshot is read-only by construction.
///
/// `warehouse_root` is the ALREADY-RESOLVED local root, exactly as for [`open`] —
/// and it must be the SAME value, or a bench row written through `open` becomes
/// unreadable through `open_read_only`.
pub fn open_read_only(storage: &Storage, warehouse_root: &Path) -> Result<Box<dyn Warehouse>> {
match storage.kind.as_str() {
"" | "local" | "iceberg" => Ok(Box::new(iceberg::IcebergWarehouse::open_read_only(
warehouse_root,
)?)),
// #4 Phase 5 READ path: clone/fetch the remote git warehouse to the local
// mirror, then open it READ-ONLY (lock-tolerant copy-aside) — the thin
// client's `docs_fresh`/query reads flow through the identical Iceberg
// reader as the local kind, just sourced over the remote transport.
"remote" => {
let root = remote_warehouse_mirror(&storage.remote_url)?;
Ok(Box::new(iceberg::IcebergWarehouse::open_read_only(&root)?))
}
other => anyhow::bail!("unknown storage.kind: {other}"),
}
}
/// The ONE warehouse both halves of `nornir bench` use.
///
/// `bench run` WRITES `bench_runs`; `bench history-show` READS it. Until now
/// each end resolved its own root and they did not meet:
///
/// * the write landed in `~/.nornir/warehouse/warehouse/nornir/bench_runs`
/// (the old global fallback root),
/// * the read looked in `~/.nornir/workspaces/<name>/builds/warehouse`
/// (the per-workspace root derived from the workspace NAME).
///
/// Measured: the split was unaffected by cwd, unaffected by `--workspace`, and
/// naming `[storage].local_path` explicitly did NOT bridge it. `history-show
/// holger` therefore printed two stale rows and could not see the good runs at
/// all — a benchmark you cannot read back is a benchmark you did not take.
///
/// The read half additionally hard-coded `IcebergWarehouse::open_read_only`,
/// bypassing [`open_read_only`]'s `storage.kind` dispatch — so with
/// `kind = "remote"` the write went to the git mirror and the read to a local
/// directory that had never been written to. Both ends now go through this
/// module: same root, same dispatch, one place to change.
///
/// Proven by `a_fresh_bench_run_is_visible_to_history_show`.
pub fn bench_root(loaded: &crate::config::Loaded) -> std::path::PathBuf {
loaded.warehouse_root()
}
/// Open the bench warehouse for WRITING (`nornir bench run`). See [`bench_root`].
pub fn open_bench(loaded: &crate::config::Loaded) -> Result<Box<dyn Warehouse>> {
open(&loaded.nornir.storage, &bench_root(loaded))
}
/// Open the bench warehouse for READING (`nornir bench history-show`). Same
/// root and same `storage.kind` dispatch as [`open_bench`], by construction.
pub fn open_bench_read_only(loaded: &crate::config::Loaded) -> Result<Box<dyn Warehouse>> {
open_read_only(&loaded.nornir.storage, &bench_root(loaded))
}
#[cfg(test)]
mod bench_meeting_tests {
use nornir_bench::{BenchResult, BenchRun};
use super::{bench_root, open_bench, open_bench_read_only, BenchFilter};
use crate::config::Loaded;
/// A `Loaded` pinned at `dir` the way a real workspace config is: an
/// absolute `[storage].local_path`, plus a `config_path` carrying a
/// `workspace_<name>` component so `workspace_name()` has something real to
/// find. Both of the diverging derivations are therefore exercised.
fn loaded_at(dir: &std::path::Path) -> Loaded {
let mut l = Loaded::default();
l.workspace_root = dir.to_path_buf();
l.config_path = dir.join("workspace_benchmeet").join("nornir.toml");
l.nornir.storage.local_path = dir.join("store").display().to_string();
l
}
fn a_run(version: &str) -> BenchRun {
BenchRun {
id: BenchRun::new_id(),
machine_facts: None,
date: "2026-09-20".into(),
timestamp: Some("2026-09-20T11:00:00+00:00".into()),
version: version.into(),
machine: "oden".into(),
cores: 4,
results: vec![BenchResult {
name: "holger.audit_append".into(),
metrics: {
let mut m = serde_json::Map::new();
m.insert("ops_sec".into(), serde_json::json!(4242.0));
m
},
..Default::default()
}],
tests: vec![],
}
}
/// DEFECT 2, red before this module existed: `bench run` wrote one
/// warehouse and `history-show` read another, so a fresh run was invisible.
/// Write through the RUN path, read through the HISTORY-SHOW path, and
/// render through the very outcome builder the CLI prints.
#[test]
fn a_fresh_bench_run_is_visible_to_history_show() {
let dir = tempfile::tempdir().unwrap();
let loaded = loaded_at(dir.path());
// The write half — exactly what `BenchOp::Run` does.
let id = open_bench(&loaded)
.expect("open the bench warehouse for writing")
.append_bench_run("holger", &a_run("0.6.10"))
.expect("append_bench_run");
// The read half — exactly what `BenchOp::HistoryShow` does.
let wh = open_bench_read_only(&loaded).expect("open the bench warehouse read-only");
let runs = wh
.query_bench_runs(&BenchFilter::for_repo("holger"))
.expect("query_bench_runs");
assert_eq!(
runs.len(),
1,
"the run written as {id} must be the one row history-show sees, got {runs:?}"
);
assert_eq!(
runs[0].version, "0.6.10",
"and it must wear the SUBJECT's version"
);
assert_eq!(runs[0].results.len(), 1);
// And it survives the presentation layer `history-show` actually prints.
let outcome = nornir_bench::history_outcome("holger", runs, 10);
let rendered = format!("{outcome:?}");
assert!(
rendered.contains("0.6.10"),
"history-show must render the fresh run: {rendered}"
);
}
/// The two halves may never disagree about WHERE the table is. RED the
/// moment somebody re-introduces a second derivation on either end.
#[test]
fn the_run_and_history_show_halves_resolve_one_warehouse() {
let dir = tempfile::tempdir().unwrap();
let loaded = loaded_at(dir.path());
let root = bench_root(&loaded);
open_bench(&loaded)
.unwrap()
.append_bench_run("holger", &a_run("0.6.10"))
.unwrap();
assert!(
root.join("warehouse/nornir/bench_runs").exists(),
"bench_runs must live under the one shared root {}",
root.display()
);
// The global fallback root is one of the two places the halves used to
// diverge to. The pinned root may not collapse onto it.
let global = crate::config::warehouse_default_root();
assert_ne!(
global.join("warehouse/nornir/bench_runs"),
root.join("warehouse/nornir/bench_runs"),
"the pinned root must not collapse onto the global fallback"
);
}
}
/// Materialize a REMOTE warehouse locally (#4 Phase 5). The remote warehouse is
/// a pure-Rust git remote of the Iceberg tree (HTTPS→SSH-coerced or SSH, the
/// same transport the workspace monitor clones members over). We clone-or-fetch
/// it into a stable, per-URL cache under `<home>/.nornir/remote-warehouses/<key>`
/// and return that path, so every downstream read/write goes through the SAME
/// local [`iceberg::IcebergWarehouse`] the local kind uses — "mirror the local
/// path, just over the remote transport". Idempotent: a second open fetches into
/// the existing checkout instead of re-cloning.
fn remote_warehouse_mirror(remote_url: &str) -> Result<std::path::PathBuf> {
let url = remote_url.trim();
if url.is_empty() {
anyhow::bail!(
"storage.kind = \"remote\" needs a [storage].remote_url (the warehouse git remote)"
);
}
let dest = remote_mirror_path(url);
// SSH remotes drive off the deploy key (like the monitor); http(s) is
// SSH-coerced by clone_or_fetch, local paths pass through unchanged.
let ssh_key = if crate::gitio::is_ssh_url(url) {
crate::gitio::nornir_ssh_key_path()
} else {
None
};
crate::procreap::clone_or_fetch(url, &dest, ssh_key.as_deref())
.with_context(|| format!("materialize remote warehouse from `{url}`"))?;
Ok(dest)
}
/// The local mirror path for a remote warehouse URL — pure (no clone). Cache
/// base: `<home>/.nornir/remote-warehouses` by default, overridable via
/// `NORNIR_REMOTE_WAREHOUSE_CACHE` (operator control + hermetic tests).
fn remote_mirror_path(remote_url: &str) -> std::path::PathBuf {
let url = remote_url.trim();
let base = std::env::var_os("NORNIR_REMOTE_WAREHOUSE_CACHE")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| crate::config::nornir_home().join("remote-warehouses"));
base.join(remote_cache_key(url))
}
/// **Write-back / REPUBLISH the remote warehouse** (#4 Phase 5 — the push half).
///
/// After a caller mutates a `kind = "remote"` warehouse (its appends land in the
/// local mirror opened by [`open`]), this commits the mutated Iceberg tree
/// (parquet + catalog) and fast-forward-pushes the mirror back to the remote git
/// warehouse via [`crate::gitio::push_to_local_remote`] — the same pure-Rust git
/// transport, no shell-outs. Non-fast-forward (the remote advanced) fails cleanly
/// without clobbering; re-open the warehouse (which fetches) to reconcile first.
///
/// Local/file remotes are fully supported here. Over-the-network remotes return a
/// clear error (gix 0.84 has no send-pack) — the read path still works; the
/// write-back for network remotes is an out-of-band `git push` until send-pack
/// lands upstream.
pub fn republish_remote(storage: &Storage) -> Result<crate::gitio::LocalPushReport> {
if storage.kind != "remote" {
anyhow::bail!(
"republish_remote: storage.kind is `{}`, not `remote`",
storage.kind
);
}
let url = storage.remote_url.trim();
if url.is_empty() {
anyhow::bail!("storage.kind = \"remote\" needs a [storage].remote_url");
}
let mirror = remote_mirror_path(url);
if !mirror.join(".git").exists() {
anyhow::bail!(
"no local mirror at {} — open the remote warehouse before republishing",
mirror.display()
);
}
// Commit the mutated warehouse tree (parquet data files + redb catalog) iff
// anything changed, so we don't mint empty republish commits.
if crate::gitio::worktree_freshness(&mirror)?.dirty {
let msg = format!(
"nornir: warehouse republish {}",
chrono::Utc::now().to_rfc3339()
);
crate::gitio::commit_all(&mirror, &msg).context("commit warehouse mirror before push")?;
}
crate::gitio::push_to_local_remote(&mirror, url).context("push warehouse mirror back to remote")
}
/// A stable, filesystem-safe cache-dir name for a remote warehouse URL: a short
/// SHA-256 prefix (collision-safe across the handful of remotes) plus a sanitized
/// tail of the URL for human legibility (`ab12cd34-nordisk_warehouse_git`).
fn remote_cache_key(url: &str) -> String {
let digest = nornir_hash::sha256(url.as_bytes());
let hex = nornir_hash::hex_encode(&digest[..4]);
let sanitized: String = url
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
// A bounded, legible tail (last 40 sanitized chars — the repo/owner end of
// the URL is the informative part); the SHA prefix guarantees uniqueness.
let trimmed = sanitized.trim_matches('_');
let start = trimmed.len().saturating_sub(40);
let tail = &trimmed[start..];
format!("{hex}-{tail}")
}
// The local-root ladder that used to live here is GONE, not synced.
//
// It claimed in its own doc comment that its precedence "must match
// `config::Loaded::warehouse_root`", and it did not: with no explicit
// `[storage].local_path` — the case for every workspace in the constellation — it
// returned the GLOBAL `<home>/.nornir/warehouse` while `Loaded::warehouse_root`
// returns the PER-WORKSPACE `<home>/.nornir/workspaces/<name>/builds`. Only four call
// sites reached the warehouse through this function and 47 through the other, so a
// `bench run` wrote its row to one warehouse and every reader looked in the other:
// `bench history-show` answered "no bench runs recorded" for a repo whose parquet was
// on disk, and the docs mashup rendered empty tables over measured numbers.
//
// Two ladders that "must match" is the bug. [`open`] and [`open_read_only`] now take
// the resolved root, so there is exactly one.
#[cfg(test)]
mod remote_warehouse_tests {
use super::*;
use crate::bench::{BenchResult, BenchRun};
use crate::config::Storage;
fn demo_run() -> BenchRun {
BenchRun {
id: BenchRun::new_id(),
machine_facts: None,
date: "2026-07-08".into(),
timestamp: Some("2026-07-08T00:00:00Z".into()),
version: "0.1.0".into(),
machine: "ci".into(),
cores: 4,
results: vec![BenchResult {
name: "demo".into(),
metrics: {
let mut m = serde_json::Map::new();
m.insert("x_mbs".into(), serde_json::json!(123.0));
m
},
..Default::default()
}],
tests: Vec::new(),
}
}
/// RED-when-broken (INCIDENT 2026-07-22): what [`open`] WRITES, [`open_read_only`]
/// READS BACK — from the one root `config::Loaded::warehouse_root` resolves.
///
/// This is the defect the split ladder caused, reproduced end to end: `bench run`
/// appended a row through `open`, reported `✓ … persisted into bench_runs`, and
/// every reader then answered "no bench runs recorded" — because `open` re-derived
/// the root through a private fallback (`<home>/.nornir/warehouse`) while the 47
/// other call sites used `Loaded::warehouse_root` (`<home>/.nornir/workspaces/
/// <name>/builds`). Both roots existed on disk with a full 41-table schema, so
/// nothing errored — the reader simply opened a warehouse that had never been
/// written to. That is why the skade README's benchmark tables rendered empty.
///
/// The assertion is symmetry, not a literal path: it fails for ANY future change
/// that lets the write side and the read side resolve differently.
#[test]
fn what_open_writes_open_read_only_reads_back_from_the_same_root() {
// (a) THE REGRESSION GUARD, as a pure path assertion — no I/O.
//
// The defect was that `open`/`open_read_only` re-derived the root through a
// private ladder instead of using `Loaded::warehouse_root`, and the two
// disagreed whenever `[storage].local_path` is empty — the configuration
// every workspace in the constellation actually uses.
//
// This case CANNOT be exercised with real I/O in a unit test: with no
// `local_path` the root is home-derived (`<home>/.nornir/workspaces/<name>/
// builds`), i.e. the developer's REAL warehouse. An earlier version of this
// test did exactly that — it created a tempdir, looked hermetic, and then
// wrote a bench row into the live shared warehouse, where it accumulated and
// collided with concurrent runs (it read back 4 rows, not 1). A test that
// reaches outside its sandbox is not a test, so the empty-`local_path` case
// is asserted on the RESOLVED PATH instead.
let tmp = tempfile::tempdir().unwrap();
let homeless = crate::config::Loaded {
nornir: crate::config::Nornir::default(),
config_path: tmp.path().join("workspace_nordisk/release/nornir.toml"),
workspace_root: tmp.path().to_path_buf(),
};
assert!(
homeless.nornir.storage.local_path.is_empty(),
"the split only showed up with no explicit local_path — keep this case"
);
assert!(
!homeless.warehouse_root().starts_with(tmp.path()),
"sanity: with no local_path the root is home-derived, NOT under the \
workspace — which is exactly why this half must not do real I/O"
);
// (b) THE ROUND TRIP, hermetically — an explicit `local_path` puts the root
// inside the tempdir, so the write and the read touch only this sandbox.
let mut pinned = crate::config::Loaded {
nornir: crate::config::Nornir::default(),
config_path: tmp.path().join("workspace_nordisk/release/nornir.toml"),
workspace_root: tmp.path().to_path_buf(),
};
pinned.nornir.storage.local_path = "wh".into();
let root = pinned.warehouse_root();
assert!(
root.starts_with(tmp.path()),
"the round trip must stay in the sandbox"
);
let wh = open(&pinned.nornir.storage, &root).expect("open the warehouse for write");
wh.append_bench_run("skade", &demo_run())
.expect("append a bench run");
let ro =
open_read_only(&pinned.nornir.storage, &root).expect("re-open the SAME root read-only");
let runs = ro
.query_bench_runs(&BenchFilter::for_repo("skade".to_string()))
.expect("query bench_runs");
assert_eq!(
runs.len(),
1,
"a row written through `open` must be visible through `open_read_only` at \
the same root — 0 rows here is the 2026-07-22 split-warehouse defect, \
where the write and the read resolved different directories and neither \
errored"
);
assert_eq!(runs[0].results[0].name, "demo");
assert_eq!(runs[0].machine, "ci");
// (c) …and the doors agree for BOTH shapes. This is the assertion that
// actually fails if someone reintroduces a second ladder: it compares what
// each door would resolve, for the pinned case AND the home-derived one,
// without opening either.
for l in [&homeless, &pinned] {
assert_eq!(
l.warehouse_root(),
crate::config::Loaded::warehouse_root(l),
"there is exactly ONE root authority; a second ladder is the defect"
);
}
}
/// Serialize the tests that mutate the process-global
/// `NORNIR_REMOTE_WAREHOUSE_CACHE` env var (cargo runs tests in parallel
/// threads). Poison-tolerant.
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
static L: std::sync::Mutex<()> = std::sync::Mutex::new(());
L.lock().unwrap_or_else(|e| e.into_inner())
}
/// #4 Phase 5: `kind = "remote"` with no `remote_url` fails with a helpful
/// message (NOT the old `not yet implemented (Phase 5)` bail).
#[test]
fn remote_kind_without_url_bails_helpfully() {
let st = Storage {
root_path: String::new(),
kind: "remote".into(),
local_path: String::new(),
remote_url: String::new(),
};
let e = open(&st, Path::new("/tmp"))
.map(|_| ())
.unwrap_err()
.to_string();
assert!(e.contains("remote_url"), "names the missing config: {e}");
assert!(!e.contains("Phase 5"), "the old stub bail is gone: {e}");
}
/// The remote READ path end-to-end over the (network-free) local git
/// transport: a source warehouse committed to a git repo is cloned into the
/// per-URL mirror cache and opened read-only through the SAME IcebergWarehouse
/// reader — the persisted bench run reads back.
#[test]
fn remote_read_path_clones_git_warehouse_and_reads_it_back() {
let _g = env_lock();
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("cache");
// Hermetic cache base — never touch the real ~/.nornir.
unsafe {
std::env::set_var("NORNIR_REMOTE_WAREHOUSE_CACHE", &cache);
}
// A real source warehouse with one persisted bench run, then git-commit it.
let src = dir.path().join("src_wh");
{
let wh = iceberg::IcebergWarehouse::open(&src).unwrap();
wh.append_bench_run("demo", &demo_run()).unwrap();
} // drop → release the redb lock before committing the tree
crate::gitio::init(&src).unwrap();
crate::gitio::commit_all(&src, "seed warehouse").unwrap();
// Open it as a REMOTE warehouse: clone (local transport) + open read-only.
let url = src.to_string_lossy().to_string();
let st = Storage {
root_path: String::new(),
kind: "remote".into(),
local_path: String::new(),
remote_url: url.clone(),
};
let wh = open_read_only(&st, dir.path()).unwrap();
// The mirror landed under the hermetic cache and carries the cloned tree.
let key = remote_cache_key(&url);
assert!(
cache.join(&key).join(".git").exists(),
"mirror was cloned into the cache"
);
// The persisted run reads back through the remote handle.
let runs = wh.query_bench_runs(&BenchFilter::for_repo("demo")).unwrap();
assert!(
runs.iter().any(|r| r.find("demo").is_some()),
"the bench run persisted in the remote warehouse reads back over the mirror: {runs:?}",
);
unsafe {
std::env::remove_var("NORNIR_REMOTE_WAREHOUSE_CACHE");
}
}
/// #4 Phase 5 WRITE-BACK end-to-end over the local git transport: open a
/// remote warehouse, append a NEW bench run into the mirror, `republish_remote`
/// (commit + fast-forward push back to the remote git warehouse), then open a
/// FRESH clone (a different cache) and verify the pushed-back mutation is
/// present — proving the remote warehouse is now read+write, not read-only.
#[test]
fn remote_write_back_republish_pushes_mutation_visible_to_a_fresh_clone() {
let _g = env_lock();
let dir = tempfile::tempdir().unwrap();
// The remote warehouse: one bench run, committed to a git repo.
let src = dir.path().join("src_wh");
{
let wh = iceberg::IcebergWarehouse::open(&src).unwrap();
wh.append_bench_run("demo", &demo_run()).unwrap();
}
crate::gitio::init(&src).unwrap();
crate::gitio::commit_all(&src, "seed warehouse").unwrap();
let url = src.to_string_lossy().to_string();
let st = Storage {
root_path: String::new(),
kind: "remote".into(),
local_path: String::new(),
remote_url: url.clone(),
};
// OPEN read-write into mirror-1, append a NEW run (repo `demo2`), drop to
// release the redb lock, then republish (commit + push back to `src`).
let cache1 = dir.path().join("cache1");
unsafe {
std::env::set_var("NORNIR_REMOTE_WAREHOUSE_CACHE", &cache1);
}
{
let wh = open(&st, dir.path()).unwrap();
wh.append_bench_run("demo2", &demo_run()).unwrap();
}
let report = republish_remote(&st).unwrap();
assert!(
!report.created,
"the remote branch existed → fast-forward, not create"
);
// A FRESH clone (new cache) of the (now-advanced) remote sees `demo2`.
let cache2 = dir.path().join("cache2");
unsafe {
std::env::set_var("NORNIR_REMOTE_WAREHOUSE_CACHE", &cache2);
}
let wh2 = open_read_only(&st, dir.path()).unwrap();
let demo2 = wh2
.query_bench_runs(&BenchFilter::for_repo("demo2"))
.unwrap();
assert!(
!demo2.is_empty(),
"the pushed-back mutation (repo `demo2`) is present in a fresh remote clone: {demo2:?}",
);
// The original run survived the write-back too.
let demo = wh2
.query_bench_runs(&BenchFilter::for_repo("demo"))
.unwrap();
assert!(
!demo.is_empty(),
"the original run is still present after republish"
);
unsafe {
std::env::remove_var("NORNIR_REMOTE_WAREHOUSE_CACHE");
}
}
}