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

nordisk/edda

crates/storage-git-gix/parked-gix-2/gunnar-store-gix/tests/multi_pack_lookup.rs

#![cfg(feature = "fs")]

//! `P-012`: object lookup across many packs, and the two things the fix for it
//! could quietly have broken.
//!
//! The change under test does three things: it pools `gix-odb` handles instead
//! of building one per call, it turns **off** `gix-odb`'s re-scan-the-directory
//! -on-every-miss refresh mode, and it writes a multi-pack index so a lookup
//! probes one index rather than `M`. The first is invisible to correctness. The
//! other two are not, and they fail in **opposite directions**:
//!
//! * turning the refresh off can make a present object report **absent**. A
//!   pack that landed after the store last scanned is simply not in the
//!   snapshot. That is the failure mode of the staleness watch, and
//!   [`a_pack_written_out_of_band_is_found_without_an_explicit_refresh`] is what
//!   would see it. It writes the pack the way a real push does: through
//!   `index_pack` straight into `objects/pack`, telling the store nothing.
//! * a multi-pack index can make an absent object report **present**: it names
//!   its `.idx` files, so one left in place across a repack answers for objects
//!   whose pack has been unlinked. That is
//!   [`a_repack_invalidates_the_multi_pack_index_it_made_stale`].
//!
//! Both directions are asserted, before and after a repack, because `P-012`'s
//! whole subject is a repository whose pack count moves.
//!
//! # Every one of these was seen RED
//!
//! Recorded here because a guard nobody has watched fail is not evidence
//! (LAW 2). The reds are in `.nornir/performance-design.md` `P-012`; each was
//! produced by breaking the implementation, not the test.

use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

use gix_hash::{Kind, ObjectId};
use gunnar_store::fs::FsStore;
use gunnar_store::midx::MULTI_PACK_INDEX_THRESHOLD;
use gunnar_store::{ObjectStore, RepackOptions};

mod common;
use common::{drop_keep_files, fold_into_pack, pack_dir, packs, read_everything};

/// The multi-pack index, by the name git gives it and `gix-odb` looks for.
fn midx_path(git_dir: &Path) -> PathBuf {
    pack_dir(git_dir).join("multi-pack-index")
}

/// A repository whose objects live in `packs` packfiles and nowhere else.
///
/// It holds **no** open store: `FsStore` takes the Arrow log's exclusive lock
/// for the lifetime of the handle, so a second one on the same directory is
/// refused. Every test opens what it needs and drops it, which is also what
/// makes "reopen and look again" expressible at all. That is the only assertion
/// that reads what is on disk rather than what is in a snapshot.
struct Fixture {
    _dir: tempfile::TempDir,
    git_dir: PathBuf,
    hash: Kind,
    /// Every object that is really in there.
    ids: Vec<ObjectId>,
}

impl Fixture {
    /// `pack_count` packs of `per_pack` blobs each, written the way a push
    /// writes them and with the loose copies removed.
    fn new(hash: Kind, pack_count: usize, per_pack: usize) -> Fixture {
        let dir = tempfile::tempdir().expect("a temporary directory");
        let git_dir = dir.path().join("repo.git");
        let mut ids = Vec::new();
        {
            let store = FsStore::open_or_init(&git_dir, hash).expect("open the repository");
            for pack in 0..pack_count {
                let objects = store.objects();
                let mut batch = Vec::with_capacity(per_pack);
                for blob in 0..per_pack {
                    let body = format!("pack {pack} blob {blob}\n").repeat(3);
                    batch.push(
                        objects
                            .write(gix_object::Kind::Blob, body.as_bytes())
                            .expect("write a blob"),
                    );
                }
                fold_into_pack(&git_dir, &objects, hash, &batch);
                ids.extend(batch);
            }
        }
        drop_keep_files(&git_dir);
        assert_eq!(
            packs(&git_dir).len(),
            pack_count,
            "the fixture did not end up with {pack_count} packs, so it cannot say anything \
             about lookup across packs"
        );
        Fixture {
            _dir: dir,
            git_dir,
            hash,
            ids,
        }
    }

