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

nordisk/gunnar

crates/gunnar-wire/tests/protocol.rs

//! Byte-level tests of both state machines, driven over `Vec<u8>` with no
//! transport at all.
//!
//! These do not replace `real_git_interop.rs` (only stock `git` can prove
//! interoperability), but they pin the things that are hard to *observe* from
//! outside: exact object counts, which parent of a merge got walked, and what a
//! filter actually removed. A clone that succeeds tells you none of that.
//!
//! Packs are checked by handing them to **gix's own indexer**, never by gunnar
//! re-reading its own bytes.

mod common;

use gix_hash::{Kind, ObjectId};
use gunnar_pktline::Packet;
use gunnar_store::git_storage_trait::{GitOps, GitServe, ObjType};
use gunnar_store::mem::MemGitServe;
use gunnar_wire::receive_pack::{AllowAll, PushGate, RefCommand};
use gunnar_wire::upload_pack::{Config, FetchRequest, LsRefsRequest, WantPolicy};
use gunnar_wire::{ProtocolVersion, ReceivePack, RefStatus, UploadPack};

use common::{
    empty_pack, index_and_count, name, push_bytes, v2_request, validating_ingest, Fixture, Response,
};

/// The fixture's five commits carry one blob and one tree each.
const OBJECTS_PER_COMMIT: u32 = 3;

fn fetch(fixture: &Fixture, request: &FetchRequest) -> (Response, u32) {
    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let mut out = Vec::new();
    let outcome = upload.fetch(request, &mut out).expect("fetch");
    (Response::parse(&out), outcome.objects_sent)
}

fn clone_request(wants: &[ObjectId]) -> FetchRequest {
    FetchRequest {
        wants: wants.to_vec(),
        done: true,
        thin_pack: true,
        ofs_delta: true,
        ..Default::default()
    }
}

// ── ls-refs ─────────────────────────────────────────────────────────────────

#[test]
fn ls_refs_reports_head_first_with_its_symref_and_the_peeled_tag() {
    let fixture = Fixture::build(Kind::Sha1);
    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let mut out = Vec::new();
    upload
        .ls_refs(
            &LsRefsRequest {
                symrefs: true,
                peel: true,
                unborn: true,
                ref_prefixes: Vec::new(),
            },
            &mut out,
        )
        .expect("ls-refs");
    let response = Response::parse(&out);
    let lines = response.lines();

    assert_eq!(
        lines[0],
        format!("{} HEAD symref-target:refs/heads/main", fixture.four),
        "HEAD must come first, with its symref target: {lines:?}"
    );
    assert!(
        lines.contains(&format!("{} refs/heads/main", fixture.four)),
        "{lines:?}"
    );
    assert!(
        lines.contains(&format!("{} refs/heads/side", fixture.three)),
        "{lines:?}"
    );
    assert!(
        lines.contains(&format!(
            "{} refs/tags/v1 peeled:{}",
            fixture.tag, fixture.merge
        )),
        "the annotated tag was not peeled: {lines:?}"
    );
    assert_eq!(response.packets.last(), Some(&Packet::Flush));
}

#[test]
fn ls_refs_without_peel_or_symrefs_reports_neither() {
    let fixture = Fixture::build(Kind::Sha1);
    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let mut out = Vec::new();
    upload
        .ls_refs(&LsRefsRequest::default(), &mut out)
        .expect("ls-refs");
    let lines = Response::parse(&out).lines();
    assert!(
        lines.iter().all(|line| !line.contains("symref-target:")),
        "symrefs were not asked for: {lines:?}"
    );
    assert!(
        lines.iter().all(|line| !line.contains("peeled:")),
        "peeling was not asked for: {lines:?}"
    );
}

#[test]
fn ref_prefix_narrows_the_answer() {
    let fixture = Fixture::build(Kind::Sha1);
    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let mut out = Vec::new();
    upload
        .ls_refs(
            &LsRefsRequest {
                ref_prefixes: vec!["refs/tags/".to_owned()],
                ..Default::default()
            },
            &mut out,
        )
        .expect("ls-refs");
    let lines = Response::parse(&out).lines();
    assert_eq!(lines.len(), 1, "a tags-only prefix returned {lines:?}");
    assert!(lines[0].ends_with("refs/tags/v1"));
}

// ── fetch ───────────────────────────────────────────────────────────────────

#[test]
fn a_clone_sized_fetch_packs_the_whole_history_on_band_one() {
    let fixture = Fixture::build(Kind::Sha1);
    let (response, sent) = fetch(&fixture, &clone_request(&[fixture.four]));

    assert!(
        response.has_line("packfile"),
        "no packfile section: {:?}",
        response.lines()
    );
    // `band(1)` is the only place the band byte is stripped, so a pack that
    // indexes out of it has proven the byte was there.
    let pack = response.band(1);
    assert!(
        !pack.is_empty(),
        "the packfile section carried no band-1 data"
    );
    assert_eq!(&pack[..4], b"PACK");

    let counted = index_and_count(&pack, Kind::Sha1);
    assert_eq!(
        counted,
        5 * OBJECTS_PER_COMMIT,
        "5 commits, 5 trees, 5 blobs"
    );
    assert_eq!(counted, sent, "the reported count disagrees with the pack");
    assert_eq!(response.packets.last(), Some(&Packet::Flush));
}

#[test]
fn wanting_the_tag_packs_the_tag_object_too() {
    let fixture = Fixture::build(Kind::Sha1);
    let (response, _) = fetch(&fixture, &clone_request(&[fixture.four, fixture.tag]));
    // The tag peels to `merge`, which is already reachable from `four`, so the
    // only new object is the tag itself.
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        5 * OBJECTS_PER_COMMIT + 1
    );
}

/// A merge has two parents. A traversal that follows only the first still
/// produces a clone that looks right at the tip, so the assertion has to be an
/// object count and not "it worked".
#[test]
fn both_parents_of_a_merge_are_walked() {
    let fixture = Fixture::build(Kind::Sha1);
    let request = FetchRequest {
        haves: vec![fixture.two],
        ..clone_request(&[fixture.merge])
    };
    let (response, _) = fetch(&fixture, &request);
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        2 * OBJECTS_PER_COMMIT,
        "with `two` excluded the pack is `merge` and `three`; half that means \
         the second parent was never followed"
    );
}

#[test]
fn a_fetch_whose_haves_cover_the_wants_sends_a_legal_empty_pack() {
    let fixture = Fixture::build(Kind::Sha1);
    let request = FetchRequest {
        haves: vec![fixture.four],
        ..clone_request(&[fixture.four])
    };
    let (response, sent) = fetch(&fixture, &request);
    assert_eq!(sent, 0);
    let pack = response.band(1);
    assert_eq!(
        index_and_count(&pack, Kind::Sha1),
        0,
        "an empty pack must still be a pack gix will index"
    );
}

#[test]
fn sha256_is_served_end_to_end_and_not_inferred_from_the_sha1_green() {
    let fixture = Fixture::build(Kind::Sha256);
    assert_eq!(fixture.four.to_hex().to_string().len(), 64);
    let (response, sent) = fetch(&fixture, &clone_request(&[fixture.four]));
    let pack = response.band(1);
    assert_eq!(index_and_count(&pack, Kind::Sha256), 5 * OBJECTS_PER_COMMIT);
    assert_eq!(sent, 5 * OBJECTS_PER_COMMIT);
}

#[test]
fn a_want_whose_hash_kind_is_wrong_is_refused() {
    let fixture = Fixture::build(Kind::Sha256);
    let sha1_want = format!("want {}", "a".repeat(40));
    let err = FetchRequest::parse(&[sha1_want.into_bytes()], Kind::Sha256, Default::default())
        .expect_err("a sha1 want on a sha256 repository must be refused");
    assert!(
        matches!(err, gunnar_wire::Error::ObjectFormatMismatch { .. }),
        "unexpected error: {err}"
    );
    let _ = fixture;
}

// ── filters ─────────────────────────────────────────────────────────────────

#[test]
fn blob_none_really_omits_the_blobs() {
    let fixture = Fixture::build(Kind::Sha1);
    let request = FetchRequest {
        filter: Some(gunnar_wire::Filter::BlobNone),
        ..clone_request(&[fixture.four])
    };
    let (response, _) = fetch(&fixture, &request);
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        10,
        "5 commits and 5 trees, no blobs"
    );
    // The unfiltered number is what makes that assertion mean something.
    let (plain, _) = fetch(&fixture, &clone_request(&[fixture.four]));
    assert_eq!(index_and_count(&plain.band(1), Kind::Sha1), 15);
}

#[test]
fn tree_zero_sends_commits_only() {
    let fixture = Fixture::build(Kind::Sha1);
    let request = FetchRequest {
        filter: Some(gunnar_wire::Filter::TreeDepth(0)),
        ..clone_request(&[fixture.four])
    };
    let (response, _) = fetch(&fixture, &request);
    assert_eq!(index_and_count(&response.band(1), Kind::Sha1), 5);
}

#[test]
fn blob_limit_keeps_the_small_ones() {
    let fixture = Fixture::build(Kind::Sha1);
    // Every fixture blob is under 16 bytes, so this filter keeps them all...
    let generous = FetchRequest {
        filter: Some(gunnar_wire::Filter::BlobLimit(1024)),
        ..clone_request(&[fixture.four])
    };
    let (response, _) = fetch(&fixture, &generous);
    assert_eq!(index_and_count(&response.band(1), Kind::Sha1), 15);
    // ...and this one keeps none, which is what shows the size is being read.
    let mean = FetchRequest {
        filter: Some(gunnar_wire::Filter::BlobLimit(1)),
        ..clone_request(&[fixture.four])
    };
    let (response, _) = fetch(&fixture, &mean);
    assert_eq!(index_and_count(&response.band(1), Kind::Sha1), 10);
}

