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/control_cli.rs

//! **`gunnar control`, driven as a real binary against a real server.**
//!
//! The four steps of the enrolment story, in order, each run as an actual process
//! with an actual exit code, because a first-time user runs a command and reads
//! what it printed. Nothing here calls a library function that the CLI happens
//! to also call.
//!
//! ```text
//!   gunnar control status                          no auth -> "OPEN"
//!   gunnar control bootstrap alice --key @…      no auth -> gunnar-admin
//!   gunnar control bootstrap …  (again)            refused, and says what to do
//!   gunnar control login -i …                      the ssh key, and no session
//!   gunnar control user add bob --key @…        authenticated
//!   gunnar control user add … (as bob)          refused: not gunnar-admin
//! ```
//!
//! Two failures are asserted to be **different sentences**, because the whole
//! design turns on them not being confusable: a server that is already enrolled
//! and a server that cannot be reached.

use std::io::BufRead as _;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};

/// A `gunnar serve` child process on ephemeral loopback ports.
struct Serving {
    child: Child,
    grpc: String,
    _dir: tempfile::TempDir,
    home: PathBuf,
    /// Where the server's stderr lands, so a test can assert on the log an
    /// operator would be reading.
    log_path: PathBuf,
}

impl Drop for Serving {
    fn drop(&mut self) {
        let _ = self.child.kill();
        let _ = self.child.wait();
    }
}

fn gunnar_binary() -> PathBuf {
    let mut dir = std::env::current_exe().expect("the test binary has a path");
    dir.pop(); // deps
    dir.pop(); // debug
    let candidate = dir.join("gunnar-cli");
    assert!(
        candidate.is_file(),
        "the `gunnar-cli` binary was not built beside this test at {}; every assertion here is \
         about what that binary PRINTS, so there is nothing to fall back to",
        candidate.display()
    );
    candidate
}

impl Serving {
    /// Start a server with **no identity at all** and wait for its banner.
    fn fresh() -> Serving {
        Serving::fresh_at("info")
    }

    /// [`Serving::fresh`] at a chosen `RUST_LOG`.
    ///
    /// A parameter because the log-level reload's acknowledgement ORDERING is only
    /// observable below `warn`: at `info` a `warn!` is printable under both the
    /// old filter and the new one, so a server started at `info` cannot tell
    /// announce-then-reload from reload-then-announce.
    fn fresh_at(rust_log: &str) -> Serving {
        let dir = tempfile::tempdir().expect("a temp dir");
        let home = dir.path().join("home");
        std::fs::create_dir_all(&home).expect("mkdir");
        // The server's log goes to a FILE, not to `/dev/null`. It was
        // `Stdio::null()` until the request id landed, which meant nothing in
        // this file could ever assert that the id the CLI prints names a line
        // the server actually wrote — and that join is the whole feature. A
        // file rather than `Stdio::piped()` because nobody is draining the
        // pipe: a server that logs more than a pipe buffer would block on the
        // write and this suite would read as a hang.
        let log_path = dir.path().join("server.log");
        let log = std::fs::File::create(&log_path).expect("create the server log");
        let mut child = Command::new(gunnar_binary())
            .arg("--data-root")
            .arg(dir.path().join("data"))
            .arg("serve")
            .arg("--ssh-bind")
            .arg("127.0.0.1:0")
            .arg("--grpc-bind")
            .arg("127.0.0.1:0")
            .env("HOME", &home)
            // Not inherited from the test runner, which may carry any filter.
            // `info` is what a shipped server runs at, and the assertions below
            // are about what an operator sees at that level and no other.
            .env("RUST_LOG", rust_log)
            .stdout(Stdio::piped())
            .stderr(Stdio::from(log))
            .spawn()
            .expect("spawn gunnar serve");
        let stdout = child.stdout.take().expect("piped");
        let mut reader = std::io::BufReader::new(stdout);
        let mut grpc = None;
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
        while std::time::Instant::now() < deadline {
            let mut line = String::new();
            match reader.read_line(&mut line) {
                Ok(0) | Err(_) => break,
                Ok(_) => {}
            }
            if let Some(rest) = line.strip_prefix("grpc ") {
                let addr = rest.split_whitespace().next().unwrap_or_default();
                if !addr.is_empty() && addr != "disabled" {
                    grpc = Some(addr.to_owned());
                    break;
                }
            }
        }
        let grpc = grpc.expect(
            "gunnar serve never announced a gRPC address. On a server with NO identities that \
             used to be a refusal to start, which is the defect this whole feature exists to \
             fix — so a missing banner here is the regression, not a flake.",
        );
        // ── keep draining stdout, for the server's LIFE ────────────────────
        //
        // Dropping `reader` here closes the read end of the pipe, and the next
        // `println!` in the server then fails — Rust ignores SIGPIPE, so a
        // failed `print` PANICS and the process dies. Nothing noticed while the
        // gRPC line happened to be the last thing the banner printed; adding
        // one line after it (the `log …` line) turned this suite into a 1-in-8
        // flake whose symptom was "the control plane could not be reached",
        // i.e. it looked exactly like a server bug.
        //
        // A thread rather than `Stdio::null()`, because the banner is evidence:
        // it is what the four-step test reads.
        std::thread::spawn(move || {
            let mut sink = String::new();
            let _ = std::io::Read::read_to_string(&mut reader, &mut sink);
        });
        Serving {
            child,
            grpc,
            _dir: dir,
            home,
            log_path,
        }
    }

    /// Everything the server has written to its own stderr so far.
    ///
    /// Read from the file each time rather than cached: the interesting lines
    /// are written *by the call the test just made*.
    fn log(&self) -> String {
        std::fs::read_to_string(&self.log_path).unwrap_or_default()
    }
}

/// What a `gunnar` invocation did.
struct Ran {
    ok: bool,
    out: String,
}

impl Ran {
    fn says(&self, needle: &str) -> bool {
        self.out.contains(needle)
    }
}

/// Run the real binary. `HOME` is the server's temp home, so key discovery and
/// any state the CLI might write land somewhere this test can inspect.
fn gunnar(home: &Path, args: &[&str]) -> Ran {
    let out = Command::new(gunnar_binary())
        .args(args)
        .env("HOME", home)
        .env_remove("GUNNAR_CONTROL_ADDR")
        .env_remove("SSH_AUTH_SOCK")
        .output()
        .expect("run gunnar");
    Ran {
        ok: out.status.success(),
        out: format!(
            "{}{}",
            String::from_utf8_lossy(&out.stdout),
            String::from_utf8_lossy(&out.stderr)
        ),
    }
}

/// Every `*.pem` directly in `dir`: what a bootstrap run from `dir` must not add to.
fn pem_files(dir: &Path) -> std::collections::BTreeSet<PathBuf> {
    std::fs::read_dir(dir)
        .map(|entries| {
            entries
                .flatten()
                .map(|e| e.path())
                .filter(|p| p.extension().is_some_and(|x| x == "pem"))
                .collect()
        })
        .unwrap_or_default()
}

/// Mint an ed25519 pair into `dir`, returning `(private path, public path)`.
fn mint(dir: &Path, name: &str) -> (PathBuf, PathBuf) {
    let private = gunnar_core::mint_ed25519().expect("mint");
    let parsed =
        gunnar_core::ssh_key::PrivateKey::from_openssh(&private).expect("the minted key parses");
    let public = parsed.public_key().to_openssh().expect("render");
    let priv_path = dir.join(name);
    let pub_path = dir.join(format!("{name}.pub"));
    std::fs::write(&priv_path, &private).expect("write");
    std::fs::write(&pub_path, format!("{public}\n")).expect("write");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt as _;
        std::fs::set_permissions(&priv_path, std::fs::Permissions::from_mode(0o600)).expect("0600");
    }
    (priv_path, pub_path)
}

/// Every file under a directory, so "the CLI wrote nothing" can be asserted
/// rather than assumed.
fn snapshot(root: &Path) -> std::collections::BTreeSet<PathBuf> {
    let mut out = std::collections::BTreeSet::new();
    let mut stack = vec![root.to_path_buf()];
    while let Some(dir) = stack.pop() {
        let Ok(entries) = std::fs::read_dir(&dir) else {
            continue;
        };
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                stack.push(path.clone());
            }
            out.insert(path);
        }
    }
    out
}

// ───────────────────────────────────────────────────────────────────────────

