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

#![cfg(all(feature = "server", feature = "client", feature = "control"))]

//! **The three verbs an operator could not run until 2026-08-31: `grant
//! list`, `revoke`, and `health`/`stats`/`storage`.**
//!
//! # The finding
//!
//! MEASURED on the live box 2026-08-31: `gunnar control` could GIVE access
//! (`grant <principal> <level>`) and could never list or take it back — the
//! only revoke was `user remove`, which takes the whole human. And the three
//! `Admin` RPCs (`Health`, `Stats`, `StorageInfo`) were served with only the
//! console calling them, so reading a headless appliance's own
//! numbers meant toggling UpCloud VNC and screen-capturing a dashboard.
//!
//! All four RPCs — `Access.ListGrants`, `Access.Revoke`, and the `Admin`
//! three — existed server-side the whole time. This file is about the verbs,
//! and every assertion drives the real binary against a real `gunnar serve`.
//!
//! # How each green here was seen red
//!
//! Every load-bearing guard was tripped by a **product mutation** in
//! `control.rs` on 2026-08-31, each failing at exactly the intended line:
//!
//! * `grant list` lists **the real grants and not everything**: a fabricated
//!   row on the never-granted repository tripped the ACL-not-catalog guard
//!   ("printed a repository nobody was granted on"). `bob`'s rows are also
//!   asserted exactly, in the server's own sort order — the first run of this
//!   file went red on that ordering, which is how the order in the assertion
//!   became the measured one rather than the assumed one.
//! * `revoke` is proven on the **applied output**: with the revoke mutated to
//!   skip the RPC and pretend success (and the two row-level guards
//!   temporarily disabled so the deepest guard was the one to fire), the SSH
//!   door assertion tripped — "his key still reaches the repository over
//!   SSH". A revoke that removes a row without removing the access is the
//!   failure this file exists to catch, and no listing can see it.
//! * the two refusals are asserted to be **different sentences** ("no such
//!   grant" versus "not allowed to do this"), each asserted positively AND as
//!   the absence of the other.
//! * `stats` is cross-checked against ground truth this test created (2
//!   principals, 1 repository) and against a second verb reading the same
//!   table: `stats` mutated to print `grants + 1` tripped the
//!   two-readers-one-table guard with "stats counts 2 grant(s) and `grant
//!   list` prints 1 row(s)". (With the fabricated-row mutation active at the
//!   same time the two cancelled and this guard stayed green — which is why
//!   the mutations were run one at a time.)
//! * the transport red: `health` mutated to print a zeros page on a connect
//!   failure tripped "`health` against a server with no control plane must
//!   FAIL" — a verb that answers zeros there is the VNC dashboard problem
//!   again, wearing a CLI.
//!
//! Row counts are taken from **stdout only** ([`served_binary::Ran::stdout`]):
//! `control.rs` announces `gunnar: signing as …` on stderr, and a count over
//! the merged streams counts the announcement too — the exact miscount that
//! broke `second_key.rs` on 2026-08-31.

use std::path::Path;

#[path = "common/served_binary.rs"]
mod served_binary;
use served_binary::{gunnar, mint, ssh_reaches, Ran, Serving};

/// The indented `<scope>\t<level>` rows under one principal's heading, from
/// stdout alone.
fn rows_of(listing: &Ran, principal: &str) -> Vec<String> {
    let mut rows = Vec::new();
    let mut inside = false;
    for line in listing.stdout.lines() {
        if line == principal {
            inside = true;
        } else if !line.starts_with("  ") {
            inside = false;
        } else if inside {
            rows.push(line.trim().to_owned());
        }
    }
    rows
}

/// `gunnar control grant list`, asserted to succeed.
fn grant_list(home: &Path, grpc: &str, ident: &Path, extra: &[&str]) -> Ran {
    let mut args = vec!["control", "grant", "list"];
    args.extend_from_slice(extra);
    args.extend_from_slice(&["-i", ident.to_str().unwrap(), "--at", grpc]);
    let ran = gunnar(home, &args);
    assert!(ran.ok, "`grant list` must succeed: {}", ran.out);
    ran
}