#[test]
fn an_unimplemented_filter_is_refused_by_name() {
    let err = gunnar_wire::Filter::parse("sparse:oid=deadbeef")
        .expect_err("gunnar does not implement sparse filters");
    assert!(
        matches!(err, gunnar_wire::Error::UnsupportedFilter { .. }),
        "unexpected error: {err}"
    );
    assert_eq!(
        gunnar_wire::Filter::parse("blob:limit=2k").unwrap(),
        gunnar_wire::Filter::BlobLimit(2048)
    );
}

// ── shallow ─────────────────────────────────────────────────────────────────

#[test]
fn depth_one_reports_a_boundary_and_packs_one_commit() {
    let fixture = Fixture::build(Kind::Sha1);
    let mut request = clone_request(&[fixture.four]);
    request.shallow.depth = Some(1);
    let (response, _) = fetch(&fixture, &request);

    assert!(
        response.has_line("shallow-info"),
        "no shallow-info section: {:?}",
        response.lines()
    );
    assert!(
        response.has_line(&format!("shallow {}", fixture.four)),
        "the boundary was not reported: {:?}",
        response.lines()
    );
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        OBJECTS_PER_COMMIT
    );
}

#[test]
fn depth_two_reaches_both_parents_of_the_merge() {
    let fixture = Fixture::build(Kind::Sha1);
    let mut request = clone_request(&[fixture.four]);
    request.shallow.depth = Some(3);
    let (response, _) = fetch(&fixture, &request);
    // four, merge, then both of merge's parents at depth 3.
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        4 * OBJECTS_PER_COMMIT
    );
}

#[test]
fn a_root_commit_is_never_reported_as_shallow() {
    let fixture = Fixture::build(Kind::Sha1);
    let mut request = clone_request(&[fixture.four]);
    request.shallow.depth = Some(99);
    let (response, _) = fetch(&fixture, &request);
    assert!(
        !response.has_line(&format!("shallow {}", fixture.one)),
        "the root commit was reported as grafted, which makes every later \
         fetch ask for a parent that does not exist: {:?}",
        response.lines()
    );
}

/// `ref-in-want` is advertised, so it has to work. An advertised feature with
/// no implementation behind it is the same bug as an implementation with no
/// advertisement, seen from the other side.
#[test]
fn want_ref_resolves_the_name_and_echoes_a_wanted_refs_section() {
    let fixture = Fixture::build(Kind::Sha1);
    let request = FetchRequest {
        want_refs: vec!["refs/heads/side".to_owned()],
        done: true,
        ..Default::default()
    };
    let (response, _) = fetch(&fixture, &request);
    assert!(
        response.has_line("wanted-refs"),
        "no wanted-refs section: {:?}",
        response.lines()
    );
    assert!(
        response.has_line(&format!("{} refs/heads/side", fixture.three)),
        "the resolved name was not echoed: {:?}",
        response.lines()
    );
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        2 * OBJECTS_PER_COMMIT,
        "`side` is `one` then `three`"
    );
}

/// Deepening a client that is **already shallow** must send the commits past
/// its own cut, even though it offered its tip as a `have`.
///
/// This is `git fetch --deepen=1` against a `--depth=1` clone, and it is the
/// one request where "the client has `four`" and "the client has `four`'s
/// ancestors" come apart. Walking that `have` without honouring the `shallow
/// four` line the client sent in the same request marks `merge` as
/// already-present, so the server promises `unshallow four` and then ships an
/// empty pack. Real git calls that state `remote did not send all necessary
/// objects` (`real_git_interop::v2_deepen_relative`); from inside, it is
/// visible as an object count.
#[test]
fn deepening_an_already_shallow_client_sends_the_commits_past_its_cut() {
    let fixture = Fixture::build(Kind::Sha1);
    let mut request = clone_request(&[fixture.four]);
    // Exactly what git 2.53.0 puts on the wire for `fetch --deepen=1` from a
    // depth-1 clone, captured 2026-08-03: the tip as both a want and a have,
    // its own boundary declared, and a *relative* depth of one.
    request.haves = vec![fixture.four];
    request.shallow.client_shallow = vec![fixture.four];
    request.shallow.depth = Some(1);
    request.shallow.relative = true;
    let (response, sent) = fetch(&fixture, &request);

    assert!(
        response.has_line(&format!("unshallow {}", fixture.four)),
        "the client's old boundary was not lifted: {:?}",
        response.lines()
    );
    assert!(
        response.has_line(&format!("shallow {}", fixture.merge)),
        "the new boundary was not reported: {:?}",
        response.lines()
    );
    // `merge` and its tree and its blob, and nothing else: `four` is genuinely
    // at the client, and `merge`'s own parents are past the new boundary. The
    // count is the assertion, because a server that answers this request with
    // an empty pack still writes both lines above.
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        OBJECTS_PER_COMMIT,
        "the pack must carry `merge`; the client cannot reach it and was just \
         told its own tip is no longer grafted"
    );
    assert_eq!(sent, OBJECTS_PER_COMMIT);
}

#[test]
fn deepen_since_cuts_on_the_committer_date() {
    let fixture = Fixture::build(Kind::Sha1);
    let mut request = clone_request(&[fixture.four]);
    // `merge` is at this second; its parents are older.
    request.shallow.since = Some(1_700_000_004);
    let (response, _) = fetch(&fixture, &request);
    assert!(
        response.has_line(&format!("shallow {}", fixture.merge)),
        "the date cut was not reported: {:?}",
        response.lines()
    );
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        2 * OBJECTS_PER_COMMIT,
        "only `four` and `merge` are new enough"
    );
}

#[test]
fn deepen_not_cuts_at_a_named_reference() {
    let fixture = Fixture::build(Kind::Sha1);
    let mut request = clone_request(&[fixture.four]);
    request.shallow.not = vec!["refs/heads/side".to_owned()];
    let (response, _) = fetch(&fixture, &request);
    // `side` is `three`, whose ancestry is `one`, so the walk keeps `four`,
    // `merge` and `two` and stops there.
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        3 * OBJECTS_PER_COMMIT
    );
    assert!(
        response.has_line(&format!("shallow {}", fixture.two)),
        "{:?}",
        response.lines()
    );
}

// ── negotiation ─────────────────────────────────────────────────────────────

/// The classic negotiation failure: a `have`/`want` loop that terminates on
/// the happy path and never on a divergent one.
///
/// The client here has forty commits the server has never seen, and sends them
/// in batches of thirty-two the way git does. The assertions are that the loop
/// **ends**, and that it ends by finding the real cut point rather than by
/// running out of patience.
#[test]
fn negotiation_terminates_on_a_divergent_history() {
    let fixture = Fixture::build(Kind::Sha1);
    let server_tip = fixture.extend(fixture.four, 40, "theirs");
    fixture.force_ref("refs/heads/main", server_tip);

    // The client's own divergent commits, which the server does not have.
    let mine = Fixture::build(Kind::Sha1);
    let mut client_haves: Vec<ObjectId> = Vec::new();
    let mut head = mine.four;
    for index in 0..40 {
        head = mine.extend(head, 1, &format!("mine{index}"));
        client_haves.push(head);
    }
    // ...and then, last, the commit both sides share.
    client_haves.push(fixture.four);

    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let mut offered = 0usize;
    let mut rounds = 0usize;
    let mut ready = false;
    while offered < client_haves.len() && rounds < 20 {
        rounds += 1;
        offered = (offered + 32).min(client_haves.len());
        let request = FetchRequest {
            wants: vec![server_tip],
            haves: client_haves[..offered].to_vec(),
            done: false,
            ..Default::default()
        };
        let mut out = Vec::new();
        let outcome = upload.fetch(&request, &mut out).expect("a fetch round");
        let response = Response::parse(&out);
        assert!(
            response.has_line("acknowledgments"),
            "every non-final round owes the client an acknowledgments \
             section, or the client waits forever: {:?}",
            response.lines()
        );
        if outcome.ready {
            ready = true;
            assert!(response.has_line("ready"));
            assert!(outcome.sent_pack, "a ready round must carry the pack");
            let sent = index_and_count(&response.band(1), Kind::Sha1);
            assert_eq!(
                sent,
                40 * OBJECTS_PER_COMMIT,
                "the cut point was found but the pack still carries history \
                 the client already has"
            );
            break;
        }
        assert!(
            !outcome.sent_pack,
            "a round with no cut point must not send a pack"
        );
    }
    assert!(
        ready,
        "negotiation ran {rounds} rounds without finding the cut point that \
         was in the client's very last batch"
    );
    assert!(rounds >= 2, "the divergence was not actually exercised");
}

#[test]
fn a_round_with_no_common_ground_answers_nak_rather_than_silence() {
    let fixture = Fixture::build(Kind::Sha1);
    let stranger = Fixture::build(Kind::Sha1).extend(Fixture::build(Kind::Sha1).one, 1, "alien");
    let request = FetchRequest {
        wants: vec![fixture.four],
        haves: vec![stranger],
        done: false,
        ..Default::default()
    };
    let (response, _) = fetch(&fixture, &request);
    assert!(response.has_line("NAK"), "{:?}", response.lines());
    assert!(!response.has_line("ready"));
    assert!(!response.has_line("packfile"));
}

#[test]
fn wait_for_done_suppresses_the_early_cut_point() {
    let fixture = Fixture::build(Kind::Sha1);
    let request = FetchRequest {
        wants: vec![fixture.four],
        haves: vec![fixture.merge],
        done: false,
        wait_for_done: true,
        ..Default::default()
    };
    let (response, _) = fetch(&fixture, &request);
    assert!(
        response.has_line(&format!("ACK {}", fixture.merge)),
        "the common commit was not acknowledged: {:?}",
        response.lines()
    );
    assert!(
        !response.has_line("ready"),
        "wait-for-done was accepted and then ignored"
    );
}