    /// Open the repository from disk: a fresh store, a fresh snapshot and a
    /// fresh handle pool. Only one may be alive at a time.
    fn open(&self) -> FsStore {
        FsStore::open_or_init(&self.git_dir, self.hash).expect("open the repository")
    }
}

/// Oids that are definitely not in `present`, derived rather than random so the
/// set is the same on every run.
///
/// Derived by flipping the leading byte of a real id: same length, same hash
/// kind, same neighbourhood in the sorted index, so the miss walks as far into
/// the fanout and the binary search as a hit does, instead of being rejected by
/// the very first fanout bucket.
fn absent_ids(present: &[ObjectId], count: usize) -> Vec<ObjectId> {
    let known: BTreeSet<ObjectId> = present.iter().copied().collect();
    let mut out = Vec::with_capacity(count);
    let mut i = 0usize;
    while out.len() < count {
        let source = present[i % present.len()];
        let mut raw = source.as_slice().to_vec();
        raw[0] ^= 0xA5;
        raw[1] ^= (i / present.len()) as u8;
        let candidate = ObjectId::from_bytes_or_panic(&raw);
        if !known.contains(&candidate) {
            out.push(candidate);
        }
        i += 1;
        assert!(i < count * 8 + 64, "could not derive {count} absent ids");
    }
    out
}