/// **Unreachable is not unenrolled.** The two sentences must not be the same
/// sentence; reading the second as the first is how a closed door reads as an
/// open one.
#[test]
fn an_unreachable_control_plane_says_unreachable_and_never_says_unenrolled() {
    let dir = tempfile::tempdir().expect("a temp dir");
    // Port 1 on loopback: nothing listens there and connecting is refused
    // immediately, so this is a transport failure and not a timeout.
    let ran = gunnar(dir.path(), &["control", "status", "--at", "127.0.0.1:1"]);
    assert!(!ran.ok, "a status against nothing must not exit 0");
    assert!(
        ran.says("could not be reached"),
        "an unreachable control plane must SAY so: {}",
        ran.out
    );
    assert!(
        !ran.says("bootstrap      OPEN"),
        "…and it must never be reported as an unenrolled server: {}",
        ran.out
    );
    // The other direction, so the assertion above is not a string that could
    // never appear: a REAL server does print the open-window line.
    let serving = Serving::fresh();
    let ran = gunnar(&serving.home, &["control", "status", "--at", &serving.grpc]);
    assert!(ran.ok, "status against a live server: {}", ran.out);
    assert!(
        ran.says("bootstrap      OPEN"),
        "a fresh server must report the window open: {}",
        ran.out
    );
    assert!(!ran.says("could not be reached"));
}

/// **A fresh box's `control status` prints its machine in `control capacity`'s own lines**
/// (T17, for the twin election): `cores`, `ram total`, `total`, `disktest`, `disk write`,
/// `disk read`, `disk sync` — label first, the raw integer or `unmeasured` next — so
/// `xtask elect twin`'s `parse_capacity` reads it with no second parser. `not-run` here:
/// no start-up disk test runs outside the appliance's PID 1, and that is a word, never a 0.
#[test]
fn a_fresh_boxs_status_prints_its_machine_in_the_capacity_lines() {
    let serving = Serving::fresh();
    let ran = gunnar(&serving.home, &["control", "status", "--at", &serving.grpc]);
    assert!(ran.ok, "{}", ran.out);
    let after = |k: &str| -> Option<String> {
        ran.out.lines().find_map(|l| {
            l.strip_prefix(k)
                .filter(|r| r.starts_with(char::is_whitespace))
                .map(|r| r.trim().to_owned())
        })
    };
    let num = |k: &str| {
        after(k).and_then(|r| {
            r.split_whitespace()
                .next()
                .and_then(|t| t.parse::<u64>().ok())
        })
    };
    assert!(
        num("cores").is_some_and(|m| m >= 1000 && m % 1000 == 0),
        "{}",
        ran.out
    );
    assert!(num("ram total").is_some_and(|b| b > 0), "{}", ran.out);
    assert!(num("total").is_some_and(|b| b > 0), "{}", ran.out);
    assert_eq!(after("disktest").as_deref(), Some("not-run"), "{}", ran.out);
    for k in ["disk write", "disk read", "disk sync"] {
        assert!(
            after(k).is_some_and(|r| r.starts_with("unmeasured")),
            "{k}: {}",
            ran.out
        );
    }
    assert!(ran.says("machine        this box is fresh"), "{}", ran.out);
}

/// **The whole four-step flow, as a first-time user types it.**
#[test]
fn the_four_step_flow_runs_end_to_end() {
    let serving = Serving::fresh();
    let keys = serving.home.join("keys");
    std::fs::create_dir_all(&keys).expect("mkdir");
    let (alice_key, alice_pub) = mint(&keys, "alice");
    let (bob_key, bob_pub) = mint(&keys, "bob");

    // ── 1: bootstrap, with NO credential and no private key in sight ──────
    let before = snapshot(&serving.home);
    let cwd = std::env::current_dir().expect("cwd");
    let pems_in_cwd_before = pem_files(&cwd);
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "bootstrap",
            "alice",
            "--key",
            &format!("@{}", alice_pub.display()),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "the first bootstrap must succeed: {}", ran.out);
    assert!(ran.says("enrolled  alice"), "{}", ran.out);
    assert!(ran.says("gunnar-admin"), "{}", ran.out);
    assert!(
        ran.says("account   alice"),
        "it must name the account the user owns: {}",
        ran.out
    );

    // ── 2: LOGGED OUT. The ONLY thing written is the credential the server
    // issued, in a directory of the user's own — and NOT in the current
    // directory, where it sat until 2026-09-10 (this crate's directory, for
    // this very test; a git checkout, for an operator).
    let after = snapshot(&serving.home);
    let created: std::collections::BTreeSet<_> = after.difference(&before).cloned().collect();
    let server = serving.grpc.replace(':', "_");
    let credential_dir = serving.home.join(".config").join("gunnar").join(&server);
    let expected: std::collections::BTreeSet<PathBuf> = [
        serving.home.join(".config"),
        serving.home.join(".config").join("gunnar"),
        credential_dir.clone(),
        credential_dir.join("alice.cert.pem"),
        credential_dir.join("alice.key.pem"),
        credential_dir.join("gunnar-issuer.pem"),
    ]
    .into_iter()
    .collect();
    assert_eq!(
        created, expected,
        "`gunnar control bootstrap` may write exactly the issued credential under \
         ~/.config/gunnar/<server>/ and nothing else: no session, no state. It must keep NO \
         session: the unauthenticated door closed behind it, and a client that held \
         privileged state would be the same defect as a server that held the door open. \
         Output:\n{}",
        ran.out
    );
    assert_eq!(
        pem_files(&cwd),
        pems_in_cwd_before,
        "bootstrap wrote a .pem into the current directory {}: that is the 2026-09-10 defect, \
         a private key one `git add` from a remote",
        cwd.display()
    );
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt as _;
        let mode = |p: &Path| std::fs::metadata(p).expect("stat").permissions().mode() & 0o777;
        assert_eq!(
            mode(&credential_dir),
            0o700,
            "the credential directory is the owner's alone"
        );
        assert_eq!(
            mode(&credential_dir.join("alice.key.pem")),
            0o600,
            "the private key is 0600"
        );
    }
    assert!(
        ran.says(&credential_dir.join("alice.key.pem").display().to_string()),
        "and the command names where it put the key, so the next flag has a path to point \
         at: {}",
        ran.out
    );
    assert!(
        ran.says("logged out"),
        "and it must say so, so the operator is not left assuming a session: {}",
        ran.out
    );

    // …proved by behaviour, not only by the filesystem: the very next
    // privileged call with NO key fails.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "user",
            "add",
            "bob",
            "--key",
            &format!("@{}", bob_pub.display()),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        !ran.ok,
        "creating a user immediately after bootstrap, with no key, must FAIL: {}",
        ran.out
    );
    assert!(
        ran.says("no ssh private key found") || ran.says("did not accept your key"),
        "and it must say the credential is missing, not something else: {}",
        ran.out
    );

    // ── 1b: a second bootstrap is refused, and says what to do instead ────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "bootstrap",
            "bob",
            "--key",
            &format!("@{}", bob_pub.display()),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(!ran.ok, "a second bootstrap must fail: {}", ran.out);
    assert!(
        ran.says("already been bootstrapped"),
        "it must name the cause: {}",
        ran.out
    );
    assert!(
        ran.says("gunnar control user add"),
        "…and say what to do instead, or the user is stuck: {}",
        ran.out
    );
    assert!(
        ran.says("does NOT reopen"),
        "…and say that deleting identities will not help, because that is what they will try \
         next: {}",
        ran.out
    );
    // ── the id, on the ONE refusal that used to lose it ───────────────────
    //
    // This arm writes its own prose instead of going through `explain`, so it
    // bypassed the single site that appends the request id and printed none —
    // on the refusal a first-time user is most likely to hit, and the one where
    // "which line in the server's log is this" is hardest to answer any other
    // way, because the caller holds no credential and cannot ask the server
    // anything else.
    //
    // Asserted against the SERVER'S OWN LOG, not merely on the shape of the
    // string: a `request-id:` clause that named a line nobody wrote would read
    // exactly like this one.
    let id = ran
        .out
        .split("request-id: ")
        .nth(1)
        .map(|rest| {
            rest.split_whitespace()
                .next()
                .unwrap_or_default()
                .to_owned()
        })
        .unwrap_or_else(|| {
            panic!(
                "the refused second bootstrap printed no request-id. This arm builds its own \
                 message and used to skip the one writer that appends it:\n{}",
                ran.out
            )
        });
    assert_eq!(
        id.len(),
        16,
        "the printed request-id {id:?} is not the 16 hex digits a server line carries"
    );
    let log = serving.log();
    assert!(
        log.lines()
            .any(|line| line.contains(&format!("req={id}")) && line.contains("grpc.request.done")),
        "the CLI printed request-id {id} and the server wrote no `grpc.request.done` line \
         carrying `req={id}`, so the id names nothing an operator can grep for. Server log:\n{log}"
    );

    // ── 3: log in with the ssh key ────────────────────────────────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "login",
            "-i",
            alice_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "login with the enrolled key: {}", ran.out);
    assert!(ran.says("logged in"), "{}", ran.out);
    assert!(
        ran.says("gunnar-admin"),
        "the first user is gunnar-admin: {}",
        ran.out
    );

    // A key nobody registered is refused, and refused as a KEY problem.
    let (stranger_key, _) = mint(&keys, "stranger");
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "login",
            "-i",
            stranger_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(!ran.ok, "an unregistered key must not log in: {}", ran.out);
    assert!(
        ran.says("did not accept your key"),
        "and it must say it is the key: {}",
        ran.out
    );
    assert!(
        !ran.says("could not be reached"),
        "…never that the server is unreachable, because it plainly is not: {}",
        ran.out
    );

    // ── 4: create a user, authenticated ───────────────────────────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "user",
            "add",
            "bob",
            "--key",
            &format!("@{}", bob_pub.display()),
            "-i",
            alice_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "a gunnar-admin may create a user: {}", ran.out);
    assert!(ran.says("created   bob"), "{}", ran.out);
    assert!(
        ran.says("ordinary user"),
        "a user created without --gunnar-admin is an ordinary user: {}",
        ran.out
    );

    // bob can log in, and is told he is NOT gunnar-admin rather than being
    // told his key failed.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "login",
            "-i",
            bob_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "an ordinary user can log in: {}", ran.out);
    assert!(ran.says("not gunnar-admin"), "{}", ran.out);
    assert!(
        ran.says("admin of your own account"),
        "…and is told what he DOES have: {}",
        ran.out
    );

    // ── and he may not create users ───────────────────────────────────────
    let (mallory_key, mallory_pub) = mint(&keys, "mallory");
    let _ = mallory_key;
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "user",
            "add",
            "mallory",
            "--key",
            &format!("@{}", mallory_pub.display()),
            "-i",
            bob_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        !ran.ok,
        "an ordinary user must NOT be able to create users: {}",
        ran.out
    );
    assert!(
        ran.says("not allowed to do this"),
        "and must be told it is authorisation, not authentication: {}",
        ran.out
    );

    // ── gunnar-admin is grantable onward, by any gunnar-admin ─────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "grant",
            "bob",
            "admin",
            "--server",
            "-i",
            alice_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        ran.ok,
        "a gunnar-admin may hand gunnar-admin on: {}",
        ran.out
    );
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "user",
            "add",
            "mallory",
            "--key",
            &format!("@{}", mallory_pub.display()),
            "-i",
            bob_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        ran.ok,
        "bob is gunnar-admin now and must be able to create users: {}",
        ran.out
    );

    // ── and removing every user still does not reopen the door ────────────
    let ran = gunnar(&serving.home, &["control", "status", "--at", &serving.grpc]);
    assert!(ran.ok);
    assert!(
        ran.says("bootstrap      closed"),
        "the window is closed and stays closed: {}",
        ran.out
    );
    assert!(
        ran.says("alice"),
        "…and the server says who claimed it: {}",
        ran.out
    );
}