#[test]
fn a_done_request_skips_the_acknowledgments_section_entirely() {
    let fixture = Fixture::build(Kind::Sha1);
    let request = FetchRequest {
        haves: vec![fixture.merge],
        ..clone_request(&[fixture.four])
    };
    let (response, _) = fetch(&fixture, &request);
    // Measured against `git-upload-pack` 2.53 on 2026-08-03: a request
    // carrying `done` gets `packfile` as its first line.
    assert!(
        !response.has_line("acknowledgments"),
        "{:?}",
        response.lines()
    );
    assert_eq!(
        response.lines().first().map(String::as_str),
        Some("packfile")
    );
}

// ── want policy ─────────────────────────────────────────────────────────────

#[test]
fn an_unreachable_want_is_refused_and_a_reachable_one_is_not() {
    let fixture = Fixture::build(Kind::Sha1);
    // Detach a commit by pointing nothing at it.
    let orphan = fixture.extend(fixture.four, 1, "orphan");

    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let mut out = Vec::new();
    let err = upload
        .fetch(&clone_request(&[orphan]), &mut out)
        .expect_err("an unreachable object must not be served");
    assert!(
        matches!(err, gunnar_wire::Error::WantNotAdvertised { .. }),
        "unexpected error: {err}"
    );

    // A non-tip object that *is* reachable is fine. This is what a partial
    // clone's lazy fetch asks for, and refusing it breaks every checkout in a
    // `blob:none` clone.
    let mut out = Vec::new();
    upload
        .fetch(&clone_request(&[fixture.one]), &mut out)
        .expect("a reachable non-tip want must be served");
    assert!(Response::parse(&out).has_line("packfile"));
}

/// **"There is no such object" and "there is, and you may not have it" are one
/// answer** (GUN-042).
///
/// `check_wants` used to run an existence check first and only then measure the
/// want against the advertisement, so the two cases came back as two different
/// errors. The difference is an oracle over exactly the thing the policy is
/// there to hide: `WantNotAdvertised`'s own doc says serving arbitrary ids
/// "leaks objects that are not reachable from any reference, which is how
/// 'deleted' branches stay fetchable" — and answering "that one IS here" one
/// `want` at a time leaks the same fact one bit at a time. An oid from a
/// force-pushed branch, a deleted branch or somebody else's paste can be tested
/// against a public repository for free.
///
/// Same merge `gunnar-ssh` makes between "no such repository" and "you have no
/// grant": one string on the wire.
///
/// The absent oid is a real sha1 of the right width that this repository
/// cannot hold — all-`f` — so the two requests differ in exactly the fact
/// under test and in nothing else.
#[test]
fn an_object_that_is_not_there_is_refused_exactly_like_one_that_is() {
    let fixture = Fixture::build(Kind::Sha1);
    let orphan = fixture.extend(fixture.four, 1, "orphan");
    let absent = gix_hash::ObjectId::from_hex(b"ffffffffffffffffffffffffffffffffffffffff")
        .expect("a legal sha1 that nothing hashes to");

    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let present_but_hidden = upload
        .fetch(&clone_request(&[orphan]), &mut Vec::new())
        .expect_err("an unreachable object must not be served");
    let not_there = upload
        .fetch(&clone_request(&[absent]), &mut Vec::new())
        .expect_err("an object that does not exist must not be served either");

    // The variant, because that is what a caller matches on…
    assert!(
        matches!(
            (&present_but_hidden, &not_there),
            (
                gunnar_wire::Error::WantNotAdvertised { .. },
                gunnar_wire::Error::WantNotAdvertised { .. }
            )
        ),
        "the two cases answer differently, which tells a caller which oids this          repository holds:
  hidden:  {present_but_hidden}
  absent:  {not_there}"
    );
    // …and the words, because the variant is not what reaches the wire. Each
    // names its own oid, so they are compared with the oid taken out.
    let shape = |err: &gunnar_wire::Error, oid: &gix_hash::ObjectId| {
        err.client_message().replace(&oid.to_string(), "<oid>")
    };
    assert_eq!(
        shape(&present_but_hidden, &orphan),
        shape(&not_there, &absent),
        "the client-visible sentences differ, so the oracle is still open"
    );

    // The identity-value guard: a server that refused every want would satisfy
    // all of the above. A reachable non-tip — what a partial clone's lazy fetch
    // asks for — must still be served.
    let mut out = Vec::new();
    upload
        .fetch(&clone_request(&[fixture.one]), &mut out)
        .expect("a reachable non-tip want must still be served");
    assert!(Response::parse(&out).has_line("packfile"));
}

#[test]
fn the_strict_policy_refuses_even_a_reachable_non_tip() {
    let fixture = Fixture::build(Kind::Sha1);
    let upload = UploadPack::new(&fixture.git, fixture.kind).with_config(Config {
        want_policy: WantPolicy::AdvertisedTips,
        ..Default::default()
    });
    let mut out = Vec::new();
    assert!(upload
        .fetch(&clone_request(&[fixture.one]), &mut out)
        .is_err());
    let mut out = Vec::new();
    assert!(upload
        .fetch(&clone_request(&[fixture.four]), &mut out)
        .is_ok());
}

// ── protocol v0 ─────────────────────────────────────────────────────────────

#[test]
fn the_v0_advertisement_carries_capabilities_after_a_nul_on_the_first_line() {
    let fixture = Fixture::build(Kind::Sha1);
    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let mut out = Vec::new();
    upload.write_ref_advertisement(&mut out).expect("advertise");
    let response = Response::parse(&out);

    let Packet::Data(first) = &response.packets[0] else {
        panic!("the first packet is not a line");
    };
    let nul = first.iter().position(|&b| b == 0).expect("a NUL separator");
    let (head, caps) = first.split_at(nul);
    let caps = String::from_utf8_lossy(&caps[1..]);
    assert_eq!(
        String::from_utf8_lossy(head),
        format!("{} HEAD", fixture.four)
    );
    assert!(
        caps.contains("symref=HEAD:refs/heads/main"),
        "no symref capability: {caps}"
    );
    assert!(caps.contains("side-band-64k"), "{caps}");
    assert!(caps.contains("object-format=sha1"), "{caps}");

    let lines = response.lines();
    assert!(
        lines.contains(&format!("{} refs/tags/v1^{{}}", fixture.merge)),
        "the peeled tag line is missing: {lines:?}"
    );
    assert_eq!(response.packets.last(), Some(&Packet::Flush));
}

#[test]
fn a_v0_clone_negotiates_and_ends_its_sideband_with_a_flush() {
    let fixture = Fixture::build(Kind::Sha1);
    let upload = UploadPack::new(&fixture.git, fixture.kind);

    let mut request = Vec::new();
    gunnar_pktline::write_text(
        &mut request,
        format!(
            "want {} side-band-64k thin-pack ofs-delta agent=test/1",
            fixture.four
        )
        .as_bytes(),
    )
    .unwrap();
    gunnar_pktline::write_flush(&mut request).unwrap();
    gunnar_pktline::write_text(&mut request, b"done").unwrap();

    let mut input = std::io::Cursor::new(request);
    let mut out = Vec::new();
    let summary = upload.serve_v0(&mut input, &mut out).expect("serve v0");
    assert_eq!(summary.protocol, ProtocolVersion::V0);

    let response = Response::parse(&out);
    assert!(response.has_line("NAK"), "{:?}", response.lines());
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        5 * OBJECTS_PER_COMMIT
    );
    // Without this terminator git reports "unexpected disconnect while
    // reading sideband packet"; found by the real-git suite, 2026-08-03.
    assert_eq!(
        response.packets.last(),
        Some(&Packet::Flush),
        "a sidebanded v0 response must end with an unframed flush-pkt"
    );
}

/// **GHUNT-1 (lane OX-GUNHUNT, 2026-10-28)**: on protocol v0 `deepen-relative` is a
/// CAPABILITY on the first `want` line, not a request line, so `git fetch --deepen=N`
/// over v0 (stock git `-c protocol.version=0`, or any v0-only client) asks for N MORE
/// commits past its cut. Read as a plain `deepen 1` the server counted from the tip:
/// the client's own boundary came back as the boundary and nothing was sent (stock git
/// 2.53: `--deepen 5` from a depth-1 clone left 5 commits, `file://` leaves 6).
#[test]
fn a_v0_deepen_relative_capability_counts_past_the_clients_cut() {
    let fixture = Fixture::build(Kind::Sha1);
    let upload = UploadPack::new(&fixture.git, fixture.kind);

    // What stock git 2.53 sends for `fetch --deepen=1` from a depth-1 clone over v0.
    let mut request = Vec::new();
    gunnar_pktline::write_text(
        &mut request,
        format!(
            "want {} side-band-64k thin-pack ofs-delta shallow deepen-relative agent=test/1",
            fixture.four
        )
        .as_bytes(),
    )
    .unwrap();
    gunnar_pktline::write_text(&mut request, format!("shallow {}", fixture.four).as_bytes())
        .unwrap();
    gunnar_pktline::write_text(&mut request, b"deepen 1").unwrap();
    gunnar_pktline::write_flush(&mut request).unwrap();
    gunnar_pktline::write_text(&mut request, format!("have {}", fixture.four).as_bytes()).unwrap();
    gunnar_pktline::write_text(&mut request, b"done").unwrap();

    let mut input = std::io::Cursor::new(request);
    let mut out = Vec::new();
    upload.serve_v0(&mut input, &mut out).expect("serve v0");
    let response = Response::parse(&out);

    assert!(
        response.has_line(&format!("unshallow {}", fixture.four)),
        "a relative deepen must lift the client's old boundary: {:?}",
        response.lines()
    );
    assert!(
        response.has_line(&format!("shallow {}", fixture.merge)),
        "the new boundary is one commit past the cut: {:?}",
        response.lines()
    );
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        OBJECTS_PER_COMMIT,
        "the pack must carry `merge`, the one commit past the client's cut"
    );
}

