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

nordisk/nornir

crates/mimir-room/tests/room/advance.rs

//! New events ADVANCE the kept room — the fold applies the lake's delta
//! instead of re-reading the log's history — and the advanced snapshot is
//! indistinguishable from a fresh full build (the oracle).

use mimir_ingest::Consumer;
use mimir_room::{Room, Scope};
use mimir_schema::{EdgeKind, Origin};

use super::common::*;

fn commit(src: &std::path::Path, n: usize) {
    std::fs::write(src.join("src/lib.rs"), format!("// commit {n}\n")).unwrap();
    nornir_git::gitio::commit_all(src, &format!("c{n}")).unwrap();
}

async fn run(log: mimir_ingest::EventLog, tmp: &std::path::Path, repo: &str, step: Step) -> mimir_ingest::EventLog {
    let mut c = Consumer::boot(log, Scripted(vec![step])).await.unwrap();
    let rep = c.run(&repo_spec(tmp, repo)).await.unwrap();
    assert!(rep.iter().all(|r| r.status == "ok"), "{rep:?}");
    c.into_parts().0
}

#[tokio::test(flavor = "current_thread")]
async fn new_events_advance_the_kept_room_and_match_a_fresh_build() {
    let _g = serial().await;
    Room::clear_cache();
    Room::set_cache_capacity(16);
    let tmp = tempfile::tempdir().unwrap();
    let log = fixture(tmp.path()).await;
    let first = Room::open(&log, &Scope::current()).await.unwrap();
    assert!(!first.cached && !first.advanced, "the first build folds");

    // new source events land: an edit of p1, a new fn, a new call
    commit(&tmp.path().join(format!("src-{PLAIN}")), 2);
    let step = Step {
        owner: "p@0.1.0:lib",
        nodes: vec![node("p p1", "fn", "p", Origin::Local, true), node("p p9", "fn", "p", Origin::Local, true)],
        edges: vec![edge("p p9", "p p1", EdgeKind::Calls)],
        binary: None,
        ring: vec![],
    };
    let log = run(log, tmp.path(), PLAIN, step).await;

    let adv = Room::open(&log, &Scope::current()).await.unwrap();
    assert!(!adv.cached && adv.advanced, "the kept fold advances by the delta, it does not refold");
    assert!(adv.room.ordinal("p p9").is_some(), "the new fn landed");

    // the advanced room is the cache's: a re-open serves it
    let again = Room::open(&log, &Scope::current()).await.unwrap();
    assert!(again.cached && again.room.same_snapshot(&adv.room), "the advanced snapshot is the kept one");

    // the oracle: a fresh full build of the same state must be identical
    Room::clear_cache();
    let full = Room::open(&log, &Scope::current()).await.unwrap();
    assert!(!full.cached && !full.advanced);
    let (a, b) = (serde_json::to_value(adv.room.stats()).unwrap(), serde_json::to_value(full.room.stats()).unwrap());
    for k in ["nodes", "edges", "node_kinds", "provenance", "spans", "files", "sites", "symbols", "duplicate_keys", "endpoint_only", "repo_rows", "universe_rows", "unknown_kinds"] {
        assert_eq!(a[k], b[k], "stats `{k}` differ between the advanced room and the fresh build");
    }
    assert_eq!(adv.room.ordinal("p p9"), full.room.ordinal("p p9"), "ordinals agree (same sorted names)");
    fn reached(r: &Room) -> Vec<String> {
        let d = r.dependents(&["p p1"], None).unwrap();
        r.monikers(&d.value.reached).into_iter().map(str::to_string).collect()
    }
    assert_eq!(reached(&adv.room), reached(&full.room), "the walks agree");
}