// ───────────────────────────────────────────────────────────────────────────
// The fresh-server story at its real scale
// ───────────────────────────────────────────────────────────────────────────

/// **Two gunnar-admins, a hundred ordinary users, and the split holds.**
///
/// The four-step flow above proves the verbs at three users; this proves the
/// same door at the population a real server starts life with: `alice` and
/// `bob` as gunnar-admins, `user1`…`user100` as ordinary users. Every call
/// is the real binary against the real live server, authenticated per call
/// with the caller's own key — nothing here reaches around the transport.
///
/// Both directions, so neither half is vacuous:
///
///  - **green** — each admin creates fifty of the hundred, which is the only
///    proof that bob's `--gunnar-admin` grant actually grants;
///  - **red** — an ordinary user is refused BOTH admin verbs over the wire
///    (create a user, grant himself gunnar-admin), and a login with the same
///    key proves the refusals are authorisation, not a broken credential.
///
/// The final sweep stops the server and reads the table itself: the admin set
/// must be EXACTLY `{alice, bob}` across all 102 principals, so one stray
/// server-wide grant to any of the hundred fails the test even if every
/// over-the-wire refusal above passed.
///
/// Seen red (measure, never assert), with a temporary `grant user57 admin
/// --server` inserted before the red legs, each guard individually: the
/// user-add refusal fails with *"user57, an ordinary user, was allowed to
/// create a user"*; the self-grant refusal with *"granted user57 Admin on the
/// whole server"*; the login guard reports *"role gunnar-admin"*; the count
/// sees 103 (mallory got created); and the sweep fails with the admin set at
/// `{"bob", "alice", "user57"}`.
#[test]
fn two_admins_create_a_hundred_users_and_the_split_holds() {
    let mut serving = Serving::fresh();
    let keys = serving.home.join("keys");
    std::fs::create_dir_all(&keys).expect("mkdir");
    let (alice_key, alice_pub) = mint(&keys, "alice");
    let (bob_key, bob_pub) = mint(&keys, "bob");

    // ── the no-auth logon: an empty auth table reports its window open ────
    let ran = gunnar(&serving.home, &["control", "status", "--at", &serving.grpc]);
    assert!(ran.ok, "status against the fresh server: {}", ran.out);
    assert!(
        ran.says("bootstrap      OPEN"),
        "an empty auth table must serve status with no credential and say the window is open: {}",
        ran.out
    );

    // ── admin 1: alice, through the unauthenticated bootstrap door ──────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "bootstrap",
            "alice",
            "--key",
            &format!("@{}", alice_pub.display()),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "bootstrap with no credential: {}", ran.out);
    assert!(ran.says("gunnar-admin"), "{}", ran.out);

    // ── admin 2: bob, created WITH the grant by the first admin ────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "user",
            "add",
            "bob",
            "--key",
            &format!("@{}", bob_pub.display()),
            "--gunnar-admin",
            "-i",
            alice_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "alice creates bob as gunnar-admin: {}", ran.out);
    assert!(
        ran.says("role      gunnar-admin"),
        "bob must come back an admin, not an ordinary user: {}",
        ran.out
    );

    // ── the hundred: fifty by each admin, every one an ordinary user ──────
    for i in 1..=100u32 {
        let name = format!("user{i}");
        let (_, user_pub) = mint(&keys, &name);
        let admin_key = if i <= 50 { &alice_key } else { &bob_key };
        let ran = gunnar(
            &serving.home,
            &[
                "control",
                "user",
                "add",
                &name,
                "--key",
                &format!("@{}", user_pub.display()),
                "-i",
                admin_key.to_str().unwrap(),
                "--at",
                &serving.grpc,
            ],
        );
        assert!(ran.ok, "{name} must be creatable: {}", ran.out);
        assert!(
            ran.says("role      ordinary user"),
            "{name} must be an ordinary user: {}",
            ran.out
        );
    }

    // ── the server's own count, over the wire ─────────────────────────────
    let ran = gunnar(&serving.home, &["control", "status", "--at", &serving.grpc]);
    assert!(ran.ok, "{}", ran.out);
    assert!(
        ran.says("principals     102"),
        "2 admins + 100 users must be 102 principals: {}",
        ran.out
    );
    assert!(
        // 102 ssh keys plus the bootstrap certificate alice left with.
        ran.says("keys           103"),
        "every principal keyed, plus alice's bootstrap certificate: {}",
        ran.out
    );

    // ── RED: an ordinary user is refused both admin verbs ─────────────────
    //
    // user57 — created by bob, so this leg also depends on admin 2 having
    // been a real admin. His own key signs both calls.
    let user57_key = keys.join("user57");
    let (_, mallory_pub) = mint(&keys, "mallory");
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "user",
            "add",
            "mallory",
            "--key",
            &format!("@{}", mallory_pub.display()),
            "-i",
            user57_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        !ran.ok,
        "user57, an ordinary user, was allowed to create a user: {}",
        ran.out
    );
    assert!(
        ran.says("not allowed to do this"),
        "and the refusal must be authorisation by name: {}",
        ran.out
    );
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "grant",
            "user57",
            "admin",
            "--server",
            "-i",
            user57_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        !ran.ok,
        "user57 was allowed to grant HIMSELF gunnar-admin, which is the whole \
         privilege-escalation door: {}",
        ran.out
    );
    assert!(ran.says("not allowed to do this"), "{}", ran.out);

    // …and the key itself is fine, so the two refusals were the ACL speaking.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "login",
            "-i",
            user57_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "user57 can log in: {}", ran.out);
    assert!(
        ran.says("not gunnar-admin"),
        "and is told what he is: {}",
        ran.out
    );

    // Nothing landed on the way out: mallory does not exist.
    let ran = gunnar(&serving.home, &["control", "status", "--at", &serving.grpc]);
    assert!(
        ran.says("principals     102"),
        "the refused calls must not have created anything: {}",
        ran.out
    );

    // ── the sweep: the admin set is EXACTLY the two, out of 102 ───────────
    //
    // The server is stopped first because a serving gunnar holds the control
    // store's exclusive lock; the read is against the same data path the live
    // assertions above were served from.
    serving.child.kill().expect("stop the server");
    serving.child.wait().expect("reap it");
    let stores = gunnar_cli::server::ServerStores::open(serving._dir.path().join("data"))
        .expect("open the stores the server just served from");
    let auth = stores.auth();
    let principals = auth.principals().expect("read the table");
    assert_eq!(principals.len(), 102, "2 admins + 100 users");
    let admins: std::collections::BTreeSet<String> = principals
        .iter()
        .filter(|p| {
            auth.may_server(&p.id, gunnar_core::Level::Admin)
                .expect("acl")
        })
        .map(|p| p.id.clone())
        .collect();
    assert_eq!(
        admins,
        ["alice", "bob"]
            .into_iter()
            .map(str::to_owned)
            .collect::<std::collections::BTreeSet<_>>(),
        "the gunnar-admin set must be exactly the two admins; anybody else in it means the \
         split did not hold"
    );
}