/// The v0 negotiation of a fetch whose `have`s and `done` share one block (no flush
/// between): what stock git sends for any v0 fetch with fewer than 16 `have`s.
fn v0_haves_then_done(fixture: &Fixture, caps: &str, have: ObjectId) -> Response {
    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let mut request = Vec::new();
    gunnar_pktline::write_text(
        &mut request,
        format!("want {} {caps} agent=test/1", fixture.four).as_bytes(),
    )
    .unwrap();
    gunnar_pktline::write_flush(&mut request).unwrap();
    gunnar_pktline::write_text(&mut request, format!("have {have}").as_bytes()).unwrap();
    gunnar_pktline::write_text(&mut request, b"done").unwrap();
    let mut input = std::io::Cursor::new(request);
    let mut out = Vec::new();
    upload.serve_v0(&mut input, &mut out).expect("serve v0");
    Response::parse(&out)
}

/// **GHUNT-2 (lane OX-GUNHUNT, 2026-10-28)**: a `have` the server holds, sent in the
/// block `done` closes, must be acknowledged: git's `upload-pack.c:get_common_commits`
/// counts every recognised `have` as it arrives (`have_obj.nr`), flush or no flush, and
/// at `done` answers `ACK <last_hex>` (multi-ack) or has already said `ACK <oid>` inline
/// (single-ACK). gunnar counted haves only at a flush, so it answered `NAK` and stock git
/// printed `warning: no common commits` on every small v0 fetch (seen on `--deepen`).
#[test]
fn a_v0_have_in_the_block_done_closes_is_acknowledged_in_both_ack_modes() {
    let fixture = Fixture::build(Kind::Sha1);
    for caps in [
        "multi_ack_detailed side-band-64k ofs-delta",
        "side-band-64k ofs-delta",
    ] {
        let response = v0_haves_then_done(&fixture, caps, fixture.three);
        assert!(
            response.has_line(&format!("ACK {}", fixture.three)),
            "[{caps}] the common `have` was not acknowledged: {:?}",
            response.lines()
        );
        assert!(
            !response.has_line("NAK"),
            "[{caps}] a common `have` answered NAK: {:?}",
            response.lines()
        );
    }
    // And a `have` the server does not hold still gets its NAK.
    let unknown = ObjectId::from_hex(b"0123456789abcdef0123456789abcdef01234567").unwrap();
    let response = v0_haves_then_done(&fixture, "multi_ack_detailed side-band-64k", unknown);
    assert!(response.has_line("NAK"), "{:?}", response.lines());
}

#[test]
fn a_v0_client_that_only_wants_the_advertisement_gets_a_clean_end() {
    let fixture = Fixture::build(Kind::Sha1);
    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let mut input = std::io::Cursor::new(Vec::new());
    let mut out = Vec::new();
    upload
        .serve_v0(&mut input, &mut out)
        .expect("`git ls-remote` hanging up is not an error");
    assert!(Response::parse(&out).lines().len() >= 3);
}

// ── receive-pack ────────────────────────────────────────────────────────────

#[test]
fn a_push_moves_the_reference_and_reports_ok() {
    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let zero = Kind::Sha1.null().to_string();
    let input = push_bytes(
        &[(&zero, &fixture.merge.to_string(), "refs/heads/from-a-push")],
        "report-status object-format=sha1 agent=test/1",
        &empty_pack(Kind::Sha1),
    );

    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .expect("serve the push")
        .expect("a push was made");

    assert_eq!(outcome.statuses, vec![RefStatus::Ok]);
    let response = Response::parse(&out);
    assert!(response.has_line("unpack ok"), "{:?}", response.lines());
    assert!(
        response.has_line("ok refs/heads/from-a-push"),
        "{:?}",
        response.lines()
    );
    assert_eq!(
        fixture.read_ref("refs/heads/from-a-push").unwrap(),
        fixture.merge
    );
}

#[test]
fn a_push_whose_stated_old_value_is_wrong_is_refused() {
    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let input = push_bytes(
        &[(
            // `refs/heads/main` is at `four`, not at `one`.
            &fixture.one.to_string(),
            &fixture.merge.to_string(),
            "refs/heads/main",
        )],
        "report-status object-format=sha1",
        &empty_pack(Kind::Sha1),
    );

    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .unwrap()
        .unwrap();

    assert!(matches!(outcome.statuses[0], RefStatus::Rejected(_)));
    assert!(
        Response::parse(&out)
            .lines()
            .iter()
            .any(|line| line.starts_with("ng refs/heads/main")),
        "the client was not told: {:?}",
        Response::parse(&out).lines()
    );
    assert_eq!(
        fixture.read_ref("refs/heads/main").unwrap(),
        fixture.four,
        "the reference moved despite the refusal"
    );
}

/// Connectivity, not indexing. An empty pack indexes perfectly; the tip it is
/// supposed to complete still is not there.
#[test]
fn a_tip_whose_objects_are_missing_is_refused() {
    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let zero = Kind::Sha1.null().to_string();
    let absent = "1".repeat(40);
    let input = push_bytes(
        &[(&zero, &absent, "refs/heads/incomplete")],
        "report-status object-format=sha1",
        &empty_pack(Kind::Sha1),
    );

    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .unwrap()
        .unwrap();

    assert!(
        outcome.unpack.is_ok(),
        "the pack itself was fine; only its closure was not"
    );
    match &outcome.statuses[0] {
        RefStatus::Rejected(reason) => assert!(
            reason.contains("missing"),
            "unhelpful rejection reason: {reason}"
        ),
        RefStatus::Ok => panic!("a reference was pointed at an object that is not here"),
    }
    assert!(fixture.read_ref("refs/heads/incomplete").is_none());
}

/// One reference closes, the next does not, and the report must say **which**.
///
/// This is the guard on the one-walk fix. Connectivity is now checked with **one** walk
/// over every non-delete tip in the push, because running it per command cost
/// O(references x history) and is why a 2 872-reference kernel import never
/// finished. But a batched walk can only report that *something* does not
/// close, and the event stream reports per reference — so a failed batch falls back to
/// walking one reference at a time purely to attribute the failure.
///
/// Nothing else can see that fallback. Every single-reference test passes with
/// it removed, and so does every test where the whole push is good or the whole
/// push is bad; only a **mixed** push distinguishes "reject the incomplete
/// reference" from "reject the push". Without this, dropping the fallback would
/// turn one bad tag in a mirror import into a total refusal, silently.
///
/// Not `atomic`: under `atomic` one refusal is supposed to take the batch down
/// with it, which would mask exactly the difference being asserted.
#[test]
fn a_mixed_push_refuses_only_the_reference_that_does_not_close() {
    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let zero = Kind::Sha1.null().to_string();
    let absent = "1".repeat(40);
    let input = push_bytes(
        &[
            (&zero, &fixture.merge.to_string(), "refs/heads/good"),
            (&zero, &absent, "refs/heads/incomplete"),
        ],
        "report-status object-format=sha1",
        &empty_pack(Kind::Sha1),
    );

    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .unwrap()
        .unwrap();

    assert_eq!(
        outcome.statuses[0],
        RefStatus::Ok,
        "refs/heads/good closes on its own and was refused anyway, so the batched \
         connectivity walk took a whole mirror push down over one bad tip"
    );
    match &outcome.statuses[1] {
        RefStatus::Rejected(reason) => assert!(
            reason.contains("missing"),
            "unhelpful rejection reason: {reason}"
        ),
        RefStatus::Ok => panic!("refs/heads/incomplete points at an object that is not here"),
    }

    // Applied output, not the report: the good reference really moved and the
    // bad one really did not.
    assert_eq!(
        fixture.read_ref("refs/heads/good"),
        Some(fixture.merge),
        "refs/heads/good was reported ok and did not move"
    );
    assert!(fixture.read_ref("refs/heads/incomplete").is_none());
    // And the client is told the same two things on the wire.
    let lines = Response::parse(&out).lines();
    assert!(
        lines.iter().any(|line| line == "ok refs/heads/good"),
        "the client was not told refs/heads/good landed: {lines:?}"
    );
    assert!(
        lines
            .iter()
            .any(|line| line.starts_with("ng refs/heads/incomplete")),
        "the client was not told refs/heads/incomplete was refused: {lines:?}"
    );
}

#[test]
fn atomic_refuses_the_whole_batch_and_leaves_nothing_applied() {
    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let zero = Kind::Sha1.null().to_string();
    let input = push_bytes(
        &[
            (&zero, &fixture.merge.to_string(), "refs/heads/good"),
            // A create for a reference that already exists: it cannot land.
            (&zero, &fixture.merge.to_string(), "refs/heads/side"),
        ],
        "report-status atomic object-format=sha1",
        &empty_pack(Kind::Sha1),
    );

    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .unwrap()
        .unwrap();

    assert!(outcome.atomic);
    assert!(
        outcome.statuses.iter().all(|status| !status.is_ok()),
        "an atomic batch reported a partial success: {:?}",
        outcome.statuses
    );
    assert!(
        fixture.read_ref("refs/heads/good").is_none(),
        "the good half of an atomic push landed; it is not atomic"
    );
    assert_eq!(fixture.read_ref("refs/heads/side").unwrap(), fixture.three);
}