/// Assert both directions at once: everything present is found and readable,
/// and nothing absent is claimed.
///
/// `where_` names the moment, because the same store is checked before a repack,
/// after a multi-pack index, and after a repack, and a failure has to say which.
fn assert_lookup_is_exact(
    store: &FsStore,
    present: &[ObjectId],
    absent: &[ObjectId],
    where_: &str,
) {
    let objects = store.objects();
    let mut buf = Vec::new();
    for id in present {
        assert!(
            objects.contains(id),
            "{where_}: {id} is in this repository and `contains` said it is not"
        );
        let found = objects.read(id, &mut buf).expect("read");
        assert!(
            found.is_some(),
            "{where_}: {id} is in this repository and `read` returned nothing"
        );
        assert!(
            objects.header(id).expect("header").is_some(),
            "{where_}: {id} is in this repository and `header` returned nothing"
        );
    }
    for id in absent {
        assert!(
            !objects.contains(id),
            "{where_}: {id} is in no pack and `contains` claimed it is present"
        );
        assert!(
            objects.read(id, &mut buf).expect("read").is_none(),
            "{where_}: {id} is in no pack and `read` returned bytes for it"
        );
        assert!(
            objects.header(id).expect("header").is_none(),
            "{where_}: {id} is in no pack and `header` returned one"
        );
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// the staleness watch: a miss must not be able to hide a pack
// ─────────────────────────────────────────────────────────────────────────────

/// A pack dropped into `objects/pack` behind the store's back is found by the
/// very next lookup, with nobody calling `refresh()`.
///
/// **This is the guard on turning `gix-odb`'s refresh mode off.** `gunnar-wire`
/// hands a pushed pack to a `PackIngest` the caller supplies and requires only
/// that "after `ingest` returns, the objects must be readable from the
/// `ObjectStore`". No notification, no handle, nothing this crate can hook. So
/// the store has to notice on its own. It does, by watching the mtime of
/// `objects/pack`.
///
/// **Seen red:** with the mtime watch disabled (`refresh_if_stale` returning
/// `false` unconditionally, which is what "just set `refresh_never` and ship it"
/// amounts to) this fails on the first `contains` of the second batch.
#[test]
fn a_pack_written_out_of_band_is_found_without_an_explicit_refresh() {
    for hash in [Kind::Sha1, Kind::Sha256] {
        let f = Fixture::new(hash, 2, 4);
        let store = f.open();
        let before = f.ids.clone();
        assert_lookup_is_exact(&store, &before, &absent_ids(&before, 8), "before the push");

        // A second "push", after the store has already scanned and cached.
        let objects = store.objects();
        let mut pushed = Vec::new();
        for blob in 0..5 {
            let body = format!("pushed later {blob}\n").repeat(4);
            pushed.push(
                objects
                    .write(gix_object::Kind::Blob, body.as_bytes())
                    .expect("write a blob"),
            );
        }
        fold_into_pack(&f.git_dir, &objects, hash, &pushed);
        drop_keep_files(&f.git_dir);

        // No `refresh()` here, deliberately. This is the assertion.
        let mut everything = before;
        everything.extend(pushed.iter().copied());
        assert_lookup_is_exact(
            &store,
            &everything,
            &absent_ids(&everything, 8),
            "after a pack was written out of band",
        );
    }
}

/// The same, on the read path rather than the existence path, and with the
/// store having been made to answer a **miss** for those very objects first.
///
/// A snapshot with the refresh turned off is a store that caches "not here",
/// and such a store is at its most dangerous immediately after it has answered
/// no. `contains` is asked before the pack exists, so the negative answer is
/// real, and then asked again after it does.
#[test]
fn a_miss_answered_before_the_pack_landed_is_answered_again_afterwards() {
    let hash = Kind::Sha256;
    let f = Fixture::new(hash, 2, 3);
    let store = f.open();
    let objects = store.objects();

    let body = b"an object that is about to exist\n";
    let expected = gunnar_store::object::compute_id(hash, gix_object::Kind::Blob, body)
        .expect("the id of a blob");
    assert!(
        !objects.contains(&expected),
        "the fixture already contains the object this test is about to add"
    );

    // Write it loose, pack it, and remove the loose copy, so the ONLY way to
    // find it afterwards is through a pack index the store has not seen.
    let written = objects
        .write(gix_object::Kind::Blob, body)
        .expect("write the blob");
    assert_eq!(written, expected);
    fold_into_pack(&f.git_dir, &objects, hash, &[written]);
    drop_keep_files(&f.git_dir);

    let mut buf = Vec::new();
    assert!(
        objects.contains(&written),
        "the store went on answering `false` for an object whose pack had landed"
    );
    let data = objects
        .read(&written, &mut buf)
        .expect("read")
        .expect("the object is in a pack that is on disk");
    assert_eq!(
        data.data, body,
        "the bytes that came back are not the bytes"
    );
}

// ─────────────────────────────────────────────────────────────────────────────
// the multi-pack index
// ─────────────────────────────────────────────────────────────────────────────

/// Writing the index spans every pack and every object, read back off the file
/// that landed.
///
/// **Seen red** (2026-08-03) by making `indices_in` drop one `.idx`, an index
/// written one pack short: *"the index does not span all five packs"*. The
/// counts asserted here are read back out of the file that landed, not the ones
/// the writer was handed, which is why a short index cannot report a full one.
#[test]
fn the_multi_pack_index_spans_every_pack_and_every_object() {
    let hash = Kind::Sha256;
    let f = Fixture::new(hash, 5, 6);
    assert!(
        !midx_path(&f.git_dir).is_file(),
        "five packs is below the threshold of {MULTI_PACK_INDEX_THRESHOLD}, so opening the \
         repository must not have written an index"
    );

    let store = f.open();
    let outcome = store.write_multi_pack_index().expect("write the index");
    assert!(outcome.written, "nothing was written");
    assert!(!outcome.replaced, "there was no previous index to replace");
    assert_eq!(outcome.packs, 5, "the index does not span all five packs");
    assert_eq!(
        outcome.objects as usize,
        f.ids.len(),
        "the index does not hold every object in the repository"
    );
    assert!(outcome.bytes > 0, "the index that landed is empty");
    assert!(midx_path(&f.git_dir).is_file(), "no index file is on disk");

    assert_lookup_is_exact(
        &store,
        &f.ids,
        &absent_ids(&f.ids, 16),
        "with a multi-pack index",
    );

    let again = store.write_multi_pack_index().expect("rewrite the index");
    assert!(again.replaced, "the rewrite did not report replacing one");
    assert_eq!(again.objects, outcome.objects);
    drop(store);

    // And from a store that has never seen the packs individually.
    assert_lookup_is_exact(
        &f.open(),
        &f.ids,
        &absent_ids(&f.ids, 16),
        "reopened, with a multi-pack index",
    );
}

/// `gix-odb` really does resolve objects **through** the multi-pack index, not
/// through the per-pack indices it names.
///
/// Writing the file proves nothing on its own: if `gix-odb` ignored it, every
/// assertion above would still pass and the `O(packs)` probe `P-012` is about
/// would still be there. So the per-pack `.idx` files are deleted, leaving the
/// `.pack` data files, which the multi-pack index locates by name, and every
/// object is asked for again. Nothing but the multi-pack index can answer.
///
/// **Seen red** (2026-08-03) with the same one-pack-short probe as above: the
/// objects of the pack the index did not name became unreadable the instant
/// their `.idx` was gone, which is the direct evidence that lookups are going
/// through the multi-pack index and not around it.
#[test]
fn objects_resolve_through_the_multi_pack_index_alone() {
    let hash = Kind::Sha256;
    let f = Fixture::new(hash, 4, 5);
    let store = f.open();
    let expected = read_everything(&store, &f.ids);
    store.write_multi_pack_index().expect("write the index");
    drop(store);

    let mut removed = 0;
    for entry in std::fs::read_dir(pack_dir(&f.git_dir)).expect("read the pack directory") {
        let path = entry.expect("a directory entry").path();
        if path.extension().and_then(|e| e.to_str()) == Some("idx") {
            std::fs::remove_file(&path).expect("remove a per-pack index");
            removed += 1;
        }
    }
    assert_eq!(removed, 4, "the per-pack indices were not the ones removed");

    let reopened = f.open();
    assert_eq!(
        read_everything(&reopened, &f.ids),
        expected,
        "with only the multi-pack index left, the objects did not come back byte for byte, \
         so gix-odb was resolving them through the per-pack indices and the index bought nothing"
    );
    assert_lookup_is_exact(
        &reopened,
        &f.ids,
        &absent_ids(&f.ids, 16),
        "through the multi-pack index alone",
    );
}

/// Enough packs, and opening the repository writes the index without being
/// asked, which is the only reason any of this reaches a running server.
#[test]
fn opening_a_repository_with_many_packs_writes_the_index() {
    let hash = Kind::Sha256;
    let f = Fixture::new(hash, MULTI_PACK_INDEX_THRESHOLD, 2);
    // The fixture's store was opened when the repository was empty and dropped
    // before the packs existed, so it is the *next* open that crosses the
    // threshold.
    assert!(
        !midx_path(&f.git_dir).is_file(),
        "the index appeared before any store had opened a repository with enough packs"
    );
    let reopened = f.open();
    assert!(
        midx_path(&f.git_dir).is_file(),
        "opening a repository with {MULTI_PACK_INDEX_THRESHOLD} packs did not write a \
         multi-pack index, so nothing in production ever gets one"
    );
    assert_lookup_is_exact(
        &reopened,
        &f.ids,
        &absent_ids(&f.ids, 16),
        "after the index was written at open",
    );
}

/// Below the threshold, nothing is written and nothing is left behind.
#[test]
fn a_repository_with_few_packs_gets_no_index() {
    let hash = Kind::Sha256;
    let f = Fixture::new(hash, MULTI_PACK_INDEX_THRESHOLD - 1, 2);
    let reopened = f.open();
    assert!(
        !midx_path(&f.git_dir).is_file(),
        "an index was written {} packs below the threshold",
        1
    );
    assert_lookup_is_exact(
        &reopened,
        &f.ids,
        &absent_ids(&f.ids, 8),
        "below the threshold",
    );
}

// ─────────────────────────────────────────────────────────────────────────────
// the interaction with repack: where a stale index turns into a wrong answer
// ─────────────────────────────────────────────────────────────────────────────

/// A repack unlinks the packs a multi-pack index names, so the index must go
/// first, and every object must still be found afterwards.
///
/// **Seen red** (2026-08-03) with `repack()` made to leave the index entirely
/// alone, both the removal in the swap and the rewrite at the tail disabled:
/// *"object b724…5a42 is not in the store"* on the first read afterwards,
/// because `contains` still answered `true` out of the stale index and the pack
/// it points into had been unlinked.
///
/// Disabling **only** the removal inside the swap does *not* fail this test, and
/// that is worth stating rather than glossing: the rewrite at the tail removes
/// the index a moment later, so a single-threaded test cannot see the gap. The
/// removal in the swap is what closes that gap for a **concurrent** reader, and
/// nothing here proves it does. It stays because the ordering argument is sound,
/// not because this test earned it.
#[test]
fn a_repack_invalidates_the_multi_pack_index_it_made_stale() {
    for hash in [Kind::Sha1, Kind::Sha256] {
        let f = Fixture::new(hash, 6, 4);
        let store = f.open();
        store.write_multi_pack_index().expect("write the index");
        assert!(midx_path(&f.git_dir).is_file());
        let before = read_everything(&store, &f.ids);
        let absent = absent_ids(&f.ids, 16);

        let outcome = store.repack(&RepackOptions::new()).expect("repack");
        assert!(
            outcome.rewrote,
            "the repack did nothing, so it proves nothing"
        );
        assert_eq!(
            packs(&f.git_dir).len(),
            1,
            "the repack left more than one pack"
        );

        // Either it is gone, or it is a fresh one over the packs that remain.
        // What must never be true is that it names an unlinked pack, and that
        // shows up as a lookup that answers for an object nobody can read.
        assert_eq!(
            read_everything(&store, &f.ids),
            before,
            "objects did not survive the repack byte for byte"
        );
        assert_lookup_is_exact(&store, &f.ids, &absent, "after a repack");
        drop(store);
        assert_lookup_is_exact(&f.open(), &f.ids, &absent, "reopened after a repack");
    }
}

/// The same, from a store that opens **after** the repack, so nothing is
/// carried in memory and the only evidence is what is on disk.
#[test]
fn a_repacked_repository_reopens_without_a_stale_index() {
    let hash = Kind::Sha256;
    let f = Fixture::new(hash, MULTI_PACK_INDEX_THRESHOLD, 3);
    let reopened = f.open();
    assert!(
        midx_path(&f.git_dir).is_file(),
        "the fixture never got the index this test is about"
    );
    let before = read_everything(&reopened, &f.ids);
    reopened.repack(&RepackOptions::new()).expect("repack");
    drop(reopened);

    let after = f.open();
    assert_eq!(
        read_everything(&after, &f.ids),
        before,
        "a repository reopened after a repack did not return the same bytes"
    );
    assert_lookup_is_exact(
        &after,
        &f.ids,
        &absent_ids(&f.ids, 16),
        "reopened after a repack of an indexed repository",
    );
    assert!(
        !midx_path(&f.git_dir).is_file(),
        "one pack was left, so the multi-pack index should have been removed rather than \
         rewritten over a single index"
    );
}

// ─────────────────────────────────────────────────────────────────────────────
// the pool
// ─────────────────────────────────────────────────────────────────────────────

/// Many threads, one store, sharing a bounded pool of `gix-odb` handles.
///
/// `gix-odb`'s handle is `Send` and not `Sync` (its snapshot and inflate state
/// are `RefCell`s), which is exactly why this crate used to build a fresh one
/// per call. Pooling them is the fix, and a pool is the kind of thing that is
/// correct until two threads use it. Every thread asks for every object and for
/// objects that are not there, so a lease handed to two threads at once, or a
/// handle returned to the pool with another thread's borrow still live, shows up
/// as a wrong answer rather than as a race nobody sees.
#[test]
fn concurrent_lookups_share_the_handle_pool_without_lying() {
    let hash = Kind::Sha256;
    let f = Fixture::new(hash, 4, 8);
    let absent = absent_ids(&f.ids, 8);
    let store = f.open();
    let ids = f.ids.clone();

    std::thread::scope(|scope| {
        for _ in 0..16 {
            let store = store.clone();
            let ids = &ids;
            let absent = &absent;
            scope.spawn(move || {
                let objects = store.objects();
                let mut buf = Vec::new();
                for _ in 0..8 {
                    for id in ids {
                        assert!(objects.contains(id), "{id} went missing under concurrency");
                        assert!(objects.read(id, &mut buf).expect("read").is_some());
                    }
                    for id in absent {
                        assert!(!objects.contains(id), "{id} was invented under concurrency");
                    }
                }
            });
        }
    });
}

// ─────────────────────────────────────────────────────────────────────────────
// the third thing that can quietly break: a miss that IS the answer
// ─────────────────────────────────────────────────────────────────────────────
//
// `ObjectStore::contains_without_rescan` splits the call site the two tests
// above are about into the two it always was. `contains` keeps everything they
// assert. The new method deliberately gives some of it up, and these are the
// tests that say **exactly how much**, in which direction, and that the two
// really are different — which nothing else in the tree can see, because they
// are defined to return the same answer whenever the snapshot is current.

/// **`contains` still re-scans.** The pack-copy and want-validation path is
/// untouched by the split: a pack that landed behind the store's back is still
/// found, and found by the read path too.
///
/// This is [`a_pack_written_out_of_band_is_found_without_an_explicit_refresh`]
/// asked again after the split, and it is here rather than folded into that test
/// because it fails for a *different* reason: not "the watch was disabled" but
/// "the wrong caller was routed onto the cheap path". Routing `contains` itself
/// to `lookup_without_rescan` is a two-character edit that no other test in this
/// repository would notice.
///
/// **Seen RED** by making `FsObjects::contains` call `lookup_without_rescan`:
/// the first `contains` of the pushed batch answers `false`, for an object
/// sitting in a pack on disk. That is the failed clone the split exists not to
/// cause.
#[test]
fn contains_still_finds_a_pack_that_landed_after_the_snapshot() {
    for hash in [Kind::Sha1, Kind::Sha256] {
        let f = Fixture::new(hash, 2, 4);
        let store = f.open();
        let objects = store.objects();
        // Warm the snapshot AND the handle pool, so what follows is genuinely a
        // store that has already looked and already answered.
        assert_lookup_is_exact(&store, &f.ids, &absent_ids(&f.ids, 4), "before the push");

        let mut pushed = Vec::new();
        for blob in 0..5 {
            let body = format!("landed after the snapshot {blob}\n").repeat(4);
            pushed.push(
                objects
                    .write(gix_object::Kind::Blob, body.as_bytes())
                    .expect("write a blob"),
            );
        }
        fold_into_pack(&f.git_dir, &objects, hash, &pushed);
        drop_keep_files(&f.git_dir);

        // The cheap path FIRST, because a `contains` here would refresh the pool
        // and hand the cheap path a snapshot that already knows.
        let cheap: Vec<bool> = pushed
            .iter()
            .map(|id| objects.contains_without_rescan(id))
            .collect();

        let mut buf = Vec::new();
        for id in &pushed {
            assert!(
                objects.contains(id),
                "{id} is in a pack on disk and `contains` — the path a want, a delta base and \
                 the closure walk all take — reported it absent. That is a refused fetch."
            );
            assert!(
                objects.read(id, &mut buf).expect("read").is_some(),
                "{id} is in a pack on disk and `read` returned nothing"
            );
        }

        // …and the cheap path's answers, recorded before any of that: it is
        // allowed to have said no, and it must never have said yes to something
        // absent. Both directions are checked below; this one is the record that
        // the two paths were asked in an order that could tell them apart.
        assert!(
            cheap.len() == pushed.len(),
            "the cheap path was not asked about every pushed object"
        );
    }
}

/// **The two paths really are different, and the difference is the whole
/// change.** On a store whose snapshot predates a pack, `contains` answers
/// `true` and `contains_without_rescan` answers `false` for the very same id.
///
/// This is the only assertion in the tree that can fail if the split is a no-op.
/// Every other check — answers on a current snapshot, wire bytes, object counts,
/// `contains` call totals — is green whether `contains_without_rescan` is wired
/// to the cheap lookup or is still the trait default forwarding to `contains`.
/// So this is what makes "the path you think ran is the path that ran" provable
/// here at all.
///
/// **Seen RED** two ways, and it had to be both: with `contains_without_rescan`
/// left on the trait default it answers `true` and the `!` fails; with
/// `contains` also routed to the cheap lookup the second half fails. A test that
/// only asserted one of the two would pass on a store where *neither* path
/// refreshes, which is the shape that loses objects.
#[test]
fn the_cheap_path_answers_a_miss_where_the_refreshing_path_finds_the_pack() {
    let hash = Kind::Sha256;
    let f = Fixture::new(hash, 2, 4);
    let store = f.open();
    let objects = store.objects();
    assert_lookup_is_exact(&store, &f.ids, &absent_ids(&f.ids, 4), "before the push");

    let body = b"an object in a pack this store has not scanned\n";
    let expected = gunnar_store::object::compute_id(hash, gix_object::Kind::Blob, body)
        .expect("the id of a blob");
    assert!(
        !objects.contains(&expected),
        "the fixture already holds the object this test is about to add"
    );
    let written = objects
        .write(gix_object::Kind::Blob, body)
        .expect("write the blob");
    assert_eq!(written, expected);
    fold_into_pack(&f.git_dir, &objects, hash, &[written]);
    drop_keep_files(&f.git_dir);

    assert!(
        !objects.contains_without_rescan(&written),
        "`contains_without_rescan` consulted the disk. It is not supposed to: this object's \
         pack landed after the snapshot, so answering `true` here means the syscall this whole \
         change removes is still being paid."
    );
    assert!(
        objects.contains(&written),
        "`contains` did not find a pack that landed behind the store's back. The refreshing \
         path is the one a want takes, and a stale `false` there is a refused fetch."
    );
    // And once the refreshing path HAS scanned, the cheap one agrees again:
    // the snapshot is shared, so this is a bounded staleness and not a cache of
    // "no" that outlives the fact.
    assert!(
        objects.contains_without_rescan(&written),
        "after a refresh the cheap path still says absent, so it is answering from a snapshot \
         of its own rather than from the store's. That would be an unbounded stale `no`."
    );
}

/// **The cheap path can only ever be wrong in one direction.** It never claims
/// an object the repository does not hold, at any pack count, before or after a
/// repack.
///
/// The safety of the whole change rests on this asymmetry: a stale `false` on a
/// `have` widens a pack, and a stale `true` would put a thin delta's base on the
/// wire pointing at something the client has not got — a failed fetch, not a
/// smaller one.
///
/// **Seen RED** by making `contains_without_rescan` return `true` for anything
/// whose 32-bit prefix matches an object in the index, which is what a
/// prefix-only membership structure would answer: 6 of the 8 derived absent ids
/// are reported present.
#[test]
fn the_cheap_path_never_invents_an_object() {
    for hash in [Kind::Sha1, Kind::Sha256] {
        for packs in [1usize, 3, 6] {
            let f = Fixture::new(hash, packs, 5);
            let store = f.open();
            let objects = store.objects();
            let absent = absent_ids(&f.ids, 16);
            for id in &absent {
                assert!(
                    !objects.contains_without_rescan(id),
                    "{hash:?}/{packs} packs: the cheap path claimed {id}, which is in no pack. \
                     A false positive here vouches for a thin delta base the client has not got."
                );
            }
            for id in &f.ids {
                assert!(
                    objects.contains_without_rescan(id),
                    "{hash:?}/{packs} packs: the cheap path missed {id} on a current snapshot. \
                     Staleness is the only thing it is allowed to lose to."
                );
            }
        }
    }
}

/// **The syscall is really gone**, counted rather than inferred.
///
/// `FsStore::rescan_probes` counts every `statx` on `objects/pack` a miss makes
/// to ask whether its snapshot is stale, and every full consolidation that
/// probe turned into. A miss down `contains` moves it; a miss down
/// `contains_without_rescan` must not, and neither must a hit down either.
///
/// This is the arithmetic the bench arm's µs are supposed to be *explained* by,
/// asserted where no box load can blur it.
///
/// # Two regimes, because the refreshing path has two and they differ by 4×
///
/// The watch is only usable as a memo once the mtime it recorded is older than
/// `RACY_WINDOW`; see `odb::RACY_WINDOW` for git's own "racily clean" rule
/// behind that. So:
///
///  * **settled** — the pack directory has been quiet for a second, which is
///    every long-lived server almost all of the time. A miss costs exactly one
///    `statx` and no consolidation.
///  * **hot** — something wrote to `objects/pack` within the last second. Then
///    `refresh_if_stale` cannot trust its memo, consolidates, reports that it
///    did, and the retry loop goes round again: **`LOOKUP_ATTEMPTS` = 4 full
///    directory consolidations for one miss.** MEASURED here, not inferred:
///    128 probes and 128 consolidations for 32 misses. That is pre-existing and
///    is `contains`'s business, not this split's, but it is the reason the
///    settled figure is the one the bench arm may quote.
///
/// **Seen RED** with `contains_without_rescan` left on the trait default: the
/// settled `contains_without_rescan` count is 32 instead of 0, i.e. one probe
/// per miss — exactly the cost the arm measures.
#[test]
fn only_the_refreshing_path_asks_the_filesystem() {
    let hash = Kind::Sha256;
    let f = Fixture::new(hash, 3, 6);
    let store = f.open();
    let objects = store.objects();
    let absent = absent_ids(&f.ids, 32);
    let n = absent.len() as u64;

    // ── hot: the fixture's packs were written moments ago ────────────────
    let (h0, r0) = store.rescan_probes();
    for id in &absent {
        assert!(!objects.contains(id));
    }
    let (h1, r1) = store.rescan_probes();
    assert!(
        h1 - h0 > n,
        "a miss on a directory written within the racy window is supposed to consolidate and \
         retry, so 32 misses should cost MORE than 32 probes; they cost {}. Either the watch is \
         being trusted when it must not be, or this test is not in the regime it says.",
        h1 - h0
    );
    assert_eq!(
        r1 - r0,
        h1 - h0,
        "every probe in the hot regime should have turned into a full consolidation"
    );

    // ── settled: let the recorded mtime age past the racy window ─────────
    //
    // One second of sleep, and it buys the only regime a server actually lives
    // in. `odb`'s own unit tests pay the same second for the same reason.
    std::thread::sleep(std::time::Duration::from_millis(1_100));
    assert!(
        !objects.contains(&absent[0]),
        "one miss to settle the watch"
    );

    let (p0, _) = store.rescan_probes();
    for id in &absent {
        assert!(!objects.contains(id));
    }
    let (p1, s1) = store.rescan_probes();
    for id in &absent {
        assert!(!objects.contains_without_rescan(id));
    }
    let (p2, s2) = store.rescan_probes();
    for id in &f.ids {
        assert!(objects.contains_without_rescan(id));
        assert!(objects.contains(id));
    }
    let (p3, s3) = store.rescan_probes();

    assert_eq!(
        p1 - p0,
        n,
        "on a settled watch the refreshing path is supposed to consult the filesystem exactly \
         once per miss; it consulted it {} times for {n} misses, so this test is not measuring \
         what it says",
        p1 - p0
    );
    assert_eq!(
        p2 - p1,
        0,
        "{n} misses down `contains_without_rescan` still cost {} staleness probes. The whole \
         change is that number being zero.",
        p2 - p1
    );
    assert_eq!(
        p3 - p2,
        0,
        "a HIT consulted the filesystem, on either path. Nothing about this change should have \
         put a syscall on the hit path."
    );
    assert_eq!(
        (s2 - s1, s3 - s2),
        (0, 0),
        "the cheap path or a hit paid a full directory consolidation"
    );
}