/// The advanced snapshot and a from-scratch refold of the same lake state
/// must be indistinguishable: every stats law, and the ordinals.
fn assert_rooms_equal(a: &Room, b: &Room, what: &str) {
    let (sa, sb) = (serde_json::to_value(a.stats()).unwrap(), serde_json::to_value(b.stats()).unwrap());
    for k in ["nodes", "edges", "node_kinds", "provenance", "spans", "files", "sites", "symbols", "duplicate_keys", "endpoint_only", "repo_rows", "universe_rows", "unknown_kinds"] {
        assert_eq!(sa[k], sb[k], "{what}: stats `{k}` differ from the fresh build");
    }
}

/// The ring room ADVANCES: a consume that moves only the repos' source
/// streams (the dependency universe untouched) carries the kept universe
/// fold by the lake's delta, and the advanced snapshot equals a from-scratch
/// refold exactly — nodes, edges, spans, stats, and a walk through the ring.
#[tokio::test(flavor = "current_thread")]
async fn new_events_advance_the_kept_ring_room_and_match_a_fresh_build() {
    let _g = serial().await;
    Room::clear_cache();
    Room::set_ring_keeps_fold(true);
    Room::set_cache_capacity(16);
    let tmp = tempfile::tempdir().unwrap();
    let log = fixture(tmp.path()).await;
    let scope = Scope::current().with_universe(true);
    let first = Room::open(&log, &scope).await.unwrap();
    assert!(!first.cached && !first.advanced, "the first ring build folds");

    // new source events land, the dependency universe does not move
    commit(&tmp.path().join(format!("src-{PLAIN}")), 2);
    let step = Step {
        owner: "p@0.1.0:lib",
        nodes: vec![node("p p1", "fn", "p", Origin::Local, true), node("p p9", "fn", "p", Origin::Local, true)],
        edges: vec![edge("p p9", "p p1", EdgeKind::Calls)],
        binary: None,
        ring: vec![],
    };
    let log = run(log, tmp.path(), PLAIN, step).await;

    let adv = Room::open(&log, &scope).await.unwrap();
    assert!(!adv.cached && adv.advanced, "the kept universe fold advances by the delta, it does not refold");
    assert!(adv.room.ordinal("p p9").is_some(), "the new fn landed");
    assert!(adv.room.ordinal("dep w").is_some(), "the ring rows carried");

    // the advanced room is the cache's: a re-open serves it
    let again = Room::open(&log, &scope).await.unwrap();
    assert!(again.cached && again.room.same_snapshot(&adv.room), "the advanced snapshot is the kept one");

    // the oracle: a fresh full build of the same state must be identical
    Room::clear_cache();
    let full = Room::open(&log, &scope).await.unwrap();
    assert!(!full.cached && !full.advanced);
    assert_rooms_equal(&adv.room, &full.room, "advanced ring room");
    assert_eq!(adv.room.ordinal("p p9"), full.room.ordinal("p p9"), "ordinals agree (same sorted names)");
    assert_eq!(adv.room.ordinal("dep w"), full.room.ordinal("dep w"), "ring ordinals agree");
    fn reached(r: &Room) -> Vec<String> {
        let d = r.dependents(&["dep w"], None).unwrap();
        r.monikers(&d.value.reached).into_iter().map(str::to_string).collect()
    }
    assert_eq!(reached(&adv.room), reached(&full.room), "the walk through the ring agrees");
    assert!(reached(&adv.room).iter().any(|m| m == "dep h"), "the ring edge dep h -> dep w stands");
}