/// Without `atomic` the same batch must apply what it can; otherwise `atomic`
/// is not a capability, it is the only behaviour.
#[test]
fn without_atomic_the_good_half_of_a_batch_lands() {
    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let zero = Kind::Sha1.null().to_string();
    let input = push_bytes(
        &[
            (&zero, &fixture.merge.to_string(), "refs/heads/good"),
            (&zero, &fixture.merge.to_string(), "refs/heads/side"),
        ],
        "report-status object-format=sha1",
        &empty_pack(Kind::Sha1),
    );

    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .unwrap()
        .unwrap();

    assert!(outcome.statuses[0].is_ok(), "{:?}", outcome.statuses);
    assert!(!outcome.statuses[1].is_ok());
    assert_eq!(fixture.read_ref("refs/heads/good").unwrap(), fixture.merge);
}

#[test]
fn a_delete_only_push_reads_no_packfile() {
    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let zero = Kind::Sha1.null().to_string();
    // Deliberately no pack after the flush: a server that waits for one here
    // blocks until the connection times out.
    let input = push_bytes(
        &[(&fixture.three.to_string(), &zero, "refs/heads/side")],
        "report-status delete-refs object-format=sha1",
        &[],
    );

    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &(|_: &mut dyn std::io::BufRead| panic!("no pack should have been read")),
            &AllowAll,
        )
        .unwrap()
        .unwrap();

    assert_eq!(outcome.statuses, vec![RefStatus::Ok]);
    assert!(outcome.pack.is_none());
    assert!(fixture.read_ref("refs/heads/side").is_none());
}

#[test]
fn push_options_are_read_only_when_the_capability_was_negotiated() {
    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let zero = Kind::Sha1.null().to_string();
    let mut input = push_bytes(
        &[(&zero, &fixture.merge.to_string(), "refs/heads/opts")],
        "report-status push-options object-format=sha1",
        &[],
    );
    // The options section sits between the command list and the pack.
    let mut options = Vec::new();
    gunnar_pktline::write_text(&mut options, b"reviewer=nobody").unwrap();
    gunnar_pktline::write_flush(&mut options).unwrap();
    let flush_at = input.len();
    input.splice(flush_at.., options);
    input.extend_from_slice(&empty_pack(Kind::Sha1));

    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .unwrap()
        .unwrap();
    assert_eq!(outcome.push_options, vec!["reviewer=nobody".to_owned()]);
}

/// **The push options reach the GATE**, not only the outcome.
///
/// Reaching the outcome is what they already did, and it is what made the
/// defect invisible: `outcome.push_options` was populated, the assertion above
/// was green, and the only consumer in the whole tree was `.len()` in a log
/// line. `PushFacts` had no options field at all, so the thing that decides
/// whether a push lands could not see them. A client that sent `-o` was told on
/// the wire that the server speaks push options and the option went nowhere.
///
/// So this asserts the *applied* fact: a gate refuses on the strength of an
/// option it read out of `PushFacts`, and the reference does not move.
#[test]
fn a_gate_refuses_on_a_push_option_it_read_out_of_the_facts() {
    /// Refuses unless the client sent `ci.ok`, and names what it saw.
    struct DemandsAnOption;
    impl PushGate for DemandsAnOption {
        fn check(
            &self,
            _command: &RefCommand,
            facts: &gunnar_wire::PushFacts<'_>,
        ) -> Result<(), String> {
            if facts.push_options.iter().any(|o| o == "ci.ok") {
                Ok(())
            } else {
                Err(format!("the gate saw {:?}", facts.push_options))
            }
        }
    }

    /// One push, with `options` in the push-options section. Returns the
    /// outcome and whether the reference exists afterwards.
    fn push_with(
        refname: &str,
        options: &[&str],
    ) -> (gunnar_wire::receive_pack::PushOutcome, bool) {
        let fixture = Fixture::build(Kind::Sha1);
        let receive = ReceivePack::new(&fixture.git, fixture.kind);
        let zero = Kind::Sha1.null().to_string();
        let mut input = push_bytes(
            &[(&zero, &fixture.merge.to_string(), refname)],
            "report-status push-options object-format=sha1",
            &[],
        );
        let mut section = Vec::new();
        for option in options {
            gunnar_pktline::write_text(&mut section, option.as_bytes()).unwrap();
        }
        gunnar_pktline::write_flush(&mut section).unwrap();
        let at = input.len();
        input.splice(at.., section);
        input.extend_from_slice(&empty_pack(Kind::Sha1));

        let mut input = std::io::Cursor::new(input);
        let mut out = Vec::new();
        let outcome = receive
            .serve(
                &mut input,
                &mut out,
                &validating_ingest(Kind::Sha1),
                &DemandsAnOption,
            )
            .unwrap()
            .unwrap();
        let exists = fixture.read_ref(refname).is_some();
        (outcome, exists)
    }

    // ── red: the option is missing, and the REFERENCE DOES NOT MOVE ────────
    let (refused, moved) = push_with("refs/heads/needs-opt", &["ci.nope"]);
    match &refused.statuses[0] {
        RefStatus::Rejected(reason) => assert!(
            reason.contains("ci.nope"),
            "the gate did not see the option the client sent: {reason}"
        ),
        other => panic!("the gate did not refuse: {other:?}"),
    }
    assert!(
        !moved,
        "the reference was applied despite the gate refusing on a push option"
    );

    // ── the same push with no options at all, also refused ─────────────────
    let (bare, moved) = push_with("refs/heads/needs-opt", &[]);
    assert!(matches!(bare.statuses[0], RefStatus::Rejected(_)));
    assert!(!moved);

    // ── green, and the reference DOES move ─────────────────────────────────
    let (allowed, moved) = push_with("refs/heads/has-opt", &["ci.ok"]);
    assert_eq!(
        allowed.statuses,
        vec![RefStatus::Ok],
        "a gate that refuses everything would satisfy both reds above"
    );
    assert!(moved, "the allowed push did not apply its reference");
}

/// **Measured, applied to the gate seam.** A gate that always allows is
/// indistinguishable from a working gate in any test that only pushes valid
/// commits, so the seam is proven by a gate that refuses.
#[test]
fn a_gate_that_refuses_is_obeyed() {
    struct RefuseEverything;
    impl PushGate for RefuseEverything {
        fn check(
            &self,
            command: &RefCommand,
            _facts: &gunnar_wire::PushFacts<'_>,
        ) -> Result<(), String> {
            Err(format!("{} is not allowed here", command.name))
        }
    }

    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let zero = Kind::Sha1.null().to_string();
    let input = push_bytes(
        &[(&zero, &fixture.merge.to_string(), "refs/heads/gated")],
        "report-status object-format=sha1",
        &empty_pack(Kind::Sha1),
    );

    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &RefuseEverything,
        )
        .unwrap()
        .unwrap();
    assert!(matches!(outcome.statuses[0], RefStatus::Rejected(_)));
    assert!(fixture.read_ref("refs/heads/gated").is_none());

    // ...and the very same push lands once the gate allows it, so the failure
    // above is the gate and not the fixture.
    let input = push_bytes(
        &[(&zero, &fixture.merge.to_string(), "refs/heads/gated")],
        "report-status object-format=sha1",
        &empty_pack(Kind::Sha1),
    );
    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .unwrap()
        .unwrap();
    assert_eq!(outcome.statuses, vec![RefStatus::Ok]);
}

/// The report rides band 1 and the connection is ended by an **unframed**
/// flush, the shape `git-receive-pack` 2.53 puts on the wire (captured
/// 2026-08-03).
#[test]
fn the_sidebanded_report_is_wrapped_and_then_the_stream_is_flushed() {
    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let zero = Kind::Sha1.null().to_string();
    let input = push_bytes(
        &[(&zero, &fixture.merge.to_string(), "refs/heads/banded")],
        "report-status side-band-64k object-format=sha1",
        &empty_pack(Kind::Sha1),
    );

    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    receive
        .serve(
            &mut input,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .unwrap();

    let response = Response::parse(&out);
    let inner = Response::parse(&response.band(1));
    assert!(inner.has_line("unpack ok"), "{:?}", inner.lines());
    assert!(
        inner.has_line("ok refs/heads/banded"),
        "{:?}",
        inner.lines()
    );
    assert_eq!(
        inner.packets.last(),
        Some(&Packet::Flush),
        "the report inside the band is not flush-terminated"
    );
    assert_eq!(
        response.packets.last(),
        Some(&Packet::Flush),
        "the outer stream is not flush-terminated"
    );
}

#[test]
fn a_client_that_only_wanted_the_advertisement_is_not_a_push() {
    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let mut input = std::io::Cursor::new(Vec::new());
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &(|_: &mut dyn std::io::BufRead| panic!("no pack")),
            &AllowAll,
        )
        .expect("hanging up after the advertisement is normal");
    assert!(outcome.is_none());
    assert!(!Response::parse(&out).lines().is_empty());
}

#[test]
fn an_empty_repository_still_advertises_its_hash_kind() {
    let store = MemGitServe::new(Kind::Sha256);
    let receive = ReceivePack::new(&store, Kind::Sha256);
    let mut out = Vec::new();
    receive.write_ref_advertisement(&mut out).unwrap();
    let response = Response::parse(&out);
    let Packet::Data(first) = &response.packets[0] else {
        panic!("no advertisement line");
    };
    let text = String::from_utf8_lossy(first);
    assert!(text.starts_with(&Kind::Sha256.null().to_string()), "{text}");
    assert!(text.contains("capabilities^{}"), "{text}");
    assert!(text.contains("object-format=sha256"), "{text}");

    let upload = UploadPack::new(&store, Kind::Sha256);
    let mut out = Vec::new();
    upload.write_ref_advertisement(&mut out).unwrap();
    let response = Response::parse(&out);
    let Packet::Data(first) = &response.packets[0] else {
        panic!("no advertisement line");
    };
    let text = String::from_utf8_lossy(first);
    assert!(text.contains("capabilities^{}"), "{text}");
    assert!(text.contains("object-format=sha256"), "{text}");
    // `FullName` died with `RefStore`; the *rule* did not, and `common::name`
    // routes through `gunnar_store::refname::validate`, which is the single
    // authority now. Keeping the line keeps the check that this suite's
    // reference names are legal ones.
    let _: String = name("refs/heads/main");
}