// ───────────────────────────────────────────────────────────────────────────
// The verbs that used to need the server stopped
// ───────────────────────────────────────────────────────────────────────────

/// **Every administrative verb below used to require stopping the server.**
///
/// The offline `gunnar namespace`, `gunnar repo delete` and the visibility and
/// default-branch settings write the control repository *directly*, and a
/// running `gunnar serve` holds that store's exclusive lock. So an operator who
/// wanted to delete a repository had to take the whole forge down to do it.
/// The RPCs to do all of it online had been served since the control plane was
/// written; `gunnar control` covered `user`, `repo create/list`, `grant` and
/// `bootstrap`, and nothing else.
///
/// The claim under test is therefore not "the verb works". It is **"the verb
/// works while the server is up"**, so the server is never stopped anywhere in
/// this test and the last act is a `list` against the same live process.
///
/// Seen red: with the `Namespace` arm removed from `ControlCommand`, clap
/// refuses the argument and the first assertion fails with *"unrecognized
/// subcommand 'namespace'"*. With `--yes` accepted but ignored, the guard arm
/// fails with *"a delete with no --yes deleted it"*.
#[test]
fn namespace_repo_delete_visibility_and_default_branch_all_work_against_a_running_server() {
    let serving = Serving::fresh();
    let keys = serving.home.join("keys");
    std::fs::create_dir_all(&keys).expect("mkdir");
    let (admin, admin_pub) = mint(&keys, "alice");
    let at = serving.grpc.clone();
    let key = admin.display().to_string();

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "bootstrap",
            "alice",
            "--key",
            &format!("@{}", admin_pub.display()),
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "bootstrap: {}", ran.out);

    // ── namespace create / list ────────────────────────────────────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "namespace",
            "create",
            "team",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(
        ran.ok,
        "`gunnar control namespace create` against a RUNNING server: {}",
        ran.out
    );
    assert!(ran.says("created  team"), "{}", ran.out);

    let ran = gunnar(
        &serving.home,
        &["control", "namespace", "list", "-i", &key, "--at", &at],
    );
    assert!(ran.ok, "namespace list: {}", ran.out);
    assert!(
        ran.says("team"),
        "list must show the namespace that was just created: {}",
        ran.out
    );

    // ── repo create, then the three settings that had no online verb ───────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "create",
            "team/project",
            "--hash",
            "sha1",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "repo create: {}", ran.out);
    assert!(ran.says("repo     team/project"), "{}", ran.out);
    assert!(
        ran.says("access   private"),
        "a new repository is private, and `create` must say which: {}",
        ran.out
    );

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "set-default-branch",
            "team/project",
            "trunk",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "set-default-branch: {}", ran.out);
    assert!(
        ran.says("branch   refs/heads/trunk"),
        "the short name must come back as the FULL ref the server stored, so an operator can \
         see what was actually written rather than what they typed: {}",
        ran.out
    );

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "set-visibility",
            "team/project",
            "public",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "set-visibility: {}", ran.out);
    assert!(ran.says("access   public"), "{}", ran.out);
    assert!(
        ran.says("anonymous READ") && ran.says("Nothing became writable"),
        "`public` is the one setting most likely to be misread as write access; the verb must \
         say what it did NOT do: {}",
        ran.out
    );

    // ── get reads back what the three writes did, in one row ───────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "get",
            "team/project",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "repo get: {}", ran.out);
    assert!(
        ran.says("hash     sha1")
            && ran.says("branch   refs/heads/trunk")
            && ran.says("access   public"),
        "`get` must report every field the setters moved, read back off the SERVER rather than \
         echoed by the setter that moved it: {}",
        ran.out
    );

    // ── a name that is taken is ALREADY_EXISTS, not an internal error ──────
    //
    // `CoreControlPlane::internal` used to map every `gunnar_core::Error` to
    // `INTERNAL` at all thirty-four of its call sites, so "that name is taken"
    // and "a bad branch name" and "this repository does not exist" all arrived
    // as "this server is broken" and sent an operator to read logs instead of
    // their own command line.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "namespace",
            "create",
            "team",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(!ran.ok, "creating a namespace twice must fail: {}", ran.out);
    assert!(
        !ran.says("Internal") && !ran.says("internal error"),
        "a name that is already taken is the CALLER's situation and must not be reported as a \
         server fault: {}",
        ran.out
    );
    assert!(
        ran.says("already exists") && ran.says("team"),
        "the refusal must name what already exists: {}",
        ran.out
    );

    // …and a branch name the server will not take is INVALID_ARGUMENT, for the
    // same reason and through the same function.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "set-default-branch",
            "team/project",
            "refs/tags/v1",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(!ran.ok, "a tag is not a default branch: {}", ran.out);
    assert!(
        !ran.says("Internal") && !ran.says("internal error"),
        "a bad branch name is an argument error, not a server fault: {}",
        ran.out
    );
    assert!(
        ran.says("invalid argument"),
        "and it must say which kind of refusal it is: {}",
        ran.out
    );

    // ── delete refuses without --yes, and the repository survives ──────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "delete",
            "team/project",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(!ran.ok, "a delete with no --yes must fail: {}", ran.out);
    // **The two refusals are DIFFERENT sentences, and the difference is the
    // point** (36b04371): a plain delete is recoverable and says so,
    // a `--purge` is not and says THAT. One warning for both would either
    // frighten people off the safe verb or fail to warn them off the dangerous
    // one — so this asserts each verb's own sentence.
    //
    // Both sides of this merge had independently found the same stale
    // assertion ("there is no undo", demanded of the SOFT delete after
    // `Repos.Undelete` shipped) and each caught something the other did not.
    // Master's positives are the more specific — they name the bytes coming
    // back, not merely the word `undelete` — and the branch's NEGATIVE is the one
    // that stops the old sentence creeping back in. Both are kept, because a
    // test that only asserts the new wording would pass again the day somebody
    // adds the old one beside it.
    assert!(
        ran.says("bytes are kept") && ran.says("undelete team/project") && ran.says("--yes"),
        "a soft delete's refusal must name the path, say the bytes come back, and name the \
         flag that means it: {}",
        ran.out
    );
    assert!(
        !ran.says("there is no undo"),
        "delete is soft now; a refusal that still says the old sentence is telling an operator \
         something false about their own data: {}",
        ran.out
    );
    let purge = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "delete",
            "team/project",
            "--purge",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(!purge.ok, "a purge with no --yes must fail: {}", purge.out);
    assert!(
        purge.says("there is no undo") && purge.says("team/project"),
        "a --purge refusal must name the path and the consequence: {}",
        purge.out
    );
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "get",
            "team/project",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(
        ran.ok && ran.says("repo     team/project"),
        "a delete with no --yes deleted it anyway: {}",
        ran.out
    );

    // ── …and with --yes it is gone, off the live server ────────────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "delete",
            "team/project",
            "--yes",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "repo delete --yes: {}", ran.out);
    assert!(ran.says("deleted  team/project"), "{}", ran.out);
    let ran = gunnar(
        &serving.home,
        &["control", "repo", "list", "-i", &key, "--at", &at],
    );
    assert!(ran.ok, "repo list: {}", ran.out);
    assert!(
        ran.says("no repositories"),
        "the repository is still listed after being deleted: {}",
        ran.out
    );

    // ── namespace delete, and the count it reports ─────────────────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "namespace",
            "delete",
            "team",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "namespace delete: {}", ran.out);
    assert!(
        ran.says("0 repositories went with it"),
        "the count is the point: a namespace that took eleven repositories with it and one that \
         took none are different events: {}",
        ran.out
    );

    // ── the server is STILL UP, which is the whole claim ───────────────────
    let ran = gunnar(
        &serving.home,
        &["control", "namespace", "list", "-i", &key, "--at", &at],
    );
    assert!(
        ran.ok,
        "the server did not survive its own administration; every verb above is supposed to be \
         the ONLINE alternative to stopping it: {}",
        ran.out
    );
    assert!(
        !ran.says("team"),
        "`team` is still listed after being deleted: {}",
        ran.out
    );
    // `alice` is still there, and must be: a user's own account is a
    // namespace and deleting somebody else's does not touch it. Without this
    // the assertion above would also pass against a `list` that printed
    // nothing at all.
    assert!(
        ran.says("alice"),
        "the bootstrapped user's own account namespace went with `team`: {}",
        ran.out
    );
}

