crates/gunnar-cli/tests/wire_config.rs
#![cfg(feature = "server")]
//! **The `gunnar-wire` knobs, asserted through a real server.**
//!
//! `Offered`, `WantPolicy`, `Config::budget` and `Limits` were fully
//! implemented in both protocol versions and reachable from nowhere: the join
//! said `UploadPack::new(objects, refs)` and never `with_config`, so every
//! deployment ran on the defaults and **an operator could not decline
//! `filter`**. The knobs existed, were documented, were unit-tested inside
//! `gunnar-wire`, and could not change what any server did.
//!
//! That is worse than not having them. A configuration surface that cannot
//! affect behaviour reads as a control that works.
//!
//! # Why every test here reads bytes rather than a return value
//!
//! A knob's applied output is **what a client sees on the wire**. So:
//!
//! * the advertisement tests read the raw pkt-lines gunnar wrote, over `ssh`,
//! in **both** protocol versions, because v0 puts capabilities on the first
//! ref line and v2 puts them in a `fetch=` command line and the two are
//! generated by different code (the audit's own lesson: v2 was green while
//! v0 was completely broken);
//! * the behaviour test asserts on **the blob being in the clone or not**,
//! which is the thing a partial clone is for, rather than on an exit status —
//! stock git, told a server does not offer `filter`, *degrades to a full
//! clone and succeeds*, so an exit-status assertion would be green either
//! way;
//! * and every arm has its opposite next to it, on a server that differs in
//! exactly one field, so a vacuous assertion has somewhere to show up.
//!
//! # A v0 gap this found and did not fix
//!
//! `WantPolicy::ReachableFromRefs` is the default, is implemented
//! (`upload_pack.rs::check_wants`), and its own doc says it is git's
//! `uploadpack.allowReachableSHA1InWant`. gunnar never advertises
//! `allow-reachable-sha1-in-want` or `allow-tip-sha1-in-want` in v0, so **stock
//! git refuses to send the want on the client side** and never reaches the
//! server's decision. Measured with git 2.53.0, a non-bare
//! `clone --filter=blob:none` over protocol v0:
//!
//! ```text
//! error: Server does not allow request for unadvertised object ffcfaab5…
//! fatal: could not fetch ffcfaab5… from promisor remote
//! ```
//!
//! v2 has no such capability and works. So a partial clone's lazy fetch is
//! broken on v0 and green on v2 — the same shape as the incremental-fetch
//! defect the fat audit found. The fix belongs in `caps.rs`, where the
//! capability table lives; it is not made here because that file is being
//! rewritten independently and one table must decide the advertisement.
mod common;
use common::{local_repo, raw_ssh, tools_present, Harness};
use gunnar_cli::server::WireConfig;
use gunnar_core::HashKind;
/// A server that declines `filter`, and nothing else changed.
fn no_filter() -> WireConfig {
let mut wire = WireConfig::default();
wire.upload.offered.filter = false;
wire
}
/// The v0 and v2 upload-pack advertisements, as bytes off the wire.
fn advertisements(harness: &Harness, repo: &str) -> (String, String) {
let command = format!("git-upload-pack '{repo}'");
let v0 = raw_ssh(harness, &[], &command);
let v2 = raw_ssh(harness, &["-o", "SetEnv=GIT_PROTOCOL=version=2"], &command);
for (name, out) in [("v0", &v0), ("v2", &v2)] {
assert_eq!(
out.status.code(),
Some(0),
"the {name} advertisement did not come back at all (stderr: {}); every assertion \
about its contents would be about an empty string",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!out.stdout.is_empty(),
"the {name} advertisement is empty, so `contains` below can only ever say false"
);
}
(
String::from_utf8_lossy(&v0.stdout).into_owned(),
String::from_utf8_lossy(&v2.stdout).into_owned(),
)
}
/// **`--no-filter` removes `filter` from what gunnar advertises, in v0 and v2.**
///
/// Both directions on one page: the default server offers it, the configured
/// one does not, and the two servers differ in exactly `offered.filter`. An
/// assertion that only checked the absence would pass against a server that
/// advertised nothing at all.
///
/// Seen red: with `.with_config(wire.upload.clone())` reverted to
/// `UploadPack::new(read_side, &refs)` — the state this shipped in — the two
/// absence assertions fail with *"a server told to decline `filter` advertised
/// it anyway"*, in both protocol versions.
#[test]
fn declining_filter_takes_it_out_of_both_advertisements() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
// ── the default: filter IS on offer ────────────────────────────────────
let offered = Harness::start("team", "offered", HashKind::Sha1);
let (v0, v2) = advertisements(&offered, "team/offered.git");
assert!(
v0.contains("filter"),
"the DEFAULT server must still advertise `filter` in v0, or the absence asserted below \
proves nothing: {v0:?}"
);
assert!(
v2.contains("fetch=") && v2.contains("filter"),
"the DEFAULT server must still advertise `filter` in the v2 `fetch=` line: {v2:?}"
);
// ── --no-filter: it is gone, and only it ───────────────────────────────
let declined = Harness::start_with_wire("team", "declined", HashKind::Sha1, no_filter());
let (v0, v2) = advertisements(&declined, "team/declined.git");
assert!(
!v0.contains("filter"),
"a server told to decline `filter` advertised it anyway in v0: {v0:?}"
);
assert!(
!v2.contains("filter"),
"a server told to decline `filter` advertised it anyway in v2: {v2:?}"
);
// The rest of the advertisement is untouched. Without this, deleting the
// whole capability list would satisfy the two assertions above.
assert!(
v0.contains("side-band-64k") && v0.contains("ofs-delta") && v0.contains("object-format="),
"declining one capability removed others from v0: {v0:?}"
);
assert!(
v2.contains("ls-refs") && v2.contains("fetch"),
"declining one capability broke the v2 command list: {v2:?}"
);
}
/// **A declined `filter` means the client gets the blob, in v0 and v2.**
///
/// The applied output of a partial clone is which objects are missing from it.
/// `git clone --filter=blob:none` against a server that does not offer `filter`
/// **succeeds** — stock git degrades to a full clone rather than failing — so
/// the only assertion that can tell the two servers apart is whether the blob
/// is present locally afterwards.
///
/// Seen red both ways: with the wiring reverted, the `--no-filter` arms report
/// *"the blob is MISSING … so this server served a partial clone after being
/// told not to offer filter"*; with `promised` inverted, the default arm
/// reports the mirror complaint.
#[test]
fn a_declined_filter_serves_a_complete_clone_in_both_protocol_versions() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
for version in ["0", "2"] {
// ── the default server hands back a PARTIAL clone ───────────────────
let offered = Harness::start("team", "partial", HashKind::Sha1);
let blob = seed(&offered, "team/partial", "a blob worth filtering out\n");
let full = clone_filtered(&offered, "team/partial", version, "offered");
assert!(
!has_blob(&full, &blob),
"protocol v{version}: the DEFAULT server was asked for --filter=blob:none and handed \
back the blob anyway, so the arm below cannot tell a declined filter from a served \
one"
);
// ── --no-filter hands back everything ───────────────────────────────
let declined = Harness::start_with_wire("team", "complete", HashKind::Sha1, no_filter());
let blob = seed(&declined, "team/complete", "a blob worth filtering out\n");
let whole = clone_filtered(&declined, "team/complete", version, "declined");
assert!(
has_blob(&whole, &blob),
"protocol v{version}: the blob is MISSING from a clone taken off a server told not to \
offer `filter`, so this server served a partial clone after being told not to. \
`--no-filter` is decoration"
);
}
}
/// **The traversal budget reaches the walk.**
///
/// `Config::budget` bounds objects visited while answering one fetch. At `1`
/// this repository cannot be served, and the client is told so rather than
/// handed a short pack — which is the distinction worth asserting: a budget
/// that silently truncated would be a wrong answer, not a refusal.
///
/// **Twelve commits, not one, and the number is load-bearing.** Written first
/// against the one-commit fixture, this test failed at every budget from 1 to
/// 3 — the walk's check is `out.len() >= budget` at the TOP of each iteration,
/// so a three-object repository never reaches it. The blind version and the
/// wired one were indistinguishable, and only the control arm below said so.
///
/// Seen red: with `.with_config(wire.upload.clone())` reverted to
/// `UploadPack::new(read_side, &refs)`, the clone succeeds and this fails with
/// *"a budget of 1 served a clone"*.
///
/// # It was red from `e31ab8f4` to 2026-08-31, and the flag was the thing wrong
///
/// The wiring above is intact; what the budget stopped reaching is the *walk*,
/// because on the storage engine gunnar now ships there is no walk.
/// `select_objects` step 0 hands the whole `want`/`have` question to the store's
/// own reachability index and returns the finished closure
/// (`gunnar-wire/src/reachable.rs`), and `Graph::charge_walk` measures
/// `meter::live()` — ids a *walker* is holding, which is zero on that path. So
/// `--fetch-budget` was parsed, validated (`0` is refused at startup), carried
/// into `Config::budget`, and then compared against nothing: **accepted and
/// ignored**, on the appliance, on every unfiltered clone.
///
/// `Graph::charge_answer` is the fix and it is the same comparison
/// (`Graph::charge_budget`) that the walk uses, charged against the count the
/// store just named. This test is the guard for it, and the second assertion
/// below — on the sentence the client is sent — is what stops it passing again
/// for some other reason.
#[test]
fn a_traversal_budget_of_one_refuses_a_clone_rather_than_truncating_it() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let mut wire = WireConfig::default();
wire.upload.budget = Some(1);
let harness = Harness::start_with_wire("team", "budgeted", HashKind::Sha1, wire);
seed_many(&harness, "team/budgeted", 12);
let back = harness.dir.path().join("back");
let out = harness.git(
harness.dir.path(),
&[
"clone",
"--quiet",
&harness.url("team", "budgeted"),
back.to_str().unwrap(),
],
);
assert!(
!out.ok,
"a budget of 1 served a clone; the traversal budget never reached the walk. git said: \
{}",
out.stderr
);
// ...and refused for THIS reason. `!out.ok` alone is satisfied by a server
// that crashed, by an ssh failure, by a missing repository — every way a
// clone can end badly looks the same from the exit code, and this test's
// whole claim is that the client is TOLD rather than handed a short pack.
// gunnar sends it up the sideband, so it is in git's own stderr.
assert!(
out.stderr.contains("traversal exceeded its budget"),
"the clone was refused, but not by the budget — so this test would stay green if \
`--fetch-budget` went inert again and something else broke instead. git said: {}",
out.stderr
);
assert!(
!back.join(".git").exists() || !has_any_pack(&back),
"the clone was refused but a repository was still left behind at {}",
back.display()
);
// The same repository, the same client, the default budget: it clones.
// Without this the assertion above would pass against a server that could
// not serve anything at all.
let control = Harness::start("team", "budgeted", HashKind::Sha1);
seed_many(&control, "team/budgeted", 12);
let back = control.dir.path().join("back");
control
.git(
control.dir.path(),
&[
"clone",
"--quiet",
&control.url("team", "budgeted"),
back.to_str().unwrap(),
],
)
.expect_ok("the same clone under the default budget");
}
/// **`--fetch-budget 0` is refused rather than accepted as "no limit".**
///
/// The walk stops when it *reaches* the budget, so zero means a server that
/// answers no fetch at all. Accepting it would be a flag that reads as
/// unbounded and bricks the server — the exact shape of defect this whole item
/// is about, reintroduced by the fix for it.
///
/// Seen red: with the `ensure!` deleted, `from_serve_args` returns `Ok` and
/// this fails with *"--fetch-budget 0 was accepted"*.
#[test]
fn a_fetch_budget_of_zero_is_refused_at_startup_and_says_why() {
use clap::Parser as _;
let cli = gunnar_cli::cli::Cli::parse_from(["gunnar", "serve", "--fetch-budget", "0"]);
let gunnar_cli::cli::Command::Serve(args) = cli.command else {
panic!("`gunnar serve` did not parse as Serve");
};
let Err(err) = WireConfig::from_serve_args(&args) else {
panic!("--fetch-budget 0 was accepted; a server that answers no fetch is not a default");
};
let text = format!("{err}");
assert!(
text.contains("refuse every fetch"),
"the refusal must say what 0 would actually do, got {text:?}"
);
// And a stated number IS taken, so the arm above is about zero and not
// about the flag being ignored.
let cli = gunnar_cli::cli::Cli::parse_from(["gunnar", "serve", "--fetch-budget", "17"]);
let gunnar_cli::cli::Command::Serve(args) = cli.command else {
panic!("`gunnar serve` did not parse as Serve");
};
let wire = WireConfig::from_serve_args(&args).expect("17 is a legal budget");
assert_eq!(
wire.upload.budget,
Some(17),
"the flag did not reach the config"
);
}
/// **The byte ceilings, and the one value that removes them.**
///
/// `Limits`' fields are `Option`, so `0` on the command line has to become
/// `None` and not `Some(0)` — `Some(0)` would refuse every request while
/// reading as "no ceiling", the same trap `--fetch-budget 0` carries.
///
/// Seen red: with `(bytes > 0).then_some(bytes)` written as `Some(bytes)`,
/// the `0` arms fail with `Some(0)` against `None`.
#[test]
fn the_fetch_byte_ceilings_are_settable_and_zero_removes_them() {
use clap::Parser as _;
let parse = |args: &[&str]| -> WireConfig {
let mut argv = vec!["gunnar", "serve"];
argv.extend_from_slice(args);
let cli = gunnar_cli::cli::Cli::parse_from(argv);
let gunnar_cli::cli::Command::Serve(args) = cli.command else {
panic!("`gunnar serve` did not parse as Serve");
};
WireConfig::from_serve_args(&args).expect("a legal configuration")
};
// Untouched: gunnar-wire's own defaults, both on.
let default = parse(&[]);
assert_eq!(
default.upload.limits,
gunnar_wire::Limits::default(),
"an operator who said nothing must get gunnar-wire's defaults"
);
assert!(
default.upload.limits.request_bytes.is_some()
&& default.upload.limits.object_bytes.is_some(),
"both ceilings are on by default; if either were already None the 0 arms below would be \
indistinguishable from doing nothing"
);
assert!(
default.upload.offered.filter,
"filter is offered unless declined"
);
assert_eq!(
default.upload.want_policy,
gunnar_wire::WantPolicy::ReachableFromRefs,
"the default want policy is the one that does not break partial clone"
);
let set = parse(&[
"--max-fetch-request-bytes",
"4096",
"--max-fetch-object-bytes",
"8192",
]);
assert_eq!(set.upload.limits.request_bytes, Some(4096));
assert_eq!(set.upload.limits.object_bytes, Some(8192));
let off = parse(&[
"--max-fetch-request-bytes",
"0",
"--max-fetch-object-bytes",
"0",
]);
assert_eq!(
off.upload.limits.request_bytes, None,
"0 must REMOVE the ceiling; Some(0) would refuse every request while reading as unlimited"
);
assert_eq!(off.upload.limits.object_bytes, None);
// And the want policy, whose three arms are the reason it is an enum.
for (flag, expected) in [
("advertised-tips", gunnar_wire::WantPolicy::AdvertisedTips),
("reachable", gunnar_wire::WantPolicy::ReachableFromRefs),
("any", gunnar_wire::WantPolicy::Any),
] {
assert_eq!(
parse(&["--want-policy", flag]).upload.want_policy,
expected,
"--want-policy {flag} did not reach the config"
);
}
assert!(
!parse(&["--no-filter"]).upload.offered.filter,
"--no-filter did not reach the config"
);
}
// ───────────────────────────────────────────────────────────────────────────
// Fixture
// ───────────────────────────────────────────────────────────────────────────
/// Push one commit and hand back the blob id its file has, so a test can ask
/// whether a clone holds that exact object.
fn seed(harness: &Harness, repo: &str, content: &str) -> String {
let (namespace, name) = repo.split_once('/').expect("<ns>/<repo>");
let work = harness.dir.path().join(format!("work-{name}"));
local_repo(harness, &work, HashKind::Sha1, content);
harness
.git(
&work,
&[
"push",
&harness.url(namespace, name),
"HEAD:refs/heads/main",
],
)
.expect_ok("the seeding push");
let out = harness.git(&work, &["rev-parse", "HEAD:hello.txt"]);
let blob = out.stdout.trim().to_owned();
assert_eq!(
blob.len(),
40,
"the fixture produced no blob id ({blob:?}); every `has_blob` below would be about \
nothing. git said: {}",
out.stderr
);
blob
}
/// `git clone --bare --filter=blob:none` at an explicit protocol version.
fn clone_filtered(harness: &Harness, repo: &str, version: &str, tag: &str) -> std::path::PathBuf {
let (namespace, name) = repo.split_once('/').expect("<ns>/<repo>");
let into = harness.dir.path().join(format!("clone-{tag}-v{version}"));
harness
.git(
harness.dir.path(),
&[
"-c",
&format!("protocol.version={version}"),
"clone",
"--quiet",
// BARE, and that is the whole reason this test can see
// anything: `git clone --filter=blob:none` with a working tree
// checks it out, and the checkout lazily fetches every blob it
// just filtered away. A non-bare clone therefore holds the blob
// on BOTH servers, and the control arm below caught exactly
// that -- this test was blind once and says so.
"--bare",
"--filter=blob:none",
&harness.url(namespace, name),
into.to_str().unwrap(),
],
)
.expect_ok("a filtered clone");
into
}
/// Is `blob` present **locally**, without going back to the promisor remote?
///
/// `cat-file -e` alone is not enough: in a partial clone it happily fetches the
/// missing object from the remote and reports success. `core.promisor` is
/// switched off for the check so the question is about this repository's own
/// object store.
fn has_blob(clone: &std::path::Path, blob: &str) -> bool {
std::process::Command::new("git")
.current_dir(clone)
.env("GIT_NO_LAZY_FETCH", "1")
.args(["cat-file", "-e", blob])
.output()
.expect("git is on PATH")
.status
.success()
}
/// Does this directory hold any packfile at all?
fn has_any_pack(repo: &std::path::Path) -> bool {
let pack_dir = repo.join(".git").join("objects").join("pack");
std::fs::read_dir(pack_dir)
.map(|entries| {
entries
.flatten()
.any(|e| e.path().extension().and_then(|x| x.to_str()) == Some("pack"))
})
.unwrap_or(false)
}
/// Push `commits` commits, each touching a new file, so a walk has something to
/// spend a budget on.
fn seed_many(harness: &Harness, repo: &str, commits: usize) {
let (namespace, name) = repo.split_once('/').expect("<ns>/<repo>");
let work = harness.dir.path().join(format!("work-many-{name}"));
local_repo(harness, &work, HashKind::Sha1, "first\n");
for n in 1..commits {
std::fs::write(work.join(format!("f{n}.txt")), format!("body {n}\n"))
.expect("write a file");
harness.git(&work, &["add", "-A"]).expect_ok("git add");
harness
.git(&work, &["commit", "--quiet", "-m", &format!("commit {n}")])
.expect_ok("git commit");
}
harness
.git(
&work,
&[
"push",
&harness.url(namespace, name),
"HEAD:refs/heads/main",
],
)
.expect_ok("the seeding push");
}
// ── the LFS quota, which no command line could reach ────────────────────────
/// **`gunnar_lfs::Limits` was a fourth knob wired to nothing.**
///
/// `Session::with_limits` and the builders behind it had zero call sites
/// outside `gunnar-lfs/tests/quota.rs`, and the SSH transport said
/// `Session::new(&lfs, operation)`. Every deployment therefore served on 8 GiB
/// / 10 000 / 128 KiB and an operator could not state any of them — the same
/// shape as `Offered`/`WantPolicy`/`budget` above, one crate over.
///
/// This drives [`WireConfig::lfs_session`], which is now the **only** place an
/// LFS session is built. A test that constructed its own `Session` could sit
/// green while the server ran on defaults forever; there is nothing else to
/// construct.
///
/// The assertion is applied output — the status codes on the wire — and not
/// `wire.lfs.max_object_bytes`, because a config field holding 1024 proves
/// nothing about what the session enforces.
#[test]
fn the_lfs_quota_flags_reach_the_session_the_server_actually_builds() {
use clap::Parser as _;
use gunnar_lfs::{DiscardingLfsStore, Oid, Operation};
use std::io::BufRead;
let parse = |args: &[&str]| -> WireConfig {
let mut argv = vec!["gunnar", "serve"];
argv.extend_from_slice(args);
let cli = gunnar_cli::cli::Cli::parse_from(argv);
let gunnar_cli::cli::Command::Serve(args) = cli.command else {
panic!("`gunnar serve` did not parse as Serve");
};
WireConfig::from_serve_args(&args).expect("a legal configuration")
};
/// `version`, one `batch` naming an upload of `size` bytes, `quit`.
fn batch_script(size: u64) -> Vec<u8> {
let oid = Oid::parse(&"1".repeat(64)).unwrap();
let mut s = Vec::new();
gunnar_pktline::write_text(&mut s, b"version 1").unwrap();
gunnar_pktline::write_flush(&mut s).unwrap();
gunnar_pktline::write_text(&mut s, b"batch").unwrap();
gunnar_pktline::write_text(&mut s, b"hash-algo=sha256").unwrap();
gunnar_pktline::write_delim(&mut s).unwrap();
gunnar_pktline::write_text(&mut s, format!("{oid} {size}").as_bytes()).unwrap();
gunnar_pktline::write_flush(&mut s).unwrap();
gunnar_pktline::write_text(&mut s, b"quit").unwrap();
gunnar_pktline::write_flush(&mut s).unwrap();
s
}
fn statuses(wire: &[u8]) -> Vec<u16> {
String::from_utf8_lossy(wire)
.split("status ")
.skip(1)
.filter_map(|rest| rest.split('\n').next()?.trim().parse().ok())
.collect()
}
let serve = |wire: &WireConfig, size: u64| -> Vec<u16> {
let store = DiscardingLfsStore::new();
let mut input = std::io::BufReader::new(std::io::Cursor::new(batch_script(size)));
let mut output = Vec::new();
let reader: &mut dyn BufRead = &mut input;
wire.lfs_session(&store, Operation::Upload)
.serve(reader, &mut output)
.expect("a well-framed conversation");
statuses(&output)
};
// Stated: the boundary moves to 1024.
let set = parse(&["--max-lfs-object-bytes", "1024"]);
assert_eq!(
set.lfs.max_object_bytes, 1024,
"the flag did not reach the config at all"
);
assert_eq!(
serve(&set, 1025),
vec![200, 413, 200],
"1025 bytes must be refused under a 1024-byte quota"
);
assert_eq!(
serve(&set, 1024),
vec![200, 200, 200],
"exactly the stated quota must be accepted; `>` and not `>=`"
);
// The control, on a server that differs in exactly one field: at the
// default quota the same 1025 bytes sail through. Without this the arm
// above could be green on a session that refuses everything.
let default = parse(&[]);
assert_eq!(
default.lfs,
gunnar_lfs::Limits::default(),
"an operator who said nothing must get gunnar-lfs's defaults"
);
assert_eq!(
serve(&default, 1025),
vec![200, 200, 200],
"1025 bytes are far under the 8 GiB default and must be accepted"
);
// The other two flags are `usize`/`u64` with no `Option`, so they are
// stated or absent; `0` is refused rather than read as "unbounded".
let both = parse(&[
"--max-lfs-batch-objects",
"3",
"--max-lfs-drain-bytes",
"4096",
]);
assert_eq!(both.lfs.max_batch_objects, 3);
assert_eq!(both.lfs.max_drain_bytes, 4096);
for zero in [
vec!["--max-lfs-object-bytes", "0"],
vec!["--max-lfs-batch-objects", "0"],
vec!["--max-lfs-drain-bytes", "0"],
] {
let mut argv = vec!["gunnar", "serve"];
argv.extend_from_slice(&zero);
let cli = gunnar_cli::cli::Cli::parse_from(argv);
let gunnar_cli::cli::Command::Serve(args) = cli.command else {
panic!("Serve");
};
let err = WireConfig::from_serve_args(&args).expect_err(&format!(
"{zero:?} must be refused, not accepted as unbounded"
));
assert!(
err.to_string().contains("0 would"),
"{zero:?} was refused without saying what 0 would do; got {err}"
);
}
}