/// **`ACK` names the head of a chain of `have`s and nothing behind it.**
///
/// A client walking its own history back from a tip sends `tip`, `tip^`,
/// `tip^^`, … Each of those tells the server about the whole ancestry behind
/// it, so every `have` after the first is something the server was just told.
/// git `ACK`s only the news (`upload-pack.c`'s `we_knew_they_have`); gunnar
/// `ACK`ed all of it.
///
/// MEASURED 2026-08-04 across the bench's 500-commit divergence: stock git
/// **1** `ACK`, gunnar **508**, gunnar's being a parent chain headed by git's
/// one — 25 400 bytes, a third of gunnar's whole wire-byte deficit on that
/// fetch.
///
/// **Seen red**: against the code this replaced the first assertion reports 40
/// `ACK`s for 40 `have`s. The second and third assertions are the ones that
/// make it a guard rather than a byte count — they say the smaller
/// acknowledgment did not change *what the client is sent*, which is the only
/// way this could have been a bug rather than a saving.
#[test]
fn ack_names_the_head_of_a_have_chain_and_nothing_behind_it() {
    let fixture = Fixture::build(Kind::Sha1);
    // A shared history the client holds every commit of...
    let mut chain: Vec<ObjectId> = Vec::new();
    let mut head = fixture.four;
    for index in 0..40 {
        head = fixture.extend(head, 1, &format!("shared{index}"));
        chain.push(head);
    }
    // ...and three commits past it that only the server has.
    let server_tip = fixture.extend(head, 3, "new");
    fixture.force_ref("refs/heads/main", server_tip);

    // Newest first, which is the order a client walking its own history sends.
    let haves: Vec<ObjectId> = chain.iter().rev().copied().collect();
    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let mut out = Vec::new();
    let outcome = upload
        .fetch(
            &FetchRequest {
                wants: vec![server_tip],
                haves: haves.clone(),
                done: false,
                ..Default::default()
            },
            &mut out,
        )
        .expect("a fetch round");
    let response = Response::parse(&out);

    let acks: Vec<String> = response
        .lines()
        .into_iter()
        .filter(|l| l.starts_with("ACK "))
        .collect();
    assert_eq!(
        acks,
        vec![format!("ACK {}", chain[chain.len() - 1])],
        "40 `have`s forming one parent chain are one piece of news, and only \
         the head of the chain is it; git sends exactly this one line"
    );

    // The saving must be bytes and nothing else. `common` is untouched, so the
    // cut point and the pack are what they were.
    assert_eq!(
        outcome.common.len(),
        chain.len(),
        "the pack is computed from every common `have`, not from the `ACK`ed \
         ones; shrinking the wire list must not shrink this"
    );
    assert!(outcome.ready && outcome.sent_pack, "{:?}", response.lines());
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        3 * OBJECTS_PER_COMMIT,
        "the client is sent the three commits it lacks and nothing else"
    );

    // A second round repeating the same `have`s is no news at all, and git
    // answers that with `NAK` rather than with a silent section.
    let mut again = Vec::new();
    upload
        .fetch(
            &FetchRequest {
                wants: vec![server_tip],
                haves,
                done: false,
                ..Default::default()
            },
            &mut again,
        )
        .expect("a second round");
    let repeat = Response::parse(&again);
    assert!(
        repeat.has_line("NAK"),
        "a round that repeats known `have`s owes the client a NAK, not an \
         empty acknowledgments section: {:?}",
        repeat.lines()
    );
    assert!(
        !repeat.lines().iter().any(|l| l.starts_with("ACK ")),
        "nothing in the second round was news: {:?}",
        repeat.lines()
    );
}

// ── the v0 acknowledgment count (2026-08-05) ────────────────────────────────
//
// git's client stops reading acknowledgments the moment it sees a plain `ACK`
// (`fetch-pack.c:find_common` zeroes `flushes` and clears `multi_ack`), so an
// extra line after it is not merely redundant — it stays in the pipe until the
// reader switches to sideband and is then demultiplexed as a band number.
// gunnar wrote two: one at the flush of the block that found the common commit,
// one more after `done`. Against stock git 2.53.0 over real SSH, EVERY v0
// incremental fetch that shared history with the server died on it:
//
// ```text
// fetch-pack: protocol error: bad band #65      (a second `ACK`)
// fetch-pack: protocol error: bad band #78      (a trailing `NAK`)
// fatal: protocol error: bad pack header
// ```
//
// MEASURED 2026-08-05, the production-shaped corpus over `gunnar serve`,
// 4 repositories x 4 cut depths: **13 of 13 v0 arms failed and 13 of 13 v2 arms
// passed**, in sha1 and in sha256 alike. A full clone was clean throughout,
// because a clone sends no `have` and so is never ACKed — which is exactly the
// shape the interop suite's v0 coverage had.
//
// These two tests are on the LINE COUNT rather than on "a pack came out",
// because a pack does come out either way; what the client cannot survive is
// how many lines precede it.

/// Build a v0 request: one `want` with capabilities, then `blocks` of `have`
/// lines each ended by a flush, then `done`.
fn v0_negotiation(want: &ObjectId, blocks: &[&[ObjectId]]) -> Vec<u8> {
    let mut request = Vec::new();
    gunnar_pktline::write_text(
        &mut request,
        format!("want {want} side-band-64k thin-pack ofs-delta agent=test/1").as_bytes(),
    )
    .unwrap();
    gunnar_pktline::write_flush(&mut request).unwrap();
    for block in blocks {
        for id in *block {
            gunnar_pktline::write_text(&mut request, format!("have {id}").as_bytes()).unwrap();
        }
        gunnar_pktline::write_flush(&mut request).unwrap();
    }
    gunnar_pktline::write_text(&mut request, b"done").unwrap();
    request
}

/// Every acknowledgment line of a v0 response, in order.
///
/// The advertisement and the sideband are filtered out and nothing else is:
/// what this exists to count is `ACK`/`NAK`, and a **surplus** one of either is
/// the defect, so narrowing to those two prefixes cannot hide it.
fn v0_acknowledgments(response: &Response) -> Vec<String> {
    response
        .packets
        .iter()
        .filter_map(|packet| match packet {
            Packet::Data(payload) => {
                // Band 1/2/3 payloads are the pack and its progress.
                if matches!(payload.first(), Some(1..=3)) {
                    return None;
                }
                let text = String::from_utf8_lossy(
                    payload.strip_suffix(b"\n").unwrap_or(payload.as_slice()),
                )
                .into_owned();
                (text == "NAK" || text.starts_with("ACK ")).then_some(text)
            }
            _ => None,
        })
        .collect()
}

#[test]
fn a_v0_negotiation_that_finds_a_common_commit_acknowledges_it_once() {
    let fixture = Fixture::build(Kind::Sha1);
    let upload = UploadPack::new(&fixture.git, fixture.kind);
    // ONE block, and it holds a commit the server has.
    let request = v0_negotiation(&fixture.four, &[&[fixture.two]]);
    let mut input = std::io::Cursor::new(request);
    let mut out = Vec::new();
    upload.serve_v0(&mut input, &mut out).expect("serve v0");

    let response = Response::parse(&out);
    let lines = v0_acknowledgments(&response);
    let acks = lines.iter().filter(|l| l.starts_with("ACK ")).count();
    assert_eq!(
        acks, 1,
        "single-ACK v0 owes the client exactly one `ACK`; a second one is read \
         as a sideband band number: {lines:?}"
    );
    assert_eq!(
        lines.iter().filter(|l| l.as_str() == "NAK").count(),
        0,
        "nothing may follow the `ACK`: {lines:?}"
    );
}

#[test]
fn a_v0_negotiation_says_nothing_more_after_the_block_that_was_acknowledged() {
    let fixture = Fixture::build(Kind::Sha1);
    let upload = UploadPack::new(&fixture.git, fixture.kind);
    // THREE blocks. The first holds nothing the server has, the second holds
    // the common commit, the third is the one a real client has already
    // pipelined by the time the ACK reaches it — and it is the one that used to
    // draw a trailing `NAK`. `fx/knut` at 40 commits back is this shape;
    // `fx/funnel-core` at five is the single-block one above, which is why the
    // small fixture never showed the defect.
    let stranger = ObjectId::from_hex(b"1111111111111111111111111111111111111111").unwrap();
    let request = v0_negotiation(
        &fixture.four,
        &[&[stranger], &[fixture.two], &[fixture.three]],
    );
    let mut input = std::io::Cursor::new(request);
    let mut out = Vec::new();
    upload.serve_v0(&mut input, &mut out).expect("serve v0");

    let response = Response::parse(&out);
    let lines = v0_acknowledgments(&response);
    // NAK for the first block, ACK for the second, and then silence.
    assert_eq!(
        lines,
        vec!["NAK".to_owned(), format!("ACK {}", fixture.two)],
        "after the acknowledgment the server owes the client nothing but the \
         pack; git reads the next byte as a band number"
    );
    // And the pack is still there, so this is not a test that passed by the
    // server having stopped talking altogether.
    assert_eq!(
        index_and_count(&response.band(1), Kind::Sha1),
        2 * OBJECTS_PER_COMMIT,
        "the pack must still carry what the negotiation did not cover"
    );
}