/// **`control repo gc` reaches a real gc on a real server, waits for it, and
/// reports the outcome — and `repo gc-status` can still say so afterwards.**
///
/// The RPC underneath is asynchronous: `Repos.Gc` enqueues and returns, because
/// the server runs one gc at a time for the whole box. The CLI is not, and this
/// asserts that seam: a person types `repo gc`, the CLI polls the queue for
/// them, and they are told what happened without having to know there was a
/// queue at all.
///
/// **The repository here is empty, and the engine refuses to gc a repository
/// with no ref** — every object in it would be dead, and a gc that deleted
/// everything is not a gc. That refusal is what this test drives to, on
/// purpose: it is the full asynchronous path (accept, queue, worker, engine,
/// job, poll, report) and it ends in the engine's own sentence rather than in a
/// stub. The DONE-with-bytes half of the same path is
/// `gunnar-server/tests/gc_rpc.rs`, which plants a ref and asserts the sealed
/// generation on disk — a live push is not reachable from this harness.
///
/// The two failure modes must come back DIFFERENT, and both halves are here: a
/// name that was never a repository is refused on the request that made it,
/// while a live repository that cannot be maintained fails its job and the
/// reason survives for `gc-status` to print.
#[test]
fn repo_gc_reaches_the_server_waits_for_the_job_and_reports_the_outcome() {
    let serving = Serving::fresh();
    let keys = serving.home.join("keys");
    std::fs::create_dir_all(&keys).expect("mkdir");
    let (admin, admin_pub) = mint(&keys, "alice");
    let at = serving.grpc.clone();
    let key = admin.display().to_string();
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "bootstrap",
            "alice",
            "--key",
            &format!("@{}", admin_pub.display()),
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "bootstrap: {}", ran.out);
    gunnar(
        &serving.home,
        &[
            "control",
            "namespace",
            "create",
            "swept",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "create",
            "swept/one",
            "--hash",
            "sha1",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "repo create: {}", ran.out);

    // Nothing has been asked for yet, and that is a different answer from a gc
    // that reclaimed nothing.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "gc-status",
            "swept/one",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "gc-status before any gc: {}", ran.out);
    assert!(
        ran.says("no job"),
        "a repository nobody has gc'd must say so, not report a run: {}",
        ran.out
    );

    // The full path, end to end: the CLI enqueues, waits out the queue, and
    // reports the engine's own refusal with a non-zero exit. The words matter —
    // an operator reads this and has to know it was their empty repository and
    // not the server that was wrong.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "gc",
            "swept/one",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(
        !ran.ok,
        "a gc the engine refused must not exit 0: {}",
        ran.out
    );
    assert!(
        ran.says("swept/one") && ran.says("no ref"),
        "the CLI must report the engine's reason, not a bare failure: {}",
        ran.out
    );

    // **The outcome outlives the run**, which is the whole reason a console can
    // poll it. A `gc-status` that answered "no job" here would be a button that
    // goes dark having never said anything.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "gc-status",
            "swept/one",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "gc-status after the gc: {}", ran.out);
    assert!(
        ran.says("FAILED") && ran.says("no ref"),
        "a finished gc must still be readable afterwards, with its reason: {}",
        ran.out
    );

    // A repository that does not exist is refused NOW, on the request that
    // named it — not accepted and then failed minutes later inside a job
    // nobody is watching.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "gc",
            "swept/nope",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(
        !ran.ok,
        "a gc of a nonexistent repository succeeded: {}",
        ran.out
    );
}

/// **A namespace that still holds a repository is not deleted by accident.**
///
/// `DeleteNamespaceRequest::delete_repos` exists so that destroying data is
/// never implicit, and the CLI has to surface both halves: the refusal, and
/// the flag that means it.
///
/// Seen red: with `delete_repos` hard-coded to `true`, the first arm fails with
/// *"a namespace holding a repository was deleted with no --delete-repos"*.
#[test]
fn deleting_a_namespace_that_holds_a_repository_needs_the_flag_that_says_so() {
    let serving = Serving::fresh();
    let keys = serving.home.join("keys");
    std::fs::create_dir_all(&keys).expect("mkdir");
    let (admin, admin_pub) = mint(&keys, "alice");
    let at = serving.grpc.clone();
    let key = admin.display().to_string();
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "bootstrap",
            "alice",
            "--key",
            &format!("@{}", admin_pub.display()),
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "bootstrap: {}", ran.out);
    gunnar(
        &serving.home,
        &[
            "control",
            "namespace",
            "create",
            "held",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    let ran = gunnar(
        &serving.home,
        &[
            "control", "repo", "create", "held/one", "--hash", "sha1", "-i", &key, "--at", &at,
        ],
    );
    assert!(ran.ok, "repo create: {}", ran.out);

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "namespace",
            "delete",
            "held",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(
        !ran.ok,
        "a namespace holding a repository was deleted with no --delete-repos: {}",
        ran.out
    );
    // Still there. The refusal is asserted by what survived it, not only by an
    // exit code.
    let ran = gunnar(
        &serving.home,
        &["control", "repo", "list", "-i", &key, "--at", &at],
    );
    assert!(ran.says("held/one"), "the repository is gone: {}", ran.out);

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "namespace",
            "delete",
            "held",
            "--delete-repos",
            "-i",
            &key,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "namespace delete --delete-repos: {}", ran.out);
    assert!(
        ran.says("1 repository went with it"),
        "the count must be the number that actually went, so an operator can tell a namespace \
         that was empty from one that was not: {}",
        ran.out
    );
}

/// **A repository path is parsed on this side of the wire.**
///
/// Every verb that takes `<account>/<name>` goes through one parser, so a typo
/// is a local error naming the argument rather than an `INVALID_ARGUMENT` from
/// a box that may be at the other end of an ssh tunnel — and so the five verbs
/// cannot disagree about what a legal path is (one implementation).
///
/// Seen red: with `repo_path` reverted to `split_once('/')` with no emptiness
/// check, `team/` is accepted and the third arm fails.
#[test]
fn a_malformed_repository_path_fails_locally_and_names_the_argument() {
    let dir = tempfile::tempdir().expect("a temp dir");
    // Port 1: nothing listens. A path error must therefore never mention the
    // connection, which is what proves it was decided before one was opened.
    for verb in [
        vec!["control", "repo", "get", "nolash"],
        vec!["control", "repo", "delete", "nolash", "--yes"],
        vec!["control", "repo", "set-visibility", "nolash", "public"],
        vec!["control", "repo", "get", "team/"],
        vec!["control", "repo", "get", "/project"],
    ] {
        let mut args = verb.clone();
        args.extend_from_slice(&["--at", "127.0.0.1:1"]);
        let ran = gunnar(dir.path(), &args);
        assert!(!ran.ok, "{verb:?} was accepted: {}", ran.out);
        assert!(
            ran.says("a repository path is <account>/<name>"),
            "{verb:?} must fail on the PATH, not on the connection: {}",
            ran.out
        );
        assert!(
            !ran.says("could not be reached"),
            "{verb:?} opened a connection before deciding the path was malformed: {}",
            ran.out
        );
    }
}