/// The ring view is PINNED against count eviction: newer non-ring rooms
/// cycle through the cache while the kept universe fold STAYS, so the next
/// consume still ADVANCES it. (Live 2026-10-20: cap 4 — the panes' per-repo
/// rooms evicted the ring view between asks and every ring ask refolded the
/// whole dependency universe, 160-175 s each on dwarf.)
#[tokio::test(flavor = "current_thread")]
async fn the_ring_view_survives_newer_rooms_and_still_advances() {
    let _g = serial().await;
    Room::clear_cache();
    Room::set_ring_keeps_fold(true);
    Room::set_cache_capacity(2);
    let tmp = tempfile::tempdir().unwrap();
    let log = fixture(tmp.path()).await;
    let scope = Scope::current().with_universe(true);
    let first = Room::open(&log, &scope).await.unwrap();
    assert!(!first.cached && !first.advanced, "the first ring build folds");

    // more non-ring rooms than the cap: without the pin they evict the ring view
    for s in [Scope::current(), Scope::repo(ROOM, None), Scope::repo(PLAIN, None)] {
        Room::open(&log, &s).await.unwrap();
    }
    let again = Room::open(&log, &scope).await.unwrap();
    assert!(again.cached && again.room.same_snapshot(&first.room), "the ring view is still served after the churn");

    // a consume lands: the kept (pinned) universe fold advances
    commit(&tmp.path().join(format!("src-{PLAIN}")), 2);
    let step = Step {
        owner: "p@0.1.0:lib",
        nodes: vec![node("p p1", "fn", "p", Origin::Local, true), node("p p9", "fn", "p", Origin::Local, true)],
        edges: vec![edge("p p9", "p p1", EdgeKind::Calls)],
        binary: None,
        ring: vec![],
    };
    let log = run(log, tmp.path(), PLAIN, step).await;
    let adv = Room::open(&log, &scope).await.unwrap();
    assert!(!adv.cached && adv.advanced, "the pinned universe fold advances after the churn");

    // zero-loss: the advanced snapshot equals a from-scratch refold
    Room::clear_cache();
    let full = Room::open(&log, &scope).await.unwrap();
    assert_rooms_equal(&adv.room, &full.room, "pinned ring advance");
    Room::set_cache_capacity(16);
}

/// The dependency universe itself moved (a new ring crate was dumped: the
/// `universe_manifest` snapshot advanced): the kept fold must REFUSE to
/// carry and the room is refolded from scratch — holding the new ring rows.
#[tokio::test(flavor = "current_thread")]
async fn a_moved_dependency_universe_refuses_the_carry() {
    let _g = serial().await;
    Room::clear_cache();
    Room::set_cache_capacity(16);
    let tmp = tempfile::tempdir().unwrap();
    let log = fixture(tmp.path()).await;
    let scope = Scope::current().with_universe(true);
    let first = Room::open(&log, &scope).await.unwrap();
    assert!(!first.cached && !first.advanced);
    assert!(first.room.ordinal("dep9 z").is_none(), "dep9 not dumped yet");

    // the consume dumps a NEW ring crate and depends on it: the universe moved
    commit(&tmp.path().join(format!("src-{PLAIN}")), 2);
    let step = Step {
        owner: "p@0.1.0:lib",
        nodes: vec![node("p", "crate", "p", Origin::Local, true)],
        edges: vec![edge("p", "dep9", EdgeKind::DependsOn)],
        binary: None,
        ring: vec![("dep9@1.0.0:lib", vec![node("dep9", "crate", "dep9", Origin::Library, true), node("dep9 z", "fn", "dep9", Origin::Library, true)], vec![])],
    };
    let log = run(log, tmp.path(), PLAIN, step).await;

    let second = Room::open(&log, &scope).await.unwrap();
    assert!(!second.cached && !second.advanced, "a moved universe manifest must force the full refold");
    assert!(second.room.ordinal("dep9 z").is_some(), "the new ring crate's rows are in the refolded room");

    // zero-loss either way: the refold equals a from-scratch build
    Room::clear_cache();
    let full = Room::open(&log, &scope).await.unwrap();
    assert_rooms_equal(&second.room, &full.room, "refused ring carry");
}