// ── the `blob:limit=` boundary (2026-08-05) ─────────────────────────────────

/// A commit whose single blob is exactly `bytes` long.
///
/// The content is generated rather than written out because the assertion is
/// about a size, and a literal that drifts by one character would move the
/// boundary this test exists to sit on.
fn commit_with_blob_of(
    git: &dyn GitServe,
    file: &str,
    bytes: usize,
    parents: &[ObjectId],
    at: i64,
) -> ObjectId {
    let content = "x".repeat(bytes).into_bytes();
    let blob = gix_object::compute_hash(Kind::Sha1, gix_object::Kind::Blob, &content)
        .expect("hash the blob");

    let mut tree = Vec::new();
    tree.extend_from_slice(b"100644 ");
    tree.extend_from_slice(file.as_bytes());
    tree.push(0);
    tree.extend_from_slice(blob.as_bytes());
    let tree_id =
        gix_object::compute_hash(Kind::Sha1, gix_object::Kind::Tree, &tree).expect("hash the tree");

    let mut text = format!("tree {}\n", tree_id.to_hex());
    for parent in parents {
        text.push_str(&format!("parent {}\n", parent.to_hex()));
    }
    text.push_str(&format!(
        "author Gunnar Test <gunnar@example.invalid> {at} +0000\n\
         committer Gunnar Test <gunnar@example.invalid> {at} +0000\n\nsized\n"
    ));

    let ids = common::write_pack_of(
        git,
        Kind::Sha1,
        &[
            (gix_object::Kind::Blob, content),
            (gix_object::Kind::Tree, tree),
            (gix_object::Kind::Commit, text.into_bytes()),
        ],
    );

    // The size this whole test is about, read back through the contract rather
    // than trusted from the writer.
    assert_eq!(
        git.header(blob.as_bytes())
            .expect("header")
            .expect("the blob exists"),
        (ObjType::Blob, bytes as u64),
        "the fixture blob is not the size this test is about"
    );
    ids[2]
}

/// `blob:limit=n` omits a blob of exactly `n` bytes, because git does.
///
/// git's `list-objects-filter.c` keeps a blob when `object_length <
/// filter_data->max_bytes`; gunnar's `graph.rs` had `<=`, so the two servers
/// disagreed about the blob sitting exactly on the limit and a partial-clone
/// client sizes its lazy-fetch expectations from git's rule.
///
/// MEASURED 2026-08-05 — one repository holding blobs of 1023, 1024 and
/// 1025 bytes, `git clone --filter=blob:limit=1024`, stock git 2.53.0 as the
/// client on both arms:
///
/// | server | blobs delivered |
/// |---|---|
/// | stock `git-upload-pack` | 1023 |
/// | gunnar, before | 1023, **1024** |
/// | gunnar, after | 1023 |
///
/// Both neighbours are asserted, not just the boundary: a filter that had
/// simply stopped working would keep the 1023 and drop the 1024 too, and pass a
/// test that only looked at the one in the middle.
#[test]
fn a_blob_limit_omits_the_blob_that_is_exactly_the_limit() {
    let store = MemGitServe::new(Kind::Sha1);
    let under = commit_with_blob_of(&store, "under.txt", 1023, &[], 1_700_000_001);
    let exact = commit_with_blob_of(&store, "exact.txt", 1024, &[under], 1_700_000_002);
    let over = commit_with_blob_of(&store, "over.txt", 1025, &[exact], 1_700_000_003);
    store
        .put_refs(&[gunnar_store::git_storage_trait::RefUpdate::set(
            "refs/heads/main",
            over.to_string(),
        )])
        .expect("install the reference");

    let upload = UploadPack::new(&store, Kind::Sha1);
    let mut out = Vec::new();
    upload
        .fetch(
            &FetchRequest {
                wants: vec![over],
                done: true,
                thin_pack: false,
                ofs_delta: true,
                filter: Some(gunnar_wire::Filter::BlobLimit(1024)),
                ..Default::default()
            },
            &mut out,
        )
        .expect("a filtered fetch");

    // What actually arrived, read back out of the pack with gix's own indexer
    // rather than from anything gunnar said about it.
    let response = Response::parse(&out);
    let pack = response.band(1);
    let delivered = blob_sizes_in_pack(&pack, Kind::Sha1);
    assert!(
        delivered.contains(&1023),
        "the blob UNDER the limit must be delivered; got {delivered:?}"
    );
    assert!(
        !delivered.contains(&1024),
        "git omits a blob of exactly the limit and so must gunnar; got {delivered:?}"
    );
    assert!(
        !delivered.contains(&1025),
        "the blob OVER the limit must be omitted; got {delivered:?}"
    );
}

/// Every blob size in `pack`, via gix's indexer and object database.
fn blob_sizes_in_pack(pack: &[u8], kind: Kind) -> Vec<u64> {
    let dir = tempfile::tempdir().expect("tempdir");
    let out = std::process::Command::new("git")
        .args(["init", "-q", "--bare"])
        .arg(format!(
            "--object-format={}",
            match kind {
                Kind::Sha256 => "sha256",
                _ => "sha1",
            }
        ))
        .arg(dir.path())
        .output()
        .expect("git init");
    assert!(out.status.success(), "git init: {out:?}");
    let mut child = std::process::Command::new("git")
        .arg("-C")
        .arg(dir.path())
        // No `--strict`: a filtered pack is INCOMPLETE by construction — that
        // is what a filter is — so a connectivity check would refuse every arm
        // of this test including the correct one. What is being judged here is
        // which blobs arrived, and git's own indexer is what reads them out.
        // The strict, both-receivers judgement of whole packs is the corpus
        // sweep's job, not this one's.
        .args(["index-pack", "--stdin"])
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .expect("git index-pack");
    use std::io::Write as _;
    child
        .stdin
        .as_mut()
        .expect("stdin")
        .write_all(pack)
        .expect("write the pack");
    let done = child.wait_with_output().expect("index-pack");
    assert!(
        done.status.success(),
        "stock git refused the pack: {}",
        String::from_utf8_lossy(&done.stderr)
    );
    let listed = std::process::Command::new("git")
        .arg("-C")
        .arg(dir.path())
        .args([
            "cat-file",
            "--batch-all-objects",
            "--batch-check=%(objecttype) %(objectsize)",
        ])
        .output()
        .expect("cat-file");
    String::from_utf8_lossy(&listed.stdout)
        .lines()
        .filter_map(|line| line.strip_prefix("blob "))
        .filter_map(|size| size.trim().parse::<u64>().ok())
        .collect()
}

// ── the refusal reason reaches the CLIENT (2026-08-05) ──────────────────────
//
// `UploadPack::report_error` writes an `ERR` pkt-line or a band-3 message, and
// it had **zero** call sites in the whole tree, tests included. So every
// refusal this crate makes reached the transport as an `Err`, was logged, and
// the channel closed — `git` printed
//
// ```text
// fatal: the remote end hung up unexpectedly
// ```
//
// while the server log carried the reason in full. That is the 03:00 failure
// mode: the one person who can see the answer is the one who is not awake.
//
// MEASURED 2026-08-05, stock git 2.53.0 over real SSH, protocol v2 (git's
// default since 2.26):
//
// | request | client saw, before | client sees, after |
// |---|---|---|
// | `fetch <unknown oid>` | `the remote end hung up unexpectedly` | `remote error: object 0000…0001 is not in this repository (a want)` |
// | `clone --filter=sparse:oid=…` | `the remote end hung up unexpectedly` | `remote error: filter "sparse:oid=HEAD:x" is not supported: gunnar implements blob:none, blob:limit=<n> and tree:<depth>` |
//
// The assertion is on the BYTES the server wrote, not on the `Err` it returned:
// the `Err` was always there, and it is exactly what nobody could see.

/// Serve one v2 command and return whatever bytes came back, error or not.
fn v2_served_bytes(fixture: &Fixture, request: &[u8]) -> (Vec<u8>, Option<String>) {
    let upload = UploadPack::new(&fixture.git, fixture.kind);
    let mut input = std::io::Cursor::new(request.to_vec());
    let mut out = Vec::new();
    let outcome = upload.serve_session(
        ProtocolVersion::V2,
        gunnar_wire::Session::Connected,
        &mut input,
        &mut out,
    );
    (out, outcome.err().map(|err| err.to_string()))
}

#[test]
fn a_refused_want_tells_the_client_which_object_and_why() {
    let fixture = Fixture::build(Kind::Sha1);
    let stranger = ObjectId::from_hex(b"0000000000000000000000000000000000000001").unwrap();
    let request = v2_request(
        "fetch",
        &["object-format=sha1"],
        &[&format!("want {stranger}"), "done"],
    );
    let (bytes, err) = v2_served_bytes(&fixture, &request);
    assert!(err.is_some(), "the fetch must still fail");

    let text = String::from_utf8_lossy(&bytes);
    assert!(
        text.contains("ERR "),
        "a refusal owes the client an `ERR` line, or git prints only \"the \
         remote end hung up unexpectedly\": {text:?}"
    );
    assert!(
        text.contains(&stranger.to_string()),
        "the `ERR` line must name the object that was refused: {text:?}"
    );
}

#[test]
fn a_refused_filter_tells_the_client_what_is_supported() {
    let fixture = Fixture::build(Kind::Sha1);
    let request = v2_request(
        "fetch",
        &["object-format=sha1"],
        &[
            &format!("want {}", fixture.four),
            "filter sparse:oid=deadbeef",
            "done",
        ],
    );
    let (bytes, err) = v2_served_bytes(&fixture, &request);
    assert!(err.is_some(), "an unsupported filter must still fail");
    let text = String::from_utf8_lossy(&bytes);
    assert!(
        text.contains("ERR ") && text.contains("sparse:oid"),
        "the client must be told which filter was refused: {text:?}"
    );
    assert!(
        text.contains("blob:none"),
        "and what it could have asked for instead: {text:?}"
    );
}