/// **The id `gunnar control` prints on a refusal names a line in the server's
/// own log**.
///
/// This is the operator's 03:00 question asked of the control plane: *someone
/// says a call failed — can I find out why?* Before this the answer was no.
/// `gunnar-grpc` emitted one audit record per **authorised** call and nothing
/// else; a refusal, a call to a method that does not exist, and a slow call
/// were all indistinguishable from silence, and nothing the caller held could
/// be grepped for.
///
/// So this asserts the whole chain, end to end, with nothing in it stubbed:
///
/// 1. a real `gunnar control user add` is refused by a real `gunnar serve`;
/// 2. the CLI's own output carries `request-id: <id>`;
/// 3. the **server's stderr on disk** — the file a journal or `podman logs`
///    would hold — carries a `grpc.request.done` line with `req=<that same
///    id>`, `grpc_status=7` (PERMISSION_DENIED) and an `elapsed_ms`.
///
/// # Why the log is read as text and not through a capture layer
///
/// Because that is what an operator has. `tracing_subscriber`'s ANSI escapes
/// wrap **field names**, so a coloured log makes `grep req=` match nothing and
/// a pipeline summing the matches report a confident zero — the defect
/// `with_ansi(stderr().is_terminal())` fixed on 2026-08-05. Asserting on a
/// captured in-process `Line` would never see that class of bug again. This
/// greps the bytes.
///
/// # Seen red
///
/// * With the `request-id` clause removed from `control::explain`, this fails
///   at *"the refusal printed no request-id"*.
/// * With `.layer(RequestSpanLayer::new(..))`'s stamp removed from the
///   response, the CLI prints no id and it fails at the same place — which is
///   why the third assertion, on the server's own bytes, is separate: only that
///   one can tell "the client was given an id nobody logged" from "no id at
///   all".
/// * With `grpc_status` dropped from the exit line, it fails at *"the server
///   line does not say WHY"* while the id assertions still pass.
#[test]
fn a_refused_control_call_prints_an_id_that_names_a_line_in_the_server_log() {
    let serving = Serving::fresh();
    let keys = tempfile::tempdir().expect("a key dir");
    let (admin_key, admin_pub) = mint(keys.path(), "admin");
    let (ordinary_key, ordinary_pub) = mint(keys.path(), "ordinary");
    let (_, victim_pub) = mint(keys.path(), "victim");

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "bootstrap",
            "admin",
            "--key",
            &format!("@{}", admin_pub.display()),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "bootstrap: {}", ran.out);
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "user",
            "add",
            "ordinary",
            "--key",
            &format!("@{}", ordinary_pub.display()),
            "-i",
            admin_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "creating the ordinary user: {}", ran.out);

    // ── the refusal an operator would be handed a ticket about ─────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "user",
            "add",
            "victim",
            "--key",
            &format!("@{}", victim_pub.display()),
            "-i",
            ordinary_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        !ran.ok,
        "an ordinary user must not be able to create a user; without a refusal this test \
         asserts nothing: {}",
        ran.out
    );

    let marker = "request-id: ";
    let id: String = ran
        .out
        .split_once(marker)
        .map(|(_, rest)| {
            rest.chars()
                .take_while(char::is_ascii_hexdigit)
                .collect::<String>()
        })
        .filter(|id| !id.is_empty())
        .unwrap_or_else(|| {
            panic!(
                "the refusal printed no request-id, so the person holding this error has \
                 nothing to give the person holding the log:\n{}",
                ran.out
            )
        });
    assert_eq!(
        id.len(),
        16,
        "a request id is sixteen fixed-width hex digits; {id:?} is not one, and a variable-width \
         id does not line up in a column an operator reads"
    );

    // ── the server's own bytes ─────────────────────────────────────────────
    let log = serving.log();
    // Non-vacuity: an empty log contains no escapes and no wrong ids either,
    // and would satisfy every "does not contain" below.
    assert!(
        log.contains("INFO") || log.contains("WARN"),
        "the server wrote nothing to stderr, so nothing below is being asserted:\n{log}"
    );
    assert!(
        !log.contains('\u{1b}'),
        "the server's log carries ANSI escapes when stderr is a FILE, so `grep req=` over a \
         captured log matches nothing:\n{}",
        log.escape_debug()
    );
    let line = log
        .lines()
        .filter(|line| line.contains("grpc.request.done"))
        .find(|line| line.contains(&format!("req={id}")))
        .unwrap_or_else(|| {
            panic!(
                "the CLI printed request-id {id} and no grpc.request.done line in the server's \
                 log carries req={id}; the caller holds an identifier that names nothing:\n{log}"
            )
        });
    assert!(
        line.contains("grpc_status=7"),
        "the server line does not say WHY the call was refused (7 = PERMISSION_DENIED):\n{line}"
    );
    assert!(
        line.contains("elapsed_ms="),
        "no elapsed_ms on the exit line, so 'was it slow or was it refused' is unanswerable — \
         and this field is the one a tonic INTERCEPTOR could never have produced:\n{line}"
    );
    assert!(
        line.contains("WARN"),
        "a refusal must be at warn or a server running RUST_LOG=warn logs nothing about \
         it:\n{line}"
    );

    // And the audit record for the SAME call inherits the same id, which is the
    // hop from "which call" to "which principal".
    assert!(
        log.lines().any(|other| other.contains(&format!("req={id}"))
            && other.contains("principal=")
            && !other.contains("grpc.request.")),
        "no line naming a principal carries req={id}, so the id stops at the transport and never \
         reaches the authorisation decision:\n{log}"
    );
}

/// **`SIGUSR1` makes a running server more verbose, and `SIGUSR2` puts it
/// back — with no restart**.
///
/// An operator debugging a live gunnar had exactly one way to see more:
/// restart it, which ends every in-flight transfer and very often stops the
/// thing they were debugging from happening. On an appliance there is no shell
/// to re-export `RUST_LOG` in and the process is PID 1.
///
/// # What makes this a real assertion and not a round-trip
///
/// It does not check that a filter object changed. It drives a **real
/// `gunnar serve`** at `RUST_LOG=info`, makes a real gRPC call three times, and
/// counts a line that only exists at `debug` — `grpc.request.begin` — in the
/// server's **stderr on disk**, before the signal, after `SIGUSR1`, and after
/// `SIGUSR2`. The before-count must be **zero** and the zero must be *earned*:
/// the `grpc.request.done` line from the same call has to be there, or an
/// unreachable server would satisfy the whole test.
///
/// That is also why the level is exercised through `tracing`'s **global
/// max-level short-circuit** rather than through a filter directive alone: a
/// `debug!` callsite under an `info` filter is not merely filtered, it is not
/// evaluated. Nothing but `Handle::reload` rebuilding the interest cache makes
/// it fire, so a "reload" that only swapped the filter object would leave this
/// test at zero.
///
/// # Seen red
///
/// * With `control.handle.reload(filter)` removed from `logging::apply` (so the
///   rung counter still advances and the acknowledgement is still printed),
///   this fails at *"SIGUSR1 was acknowledged and the log did not get more
///   verbose"* — which is precisely the shape of a level control that reports
///   success and does nothing.
/// * Removing `apply`'s `quieter` branch does **not** fail this test, and that
///   is recorded rather than hidden: at `RUST_LOG=info` a `warn!` is printable
///   under both filters, so the ordering is invisible here. It is owned by
///   `a_quiet_server_still_gets_told_its_level_went_back_down`.
#[test]
fn a_signal_changes_a_running_servers_log_level_without_restarting_it() {
    use rustix::process::{kill_process, Signal};

    let serving = Serving::fresh();
    let pid =
        rustix::process::Pid::from_raw(serving.child.id() as i32).expect("the child has a pid");

    /// One `Bootstrap.Status` over gRPC: no credential, no key, always served.
    fn poke(serving: &Serving) {
        let ran = gunnar(&serving.home, &["control", "status", "--at", &serving.grpc]);
        assert!(ran.ok, "control status against a live server: {}", ran.out);
    }

    /// Wait until `needle` appears in the server's log, or fail saying what was
    /// there instead. A signal is asynchronous; the alternative to waiting is a
    /// flake that reads exactly like the bug.
    fn wait_for(serving: &Serving, needle: &str, what: &str) {
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
        while std::time::Instant::now() < deadline {
            if serving.log().contains(needle) {
                return;
            }
            std::thread::sleep(std::time::Duration::from_millis(25));
        }
        panic!(
            "{what}; the server's log never contained {needle:?}:\n{}",
            serving.log()
        );
    }

    fn count(serving: &Serving, needle: &str) -> usize {
        serving
            .log()
            .lines()
            .filter(|line| line.contains(needle))
            .count()
    }

    // ── before: info, and the debug line is absent ─────────────────────────
    poke(&serving);
    wait_for(
        &serving,
        "grpc.request.done",
        "the server never logged the call at all, so the zero below would be meaningless",
    );
    assert_eq!(
        count(&serving, "grpc.request.begin"),
        0,
        "a debug-only line is in an `info` server's log, so this test cannot tell a working \
         reload from a broken filter:\n{}",
        serving.log()
    );
    let done_before = count(&serving, "grpc.request.done");
    assert!(done_before > 0);

    // ── SIGUSR1: one rung up ───────────────────────────────────────────────
    kill_process(pid, Signal::USR1).expect("send SIGUSR1");
    wait_for(
        &serving,
        "level=debug",
        "SIGUSR1 was not acknowledged. Note that the DEFAULT disposition of SIGUSR1 is to \
         TERMINATE, so an unhandled one would have killed the server",
    );
    poke(&serving);
    wait_for(
        &serving,
        "grpc.request.begin",
        "SIGUSR1 was acknowledged and the log did not get more verbose, so the level control \
         reports a change it did not make",
    );
    let begin_after_usr1 = count(&serving, "grpc.request.begin");
    assert!(begin_after_usr1 > 0);

    // ── SIGUSR2: straight back to the startup filter ───────────────────────
    kill_process(pid, Signal::USR2).expect("send SIGUSR2");
    wait_for(
        &serving,
        "level=info",
        "SIGUSR2 was not acknowledged, or the acknowledgement was written under the QUIETER \
         filter and therefore never printed",
    );
    let done_before_third = count(&serving, "grpc.request.done");
    poke(&serving);
    // Wait for the third call to have been served before counting, or the
    // assertion below could pass simply by looking too early.
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
    while count(&serving, "grpc.request.done") == done_before_third
        && std::time::Instant::now() < deadline
    {
        std::thread::sleep(std::time::Duration::from_millis(25));
    }
    assert!(
        count(&serving, "grpc.request.done") > done_before_third,
        "the third call was never served:\n{}",
        serving.log()
    );
    assert_eq!(
        count(&serving, "grpc.request.begin"),
        begin_after_usr1,
        "SIGUSR2 was acknowledged and the server is still at debug, so an operator cannot get \
         their production log level back without a restart:\n{}",
        serving.log()
    );

    // …and the server is still the same process, which is the entire claim.
    assert_eq!(
        serving.child.id() as i32,
        pid.as_raw_nonzero().get(),
        "the pid moved, so something restarted and this proved nothing"
    );
    let ran = gunnar(&serving.home, &["control", "status", "--at", &serving.grpc]);
    assert!(
        ran.ok,
        "the server did not survive two signals whose default action is to terminate it: {}",
        ran.out
    );
}

