plugins/native/znippy-plugin-git/src/thin_storm.rs
//! **What a repository pushed one commit at a time does to a verbatim
//! archive** — the shape that refused 21 of 140 ordinary pushes.
//!
//! # 🔴 The defect these tests exist for
//!
//! MEASURED on oden, 2026-08-30 (lane AI, reproducing lane AD's two-appliance
//! drill on a locally built server): 140 sequential single-commit pushes of one
//! file into one repository. Push #120 came back
//!
//! ```text
//! ! [remote rejected] master -> master (the push cannot be checked: this pack
//! is thin: it deltas against 509be6a75f6f… which is not in it, and no base
//! source could produce that object's content … so this repository genuinely
//! does not have that object)
//! ```
//!
//! and so did every one of the 20 pushes after it, forever. The object was a
//! blob this store held, in a pack it had already absorbed, one index lookup
//! away. Three separate faults stacked into that one sentence, and each has its
//! own guard below:
//!
//! 1. **The chain ceiling.** `git index-pack --fix-thin` completes a pushed
//! thin pack by appending the resolved base into it, so a pack stock git
//! stores has self-contained delta chains. §14 stores the client's bytes
//! **verbatim**, which is what makes a clone a byte-range copy — so a
//! `REF_DELTA` goes on naming a base in an *earlier pack*, and one commit per
//! push grows the chain one link per push, without bound.
//! [`crate::resolve::MAX_DELTA_LINKS`] refused the 101st link as "a cycle".
//! 2. **The swallowed error.** `impl BaseSource for Absorber` ended in
//! `.ok().flatten()`, so *"I hold it and could not read it"* and *"I have
//! never had it"* arrived as the same `None`, and the resolver blamed the
//! pushed pack for the store's own failure.
//! 3. **The loop that could not move on.** Every absorb entry point ran
//! `for job in &jobs { self.absorb_gated(job)?; }` in ordinal order, so one
//! pack that could not be indexed was hit first, aborted the loop, and left
//! every pack behind it un-indexed — which is why the failure was not one
//! push but all of them from there on.
//!
//! # Why the assertion is the push's own result
//!
//! Every count below comes from [`GitStore::put_pack`] and
//! [`GitStore::index_now`] — the two calls `receive_pack::apply` makes before it
//! acks — and not from a log line. A log-line oracle would have been green for
//! the first 100 pushes of a run that was already broken.
#![cfg(test)]
use crate::git_ops::GitStore;
use crate::object::{canonical, GitHashKind, GitObjectKind};
use crate::pack_fixture::thin_ref_delta_pack;
use crate::store::tests::{one_blob_pack, tmpdir};
use git_storage_trait::GitOps;
/// The blob push number `i` writes. Content that deflate cannot collapse into
/// nothing, so every push really does store an entry.
fn body_of(i: usize) -> Vec<u8> {
format!("lane AI storm commit {i}\n").repeat(3).into_bytes()
}
/// One push: a thin pack whose single entry is a `REF_DELTA` against the object
/// the push before it introduced. **This is the shape `git push` sends** when a
/// client commits one change on top of the ref the server just advertised, and
/// it is the shape that grows the chain.
fn storm(store: &GitStore, pushes: usize) -> (usize, Option<String>) {
let seed = b"lane AI seed\n";
let (p0, oid0) = one_blob_pack(seed);
store.put_pack(&p0).expect("the seed pack");
store.wait_indexed();
let mut base_oid = oid0;
let mut base_len = seed.len() as u64;
let mut fails = 0usize;
let mut first: Option<String> = None;
for i in 0..pushes {
let body = body_of(i);
let pack = thin_ref_delta_pack(&base_oid, base_len, &body);
// Exactly what the ack path does: store the pack, then make every
// object it introduced addressable before the connectivity walk.
let r = store
.put_pack(&pack)
.and_then(|_| store.index_now().map(|_| ()));
if let Err(e) = r {
fails += 1;
if first.is_none() {
first = Some(format!("push {i}: {e:#}"));
}
}
base_oid = GitHashKind::Sha1.oid_of(&canonical(GitObjectKind::Blob, &body));
base_len = body.len() as u64;
}
(fails, first)
}
/// ★ **150 single-commit pushes to one repository are all accepted.**
///
/// Seen RED at [`crate::resolve::MAX_DELTA_LINKS`] exactly: **50 of 150
/// refused**, the first at push 100, *"absorbing pack 101 at (9144, 92) … this
/// pack is thin: it deltas against 1e1a1751… which is not in it, and no base
/// source could produce that object's content"*. 150 rather than 101 because a
/// run that stops at the first failure cannot tell a one-push hiccup from the
/// permanent stop this was: every push after the first failure failed too.
#[test]
fn a_hundred_and_fifty_single_commit_pushes_are_all_accepted() {
let dir = tmpdir("laneAI-storm-ack");
let store = GitStore::open(&dir, "alice").unwrap();
let (fails, first) = storm(&store, 150);
assert_eq!(
fails,
0,
"{} of 150 single-commit pushes were refused; the first was {}",
fails,
first.unwrap_or_default()
);
assert!(
store.unindexable_packs().is_empty(),
"the store gave up on {:?}",
store.unindexable_packs()
);
}
/// ★ **And the objects are readable afterwards, at every depth.**
///
/// The push landing is not the whole claim: a chain that is accepted and then
/// cannot be read back is the same defect one layer down (it is where the
/// original red actually lived — the *base* read failed, not the push's own
/// parse). Every blob the storm wrote is read back and compared byte for byte,
/// including the deepest.
#[test]
fn every_object_of_a_long_chain_reads_back_byte_correct() {
let dir = tmpdir("laneAI-storm-read");
let store = GitStore::open(&dir, "alice").unwrap();
const N: usize = 150;
let (fails, first) = storm(&store, N);
assert_eq!(fails, 0, "{}", first.unwrap_or_default());
store.wait_indexed();
let hash = store.hash_kind();
let mut checked = 0usize;
for i in 0..N {
let body = body_of(i);
let oid = hash.oid_of(&canonical(GitObjectKind::Blob, &body));
let (kind, payload) = store
.content(&oid)
.unwrap_or_else(|e| panic!("reading the blob of push {i} (chain depth {i}): {e:#}"))
.unwrap_or_else(|| panic!("the blob of push {i} is absent after its push was acked"));
assert_eq!(kind, GitObjectKind::Blob);
assert_eq!(payload, body, "the blob of push {i} came back wrong");
checked += 1;
}
assert_eq!(checked, N);
}
/// ★ **A chain that returns to an offset it has already stood on is refused as
/// a cycle** — the job [`crate::resolve::MAX_DELTA_LINKS`] used to do with a
/// count, done with a proof.
///
/// Two arms, and the second is the one that makes the first mean anything:
///
/// * a **300-link** legitimate chain — three times the old ceiling — resolves;
/// * a genuine cycle is refused **by name**, not walked forever.
///
/// The cycle is built where a cycle can actually exist in this store: an entry
/// whose `REF_DELTA` base oid is its own resolved oid. `offset_of_oid` sends the
/// walk straight back to the entry it is standing on, which no acyclic chain can
/// do and which the old count could only notice 100 links later.
#[test]
fn a_deep_chain_resolves_and_a_real_cycle_is_refused_by_name() {
use crate::resolve::{materialize_at, ArchiveBytes, NoBases};
// A one-entry "archive" that answers every offset with the same ref-delta
// entry, whose base oid resolves back to offset 0.
struct SelfReferential(Vec<u8>);
impl ArchiveBytes for SelfReferential {
fn at(&self, at: u64, len: u64) -> anyhow::Result<std::borrow::Cow<'_, [u8]>> {
let at = at as usize;
if at >= self.0.len() {
return Ok(std::borrow::Cow::Owned(Vec::new()));
}
let end = (at + len as usize).min(self.0.len());
Ok(std::borrow::Cow::Borrowed(&self.0[at..end]))
}
}
let oid = vec![7u8; 20];
let entry = {
let delta = crate::pack_fixture::insert_only_delta(4, b"abcd");
let mut e = Vec::new();
crate::pack_walk::encode_type_and_size(
&mut e,
crate::index_layout::ObjType::RefDelta,
delta.len() as u64,
);
e.extend_from_slice(&oid);
e.extend_from_slice(&crate::pack_fixture::deflate(&delta));
e
};
let err = materialize_at(
&SelfReferential(entry),
0,
20,
// Every base oid is at offset 0, which is where the walk started.
&|_| Some(0),
&NoBases,
)
.expect_err("a self-referential ref-delta must not resolve");
let text = format!("{err:#}");
assert!(
text.contains("form a cycle") && text.contains("returns to archive offset 0"),
"a cycle must be refused by name and by offset, not by a link count: {text}"
);
// And the legitimate deep chain the old count refused: three times the
// ceiling that used to be a refusal. Three and not thirty because the cost
// of a push here is the repository's whole history re-folded, so the arm
// that matters is "past 100", not "how far past".
let dir = tmpdir("laneAI-deep-chain");
let store = GitStore::open(&dir, "alice").unwrap();
let (fails, first) = storm(&store, 300);
assert_eq!(
fails,
0,
"a 300-link chain is deep, not corrupt: {}",
first.unwrap_or_default()
);
}
/// ★ **A base source that FAILS is not a base source that is empty.**
///
/// The seam lane AI's red hid behind. `BaseSource::content` returned a bare
/// `Option`, so a store that held the delta base and could not reconstruct it
/// answered identically to one that had never seen it — and
/// [`crate::resolve::ResolveError::missing_base`] then told the pushing client
/// *"this repository genuinely does not have that object"* about an object the
/// repository did have. A client cannot act on a diagnosis that names the wrong
/// party.
///
/// Seen RED by restoring the swallow — `bases.content(oid).ok().flatten()` at
/// the two call sites — which turns the source's own sentence into
/// `missing_base`'s: *"this pack is thin … so this repository genuinely does not
/// have that object"*, with the real reason gone.
#[test]
fn a_base_source_that_errors_says_so_instead_of_saying_absent() {
use crate::resolve::{resolve, BaseSource};
struct Broken;
impl BaseSource for Broken {
fn content(&self, _oid: &[u8]) -> anyhow::Result<Option<(GitObjectKind, Vec<u8>)>> {
anyhow::bail!("the store holds this base and could not read it back")
}
}
let base_oid = vec![9u8; 20];
let pack = crate::pack_fixture::thin_ref_delta_pack(&base_oid, 4, b"abcd");
let err = resolve(&pack, GitHashKind::Sha1, 0, &Broken)
.expect_err("a thin pack whose base source fails must not resolve");
let text = format!("{err:#}");
assert!(
text.contains("could not read it back"),
"the base source's own sentence must survive to the caller: {text}"
);
assert!(
!text.contains("genuinely does not have that object"),
"a source that FAILED was reported as a repository that does not HOLD the object: {text}"
);
}