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

nordisk/gunnar

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}"
        );
    }
}