/// **A server quieter than `warn` is still told when its level goes back
/// down**.
///
/// The acknowledgement is at `warn`, so on a box running `RUST_LOG=error` the
/// order the reload and the announcement happen in decides whether the operator
/// sees anything at all. Going **up** is safe either way — the new filter is
/// `debug`. Going **back down** is not: announce after reloading and the line
/// is written under `error`, where a `warn!` is not printable, and the operator
/// who just turned `trace` off on a loaded server gets silence as their
/// confirmation.
///
/// So `logging::apply` announces under whichever of the two filters is the more
/// verbose: after the reload on the way up, before it on the way down. This is
/// the test that can see the difference; the `info` one above cannot, and says
/// so.
///
/// Seen red: with `apply`'s `quieter` branch removed, the wait for the `USR2`
/// acknowledgement times out and this fails, while
/// `a_signal_changes_a_running_servers_log_level_without_restarting_it` still
/// passes.
#[test]
fn a_quiet_server_still_gets_told_its_level_went_back_down() {
    use rustix::process::{kill_process, Signal};

    let serving = Serving::fresh_at("error");
    let pid =
        rustix::process::Pid::from_raw(serving.child.id() as i32).expect("the child has a pid");

    let wait_for = |needle: &str, what: &str| {
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
        while std::time::Instant::now() < deadline {
            if serving.log().contains(needle) {
                return;
            }
            std::thread::sleep(std::time::Duration::from_millis(25));
        }
        panic!(
            "{what}; the server's log never contained {needle:?}:\n{}",
            serving.log()
        );
    };

    // Non-vacuity: at `error` the server says nothing at all about a served
    // call, which is what makes this the interesting rung.
    let ran = gunnar(&serving.home, &["control", "status", "--at", &serving.grpc]);
    assert!(ran.ok, "control status: {}", ran.out);
    assert!(
        !serving.log().contains("grpc.request.done"),
        "a server at RUST_LOG=error logged a served call, so `error` is not being honoured and \
         the whole premise of this test is wrong:\n{}",
        serving.log()
    );

    kill_process(pid, Signal::USR1).expect("send SIGUSR1");
    wait_for(
        "level=debug",
        "the way UP was not acknowledged, which is the easy direction",
    );

    kill_process(pid, Signal::USR2).expect("send SIGUSR2");
    wait_for(
        "level=error",
        "the way DOWN was not acknowledged: the line was written AFTER the reload, under a \
         filter that cannot print a warn, so an operator turning trace off on a loaded server \
         gets silence for a confirmation",
    );

    // And it really is back down: nothing at `info` reaches the log again.
    let done_before = serving
        .log()
        .lines()
        .filter(|line| line.contains("grpc.request.done"))
        .count();
    let ran = gunnar(&serving.home, &["control", "status", "--at", &serving.grpc]);
    assert!(ran.ok, "control status: {}", ran.out);
    std::thread::sleep(std::time::Duration::from_millis(300));
    assert_eq!(
        serving
            .log()
            .lines()
            .filter(|line| line.contains("grpc.request.done"))
            .count(),
        done_before,
        "the acknowledgement said `error` and the server is still logging at info:\n{}",
        serving.log()
    );
}

/// **A mirror that will create its repository reads back before its first sync**
/// (GUN-FIELD-TEST, 2026-10-31).
///
/// `control mirror add <ns>/<new> <url> --create` registers the mirror and leaves the
/// repository to the first sync (owner order 2026-10-18), and the CLI then reads the
/// registration back with `Mirror.Status`. Status asked for `read` on a repository that does
/// not exist yet, which nobody holds, so the field test's server admin got "principal alice
/// has no read grant on repo:field/mpull-http3" from an add that had worked, and `status`
/// refused until the sync had created it. `Mirror.Sync` already takes the namespace's grant
/// for a pending repository (`mirror_scope`); `Status` must too.
#[test]
fn a_mirror_that_creates_its_repository_reads_back_before_its_first_sync() {
    let serving = Serving::fresh();
    let keys = serving.home.join("keys");
    std::fs::create_dir_all(&keys).expect("mkdir");
    let (alice_key, alice_pub) = mint(&keys, "alice");
    let alice = alice_key.to_str().unwrap().to_owned();
    let at = serving.grpc.clone();
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "bootstrap",
            "alice",
            "--key",
            &format!("@{}", alice_pub.display()),
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "bootstrap: {}", ran.out);
    // A loopback upstream is this server's own network: allowed deliberately, as the
    // refusal says to. The add dials nothing.
    std::fs::write(
        serving
            ._dir
            .path()
            .join("data")
            .join("mirror_allow_private_destinations"),
        "",
    )
    .expect("allow a loopback upstream");
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "mirror",
            "add",
            "alice/later",
            "http://127.0.0.1:9/up.git",
            "--create",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(
        ran.ok,
        "the add registered the mirror, and reading it back was refused: {}",
        ran.out
    );
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "mirror",
            "status",
            "alice/later",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(
        ran.ok && ran.says("alice/later"),
        "the namespace's owner cannot see the mirror they just registered: {}",
        ran.out
    );
}

/// **`policy gates` names every gate that refuses a push** (GUN-FIELD-TEST, 2026-10-31).
///
/// On a fresh repository the field test pushed a commit with a `Co-Authored-By: Claude`
/// trailer and the ai-trailers gate refused it, as it must (on by default) — while
/// `control policy gates` on the same repository answered "0 of 4 configured … Every gate is
/// off, so the pre-receive gate rejects NOTHING". `Policy.ListGates` listed a fixed v1 set
/// derived from the protection rules and never asked the policy the push path builds
/// (`PolicyConfig::gates`), so the default-on gate, the push-option gate and the
/// repository-wide size limits were invisible to it, and the console's settings read the same
/// answer.
#[test]
fn the_gate_list_names_the_default_on_ai_trailer_gate_on_a_fresh_repository() {
    let serving = Serving::fresh();
    let keys = serving.home.join("keys");
    std::fs::create_dir_all(&keys).expect("mkdir");
    let (alice_key, alice_pub) = mint(&keys, "alice");
    let alice = alice_key.to_str().unwrap().to_owned();
    let at = serving.grpc.clone();
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "bootstrap",
            "alice",
            "--key",
            &format!("@{}", alice_pub.display()),
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "bootstrap: {}", ran.out);
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "create",
            "alice/fresh",
            "--hash",
            "sha1",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "repo create: {}", ran.out);
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "gates",
            "alice/fresh",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "listing gates: {}", ran.out);
    assert!(
        ran.says("ai-trailers") && ran.says("ai-trailers        ON"),
        "the ai-trailers gate refuses every push carrying an AI trailer on this repository, \
         and the gate list does not say so: {}",
        ran.out
    );
    assert!(
        !ran.says("rejects NOTHING"),
        "a repository whose push path refuses AI trailers is described as rejecting nothing: {}",
        ran.out
    );
}