#[test]
fn a_client_message_never_carries_a_server_path() {
    // The three `#[error(transparent)]` variants were passed through verbatim,
    // and `gunnar_store::Error::StoreLocked` renders the absolute path of the
    // lock file. `receive_pack` puts `client_message()` straight into the
    // `unpack` line of a push report, so `git push` printed it to whoever
    // pushed.
    let leaky = gunnar_store::Error::StoreLocked {
        path: std::path::PathBuf::from("/srv/gunnar/data/repos/acme/secret.git/gunnar-store.lock"),
    };
    let rendered = leaky.to_string();
    assert!(
        rendered.contains("/srv/gunnar/data/repos/acme/secret.git"),
        "the fixture must actually carry a path, or this test proves nothing: \
         {rendered}"
    );

    let wrapped: gunnar_wire::Error = leaky.into();
    let message = wrapped.client_message();
    assert!(
        !message.contains('/'),
        "a client message must not carry a server path: {message:?}"
    );
    assert!(
        !message.is_empty(),
        "and it must still say something, or the client learns nothing"
    );

    // The other direction, so this cannot pass by redacting everything: a
    // protocol refusal is the client's own words and stays verbatim, because
    // that is what makes it actionable without a server login.
    let named = gunnar_wire::Error::UnsupportedFilter {
        spec: "sparse:oid=deadbeef".to_owned(),
        reason: "gunnar implements blob:none, blob:limit=<n> and tree:<depth>",
    };
    assert!(
        named.client_message().contains("sparse:oid=deadbeef"),
        "a refusal that names the client's own request must not be redacted"
    );
}

// ── a shallow client's grafts are not the capability line (2026-08-05) ──────

/// A push command list preceded by `shallow <oid>` graft announcements, which
/// is exactly what stock `git push` sends from a `--depth` clone.
fn shallow_push_bytes(
    grafts: &[ObjectId],
    commands: &[(&str, &str, &str)],
    capabilities: &str,
    pack: &[u8],
) -> Vec<u8> {
    let mut out = Vec::new();
    for graft in grafts {
        gunnar_pktline::write_text(&mut out, format!("shallow {graft}").as_bytes()).unwrap();
    }
    for (index, (old, new, name)) in commands.iter().enumerate() {
        let mut line = format!("{old} {new} {name}").into_bytes();
        if index == 0 {
            line.push(0);
            line.extend_from_slice(capabilities.as_bytes());
        }
        gunnar_pktline::write_text(&mut out, &line).unwrap();
    }
    gunnar_pktline::write_flush(&mut out).unwrap();
    out.extend_from_slice(pack);
    out
}

/// Every push from a shallow clone was refused, because the capability section
/// was taken off the wrong line.
///
/// `first` meant "the first packet", and a shallow client spends that on a
/// `shallow <oid>` graft. That line has no NUL, so nothing was parsed off it,
/// `first` went false, and the real first command reached `parse_command` with
/// its whole capability section still glued to the reference name:
///
/// ```text
/// invalid reference name "refs/heads/x\0 report-status-v2 side-band-64k quiet
///   object-format=sha1 agent=git/2.53.0-Linux": control character or space
/// ```
///
/// ISOLATED 2026-08-05, stock git 2.53.0 over real SSH against a live
/// `gunnar serve`, one client, one commit, one server:
///
/// | client                                    | push        |
/// |-------------------------------------------|-------------|
/// | `clone --depth 3`                         | **refused** |
/// | the same clone after `fetch --unshallow`  | accepted    |
///
/// and the client saw only `the remote end hung up unexpectedly`.
/// `actions/checkout` clones at `fetch-depth: 1` by default, so a CI job that
/// pushes back is the ordinary shape of this, not an exotic one.
///
/// Both halves are asserted, because the ref name failing is the loud half and
/// the capabilities being lost is the quiet one: with the name accepted anyway,
/// a shallow pusher would still have had `report-status`, `side-band-64k`,
/// `atomic`, `push-options` and the `object-format` check silently off.
#[test]
fn a_push_from_a_shallow_client_is_not_derailed_by_its_grafts() {
    let fixture = Fixture::build(Kind::Sha1);
    let receive = ReceivePack::new(&fixture.git, fixture.kind);
    let zero = Kind::Sha1.null().to_string();
    let input = shallow_push_bytes(
        &[fixture.one, fixture.two],
        &[(&zero, &fixture.merge.to_string(), "refs/heads/from-shallow")],
        "report-status side-band-64k object-format=sha1 agent=git/2.53.0",
        &empty_pack(Kind::Sha1),
    );

    let mut input = std::io::Cursor::new(input);
    let mut out = Vec::new();
    let outcome = receive
        .serve(
            &mut input,
            &mut out,
            &validating_ingest(Kind::Sha1),
            &AllowAll,
        )
        .expect("a shallow client's push is still a push")
        .expect("it carried a command");

    assert_eq!(
        outcome.statuses,
        vec![RefStatus::Ok],
        "the graft lines are not commands and must not reach the reference name"
    );
    assert_eq!(
        fixture.read_ref("refs/heads/from-shallow"),
        Some(fixture.merge),
        "and the reference has to have actually moved"
    );
    // The quiet half. `side-band-64k` was asked for, so the report must be
    // sidebanded — which is only true if the capability line was read at all.
    let response = Response::parse(&out);
    assert!(
        response
            .packets
            .iter()
            .any(|packet| matches!(packet, Packet::Data(payload) if payload.first() == Some(&1))),
        "the capabilities rode the first COMMAND, not the first packet; without \
         them a shallow pusher silently loses report-status, side-band-64k, \
         atomic, push-options and the object-format check: {:?}",
        response.lines()
    );
}

/// **The connectivity budget refuses the push, and the client is told.**
///
/// `receive_pack::Config::budget` bounds the walk that proves a push closes.
/// Nothing exercised it: `caps::SERVER_POLICIES` classifies it as
/// `Declared::Refuses` — not advertised, because the peer finds out from an
/// error — and the only test that citation could name was
/// `a_mixed_push_refuses_only_the_reference_that_does_not_close`, which proves
/// the *reporting mechanism* and never sets a budget. A citation to an adjacent
/// test is the claim without the evidence, which is the thing that census
/// exists to stop, so here is the evidence.
///
/// The applied output is the **status report the client reads** and the state
/// of the reference afterwards, not a returned error: a budget that refused
/// internally and still moved the ref, or that reported `ok` and did not, would
/// both satisfy an exit-code assertion.
///
/// Seen red: with `.with_config(…)` dropped so the default
/// `graph::CONNECTIVITY_BUDGET` applies, this fails with *"a budget of 1 served
/// a push over 6 new commits"* — which is also what says the budget, and not
/// some unrelated refusal, is what fired.
#[test]
fn the_connectivity_budget_refuses_a_push_rather_than_accepting_it_unchecked() {
    let fixture = Fixture::build(Kind::Sha1);
    // Six commits that no reference reaches, so the walk really has to cross
    // them before it meets the frontier at `four`.
    let tip = fixture.extend(fixture.four, 6, "budget");
    let zero = Kind::Sha1.null().to_string();

    let push = |budget: Option<usize>| {
        let receive = ReceivePack::new(&fixture.git, fixture.kind).with_config(
            gunnar_wire::receive_pack::Config {
                budget,
                ..gunnar_wire::receive_pack::Config::default()
            },
        );
        let input = push_bytes(
            &[(&zero, &tip.to_string(), "refs/heads/budgeted")],
            "report-status object-format=sha1",
            &empty_pack(Kind::Sha1),
        );
        let mut input = std::io::Cursor::new(input);
        let mut out = Vec::new();
        receive
            .serve(
                &mut input,
                &mut out,
                &validating_ingest(Kind::Sha1),
                &AllowAll,
            )
            .expect("the session itself completes")
            .expect("a push outcome")
    };

    // ── the control, FIRST: this push is servable ──────────────────────────
    //
    // Without it, "budget 1 refuses" would pass against a fixture that could
    // not be pushed at all.
    let served = push(gunnar_wire::receive_pack::Config::default().budget);
    assert_eq!(
        served.statuses[0],
        RefStatus::Ok,
        "the same push under the default budget was refused, so the arm below is not about the \
         budget"
    );
    // `read_ref` answers only for a DIRECT reference, so a symbolic one here
    // would come back `None` and fail this — which is the same refusal the
    // `Target::Symbolic(_) => panic!` arm used to spell out, one layer down.
    assert_eq!(
        fixture.read_ref("refs/heads/budgeted"),
        Some(tip),
        "the control reported ok and the reference did not move"
    );

    // Put it back, so the second arm starts where the first did.
    fixture
        .git
        .update_ref("refs/heads/budgeted", Some(tip.as_bytes()), None)
        .expect("remove the control's reference");

    // ── a budget of 1 cannot prove this push closes ────────────────────────
    let refused = push(Some(1));
    match &refused.statuses[0] {
        RefStatus::Rejected(reason) => assert!(
            !reason.is_empty(),
            "the reference was rejected with an EMPTY reason, so `git push` prints a refusal \
             that says nothing"
        ),
        RefStatus::Ok => panic!(
            "a budget of 1 served a push over 6 new commits: the connectivity walk either never \
             ran or ran unbounded, and either way the ceiling is decoration"
        ),
    }
    assert!(
        fixture.read_ref("refs/heads/budgeted").is_none(),
        "the push was reported as rejected and the reference moved anyway, which is the worst of \
         both answers"
    );
}