/// **`grant list` answers "what can `bob` see?" — with the real grants, not
/// with everything.**
#[test]
fn grant_list_shows_the_grants_that_were_given_and_no_others() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let (data_root, home) = (dir.path().join("data"), dir.path().join("home"));
    let keys = 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 serving = Serving::start(&data_root, &home, &dir.path().join("server.log"));

    let ran = gunnar(
        &home,
        &[
            "control",
            "bootstrap",
            "alice",
            "--key",
            &format!("@{}", alice_pub.display()),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "bootstrap: {}", ran.out);
    let rick = alice_key.to_str().unwrap();
    for args in [
        vec!["control", "repo", "create", "alice/proj"],
        // A repository that EXISTS and is granted to nobody. This is the
        // listing's red: an implementation that answers "what is on the
        // server" instead of "what was granted" shows it, and the assertion
        // below is what tells those apart.
        vec!["control", "repo", "create", "alice/ungranted"],
        vec![
            "control",
            "user",
            "add",
            "bob",
            "--key",
            &format!("@{}", bob_pub.display()),
        ],
        vec!["control", "grant", "bob", "read", "--repo", "alice/proj"],
        vec!["control", "grant", "bob", "read", "--namespace", "tools"],
    ] {
        let mut full = args.clone();
        full.extend_from_slice(&["-i", rick, "--at", &serving.grpc]);
        let ran = gunnar(&home, &full);
        assert!(ran.ok, "{args:?}: {}", ran.out);
    }

    // ── the listing, whole ────────────────────────────────────────────────
    let listing = grant_list(&home, &serving.grpc, &alice_key, &[]);
    let bob = rows_of(&listing, "bob");
    // The order is the SERVER's — `AuthTable::grant` keeps a principal's
    // grants sorted, namespace scopes before repo scopes — asserted exactly so
    // a listing that re-sorts or interleaves shows up here.
    assert_eq!(
        bob,
        vec!["namespace:tools\tread", "repo:alice/proj\tread"],
        "bob must hold EXACTLY the two grants he was given — no more (a listing that shows \
         everything), no fewer (a listing that shows nothing), and with the scope spelled so an \
         operator can read it at a glance:\n{}",
        listing.stdout
    );
    assert_eq!(
        rows_of(&listing, "alice"),
        vec!["server\tadmin"],
        "the bootstrap admin's own server grant is a row like any other:\n{}",
        listing.stdout
    );
    // The grant nobody gave. `alice/ungranted` is IN the catalog; only a
    // listing that reads the grant table and not the catalog omits it.
    assert!(
        !listing.stdout.contains("ungranted"),
        "`grant list` printed a repository nobody was granted on, so it is listing the server's \
         contents rather than the ACL:\n{}",
        listing.stdout
    );

    // ── filtered to one principal ─────────────────────────────────────────
    let filtered = grant_list(&home, &serving.grpc, &alice_key, &["--principal", "bob"]);
    assert_eq!(
        rows_of(&filtered, "bob"),
        bob,
        "the filter must not change bob's rows:\n{}",
        filtered.stdout
    );
    assert!(
        !filtered.stdout.lines().any(|l| l == "alice"),
        "--principal bob must not list alice:\n{}",
        filtered.stdout
    );

    // ── and an ungranted principal is a sentence, not an empty page ───────
    let none = grant_list(
        &home,
        &serving.grpc,
        &alice_key,
        &["--principal", "nobody-here"],
    );
    assert!(
        none.stdout.contains("holds no grants"),
        "no grants must be said out loud, or an operator cannot tell it from a broken \
         listing:\n{}",
        none.stdout
    );
}