/// **`gunnar control policy`, end to end against a live server — and the
/// write half refused to a user who may push.**
///
/// # Why this test exists
///
/// The server has served all six `Policy.*` RPCs since `gunnar_core::PolicyStore`
/// gave the policy document a home at `refs/gunnar/policy`. `gunnar control
/// --help` listed eight verbs and none of them reached it, and the console
/// rendered the same six as disabled panels off a coverage row that had gone
/// stale. So a merge required a verified signed approval naming the exact
/// commit, and a **direct push to the same branch** was refused by nothing —
/// not because the gate was missing, but because nothing could turn it on.
///
/// Everything here is the real binary against the real live server,
/// authenticated per call with the caller's own key.
///
/// # Both directions
///
/// * **green** — `protect` writes a rule, `list` reads it back, `gates` reports
///   `branch-protection` as ON *because* a rule exists, `set-ref-names` and
///   `ref-names` round-trip, and `unprotect` removes it;
/// * **red** — a bare branch name is refused LOCALLY before a connection is
///   made; `unprotect` of a rule that is not there is an error and not a
///   cheerful success; and bob, who has **write** on the namespace and can
///   therefore push, is refused `protect` over the wire while still being
///   allowed to `list`. That last one is the whole asymmetry: pushing and
///   deciding what may be pushed are different levels, and the console's
///   viewer must land on the same side of it.
///
/// Seen red (measure, never assert) by deleting the `refs/`-prefix guard in
/// `control::ref_pattern` — the local leg then reports the rule as stored and
/// the `gates` assertion fails, because a rule on `main` matches no ref and
/// enables nothing.
#[test]
fn the_policy_verbs_configure_the_gate_and_refuse_a_user_who_may_only_push() {
    let serving = Serving::fresh();
    let keys = serving.home.join("keys");
    std::fs::create_dir_all(&keys).expect("mkdir");
    let (alice_key, alice_pub) = mint(&keys, "alice");
    let (bob_key, bob_pub) = mint(&keys, "bob");
    let alice = alice_key.to_str().unwrap().to_owned();
    let bob = bob_key.to_str().unwrap().to_owned();
    let at = serving.grpc.clone();

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "bootstrap",
            "alice",
            "--key",
            &format!("@{}", alice_pub.display()),
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "bootstrap: {}", ran.out);

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "repo",
            "create",
            "alice/guarded",
            "--hash",
            "sha1",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "creating the repository: {}", ran.out);

    // ── red, and locally: a bare branch name is not a ref pattern ─────────
    //
    // Refused before a socket is opened. A rule on `main` matches no ref, so
    // the server would accept it, `list` would print it and it would guard
    // nothing — a protection that reads as present and is not, which is this
    // whole feature's bug re-introduced one typo at a time.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "protect",
            "alice/guarded",
            "main",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(
        !ran.ok,
        "a bare branch name was accepted as a pattern: {}",
        ran.out
    );
    assert!(
        ran.says("refs/heads/main"),
        "the refusal must name the fix, not just the fault: {}",
        ran.out
    );

    // ── the empty state, said as a word ──────────────────────────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "list",
            "alice/guarded",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "listing an unconfigured repository: {}", ran.out);
    assert!(
        ran.says("NOTHING is protected here"),
        "an unprotected repository and a failed read are the same empty list and very \
         different facts: {}",
        ran.out
    );

    // ── red: removing a rule that was never there ────────────────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "unprotect",
            "alice/guarded",
            "refs/heads/main",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(
        !ran.ok,
        "removing a rule that does not exist reported success: {}",
        ran.out
    );

    // ── green: the rule ──────────────────────────────────────────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "protect",
            "alice/guarded",
            "refs/heads/main",
            "--require-signature",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "writing a protection rule: {}", ran.out);
    assert!(
        ran.says("force-push REFUSED") && ran.says("delete REFUSED"),
        "the rule must print what it FORBIDS, not the proto's double negative: {}",
        ran.out
    );

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "list",
            "alice/guarded",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "listing: {}", ran.out);
    assert!(
        ran.says("refs/heads/main") && ran.says("1 rule(s)"),
        "the rule written over the control plane did not read back: {}",
        ran.out
    );

    // ── the gates, which is where a rule that guards nothing shows up ────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "gates",
            "alice/guarded",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "listing gates: {}", ran.out);
    assert!(
        ran.says("branch-protection  ON"),
        "a rule exists and `branch-protection` is still reported off, so `ListGates` is not \
         reading the same document `SetProtection` wrote: {}",
        ran.out
    );
    assert!(
        ran.says("signed-commits     ON"),
        "`--require-signature` was stored and the gate list does not reflect it: {}",
        ran.out
    );

    // ── ref names, and the refusal that keeps a clear from being a typo ──
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "set-ref-names",
            "alice/guarded",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(
        !ran.ok,
        "a set-ref-names with neither --allow nor --deny silently CLEARED the policy: {}",
        ran.out
    );

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "set-ref-names",
            "alice/guarded",
            "--allow",
            "refs/heads/**",
            "--deny",
            "refs/heads/wip/**",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "setting a ref-name policy: {}", ran.out);
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "ref-names",
            "alice/guarded",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "reading the ref-name policy: {}", ran.out);
    assert!(
        ran.says("DENY   refs/heads/wip/**") && ran.says("deny wins"),
        "the ref-name policy did not read back, or does not say which side wins: {}",
        ran.out
    );

    // ── red, over the wire: a user who may PUSH may not decide what may ──
    //
    // bob gets `write` on the namespace, which is exactly the level that
    // lets him push to `refs/heads/main`. Setting protection is `Admin` on
    // gunnar's side ("turning the pre-receive gate off for a ref is an ACL
    // act, not a push"), so he must be refused — while still being allowed to
    // READ the rule, because a pusher who cannot see the gate that will reject
    // him is worse than no gate.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "user",
            "add",
            "bob",
            "--key",
            &format!("@{}", bob_pub.display()),
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "creating bob: {}", ran.out);
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "grant",
            "bob",
            "write",
            "--namespace",
            "alice",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "granting bob write: {}", ran.out);

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "list",
            "alice/guarded",
            "-i",
            &bob,
            "--at",
            &at,
        ],
    );
    assert!(
        ran.ok && ran.says("refs/heads/main"),
        "a user with write may not read the gate that will reject him: {}",
        ran.out
    );

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "protect",
            "alice/guarded",
            "refs/heads/main",
            "--allow-force-push",
            "-i",
            &bob,
            "--at",
            &at,
        ],
    );
    assert!(
        !ran.ok,
        "bob has WRITE and was allowed to turn off the force-push guard on main, which \
         means anyone who can push can also remove the reason they cannot: {}",
        ran.out
    );

    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "unprotect",
            "alice/guarded",
            "refs/heads/main",
            "-i",
            &bob,
            "--at",
            &at,
        ],
    );
    assert!(
        !ran.ok,
        "bob has WRITE and was allowed to DELETE the protection rule: {}",
        ran.out
    );

    // The rule survived both attempts. Asserting the refusals alone would pass
    // against a server that refused the call and applied it anyway.
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "list",
            "alice/guarded",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(
        ran.says("force-push REFUSED"),
        "the refusals were reported and the rule was widened anyway: {}",
        ran.out
    );

    // ── green: the admin removes it ──────────────────────────────────────
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "unprotect",
            "alice/guarded",
            "refs/heads/main",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.ok, "removing the rule as admin: {}", ran.out);
    let ran = gunnar(
        &serving.home,
        &[
            "control",
            "policy",
            "list",
            "alice/guarded",
            "-i",
            &alice,
            "--at",
            &at,
        ],
    );
    assert!(ran.says("NOTHING is protected here"), "{}", ran.out);

    drop(serving);
}