/// The universe tables did not move, but the scope's dependency CLOSURE did:
/// a new local DEPENDS_ON edge pulls an already-dumped ring crate into the
/// room. The kept fold's ring scan never read that crate's rows, so the
/// carry must be refused and the refold holds them.
#[tokio::test(flavor = "current_thread")]
async fn a_grown_dependency_closure_refuses_the_carry() {
    let _g = serial().await;
    Room::clear_cache();
    Room::set_cache_capacity(16);
    let tmp = tempfile::tempdir().unwrap();
    let log = fixture(tmp.path()).await;
    // dump a ring crate NOTHING depends on yet (the manifest moves here)
    commit(&tmp.path().join(format!("src-{PLAIN}")), 2);
    let step = Step {
        owner: "p@0.1.0:lib",
        nodes: vec![node("p p1", "fn", "p", Origin::Local, true)],
        edges: vec![],
        binary: None,
        ring: vec![("dep8@1.0.0:lib", vec![node("dep8", "crate", "dep8", Origin::Library, true), node("dep8 z", "fn", "dep8", Origin::Library, true)], vec![])],
    };
    let log = run(log, tmp.path(), PLAIN, step).await;
    let scope = Scope::current().with_universe(true);
    let first = Room::open(&log, &scope).await.unwrap();
    assert!(!first.cached && !first.advanced);
    assert!(first.room.ordinal("dep8 z").is_none(), "dumped but not depended on: outside the closure");

    // the next commit DEPENDS_ON it: the manifest is unchanged, the closure grew
    commit(&tmp.path().join(format!("src-{PLAIN}")), 3);
    let step = Step {
        owner: "p@0.1.0:lib",
        nodes: vec![node("p", "crate", "p", Origin::Local, true)],
        edges: vec![edge("p", "dep8", EdgeKind::DependsOn)],
        binary: None,
        ring: vec![],
    };
    let log = run(log, tmp.path(), PLAIN, step).await;

    let second = Room::open(&log, &scope).await.unwrap();
    assert!(!second.cached && !second.advanced, "a grown closure must force the full refold (the kept scan never read dep8)");
    assert!(second.room.ordinal("dep8 z").is_some(), "the newly depended-on ring crate's rows are in the room");

    // zero-loss either way: the refold equals a from-scratch build
    Room::clear_cache();
    let full = Room::open(&log, &scope).await.unwrap();
    assert_rooms_equal(&second.room, &full.room, "refused ring carry (closure)");
}

/// The LIVE order — consume, then the post-consume compaction REWRITES the
/// window's appended files (stamped row-preserving replace), then the room
/// opens — must still ADVANCE: the delta reads the replace's merged files
/// through the kept track (re-seen rows are idempotent) instead of refusing.
/// (Live 2026-10-20: every real commit's window was swallowed this way —
/// 13:40:31 consume, 13:40:42 replace, 13:41:32 'the node stream's window
/// holds a commit a delta cannot express' — and every ring ask paid a
/// 140-200 s refold; the one advance that fired rode a +0/−0 consume whose
/// compaction had nothing to rewrite.)
#[tokio::test(flavor = "current_thread")]
async fn a_compaction_after_the_consume_still_advances() {
    let _g = serial().await;
    Room::clear_cache();
    Room::set_ring_keeps_fold(true);
    Room::set_cache_capacity(16);
    let tmp = tempfile::tempdir().unwrap();
    let log = fixture(tmp.path()).await;
    let scope = Scope::current().with_universe(true);
    let first = Room::open(&log, &scope).await.unwrap();
    assert!(!first.cached && !first.advanced, "the first ring build folds");
    let repo_first = Room::open(&log, &Scope::current()).await.unwrap();
    assert!(!repo_first.cached && !repo_first.advanced);

    // a commit that MODIFIES an existing fn, ADDS one and CLOSES others
    // (p p2 and the k-a stub leave the dump), then the compaction rewrites
    // the window's files BEFORE anyone reads them — the live order
    commit(&tmp.path().join(format!("src-{PLAIN}")), 2);
    let step = Step {
        owner: "p@0.1.0:lib",
        nodes: vec![node("p p1", "fn", "p", Origin::Local, true), node("p p9", "fn", "p", Origin::Local, true)],
        edges: vec![edge("p p9", "p p1", EdgeKind::Calls)],
        binary: None,
        ring: vec![],
    };
    let log = run(log, tmp.path(), PLAIN, step).await;
    let opts = mimir_ingest::CompactOptions { grace: std::time::Duration::ZERO, ..Default::default() };
    let reports = log.compact(&opts).await.unwrap();
    assert!(reports.iter().any(|r| r.snapshot.is_some()), "a replace landed over the window: {reports:?}");

    let adv = Room::open(&log, &scope).await.unwrap();
    assert!(!adv.cached && adv.advanced, "the ring fold advances THROUGH the replace that swallowed the window");
    assert!(adv.room.ordinal("p p9").is_some(), "the swallowed commit's fn landed");
    let repo_adv = Room::open(&log, &Scope::current()).await.unwrap();
    assert!(!repo_adv.cached && repo_adv.advanced, "the repo fold advances through it too");

    // the oracle: a fresh full build of the same state must be identical
    Room::clear_cache();
    let full = Room::open(&log, &scope).await.unwrap();
    assert!(!full.cached && !full.advanced);
    assert_rooms_equal(&adv.room, &full.room, "advance through the post-consume replace");
    assert_eq!(adv.room.ordinal("p p9"), full.room.ordinal("p p9"), "ordinals agree");
    assert_eq!(adv.room.ordinal("p p2"), full.room.ordinal("p p2"), "the closed fn agrees (gone from both)");
    let full_repo = Room::open(&log, &Scope::current()).await.unwrap();
    assert_rooms_equal(&repo_adv.room, &full_repo.room, "repo advance through the post-consume replace");
}