/// **`revoke` takes the access back — proven at the SSH door, not in the
/// listing** — and its two refusals are different sentences.
#[test]
fn revoke_takes_access_back_at_the_ssh_door() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let (data_root, home) = (dir.path().join("data"), dir.path().join("home"));
    let keys = 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 serving = Serving::start(&data_root, &home, &dir.path().join("server.log"));

    let ran = gunnar(
        &home,
        &[
            "control",
            "bootstrap",
            "alice",
            "--key",
            &format!("@{}", alice_pub.display()),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "bootstrap: {}", ran.out);
    let rick = alice_key.to_str().unwrap();
    for args in [
        vec!["control", "repo", "create", "alice/proj"],
        vec![
            "control",
            "user",
            "add",
            "bob",
            "--key",
            &format!("@{}", bob_pub.display()),
        ],
    ] {
        let mut full = args.clone();
        full.extend_from_slice(&["-i", rick, "--at", &serving.grpc]);
        let ran = gunnar(&home, &full);
        assert!(ran.ok, "{args:?}: {}", ran.out);
    }

    // ── RED FIRST: without the grant, bob's key is refused over SSH ────
    let ran = ssh_reaches(&home, &serving.ssh, &bob_key, "alice/proj");
    assert!(
        !ran.ok,
        "bob reached alice/proj with NO grant, so the revoke assertion below could never \
         fail: {}",
        ran.out
    );

    // ── grant, and the applied green ──────────────────────────────────────
    let ran = gunnar(
        &home,
        &[
            "control",
            "grant",
            "bob",
            "read",
            "--repo",
            "alice/proj",
            "-i",
            rick,
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "grant: {}", ran.out);
    let ran = ssh_reaches(&home, &serving.ssh, &bob_key, "alice/proj");
    assert!(
        ran.ok,
        "with the read grant, bob must reach the repository over real SSH: {}",
        ran.out
    );

    // ── the revoke ────────────────────────────────────────────────────────
    let ran = gunnar(
        &home,
        &[
            "control",
            "revoke",
            "bob",
            "--repo",
            "alice/proj",
            "-i",
            rick,
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "revoke: {}", ran.out);
    assert!(
        ran.stdout.contains("revoked bob on repo:alice/proj"),
        "{}",
        ran.out
    );
    assert!(
        ran.stdout.contains("bob now holds no grants"),
        "the verb must read the remaining grants BACK, not assume them: {}",
        ran.out
    );

    // ── the listing agrees: zero rows for bob ──────────────────────────
    let listing = grant_list(&home, &serving.grpc, &alice_key, &[]);
    assert!(
        rows_of(&listing, "bob").is_empty(),
        "after the revoke, `grant list` must show bob holding nothing:\n{}",
        listing.stdout
    );

    // ── and the DOOR agrees, which is the half that matters (measure,
    //    never assert): a revoked grant that still clones is the failure this
    //    test exists to catch, and no listing can see it. ───────────────────
    let ran = ssh_reaches(&home, &serving.ssh, &bob_key, "alice/proj");
    assert!(
        !ran.ok,
        "bob's grant was revoked and his key still reaches the repository over SSH — the \
         revoke removed a row without removing the access: {}",
        ran.out
    );

    // ── refusal 1: no such grant (the scope is wrong, not the caller) ─────
    let ran = gunnar(
        &home,
        &[
            "control",
            "revoke",
            "bob",
            "--repo",
            "alice/proj",
            "-i",
            rick,
            "--at",
            &serving.grpc,
        ],
    );
    assert!(!ran.ok, "revoking the revoked grant must fail: {}", ran.out);
    assert!(
        ran.says("no such grant"),
        "…and say the grant does not exist: {}",
        ran.out
    );
    assert!(
        !ran.says("not allowed to do this"),
        "…never that the caller lacks permission, because alice is gunnar-admin and the two \
         demand opposite reactions: {}",
        ran.out
    );

    // ── refusal 2: not allowed (the grant exists, the caller may not) ─────
    let ran = gunnar(
        &home,
        &[
            "control",
            "revoke",
            "alice",
            "--server",
            "-i",
            bob_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        !ran.ok,
        "an ordinary user must not revoke the admin's server grant: {}",
        ran.out
    );
    assert!(
        ran.says("not allowed to do this"),
        "…and be told it is authorisation: {}",
        ran.out
    );
    assert!(
        !ran.says("no such grant"),
        "…never that the grant is missing — it exists, and telling an unauthorised caller \
         which grants exist would be the probe the gate refuses: {}",
        ran.out
    );
}

/// **`health`, `stats` and `storage` print the numbers the server holds** —
/// cross-checked against ground truth this test created and against a second
/// verb over the same table, and refused without a credential.
#[test]
fn health_stats_and_storage_answer_from_the_cli() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let (data_root, home) = (dir.path().join("data"), dir.path().join("home"));
    let keys = 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 serving = Serving::start(&data_root, &home, &dir.path().join("server.log"));

    let ran = gunnar(
        &home,
        &[
            "control",
            "bootstrap",
            "alice",
            "--key",
            &format!("@{}", alice_pub.display()),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(ran.ok, "bootstrap: {}", ran.out);
    let rick = alice_key.to_str().unwrap();
    for args in [
        vec!["control", "repo", "create", "alice/proj"],
        vec![
            "control",
            "user",
            "add",
            "bob",
            "--key",
            &format!("@{}", bob_pub.display()),
        ],
    ] {
        let mut full = args.clone();
        full.extend_from_slice(&["-i", rick, "--at", &serving.grpc]);
        let ran = gunnar(&home, &full);
        assert!(ran.ok, "{args:?}: {}", ran.out);
    }

    // ── health: the binds it prints are the binds this test dialed ────────
    let health = gunnar(
        &home,
        &["control", "health", "-i", rick, "--at", &serving.grpc],
    );
    assert!(health.ok, "health: {}", health.out);
    assert!(
        health.stdout.contains("serving        yes"),
        "{}",
        health.out
    );
    assert!(
        health.stdout.contains(&serving.grpc),
        "the gRPC bind `health` prints must be the address this test is talking to — the \
         server's number, not a default: {}",
        health.stdout
    );
    assert!(
        health.stdout.contains(&serving.ssh),
        "…and the SSH bind likewise: {}",
        health.stdout
    );
    // ★ ACCT-2: the version line is the Gunnar RELEASE (`gunnar_common::RELEASE`, the gunnar
    // crate's version), not gunnar-grpc's own semver.
    let release = gunnar_common::RELEASE;
    assert!(
        health.stdout.contains(&format!("version        {release}")),
        "the version is the Gunnar release ({release}), because the server beside this test was \
         built from this workspace: {}",
        health.stdout
    );
    assert!(
        health.stdout.contains("authentication required"),
        "an ordinary server must report its gate on: {}",
        health.stdout
    );

    // ── stats: exactly what this test created, not merely "some numbers" ──
    let stats = gunnar(
        &home,
        &["control", "stats", "-i", rick, "--at", &serving.grpc],
    );
    assert!(stats.ok, "stats: {}", stats.out);
    assert!(
        stats.stdout.contains("principals     2"),
        "this server holds alice and bob and nobody else: {}",
        stats.stdout
    );
    assert!(
        stats.stdout.contains("repos          1"),
        "one repository was created: {}",
        stats.stdout
    );
    // The grants counter must agree with the OTHER verb that reads the same
    // table. Two readers, one table: `stats` says a number, `grant list`
    // shows the rows, and a disagreement means one of them is lying.
    let counted: u64 = stats
        .stdout
        .lines()
        .find_map(|l| l.strip_prefix("grants"))
        .expect("stats prints a grants line")
        .trim()
        .parse()
        .expect("the grants line carries a number");
    let listing = grant_list(&home, &serving.grpc, &alice_key, &[]);
    let rows = listing
        .stdout
        .lines()
        .filter(|l| l.starts_with("  "))
        .count() as u64;
    assert_eq!(
        counted, rows,
        "`stats` counts {counted} grant(s) and `grant list` prints {rows} row(s) — two verbs \
         reading one table disagree:\nstats:\n{}\nlist:\n{}",
        stats.stdout, listing.stdout
    );
    assert!(
        counted > 0,
        "the bootstrap admin's server grant alone makes this at least 1; 0 is the \
         identity-value trap: {}",
        stats.stdout
    );

    // ── storage: the repository row is there, behind a named backend ──────
    let storage = gunnar(
        &home,
        &["control", "storage", "-i", rick, "--at", &serving.grpc],
    );
    assert!(storage.ok, "storage: {}", storage.out);
    assert!(
        storage.stdout.contains("alice/proj"),
        "the created repository must appear as a row: {}",
        storage.stdout
    );
    let backend = storage
        .stdout
        .lines()
        .find_map(|l| l.strip_prefix("objects"))
        .expect("storage prints an object-backend line")
        .trim();
    assert!(
        !backend.is_empty(),
        "the backend is NAMED — an empty name would be a page about nothing: {}",
        storage.stdout
    );

    // ── usage: the meters, per namespace, and one namespace on request ─────
    let usage = gunnar(
        &home,
        &["control", "usage", "-i", rick, "--at", &serving.grpc],
    );
    assert!(usage.ok, "usage: {}", usage.out);
    let row = usage
        .stdout
        .lines()
        .find(|l| l.starts_with("alice"))
        .unwrap_or_else(|| {
            panic!(
                "no row for the namespace this test created:\n{}",
                usage.stdout
            )
        });
    assert!(
        row.contains("repos 1"),
        "the one repository this test created is alice's: {row}"
    );
    // A freshly created store already weighs something (the engine lays its
    // index down at create), and the meter must say so: a zero here would be
    // the identity-value trap the `stats` block above names.
    let pack: u64 = row
        .split_whitespace()
        .skip_while(|w| *w != "pack")
        .nth(1)
        .and_then(|w| w.parse().ok())
        .unwrap_or_else(|| panic!("the row carries `pack <bytes>`: {row}"));
    assert!(
        pack > 0,
        "an empty repository still has a store on disk: {row}"
    );
    let one = gunnar(
        &home,
        &[
            "control",
            "usage",
            "alice",
            "-i",
            rick,
            "--at",
            &serving.grpc,
        ],
    );
    assert!(one.ok, "usage alice: {}", one.out);
    assert_eq!(
        one.stdout
            .lines()
            .filter(|l| l.starts_with("alice"))
            .count(),
        1,
        "asked for one namespace, print one row: {}",
        one.stdout
    );
    let nobody = gunnar(
        &home,
        &[
            "control",
            "usage",
            "nobody",
            "-i",
            rick,
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        !nobody.ok,
        "an unknown namespace is an error, not an empty bill: {}",
        nobody.out
    );

    // ── capacity: "could you serve a cap of N bytes?", both arms, over the
    //    wire, against this box's real filesystem ────────────────────────────
    //
    // The YES and the NO come out of the SAME running server on the SAME
    // disk; only the number asked for changes. That is what makes the no arm
    // reachable rather than a branch nothing drives.
    let yes = gunnar(
        &home,
        &[
            "control",
            "capacity",
            "--bytes",
            "1",
            "-i",
            rick,
            "--at",
            &serving.grpc,
        ],
    );
    assert!(yes.ok, "capacity: {}", yes.out);
    assert!(
        yes.stdout.contains("verdict     YES"),
        "one byte fits on any disk with room: {}",
        yes.stdout
    );
    // 8 PiB. No disk on this machine, and none it could ever be given.
    let no = gunnar(
        &home,
        &[
            "control",
            "capacity",
            "--bytes",
            "9007199254740992",
            "-i",
            rick,
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        no.ok,
        "a `no` is an ANSWER, not a failed command: {}",
        no.out
    );
    assert!(
        no.stdout.contains("verdict     NO"),
        "8 PiB must be refused, and the refusal must be reachable through the whole stack: {}",
        no.stdout
    );
    assert!(
        no.stdout.contains("short"),
        "and must name what is MISSING, not only what is free: {}",
        no.stdout
    );
    // **The growth answer is its own line, on both arms.** A caller that read
    // only `verdict` would answer `NO` with "buy another disk"; on the
    // appliance that disk is billed and invisible, so the fact travels beside
    // the verdict and never instead of it.
    for out in [&yes.stdout, &no.stdout] {
        let growth = out
            .lines()
            .find_map(|l| l.strip_prefix("growth"))
            .unwrap_or_else(|| panic!("every answer carries a growth line:\n{out}"))
            .trim();
        assert!(
            !growth.is_empty() && !growth.starts_with('?'),
            "the server must FILL the growth answer in, not leave it unspecified: {growth}"
        );
        assert!(
            !growth.starts_with("SEALED"),
            "gunnar is not PID 1 in this test, so nothing may report a sealed appliance: {growth}"
        );
    }

    // **Two verbs, one table** — the idiom this file already uses for `stats`
    // and `grant list`: what `capacity --namespace` reports as HELD must be
    // the very byte count `usage` printed as `pack` above. A raise is charged
    // the difference, so a `held` that disagreed with the meter would quote a
    // tenant against bytes nobody is billing them for.
    let held_out = gunnar(
        &home,
        &[
            "control",
            "capacity",
            "--bytes",
            "1",
            "--namespace",
            "alice",
            "-i",
            rick,
            "--at",
            &serving.grpc,
        ],
    );
    assert!(held_out.ok, "capacity --namespace: {}", held_out.out);
    let held: u64 = held_out
        .stdout
        .lines()
        .find(|l| l.starts_with("held"))
        .and_then(|l| l.split_whitespace().nth(1))
        .and_then(|w| w.parse().ok())
        .unwrap_or_else(|| {
            panic!(
                "a named namespace prints a `held` row:\n{}",
                held_out.stdout
            )
        });
    assert_eq!(
        held, pack,
        "`capacity --namespace alice` says the namespace holds {held} B and `usage` says \
         {pack} B — two verbs reading one meter disagree:\n{}\n{}",
        held_out.stdout, usage.stdout
    );
    assert!(
        held > 0,
        "0 held would satisfy this comparison with any broken meter"
    );

    // ── the gate: an ordinary user's key is refused, a stranger's key is
    //    refused differently — Admin.* is gated exactly like the rest ──────
    let ran = gunnar(
        &home,
        &[
            "control",
            "stats",
            "-i",
            bob_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        !ran.ok,
        "bob holds no server grant and Admin.Stats must refuse him: {}",
        ran.out
    );
    assert!(ran.says("not allowed to do this"), "{}", ran.out);
    // The same gate on the capacity quote: it names the box's free space and
    // its disk floor, which is the whole filesystem's business and not a
    // tenant's.
    let ran = gunnar(
        &home,
        &[
            "control",
            "capacity",
            "--bytes",
            "1",
            "-i",
            bob_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(
        !ran.ok,
        "bob holds no server grant and Admin.Capacity must refuse him: {}",
        ran.out
    );
    assert!(ran.says("not allowed to do this"), "{}", ran.out);
    let (stranger_key, _) = mint(&keys, "stranger");
    let ran = gunnar(
        &home,
        &[
            "control",
            "health",
            "-i",
            stranger_key.to_str().unwrap(),
            "--at",
            &serving.grpc,
        ],
    );
    assert!(!ran.ok, "an unregistered key must be refused: {}", ran.out);
    assert!(ran.says("did not accept your key"), "{}", ran.out);
}

/// **The transport red (measure, never assert): `health` against a `--no-grpc`
/// server fails to connect and prints NO page.**
///
/// A verb that answered zeros with nothing listening would be a dashboard
/// drawing a healthy server out of its own defaults — the exact confusion
/// (`0 = never`, `0 = fine`) this whole verb family exists to end.
#[test]
fn health_against_a_no_grpc_server_fails_to_connect_and_prints_nothing() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let (data_root, home) = (dir.path().join("data"), dir.path().join("home"));
    let keys = home.join("keys");
    std::fs::create_dir_all(&keys).expect("mkdir");
    let (alice_key, alice_pub) = mint(&keys, "alice");

    // A port with NOTHING on it, reserved the honest way: bind :0, read the
    // number back, close it. This is where the server's control plane would
    // have been.
    let dead = {
        let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
        let addr = listener.local_addr().expect("addr").to_string();
        drop(listener);
        addr
    };

    // `--admin-key` because a `--no-grpc` server with an empty identity table
    // is a box nobody could ever enrol on, and the server says so at boot.
    let serving = Serving::start_with(
        &data_root,
        &home,
        &dir.path().join("server.log"),
        &[
            "--no-grpc",
            "--admin-key",
            &format!("@{}", alice_pub.display()),
        ],
    );
    assert_eq!(
        serving.grpc, "disabled",
        "a --no-grpc server must announce `grpc  disabled`, or this test is pointing its \
         refusal at the wrong cause"
    );

    let ran = gunnar(
        &home,
        &[
            "control",
            "health",
            "-i",
            alice_key.to_str().unwrap(),
            "--at",
            &dead,
        ],
    );
    assert!(
        !ran.ok,
        "`health` against a server with no control plane must FAIL: {}",
        ran.out
    );
    assert!(
        ran.says("could not be reached"),
        "…and say the transport is the problem: {}",
        ran.out
    );
    assert!(
        ran.stdout.trim().is_empty(),
        "…and print NO page — a `serving`/`uptime` line made of zeros here would be a report \
         about a server it never spoke to: {:?}",
        ran.stdout
    );
}