/// Wave item 8 — a lake compaction between consumes no longer forces a full
/// rebuild: the kept fold advances across the stamped row-preserving replace
/// (`Opened.advanced` stays true) and the room equals a fresh build.
#[tokio::test(flavor = "current_thread")]
async fn a_compaction_between_consumes_does_not_force_a_rebuild() {
    let _g = serial().await;
    Room::clear_cache();
    Room::set_cache_capacity(16);
    let tmp = tempfile::tempdir().unwrap();
    let log = fixture(tmp.path()).await;
    let first = Room::open(&log, &Scope::current()).await.unwrap();
    assert!(!first.cached && !first.advanced, "the first build folds");

    // the lake compacts: stamped replace per table, older snapshots expired
    let opts = mimir_ingest::CompactOptions { grace: std::time::Duration::ZERO, ..Default::default() };
    let reports = log.compact(&opts).await.unwrap();
    assert!(reports.iter().any(|r| r.snapshot.is_some()), "a replace landed: {reports:?}");

    // a release lands after the compaction
    commit(&tmp.path().join(format!("src-{PLAIN}")), 2);
    let step = Step {
        owner: "p@0.1.0:lib",
        nodes: vec![node("p p1", "fn", "p", Origin::Local, true), node("p p9", "fn", "p", Origin::Local, true)],
        edges: vec![edge("p p9", "p p1", EdgeKind::Calls)],
        binary: None,
        ring: vec![],
    };
    let log = run(log, tmp.path(), PLAIN, step).await;

    // THE POINT: the kept fold advances ACROSS the compaction, no refold
    let adv = Room::open(&log, &Scope::current()).await.unwrap();
    assert!(!adv.cached && adv.advanced, "the compaction must not force a full rebuild");
    assert!(adv.room.ordinal("p p9").is_some(), "the post-compaction fn landed");

    // the oracle: a fresh full build of the same state is identical
    Room::clear_cache();
    let full = Room::open(&log, &Scope::current()).await.unwrap();
    assert!(!full.cached && !full.advanced);
    let (a, b) = (serde_json::to_value(adv.room.stats()).unwrap(), serde_json::to_value(full.room.stats()).unwrap());
    for k in ["nodes", "edges", "node_kinds", "provenance", "spans", "files", "sites", "symbols", "duplicate_keys", "endpoint_only", "repo_rows", "universe_rows", "unknown_kinds"] {
        assert_eq!(a[k], b[k], "stats `{k}` differ between the advanced room and the fresh build");
    }
    assert_eq!(adv.room.ordinal("p p9"), full.room.ordinal("p p9"), "ordinals agree");
}