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

nordisk/gunnar-ui

uitest/src/matrix.rs

//! The situations.
//!
//! ## The order is load-bearing
//!
//! Every API row runs BEFORE the first passkey exists, because the console
//! retires the break-glass admin token the moment an admin passkey is
//! registered ([`auth::Webauthn::admin_token_usable`]). That retirement is not
//! an obstacle to work around — it is situation `admin-token-retires`, and the
//! rows above it are what prove the token worked right up until then. Reordering
//! them turns a real guarantee into a red row.
//!
//! ## Every row is checked against the server, not against the page
//!
//! A console that renders a repository it invented is a bug the console itself
//! cannot see, so the truth for a repository is `gunnar-cli control`, the truth
//! for a commit is `git rev-parse` in the working tree that pushed it, and the
//! truth for a passkey is `WebAuthn.getCredentials` — what the authenticator is
//! actually holding, not what the page said happened.

use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};

use crate::cdp::{Cdp, Chrome};
use crate::http;
use crate::json::{esc, Json};
use crate::rig::Rig;

/// "The login ceremony has finished, one way or the other."
///
/// It has to be asked in this order. A successful login NAVIGATES, so the
/// login page's `#msg` element is gone by the time the answer is known — and
/// `document.getElementById('msg').classList` then throws before a trailing
/// `|| location.pathname !== '/login'` is ever reached. Asking the server
/// first, and reaching for the element only with `?.`, is what makes the
/// success case and the failure case both answerable.
const SETTLED: &str = "fetch('/auth/status').then(r => r.json()).then(j => \
     j.authenticated === true || !!document.getElementById('msg')?.classList.contains('err'))";

pub struct Row {
    pub name: &'static str,
    pub ok: bool,
    pub ms: u128,
    pub detail: String,
}

/// Print each row's detail WHOLE rather than cut to a terminal width.
///
/// The rows here carry measurements — five pairs of pixels, four computed
/// colours, a length beside the length it was compared against — and a
/// hundred-and-fifty-character cut drops the end of exactly the ones worth
/// reading. The default stays narrow because twenty rows of that is a wall;
/// `--full` is for when a row is the answer to the question being asked.
pub static FULL: AtomicBool = AtomicBool::new(false);

/// `--only <glob>[,<glob>…]`: the situations to run. Unset: every one (the
/// default, and the only run that may call the console green).
pub static ONLY: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
/// Rows run as another row's SETUP under `--only`, and rows skipped by it.
pub static SETUP_RAN: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
pub static SKIPPED: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);

/// **The rows other rows stand on.** Under `--only` these run whether or not they
/// are selected — each makes state that every later browser row reads, inside its
/// own closure: the operator `bob` and his first invite, the authenticator, bob's
/// admin passkey and his logged-in session (`sess`), the invite and kim's isolated
/// browser, passkey and session (`sess2`, the `person` of places/addusers). Every OTHER unselected row is skipped. A
/// selected row that reads what a skipped row made (an API row's pull request, a
/// paid month before its prolong) fails by name: select that row too (a glob or a
/// list), and confirm any verdict with the full matrix.
pub const SETUP: &[&str] = &[
    // the operator `bob`: his namespace conjured by a first push, then claimed by
    // the installer's word with the break-glass token as his first invite — the
    // invite his passkey registers with
    "a-first-push-conjures-the-operators-namespace-first",
    "the-installer-claims-the-operator-and-his-first-invite",
    "chrome-starts-headless",
    "devtools-protocol-attaches",
    "a-virtual-authenticator-is-installed",
    // it opens /login, the page the registration row fills in
    "the-login-page-offers-a-passkey",
    "a-passkey-registers-with-the-admin-token",
    "the-passkey-logs-in",
    "an-admin-mints-an-invite-link",
    "the-invited-user-gets-their-own-browser",
    "the-invite-link-registers-a-second-passkey",
    "the-second-passkey-logs-in-as-a-viewer",
];

/// `*` any run of characters, `?` one character, everything else itself. Pure.
pub fn glob(pat: &str, name: &str) -> bool {
    let (p, n): (Vec<char>, Vec<char>) = (pat.chars().collect(), name.chars().collect());
    let (mut pi, mut ni, mut star, mut mark) = (0usize, 0usize, None::<usize>, 0usize);
    while ni < n.len() {
        if pi < p.len() && (p[pi] == '?' || p[pi] == n[ni]) {
            pi += 1;
            ni += 1;
        } else if pi < p.len() && p[pi] == '*' {
            star = Some(pi);
            mark = ni;
            pi += 1;
        } else if let Some(s) = star {
            pi = s + 1;
            mark += 1;
            ni = mark;
        } else {
            return false;
        }
    }
    p[pi..].iter().all(|c| *c == '*')
}

/// Is `name` selected by `--only` (always, without it)?
pub fn selected(name: &str) -> bool {
    ONLY.get().is_none_or(|pats| pats.iter().any(|p| glob(p, name)))
}

pub(crate) fn go(rows: &mut Vec<Row>, name: &'static str, f: impl FnOnce() -> Result<String, String>) -> bool {
    let setup = !selected(name);
    if setup && !SETUP.contains(&name) {
        // Skipped by --only: the rows after it go on as if it had passed; one that
        // needed its state says so by failing.
        SKIPPED.fetch_add(1, Ordering::Relaxed);
        return true;
    }
    let t = Instant::now();
    let (ok, detail) = match f() {
        Ok(d) => (true, d),
        Err(e) => (false, e),
    };
    let ms = t.elapsed().as_millis();
    let shown = if FULL.load(Ordering::Relaxed) { detail.clone() } else { http::cut(&detail, 150) };
    println!(
        "{:<4} {:<44} {:>6} ms  {}",
        match (setup, ok) {
            (false, true) => "PASS",
            (false, false) => "FAIL",
            (true, true) => "setup",
            (true, false) => "SETUP-FAIL",
        },
        name,
        ms,
        shown
    );
    if setup {
        SETUP_RAN.fetch_add(1, Ordering::Relaxed);
        // A setup row that passed is not a situation of this run; one that failed
        // makes the run red (nothing it stands under can be judged).
        if !ok {
            rows.push(Row { name, ok, ms, detail });
        }
    } else {
        rows.push(Row { name, ok, ms, detail });
    }
    ok
}

pub(crate) fn want(cond: bool, msg: impl FnOnce() -> String) -> Result<(), String> {
    if cond {
        Ok(())
    } else {
        Err(msg())
    }
}

/// The fixture: a repository with two commits, pushed by stock git over the
/// rig's SSH, which is what every read-side row is compared against.
pub struct Fixture {
    pub head: String,
    pub prev: String,
    pub lib_oid: String,
}

fn fixture(r: &Rig) -> Result<Fixture, String> {
    let wt = r.work.join("wt");
    std::fs::create_dir_all(&wt).map_err(|e| e.to_string())?;
    r.git(&wt, &["init", "-q", "--object-format=sha256", "-b", "main", "."])?;
    r.git(&wt, &["config", "user.name", "uitest"])?;
    r.git(&wt, &["config", "user.email", "info@example.invalid"])?;
    std::fs::create_dir_all(wt.join("src")).map_err(|e| e.to_string())?;
    std::fs::write(wt.join("README"), "gunnar uitest\n").map_err(|e| e.to_string())?;
    std::fs::write(wt.join("src/lib.rs"), "pub fn hello() {}\n").map_err(|e| e.to_string())?;
    r.git(&wt, &["add", "README", "src/lib.rs"])?;
    r.git(&wt, &["commit", "-qm", "first commit"])?;
    std::fs::write(wt.join("README"), "gunnar uitest\nsecond line\n").map_err(|e| e.to_string())?;
    std::fs::write(wt.join("src/second.rs"), "pub fn second() {}\n").map_err(|e| e.to_string())?;
    r.git(&wt, &["add", "README", "src/second.rs"])?;
    r.git(&wt, &["commit", "-qm", "second commit"])?;
    r.git(&wt, &["push", "-q", &r.remote("alice/uitest"), "main"])?;
    Ok(Fixture {
        head: r.git(&wt, &["rev-parse", "HEAD"])?.trim().to_owned(),
        prev: r.git(&wt, &["rev-parse", "HEAD~1"])?.trim().to_owned(),
        lib_oid: String::new(),
    })
}

pub struct Api<'a> {
    r: &'a Rig,
}

impl Api<'_> {
    fn get(&self, path: &str) -> Result<(u16, Json), String> {
        let h = [("Authorization", format!("Bearer {}", self.r.token))];
        let resp = http::req(&self.r.ui_addr(), "GET", path, &h, None)?;
        let body = resp.json().unwrap_or(Json::Null);
        Ok((resp.status, body))
    }
    fn get_raw(&self, path: &str) -> Result<(u16, String), String> {
        let h = [("Authorization", format!("Bearer {}", self.r.token))];
        let resp = http::req(&self.r.ui_addr(), "GET", path, &h, None)?;
        Ok((resp.status, resp.body))
    }
    fn anon(&self, path: &str) -> Result<(u16, String), String> {
        let resp = http::req(&self.r.ui_addr(), "GET", path, &[], None)?;
        Ok((resp.status, resp.body))
    }
    fn post(&self, path: &str, body: &str) -> Result<(u16, Json, String), String> {
        let h = [
            ("Authorization", format!("Bearer {}", self.r.token)),
            ("Content-Type", "application/json".to_owned()),
        ];
        let resp = http::req(&self.r.ui_addr(), "POST", path, &h, Some(body))?;
        let j = resp.json().unwrap_or(Json::Null);
        Ok((resp.status, j, resp.body))
    }
    fn delete(&self, path: &str) -> Result<(u16, String), String> {
        let h = [("Authorization", format!("Bearer {}", self.r.token))];
        let resp = http::req(&self.r.ui_addr(), "DELETE", path, &h, None)?;
        Ok((resp.status, resp.body))
    }
}

/// The oid at `refs/heads/main`, read off the server's own ref advertisement
/// over git — the same witness the force-push rows trust, and a stronger one
/// than the console's own answer that a merge did or did not move the branch.
/// **A POST the way the web's `api::call` sends it** (GHUNT-19): a 403
/// `"code":"stepup"` is answered with a fresh touch of the session's own key
/// (`/auth/passkey/step-up`, the virtual authenticator), then the same call once
/// more. Evaluates to the final HTTP status. `body` is a JS expression.
fn stepped_post(url: &str, body: &str) -> String {
    format!(
        "(async () => {{
           const b2u = s => {{ s = s.replace(/-/g,'+').replace(/_/g,'/'); while (s.length % 4) s += '=';
                               return Uint8Array.from(atob(s), c => c.charCodeAt(0)).buffer; }};
           const u2b = b => btoa(String.fromCharCode(...new Uint8Array(b))).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=+$/,'');
           const send = () => fetch({url}, {{method:'POST', headers:{{'Content-Type':'application/json'}}, body: {body}}});
           let r = await send();
           if (r.status === 403) {{
             const j = await r.clone().json().catch(() => ({{}}));
             if (j.code === 'stepup') {{
               const o = await (await fetch('/auth/passkey/step-up')).json();
               const pk = o.publicKey;
               pk.challenge = b2u(pk.challenge);
               pk.allowCredentials = (pk.allowCredentials || []).map(c => ({{ ...c, id: b2u(c.id) }}));
               const a = await navigator.credentials.get({{ publicKey: pk }});
               const s = await fetch('/auth/passkey/step-up', {{method:'POST', headers:{{'Content-Type':'application/json'}},
                 body: JSON.stringify({{ ceremony_id: o.ceremony_id, id: u2b(a.rawId), response: {{
                   clientDataJSON: u2b(a.response.clientDataJSON), authenticatorData: u2b(a.response.authenticatorData),
                   signature: u2b(a.response.signature) }} }})}});
               if (!s.ok) return s.status;
               r = await send();
             }}
           }}
           return r.status;
         }})()",
        url = esc(url),
    )
}

fn server_main(r: &Rig, repo: &str) -> Result<String, String> {
    let out = r.git(&r.work, &["ls-remote", &r.remote(repo), "refs/heads/main"])?;
    Ok(out.split_whitespace().next().unwrap_or("").to_owned())
}

/// The byte string a number renders to on the page, for an EXACT cross-check
/// against what the console draws. It mirrors the console's own `fmt::bytes`
/// (crates/gunnar-ui-web/src/fmt.rs) — a dozen lines the test cannot call
/// across the wasm boundary, kept identical on purpose so a row that asserts a
/// rendered byte figure is checking the server's number, not the page's claim.
fn fmt_bytes(count: u64) -> String {
    const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
    if count < 1024 {
        return format!("{count} B");
    }
    let mut value = count as f64;
    let mut unit = 0;
    while value >= 1024.0 && unit + 1 < UNITS.len() {
        value /= 1024.0;
        unit += 1;
    }
    format!("{value:.1} {}", UNITS[unit])
}

/// [`fmt_bytes`] read back: `714.6 GiB` → bytes (to the rounding of its one decimal).
fn parse_bytes(shown: &str) -> Option<u64> {
    const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
    let (value, unit) = shown.trim().split_once(' ')?;
    let power = UNITS.iter().position(|u| *u == unit.trim())?;
    let value: f64 = value.parse().ok()?;
    Some((value * 1024f64.powi(power as i32)) as u64)
}

/// One `name <number>` field out of a `gunnar-cli control usage` line.
///
/// The line is tab separated and each byte figure carries the raw count first
/// and the rounded form in brackets — `pack 7340032 (7.0 MiB)` — because the
/// raw number is the one a later reader can diff or compare. That is the one
/// taken here: comparing rounded strings would let a console that lost a field
/// agree by accident whenever two meters rounded the same way.
/// **The usage page has no money in it, and this is the check.** Every word a
/// bill would use, as a substring of the lower-cased page text, so a tooltip
/// or a footnote is caught as surely as a heading. Extended 2026-09-05 with
/// `price`, `pay`, `credit` and `top up` when the page gained the allowance —
/// the one place a limit could have grown a sales pitch.
fn sweep_for_money(seen: &str) -> Result<(), String> {
    let lower = seen.to_lowercase();
    for word in [
        "plan", "billing", "invoice", "quota", "upgrade", "subscription", "price", "pay", "credit", "top up",
    ] {
        want(!lower.contains(word), || {
            format!("the usage page says {word:?}, which it must not: {}", http::cut(seen, 300))
        })?;
    }
    Ok(())
}

fn field(line: &str, name: &str) -> Option<u64> {
    line.split('\t')
        .map(str::trim)
        .find_map(|piece| piece.strip_prefix(name)?.trim_start().split_whitespace().next()?.parse().ok())
}

/// Does an array of `{items:[…]}` hold an object whose `field` equals `value`?
fn items_have(j: &Json, field: &str, value: &str) -> bool {
    j.path("items")
        .map(|i| i.arr().iter().any(|e| e.text(field) == value))
        .unwrap_or(false)
}

pub fn run(r: &Rig, chrome_bin: Option<&str>, authenticator: bool) -> Vec<Row> {
    let mut rows: Vec<Row> = Vec::new();
    let a = Api { r };

    // The inline-comment fixture: a pull request and the blob its comment
    // anchors to, set by the API rows and read by the browser rows below.
    let inline_pull = std::cell::RefCell::new(String::new());
    let inline_blob = std::cell::RefCell::new(String::new());

    let fx = match fixture(r) {
        Ok(f) => f,
        Err(e) => {
            go(&mut rows, "fixture-pushes-two-commits", || Err(e));
            return rows;
        }
    };
    go(&mut rows, "fixture-pushes-two-commits", || {
        Ok(format!("head {}", &fx.head[..12]))
    });

    // ── the console answers for itself ────────────────────────────────────
    go(&mut rows, "health-is-plain-ok", || {
        let (s, b) = a.get_raw("/health")?;
        want(s == 200 && b.trim() == "ok", || format!("{s} {b:?}"))?;
        Ok("200 ok".into())
    });

    go(&mut rows, "api-refuses-without-a-credential", || {
        // The red that makes every green below mean something. `/api/repos`
        // is deliberately NOT in this list: since the rig has a guest
        // identity (2026-09-10) a stranger is answered there with the public
        // subset, as on gunnar.rs, and what that answer must not contain is
        // asserted where it matters (`a-public-repository-opens-the-anonymous-door`,
        // `logging-out-ends-the-session`). These three are on no allow-list
        // and must be 401 to a stranger whatever the guest can see.
        let mut seen = Vec::new();
        for p in ["/api/storage", "/auth/passkey/invites", "/api/principals"] {
            let (s, b) = a.anon(p)?;
            want(s == 401, || format!("{p} answered {s}, not 401: {}", http::cut(&b, 80)))?;
            seen.push(p);
        }
        Ok(format!("401 on {}", seen.len()))
    });

    go(&mut rows, "api-accepts-the-admin-token", || {
        let (s, j) = a.get("/api/repos")?;
        want(s == 200, || format!("status {s}"))?;
        want(j.path("items").is_some(), || format!("no items in {}", j.dump()))?;
        Ok(format!("{} repos", j.path("total").and_then(|t| t.as_f64()).unwrap_or(-1.0)))
    });

    go(&mut rows, "version-names-a-real-build", || {
        let (s, j) = a.get("/api/version")?;
        want(s == 200, || format!("status {s}"))?;
        let r_ref = j.text("build_ref");
        want(!r_ref.is_empty(), || format!("no build_ref in {}", j.dump()))?;
        want(j.text("version").contains(&r_ref), || {
            format!("the version line {:?} does not carry the git hash {r_ref}", j.text("version"))
        })?;
        Ok(j.text("version"))
    });

    go(&mut rows, "health-names-the-server-it-talks-to", || {
        let (s, j) = a.get("/api/health")?;
        want(s == 200, || format!("status {s}"))?;
        want(j.path("serving").and_then(|v| v.as_bool()) == Some(true), || "not serving".into())?;
        want(j.text("grpc_bind") == r.at(), || {
            format!("console reports grpc_bind {:?}, the rig started {}", j.text("grpc_bind"), r.at())
        })?;
        Ok(format!("{} on {}", j.text("agent"), j.text("grpc_bind")))
    });

    go(&mut rows, "bootstrap-is-closed-and-names-its-claimer", || {
        let (s, j) = a.get("/api/bootstrap")?;
        want(s == 200, || format!("status {s}"))?;
        want(j.path("bootstrapped").and_then(|v| v.as_bool()) == Some(true), || "not bootstrapped".into())?;
        want(j.path("window_open").and_then(|v| v.as_bool()) == Some(false), || {
            "the enrolment window is still open".into()
        })?;
        want(j.text("bootstrapped_principal") == "alice", || {
            format!("claimed by {:?}", j.text("bootstrapped_principal"))
        })?;
        Ok(j.text("bootstrapped_principal"))
    });

    // ── what was pushed is what is shown ──────────────────────────────────
    go(&mut rows, "a-pushed-repo-appears-in-the-console", || {
        let (_, j) = a.get("/api/repos")?;
        want(items_have(&j, "path", "alice/uitest"), || {
            format!("alice/uitest is not in {}", http::cut(&j.dump(), 200))
        })?;
        // and the console is not inventing it: the server says so too.
        let listed = r.control(&["repo", "list"])?;
        want(listed.contains("uitest"), || format!("gunnar itself does not list it: {}", http::cut(&listed, 200)))?;
        Ok("in the console and in gunnar".into())
    });

    go(&mut rows, "refs-carry-the-oid-git-pushed", || {
        let (s, j) = a.get("/api/repos/alice/uitest/refs")?;
        want(s == 200, || format!("status {s}"))?;
        let main = j
            .path("items")
            .and_then(|i| i.arr().iter().find(|e| e.text("ref_name") == "refs/heads/main").cloned())
            .ok_or_else(|| format!("no refs/heads/main in {}", http::cut(&j.dump(), 200)))?;
        want(main.text("oid") == fx.head, || {
            format!("console says {}, git says {}", main.text("oid"), fx.head)
        })?;
        Ok(format!("main = {}", &fx.head[..12]))
    });

    go(&mut rows, "head-is-symbolic-to-main", || {
        let (_, j) = a.get("/api/repos/alice/uitest/head")?;
        want(j.text("symbolic_target") == "refs/heads/main", || format!("{}", j.dump()))?;
        want(j.text("oid") == fx.head, || format!("oid {}", j.text("oid")))?;
        Ok("HEAD → refs/heads/main".into())
    });

    go(&mut rows, "the-log-is-the-history-git-made", || {
        let (_, j) = a.get("/api/repos/alice/uitest/log")?;
        let items = j.path("items").map(|i| i.arr().to_vec()).unwrap_or_default();
        want(items.len() >= 2, || format!("{} commits", items.len()))?;
        want(items[0].text("oid") == fx.head, || format!("first is {}", items[0].text("oid")))?;
        want(items[0].text("message").starts_with("second commit"), || {
            format!("subject {:?}", items[0].text("message"))
        })?;
        want(items[1].text("oid") == fx.prev, || format!("second is {}", items[1].text("oid")))?;
        want(items[0].path("parent_oids").map(|p| p.arr().iter().any(|o| o.as_str() == Some(fx.prev.as_str()))) == Some(true), || {
            "the head commit does not name its parent".into()
        })?;
        Ok(format!("{} commits, newest first", items.len()))
    });

    go(&mut rows, "browsing-the-root-lists-what-was-pushed", || {
        let (_, j) = a.get("/api/repos/alice/uitest/browse")?;
        let names: Vec<String> = j.path("entries").map(|e| e.arr().iter().map(|x| x.text("name")).collect()).unwrap_or_default();
        want(names.iter().any(|n| n == "README"), || format!("entries {names:?}"))?;
        want(names.iter().any(|n| n == "src"), || format!("entries {names:?}"))?;
        want(j.text("revision.subject") == "second commit", || {
            format!("revision subject {:?}", j.text("revision.subject"))
        })?;
        Ok(format!("{names:?}"))
    });

    let lib_oid = std::cell::RefCell::new(String::new());
    go(&mut rows, "browsing-a-subdirectory-descends", || {
        let (_, j) = a.get("/api/repos/alice/uitest/browse?path=src")?;
        let entries = j.path("entries").map(|e| e.arr().to_vec()).unwrap_or_default();
        let lib = entries
            .iter()
            .find(|e| e.text("name") == "lib.rs")
            .ok_or_else(|| format!("no lib.rs in {:?}", entries.iter().map(|e| e.text("name")).collect::<Vec<_>>()))?;
        want(lib.text("kind") == "file", || format!("lib.rs is a {}", lib.text("kind")))?;
        *lib_oid.borrow_mut() = lib.text("oid");
        want(entries.iter().any(|e| e.text("name") == "second.rs"), || "no second.rs".into())?;
        Ok(format!("{} entries", entries.len()))
    });

    go(&mut rows, "an-object-serves-the-bytes-that-were-committed", || {
        let oid = lib_oid.borrow().clone();
        want(!oid.is_empty(), || "the browse row did not find lib.rs".into())?;
        let (s, j) = a.get(&format!("/api/repos/alice/uitest/object?oid={oid}"))?;
        want(s == 200, || format!("status {s}"))?;
        let text = j.text("text");
        let text = if text.is_empty() { j.text("content") } else { text };
        want(text.contains("pub fn hello()"), || {
            format!("the blob does not hold what git committed: {}", http::cut(&j.dump(), 200))
        })?;
        Ok(format!("{} bytes", j.path("size").and_then(|s| s.as_f64()).unwrap_or(-1.0)))
    });

    // ── a README's relative link, and the bytes at the end of it ──────────
    go(&mut rows, "a-readmes-relative-link-opens-the-file-it-names", || {
        // The defect measured on gunnar.rs 2026-09-11: the repository's README
        // links to its own PDF, the console drew the words and no link, and
        // there was no path-addressed route for a file to be handed to a
        // browser at all. `readme::links` rewrites the target and
        // `api::browse::raw` serves the bytes, and both have unit tests — but
        // nothing joined them to a README that a git client actually pushed.
        // This is the join: one repository, one relative link, and the file at
        // the other end of the href the server itself wrote.
        let wt = r.work.join("wt-readme");
        std::fs::create_dir_all(wt.join("docs")).map_err(|e| e.to_string())?;
        r.git(&wt, &["init", "-q", "--object-format=sha256", "-b", "main", "."])?;
        r.git(&wt, &["config", "user.name", "uitest"])?;
        r.git(&wt, &["config", "user.email", "info@vetra.se"])?;
        // A PDF that is a real one as far as anything here is concerned: the
        // route decides the content type from the NAME and never sniffs, and
        // the assertion below is on the bytes, so a header is all it needs.
        let pdf = "%PDF-1.4\n% uitest\n1 0 obj<</Type/Catalog>>endobj\ntrailer<</Root 1 0 R>>\n%%EOF\n";
        std::fs::write(wt.join("docs/guide.pdf"), pdf).map_err(|e| e.to_string())?;
        std::fs::write(
            wt.join("README.md"),
            "# links\n\nRead [the guide](docs/guide.pdf), or the [notes](docs/notes.txt).\n",
        )
        .map_err(|e| e.to_string())?;
        std::fs::write(wt.join("docs/notes.txt"), "notes\n").map_err(|e| e.to_string())?;
        r.git(&wt, &["add", "README.md", "docs/guide.pdf", "docs/notes.txt"])?;
        r.git(&wt, &["commit", "-qm", "a readme with relative links"])?;
        r.git(&wt, &["push", "-q", &r.remote("alice/readme-links"), "main"])?;

        let (s, j) = a.get("/api/repos/alice/readme-links/readme")?;
        want(s == 200, || format!("the readme route answered {s}: {}", http::cut(&j.dump(), 200)))?;
        let body = j.text("body");
        want(!body.is_empty(), || format!("the readme rendered nothing: {}", http::cut(&j.dump(), 250)))?;
        // The PDF goes to the BYTES, and the text file to the console's own
        // file view — the two halves of the table in `readme::links`, so a
        // rewrite that sent everything to one of them could not pass.
        let raw = "/api/repos/alice/readme-links/raw?path=docs/guide.pdf";
        want(body.contains(raw), || {
            format!("the README's link to its PDF is not {raw:?}: {}", http::cut(&body, 300))
        })?;
        // GUN-GUEST (2026-10-30): the console's one address scheme, `/{ns}/{name}/tree/{path}`.
        want(body.contains("/alice/readme-links/tree/docs/notes.txt"), || {
            format!("the link to a text file does not open the file view: {}", http::cut(&body, 300))
        })?;
        // And the href the server wrote actually serves the file, inline, as a
        // PDF. Asserted on the applied output — the bytes and the header a
        // browser decides with — because a route that answered 200 with the
        // console's index.html would satisfy a status check.
        let h = [("Authorization", format!("Bearer {}", r.token))];
        let resp = http::req(&r.ui_addr(), "GET", raw, &h, None)?;
        want(resp.status == 200, || {
            format!("the raw route answered {}: {}", resp.status, http::cut(&resp.body, 200))
        })?;
        want(resp.header("content-type") == Some("application/pdf"), || {
            format!("a PDF is served as {:?}", resp.header("content-type"))
        })?;
        want(resp.body.starts_with("%PDF-1.4"), || {
            format!("the bytes are not the file that was pushed: {}", http::cut(&resp.body, 120))
        })?;
        want(resp.body.len() == pdf.len(), || {
            format!("{} bytes came back for a {}-byte file", resp.body.len(), pdf.len())
        })?;
        Ok(format!(
            "the README's relative link resolves to {raw} and serves {} bytes of application/pdf",
            resp.body.len()
        ))
    });

    go(&mut rows, "a-diff-between-two-commits-is-the-edit", || {
        let (s, j) = a.get(&format!(
            "/api/repos/alice/uitest/diff?from={}&to={}",
            fx.prev, fx.head
        ))?;
        want(s == 200, || format!("status {s}: {}", http::cut(&j.dump(), 150)))?;
        let d = j.dump();
        want(d.contains("second.rs"), || format!("the added file is not in the diff: {}", http::cut(&d, 250)))?;
        want(d.contains("README"), || format!("the edited file is not in the diff: {}", http::cut(&d, 250)))?;
        Ok("README edited, second.rs added".into())
    });

    go(&mut rows, "storage-accounts-for-the-repository", || {
        let (_, j) = a.get("/api/storage")?;
        let repo = j
            .path("repos")
            .and_then(|r| r.arr().iter().find(|e| e.text("repo") == "alice/uitest").cloned())
            .ok_or_else(|| format!("alice/uitest is not accounted for: {}", http::cut(&j.dump(), 200)))?;
        want(repo.text("hash_kind") == "sha256", || format!("hash {}", repo.text("hash_kind")))?;
        want(repo.path("ref_count").and_then(|c| c.as_f64()).unwrap_or(0.0) >= 1.0, || "no refs counted".into())?;
        Ok(format!("{} backend, {} refs", http::cut(&j.text("object_backend"), 40), repo.path("ref_count").and_then(|c| c.as_f64()).unwrap_or(0.0)))
    });

    go(&mut rows, "stats-count-what-exists", || {
        let (_, j) = a.get("/api/stats")?;
        let repos = j.path("repos").and_then(|v| v.as_f64()).unwrap_or(0.0);
        want(repos >= 1.0, || format!("repos {repos}"))?;
        want(j.path("principals").and_then(|v| v.as_f64()).unwrap_or(0.0) >= 2.0, || "principals".into())?;
        Ok(format!("{repos} repos"))
    });

    go(&mut rows, "stats-carry-the-index-progress-rows", || {
        // The field the console's Indexing panel is drawn from. It is `[]` on
        // an idle box, and the ASSERTION IS THAT IT IS AN ARRAY AT ALL: the
        // console tolerates its absence (`serde(default)`, for appliances older
        // than 2026-08-30), which means a server that stopped sending it would
        // cost the operator the panel and nothing would say so.
        let (_, j) = a.get("/api/stats")?;
        let index = j.path("index").ok_or_else(|| {
            format!("/api/stats has no `index` field at all: {}", http::cut(&j.dump(), 200))
        })?;
        want(matches!(index, Json::Arr(_)), || {
            format!("`index` is not an array: {}", index.dump())
        })?;
        Ok(format!("{} repositories indexing", index.arr().len()))
    });

    go(&mut rows, "stats-carry-the-replication-standbys", || {
        let (_, j) = a.get("/api/stats")?;
        want(j.path("replication_configured").and_then(|v| v.as_bool()).is_some(), || {
            format!("/api/stats does not report whether replication is configured: {}", http::cut(&j.dump(), 200))
        })?;
        let standbys = j.path("standbys").ok_or("no standbys field")?;
        want(matches!(standbys, Json::Arr(_)), || "standbys is not an array".into())?;
        // Empty with `configured` false is nothing set up. Empty with it TRUE
        // would be a bug, and gunnar's own proto says so — assert the pair,
        // not either half.
        let configured = j.path("replication_configured").and_then(|v| v.as_bool()) == Some(true);
        want(!(configured && standbys.arr().is_empty()), || {
            "replication reports itself configured with no standby at all".into()
        })?;
        Ok(format!("configured={configured}, {} standby(s)", standbys.arr().len()))
    });

    go(&mut rows, "principals-keys-and-grants-agree-with-gunnar", || {
        let (_, p) = a.get("/api/principals")?;
        want(items_have(&p, "id", "alice"), || format!("no alice: {}", http::cut(&p.dump(), 150)))?;
        let (_, g) = a.get("/api/grants")?;
        let admin = g
            .path("items")
            .map(|i| i.arr().iter().any(|e| e.text("principal_id") == "alice" && e.text("level") == "admin"))
            .unwrap_or(false);
        want(admin, || format!("alice is not a server admin: {}", http::cut(&g.dump(), 200)))?;
        let (_, k) = a.get("/api/keys")?;
        want(k.path("items").map(|i| !i.arr().is_empty()).unwrap_or(false), || "no keys".into())?;
        Ok("alice is admin, keys listed".into())
    });

    // ── the console can change things, and gunnar sees the change ─────────
    go(&mut rows, "the-console-creates-a-repository-gunnar-then-has", || {
        let (s, j, body) = a.post("/api/repos", r#"{"namespace":"alice","name":"made-by-console"}"#)?;
        want(s == 200, || format!("status {s}: {}", http::cut(&body, 150)))?;
        want(j.text("path") == "alice/made-by-console", || format!("{}", j.dump()))?;
        let listed = r.control(&["repo", "list"])?;
        want(listed.contains("made-by-console"), || {
            format!("gunnar does not have it: {}", http::cut(&listed, 200))
        })?;
        Ok("created and confirmed by gunnar".into())
    });

    go(&mut rows, "creating-the-same-repository-twice-is-refused", || {
        let (s, _, body) = a.post("/api/repos", r#"{"namespace":"alice","name":"made-by-console"}"#)?;
        want(s != 200, || "the second create succeeded".into())?;
        Ok(format!("{s} {}", http::cut(&body, 90)))
    });

    go(&mut rows, "an-unknown-repository-does-not-confirm-it-is-unknown", || {
        // The account model (owner, 2026-10-03): a repository the reader
        // cannot see — absent, or private and not theirs — is ONE answer, the
        // uniform 404 "this page does not exist", byte for byte what a
        // namespace that does not exist answers. A 403 here would confirm the
        // name; so would a body that names it.
        let (s, b) = a.get_raw("/api/repos/alice/no-such-repo")?;
        want(s == 404, || format!("status {s}, not the uniform 404: {}", http::cut(&b, 120)))?;
        let (s2, b2) = a.get_raw("/api/repos/no-such-namespace/no-such-repo")?;
        want((s, b.trim()) == (s2, b2.trim()), || {
            format!("an absent repository ({s} {}) and an absent namespace ({s2} {}) answer differently", http::cut(&b, 100), http::cut(&b2, 100))
        })?;
        want(!b.contains("no-such-repo") && !b.contains("no such") && !b.contains("not found"), || {
            format!("the refusal says what is missing: {}", http::cut(&b, 120))
        })?;
        Ok(format!("404 {}, the same as for an absent namespace", http::cut(&b, 80)))
    });

    go(&mut rows, "the-console-deletes-a-repository-gunnar-then-lacks", || {
        let (s, b) = a.delete("/api/repos/alice/made-by-console")?;
        want((200..300).contains(&s), || format!("status {s}: {}", http::cut(&b, 120)))?;
        let (_, j) = a.get("/api/repos")?;
        want(!items_have(&j, "path", "alice/made-by-console"), || "still listed by the console".into())?;
        let listed = r.control(&["repo", "list"])?;
        want(!listed.contains("made-by-console"), || format!("gunnar still has it: {}", http::cut(&listed, 200)))?;
        Ok("gone from both".into())
    });

    go(&mut rows, "a-deleted-repository-is-still-charged-for", || {
        // The console's delete is SOFT (purge: false, the server's retention
        // window), so the bytes do not go away — and a customer who deletes a
        // repository and sees no change in their usage is looking at that
        // window, not at a bug. This is the situation that makes the window
        // visible rather than a surprise.
        let listed = r.control(&["repo", "list-deleted"])?;
        want(listed.contains("made-by-console"), || {
            format!("gunnar does not have it tombstoned: {}", http::cut(&listed, 200))
        })?;
        let (_, j) = a.get("/api/storage")?;
        let held = j.path("total_tombstoned_bytes").and_then(|v| v.as_f64()).ok_or_else(|| {
            format!("/api/storage does not report tombstoned bytes at all: {}", http::cut(&j.dump(), 200))
        })?;
        want(held > 0.0, || "a tombstoned repository is holding zero bytes".into())?;
        let named = j
            .path("tombstoned")
            .map(|t| t.arr().iter().any(|e| e.text("repo") == "alice/made-by-console"))
            .unwrap_or(false);
        want(named, || {
            format!("the bytes are reported but not which repository holds them: {}", http::cut(&j.dump(), 250))
        })?;
        // And they are NOT double-counted into the live totals.
        let live = j
            .path("repos")
            .map(|rs| rs.arr().iter().any(|e| e.text("repo") == "alice/made-by-console"))
            .unwrap_or(false);
        want(!live, || "a tombstoned repository is still listed as a live one".into())?;
        Ok(format!("{held:.0} bytes held, named, and not double-counted"))
    });

    // ── the bytes nobody could see, and the undo nobody could reach ──────
    //
    // Three RPCs the console listed as "not surfaced" until 2026-08-31, and
    // every one of them is about storage an account is being charged for.
    go(&mut rows, "the-deleted-list-names-what-was-deleted", || {
        // `made-by-console` was soft-deleted by the situation above.
        let (s, j) = a.get("/api/repos/deleted")?;
        want(s == 200, || format!("status {s}"))?;
        let row = j
            .path("items")
            .and_then(|i| i.arr().iter().find(|e| e.text("path") == "alice/made-by-console").cloned())
            .ok_or_else(|| {
                format!("the deleted list does not name it: {}", http::cut(&j.dump(), 250))
            })?;
        want(row.path("bytes").and_then(|b| b.as_f64()).unwrap_or(0.0) > 0.0, || {
            "a tombstoned repository is listed as holding zero bytes".into()
        })?;
        want(row.path("deleted_at_unix_ms").and_then(|d| d.as_f64()).unwrap_or(0.0) > 0.0, || {
            "the row carries no delete time".into()
        })?;
        // The red half: a LIVE repository must not be in this list, or the
        // list is just the catalogue with a different name.
        want(!items_have(&j, "path", "alice/uitest"), || {
            "a live repository is in the deleted list".into()
        })?;
        Ok(format!("{} deleted, named, with bytes", j.path("items").map(|i| i.arr().len()).unwrap_or(0)))
    });

    go(&mut rows, "undo-restores-a-deleted-repository-and-its-objects", || {
        // A repository with REAL CONTENT, because the thing worth proving is
        // not that a 200 came back: it is that the objects are readable again
        // afterwards. A metadata row can be restored over a store that was
        // reclaimed, and that is exactly the failure this asserts against.
        let wt = r.work.join("undo-wt");
        std::fs::create_dir_all(&wt).map_err(|e| e.to_string())?;
        r.git(&wt, &["init", "-q", "--object-format=sha256", "-b", "main", "."])?;
        r.git(&wt, &["config", "user.name", "uitest"])?;
        r.git(&wt, &["config", "user.email", "info@example.invalid"])?;
        std::fs::write(wt.join("KEEP"), "bytes worth an undo\n").map_err(|e| e.to_string())?;
        r.git(&wt, &["add", "KEEP"])?;
        r.git(&wt, &["commit", "-qm", "the commit the undo has to bring back"])?;
        r.git(&wt, &["push", "-q", &r.remote("alice/undo-me"), "main"])?;
        let head = r.git(&wt, &["rev-parse", "HEAD"])?.trim().to_owned();

        let (_, before) = a.get("/api/repos/alice/undo-me/refs")?;
        let listed = before
            .path("items")
            .and_then(|i| i.arr().iter().find(|e| e.text("ref_name") == "refs/heads/main").cloned())
            .ok_or("the console cannot read the repository before it is deleted")?;
        want(listed.text("oid") == head, || format!("console {} git {head}", listed.text("oid")))?;

        let (s, body) = a.delete("/api/repos/alice/undo-me")?;
        want((200..300).contains(&s), || format!("delete answered {s}: {}", http::cut(&body, 120)))?;
        // RED: gone, and gone in the way that matters — the console cannot
        // read a ref out of it any more. Without this the undo below could
        // "restore" something that was never away.
        let (gone, refused) = a.get_raw("/api/repos/alice/undo-me/refs")?;
        want(gone != 200, || {
            format!("a deleted repository still serves its refs: {}", http::cut(&refused, 150))
        })?;
        let (_, deleted) = a.get("/api/repos/deleted")?;
        want(items_have(&deleted, "path", "alice/undo-me"), || {
            format!("not in the deleted list: {}", http::cut(&deleted.dump(), 200))
        })?;

        let (s, _, body) = a.post("/api/repos/alice/undo-me/undelete", "{}")?;
        want((200..300).contains(&s), || format!("undelete answered {s}: {}", http::cut(&body, 150)))?;

        // …and the proof: the SAME commit git pushed is readable again, not
        // merely a catalogue row with the right name.
        let (s, after) = a.get("/api/repos/alice/undo-me/refs")?;
        want(s == 200, || format!("refs answered {s} after the undo"))?;
        let main = after
            .path("items")
            .and_then(|i| i.arr().iter().find(|e| e.text("ref_name") == "refs/heads/main").cloned())
            .ok_or_else(|| format!("no refs/heads/main after the undo: {}", http::cut(&after.dump(), 200)))?;
        want(main.text("oid") == head, || {
            format!("the restored branch is {} and git pushed {head}", main.text("oid"))
        })?;
        // The objects, not just the ref: read the commit back through the
        // console the way a person browsing it would.
        let (s, obj) = a.get(&format!("/api/repos/alice/undo-me/object?oid={head}"))?;
        want(s == 200, || format!("the restored commit object answered {s}"))?;
        want(obj.dump().contains("the commit the undo has to bring back"), || {
            format!("the restored object is not the commit that was pushed: {}", http::cut(&obj.dump(), 200))
        })?;
        // …and gunnar itself agrees, on both sides of the ledger.
        let live = r.control(&["repo", "list"])?;
        want(live.contains("undo-me"), || format!("gunnar does not have it back: {}", http::cut(&live, 200)))?;
        let (_, still) = a.get("/api/repos/deleted")?;
        want(!items_have(&still, "path", "alice/undo-me"), || {
            "restored, and still listed as deleted".into()
        })?;
        Ok(format!("deleted and restored at {}", &head[..12]))
    });

    go(&mut rows, "an-orphan-store-is-listed-once-one-exists", || {
        // RED FIRST, and it is the half that makes the green mean anything: on
        // a server where every directory is accounted for, the orphan list is
        // EMPTY. A listing that always answers with something would pass the
        // green below while reporting nothing real.
        let (s, before) = a.get("/api/orphans")?;
        want(s == 200, || format!("status {s}"))?;
        let items = before.path("items").ok_or_else(|| {
            format!("/api/orphans has no items array at all: {}", http::cut(&before.dump(), 200))
        })?;
        want(items.arr().is_empty(), || {
            format!("a clean server reports orphans: {}", http::cut(&before.dump(), 250))
        })?;

        // An orphan is exactly this: a `<name>.git` directory under the
        // repository root with no catalog row and no tombstone. Made by hand
        // here for the same reason the appliance's was made by accident — no
        // gunnar verb can produce one, which is the whole problem.
        let stray = r.work.join("data/repos/alice/stray.git");
        std::fs::create_dir_all(&stray).map_err(|e| e.to_string())?;
        std::fs::write(stray.join("OBJECT_FORMAT"), "sha256\n").map_err(|e| e.to_string())?;
        std::fs::write(stray.join("stranded.pack"), vec![7u8; 4096]).map_err(|e| e.to_string())?;

        let (_, after) = a.get("/api/orphans")?;
        let row = after
            .path("items")
            .and_then(|i| i.arr().iter().find(|e| e.text("path") == "alice/stray").cloned())
            .ok_or_else(|| {
                format!("the orphan is invisible to the console: {}", http::cut(&after.dump(), 250))
            })?;
        want(row.path("bytes").and_then(|b| b.as_f64()).unwrap_or(0.0) >= 4096.0, || {
            format!("the row does not carry the stranded bytes: {}", row.dump())
        })?;
        want(row.text("hash_marker") == "sha256", || {
            format!("the row does not carry the marker `--adopt` needs: {}", row.dump())
        })?;
        want(row.text("dir").contains("stray.git"), || {
            format!("the row does not say where it is: {}", row.dump())
        })?;
        // …and it is in NEITHER of the other two listings, which is what makes
        // it an orphan rather than a repository or a tombstone.
        let (_, repos) = a.get("/api/repos")?;
        want(!items_have(&repos, "path", "alice/stray"), || "an orphan is in the catalogue".into())?;
        let (_, deleted) = a.get("/api/repos/deleted")?;
        want(!items_have(&deleted, "path", "alice/stray"), || "an orphan is in the deleted list".into())?;
        // …and gunnar's own listing says the same thing.
        let theirs = r.control(&["repo", "list-orphans"])?;
        want(theirs.contains("stray"), || format!("gunnar does not list it: {}", http::cut(&theirs, 200)))?;
        Ok(format!("{} bytes stranded, in no other listing", row.path("bytes").and_then(|b| b.as_f64()).unwrap_or(0.0)))
    });

    go(&mut rows, "the-standby-verdict-is-the-word-gunnar-decided", || {
        // A standby at a port nothing is listening on: it has never taken a
        // byte, and NEVER is its own verdict — never "stale", which is what an
        // idle appliance was told it was all weekend.
        //
        // The lie itself (a HEALTHY standby with an old success reading STALE)
        // needs fifty minutes of wall clock and a real second gunnar; it is
        // pinned in gunnar's own suite instead
        // (`replication_view::an_idle_standby_is_not_stale_and_a_stuck_one_still_is`,
        // seen red there). What this row is for is the other half of that fix:
        // the console and the CLI must print the SAME word, because two
        // surfaces deciding it separately is how they came to disagree.
        //
        // A salvage ref is pushed first, so the ref-coverage numbers below are
        // taken on a server that really has one. gunnar walks the refs once and
        // reuses the count for a minute, so it has to exist before the
        // replicator does.
        let wt = r.work.join("wt");
        r.git(&wt, &["push", "-q", &r.remote("alice/uitest"), "main:refs/superseded/wip-pre-reset"])?;
        r.git(&wt, &["push", "-q", &r.remote("alice/uitest"), "main:refs/salvage/main"])?;
        r.control(&["replication", "add", "uitest-standby", "ssh://git@127.0.0.1:1"])?;

        let (s, j) = a.get("/api/stats")?;
        want(s == 200, || format!("status {s}"))?;
        let row = j
            .path("standbys")
            .and_then(|s| s.arr().first().cloned())
            .ok_or_else(|| format!("the console reports no standby: {}", http::cut(&j.dump(), 250)))?;
        let verdict = row.text("verdict");
        want(verdict == "NEVER", || {
            format!("a standby that has never taken a byte reads {verdict:?}, not NEVER: {}", row.dump())
        })?;
        // …and gunnar's own CLI says the same word about the same standby.
        let listed = r.control(&["replication", "list"])?;
        want(listed.contains("NEVER"), || {
            format!("the CLI does not say NEVER: {}", http::cut(&listed, 300))
        })?;
        want(!listed.contains("STALE"), || {
            format!("the CLI calls a never-synced standby STALE as well: {}", http::cut(&listed, 300))
        })?;

        // ★ The refs no standby carries. Two were pushed above, both outside
        // refs/heads and refs/tags, and the count must be EXACTLY two — a
        // counter that answers "some" is what let 567 unmirrored refs sit
        // behind a green row.
        //
        // Since the ref-coverage work this count is OPT-IN: walking it opens every
        // repository's ref database, so a plain `replication list` /
        // `Admin.Stats` peeks the last on-demand walk and reads ZERO until one
        // is asked for. That zero is not "nothing unmirrored" — the
        // console renders it "Gunnar did not report a ref count". So the honest
        // shape is not-computed first, then exactly two once the walk is
        // requested.
        want(
            j.path("replication_refs_total").and_then(|v| v.as_f64()) == Some(0.0),
            || {
                format!(
                    "a plain /api/stats walked the refs instead of leaving the count \
                     opt-in: {}",
                    http::cut(&j.dump(), 200)
                )
            },
        )?;

        // The opt-in walk: `--refs` is the one flag that opens every ref
        // database, and it warms the cache the polled surfaces peek. Its own
        // summary carries the shortfall.
        let refs = r.control(&["replication", "list", "--refs"])?;
        want(refs.contains("NOT mirrored"), || {
            format!("the CLI summary says nothing about unmirrored refs: {}", http::cut(&refs, 400))
        })?;

        // Now the console's card, fed by that same cached walk via `Admin.Stats`.
        // Polled: the BFF holds `/api/stats` for a few seconds, so the real
        // number appears once its cache turns over.
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
        let (total, unmirrored) = loop {
            let (_, s) = a.get("/api/stats")?;
            let total = s.path("replication_refs_total").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let unmirrored =
                s.path("replication_refs_unmirrored").and_then(|v| v.as_f64()).unwrap_or(0.0);
            if unmirrored > 0.0 || std::time::Instant::now() >= deadline {
                break (total, unmirrored);
            }
            std::thread::sleep(std::time::Duration::from_millis(250));
        };
        want(unmirrored == 2.0, || {
            format!(
                "{unmirrored} refs reported as unmirrored on the console card; exactly 2 were \
                 pushed outside refs/heads"
            )
        })?;
        want(total > unmirrored, || {
            format!("the total ({total}) does not exceed the shortfall ({unmirrored})")
        })?;
        r.control(&["replication", "remove", "uitest-standby"])?;
        Ok(format!(
            "NEVER on both surfaces; {unmirrored} of {total} refs unmirrored after the on-demand walk"
        ))
    });

    go(&mut rows, "storage-reports-the-read-cache-and-its-budget", || {
        let (_, j) = a.get("/api/storage")?;
        for key in ["total_explode_bytes", "total_explode_budget_bytes"] {
            want(j.path(key).and_then(|v| v.as_f64()).is_some(), || {
                format!("/api/storage has no {key}: {}", http::cut(&j.dump(), 200))
            })?;
        }
        let repo = j
            .path("repos")
            .and_then(|rs| rs.arr().iter().find(|e| e.text("repo") == "alice/uitest").cloned())
            .ok_or("alice/uitest is not in the storage rows")?;
        // The cache is a KNOB, and the budget is the whole reason the disk
        // plateau is a choice rather than a fact — so the row must carry it.
        want(repo.path("explode_bytes").and_then(|v| v.as_f64()).is_some(), || {
            format!("no per-repository explode_bytes: {}", http::cut(&repo.dump(), 200))
        })?;
        want(repo.path("explode_budget_bytes").and_then(|v| v.as_f64()).is_some(), || {
            "no per-repository explode_budget_bytes".into()
        })?;
        Ok(format!(
            "cache {} of {} server-wide",
            j.path("total_explode_bytes").and_then(|v| v.as_f64()).unwrap_or(-1.0),
            j.path("total_explode_budget_bytes").and_then(|v| v.as_f64()).unwrap_or(-1.0)
        ))
    });

    // ── the Machine card: the estate and the disks, as gunnar says ────────
    //
    // Owner, 2026-10-04: the console is a THIN layer. The card's picture is
    // `Admin.Topology` drawn by the suite's one estate renderer, its lines are
    // `StorageReality.glance` — so each row holds the console's answer to the
    // SERVER's own (`gunnar control topology` / `control storage`), never to
    // the page's claim.
    go(&mut rows, "the-machine-card-reads-the-estate-from-gunnar", || {
        let (s, j) = a.get("/api/machine/topology")?;
        want(s == 200, || format!("status {s}: {}", http::cut(&j.dump(), 150)))?;
        let boxes = j.path("boxes").map(|b| b.arr().to_vec()).unwrap_or_default();
        want(!boxes.is_empty(), || format!("no boxes: {}", http::cut(&j.dump(), 200)))?;
        let first = &boxes[0];
        want(first.path("this_box").and_then(|v| v.as_bool()) == Some(true) && first.text("role") == "primary" && first.text("state") == "up", || {
            format!("the first box is not this primary, up: {}", http::cut(&first.dump(), 200))
        })?;
        // The console asked, with its own machine key: it is on the picture it got.
        want(boxes.iter().any(|b| b.text("name") == "uitest-console" && b.text("role") == "console" && b.text("state") == "up"), || {
            format!("the console that asked is not a box: {}", http::cut(&j.dump(), 300))
        })?;
        // gunnar's own rows, asked by the CLI: the same names, roles and states.
        let cli = r.control(&["topology"])?;
        for b in &boxes {
            // A box's own line starts `* ` (this box) or two blanks, then the
            // name (which may hold a blank: "this box"); the sentence under it
            // is indented four.
            let name = b.text("name");
            let line = cli.lines().find(|l| {
                l.strip_prefix("* ")
                    .or_else(|| l.strip_prefix("  ").filter(|rest| !rest.starts_with(' ')))
                    .is_some_and(|rest| rest.starts_with(&format!("{name} ")))
            });
            let line = line.ok_or_else(|| format!("gunnar's own topology has no {:?}: {}", b.text("name"), http::cut(&cli, 300)))?;
            want(line.contains(&b.text("role")) && line.contains(&b.text("state").to_uppercase()), || {
                format!("the console says {} {} {}, gunnar says {line:?}", b.text("name"), b.text("role"), b.text("state"))
            })?;
        }
        // A rig is a solo gunnar nobody laid: no installer record, and gunnar
        // says so on both doors (an installed estate says `yes`, P10 in the kit).
        let recorded = j.path("recorded").and_then(|v| v.as_bool()).unwrap_or(true);
        want(!recorded && cli.lines().any(|l| l.starts_with("recorded       no")), || {
            format!("a rig nobody laid reads recorded={recorded}; gunnar says {:?}", cli.lines().find(|l| l.starts_with("recorded")))
        })?;
        let count = cli.lines().find_map(|l| l.strip_prefix("boxes")).map(str::trim).unwrap_or("");
        want(count == boxes.len().to_string(), || format!("gunnar counts {count:?} boxes, the console {}", boxes.len()))?;
        let names: Vec<String> = boxes.iter().map(|b| format!("{} ({}, {})", b.text("name"), b.text("role"), b.text("state"))).collect();
        Ok(names.join(", "))
    });

    go(&mut rows, "the-estate-picture-is-the-shared-renderers-drawing-of-those-boxes", || {
        let (_, j) = a.get("/api/machine/topology")?;
        let svg = j.text("svg");
        want(svg.starts_with("<svg") && svg.trim_end().ends_with("</svg>"), || format!("not one SVG document: {}", http::cut(&svg, 80)))?;
        let boxes = j.path("boxes").map(|b| b.arr().to_vec()).unwrap_or_default();
        for b in &boxes {
            want(svg.contains(&format!(">{}<", b.text("name"))), || format!("{:?} is not on the picture", b.text("name")))?;
        }
        want(svg.contains("this box"), || "this box is not marked".into())?;
        // The open console never says anything about buying, and a topology
        // knows no plan or growth: the shared renderer's disk-only board.
        for word in ["buyer", "plan", "iron comes from", "grow"] {
            want(!svg.contains(word), || format!("`{word}` on the console's picture"))?;
        }
        let (anon, _) = a.anon("/api/machine/topology")?;
        want(anon == 401, || format!("a stranger got the picture: {anon}"))?;
        Ok(format!("{} bytes, {} chip(s), a stranger 401", svg.len(), boxes.len()))
    });

    go(&mut rows, "the-glance-lines-are-gunnars-own", || {
        let (s, j) = a.get("/api/machine")?;
        want(s == 200, || format!("status {s}"))?;
        let glance = j.path("glance").map(|g| g.arr().to_vec()).unwrap_or_default();
        let keys: Vec<String> = glance.iter().map(|g| g.text("key")).collect();
        want(keys == ["data", "serialdat", "free", "speed"], || format!("the glance keys are {keys:?}: {}", http::cut(&j.dump(), 300)))?;
        for g in &glance {
            want(["ok", "warn", "bad", "unknown"].contains(&g.text("state").as_str()), || format!("a state gunnar does not use: {}", g.dump()))?;
        }
        // The CLI prints gunnar's same lines: `STATE    line`. The free line is measured ON EACH CALL
        // (gunnar glance.rs `free_line`), and on a busy box its GiB move between the console's read and the
        // CLI's (round 5.3: "OK /data: 212.4 GiB free" vs 212.3): its figures are masked, its words are not.
        let cli = r.control(&["storage"])?;
        let figures = |s: &str| s.chars().map(|c| if c.is_ascii_digit() { '#' } else { c }).collect::<String>();
        for g in &glance {
            let want_line = format!("{:<8} {}", g.text("state").to_uppercase(), g.text("line"));
            let said = if g.text("key") == "free" { cli.lines().any(|l| figures(l) == figures(&want_line)) } else { cli.contains(&want_line) };
            want(said, || format!("gunnar's CLI does not say {want_line:?}: {}", http::cut(cli.split("── at a glance").nth(1).unwrap_or(&cli), 400)))?;
        }
        // And the mislabel stays fixed: /serialdat's disk is said as /serialdat's.
        want(!cli.lines().any(|l| l.starts_with("backing ")), || "the CLI still says `backing`".into())?;
        want(cli.lines().any(|l| l.starts_with("serialdat on ")), || format!("the CLI does not say `serialdat on`: {}", http::cut(&cli, 300)))?;
        let shown: Vec<String> = glance.iter().map(|g| format!("{}={}", g.text("key"), g.text("state"))).collect();
        Ok(shown.join(" "))
    });

    // ── identities: the console grants access, and git proves it ─────────
    //
    // Every row here ends at stock git over SSH rather than at a 200 from the
    // console. A grant that lists correctly and opens nothing is the failure
    // that matters, and only a real clone can tell the two apart.
    go(&mut rows, "a-new-principal-with-a-key-can-do-nothing-yet", || {
        let (s, _, body) = a.post("/api/principals", r#"{"name":"tenant-bot","kind":"machine"}"#)?;
        want((200..300).contains(&s), || format!("create answered {s}: {}", http::cut(&body, 150)))?;
        let pubkey = std::fs::read_to_string(r.tenant_key.with_extension("pub"))
            .map_err(|e| format!("reading the tenant key: {e}"))?
            .trim()
            .to_owned();
        let (s, _, body) = a.post(
            "/api/keys",
            &format!(
                r#"{{"principal_id":"tenant-bot","openssh_public_key":{},"comment":"uitest"}}"#,
                esc(&pubkey)
            ),
        )?;
        want((200..300).contains(&s), || format!("add-key answered {s}: {}", http::cut(&body, 150)))?;
        // Registered, and holding nothing: the clone must be refused.
        let dir = r.work.join("as-tenant");
        std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
        let e = r
            .git_as(&r.tenant_key, &dir, &["clone", "-q", &r.remote_as("tenant-bot", "alice/uitest"), "no-grant"])
            .err()
            .ok_or("a principal with no grant cloned the repository")?;
        Ok(format!("registered, and refused: {}", http::cut(&e, 80)))
    });

    go(&mut rows, "a-grant-from-the-console-lets-git-clone", || {
        let (s, _, body) = a.post(
            "/api/grants",
            r#"{"principal_id":"tenant-bot","namespace":"alice","repo":"","level":"read"}"#,
        )?;
        want((200..300).contains(&s), || format!("grant answered {s}: {}", http::cut(&body, 150)))?;
        let (_, g) = a.get("/api/grants")?;
        want(
            g.path("items")
                .map(|i| i.arr().iter().any(|e| e.text("principal_id") == "tenant-bot"))
                .unwrap_or(false),
            || format!("not listed: {}", http::cut(&g.dump(), 200)),
        )?;
        let dir = r.work.join("as-tenant");
        let out = r.git_as(&r.tenant_key, &dir, &["clone", "-q", &r.remote_as("tenant-bot", "alice/uitest"), "granted"])
            .map_err(|e| format!("the grant listed but git was still refused: {}", http::cut(&e, 150)))?;
        let readme = dir.join("granted/README");
        want(readme.exists(), || format!("cloned but empty: {}", http::cut(&out, 100)))?;
        Ok("read granted, and git cloned it".into())
    });

    // ── usage: the meters, and whose they are ────────────────────────────
    //
    // `Usage` is open source and always on — gunnar counts what a namespace
    // holds whether or not anything ever bills from it — and `Usage.Read`
    // takes `read` on the ONE namespace it names. The console's usage page is
    // built on that split, so these rows ask gunnar itself whether the split
    // is real, with the credential that decides it.
    //
    // `tenant-bot` holds `read` on `alice` and nothing else at this point in
    // the walk, which is exactly the credential the question needs.
    go(&mut rows, "a-tenant-reads-its-own-namespaces-meters-and-not-anothers", || {
        // A namespace the tenant has no grant on. Created here rather than
        // reused, so the refusal below cannot be about a name that is missing.
        let (s, _, body) = a.post(
            "/api/namespaces",
            r#"{"name":"strangers","description":"not the tenant's"}"#,
        )?;
        want((200..300).contains(&s), || format!("creating strangers answered {s}: {}", http::cut(&body, 150)))?;

        // The tenant's own namespace: answered, with numbers in it.
        let mine = r
            .control_as(&r.tenant_key, &["usage", "alice"])
            .map_err(|e| format!("a read grant on `alice` did not open its own meters: {}", http::cut(&e, 200)))?;
        want(mine.contains("alice\t") || mine.starts_with("alice"), || {
            format!("the answer is not about `alice`: {}", http::cut(&mine, 200))
        })?;

        // Somebody else's: refused. And the control that makes the refusal mean
        // something — the SAME call, the same server, the same namespace, with
        // only the credential changed. The admin key reads it fine.
        let refused = r
            .control_as(&r.tenant_key, &["usage", "strangers"])
            .err()
            .ok_or("a key with read on `alice` alone read `strangers`\'s meters")?;
        r.control(&["usage", "strangers"])
            .map_err(|e| format!("the admin key could not read `strangers` either, so the refusal above proves nothing: {}", http::cut(&e, 200)))?;

        Ok(format!("own namespace read, another refused: {}", http::cut(&refused, 90)))
    });

    go(&mut rows, "the-consoles-usage-route-carries-gunnars-own-numbers", || {
        // The console must not have a second opinion about a meter. `gunnar-cli
        // control usage` prints the raw byte count beside the rounded one; the
        // raw number is what is compared, so a rounding that agrees by accident
        // cannot pass for a wire-up.
        let printed = r.control(&["usage", "alice"])?;
        let pack = field(&printed, "pack").ok_or_else(|| format!("no `pack` in {}", http::cut(&printed, 200)))?;
        let objects = field(&printed, "objects").ok_or_else(|| format!("no `objects` in {}", http::cut(&printed, 200)))?;
        // A namespace that measures 0 could not tell a working route from a
        // dead one — and `lfs` really IS 0 on every gunnar, so the meter to
        // stand on has to be one that is not.
        want(pack > 0, || format!("`alice` holds no packs, so this would compare two zeroes: {}", http::cut(&printed, 200)))?;

        let (s, u) = a.get("/api/namespaces/alice/usage")?;
        want(s == 200, || format!("the console answered {s}: {}", http::cut(&u.dump(), 200)))?;
        want(u.text("namespace") == "alice", || format!("about the wrong namespace: {}", http::cut(&u.dump(), 200)))?;
        let seen_pack = u.path("pack_bytes").and_then(|v| v.as_f64()).unwrap_or(-1.0) as i64;
        want(seen_pack == pack as i64, || {
            format!("the console says pack_bytes {seen_pack} and gunnar says {pack}: {}", http::cut(&u.dump(), 200))
        })?;
        let seen_objects = u.path("objects").and_then(|v| v.as_f64()).unwrap_or(-1.0) as i64;
        want(seen_objects == objects as i64, || {
            format!("the console says objects {seen_objects} and gunnar says {objects}")
        })?;
        Ok(format!("pack {pack} B and {objects} objects, matching gunnar exactly"))
    });

    go(&mut rows, "the-usage-route-refuses-a-namespace-gunnar-does-not-have", || {
        // The friendly failure this rules out: an unknown namespace rendered as
        // one that exists and holds nothing.
        let (s, body) = a.get_raw("/api/namespaces/no-such-namespace/usage")?;
        want(s == 404 || s == 403, || format!("answered {s}: {}", http::cut(&body, 200)))?;
        want(!body.contains("pack_bytes"), || format!("a refusal carried meters: {}", http::cut(&body, 200)))?;
        Ok(format!("{s}, and no meters in it"))
    });

    go(&mut rows, "a-read-grant-refuses-the-very-push-a-write-grant-accepts", || {
        // "The push was refused" is worth nothing on its own — a push fails for
        // a dozen reasons that have nothing to do with authorisation. So the
        // control is the SAME push, from the same clone, with the same key, to
        // the same ref, with only the grant changed. Refused under read,
        // accepted under write: now the refusal is about the grant and can be
        // about nothing else.
        let dir = r.work.join("as-tenant/granted");
        std::fs::write(dir.join("README"), "a reader must not be able to write this\n")
            .map_err(|e| e.to_string())?;
        r.git_as(&r.tenant_key, &dir, &["config", "user.name", "tenant"])?;
        r.git_as(&r.tenant_key, &dir, &["config", "user.email", "tenant@example.invalid"])?;
        r.git_as(&r.tenant_key, &dir, &["add", "README"])?;
        r.git_as(&r.tenant_key, &dir, &["commit", "-qm", "a reader's edit"])?;
        let refused = r
            .git_as(&r.tenant_key, &dir, &["push", "-q", "origin", "HEAD:refs/heads/tenant-edit"])
            .err()
            .ok_or("a read grant accepted a push")?;
        // Now the only thing that changes is the grant.
        let (s, _, body) = a.post(
            "/api/grants",
            r#"{"principal_id":"tenant-bot","namespace":"alice","repo":"","level":"write"}"#,
        )?;
        want((200..300).contains(&s), || format!("the write grant answered {s}: {}", http::cut(&body, 150)))?;
        r.git_as(&r.tenant_key, &dir, &["push", "-q", "origin", "HEAD:refs/heads/tenant-edit"])
            .map_err(|e| {
                format!("write was granted and the same push still failed, so the first refusal proved nothing: {}", http::cut(&e, 150))
            })?;
        // And the ref is really there, on the server.
        let (_, refs) = a.get("/api/repos/alice/uitest/refs")?;
        want(
            refs.path("items")
                .map(|i| i.arr().iter().any(|e| e.text("ref_name") == "refs/heads/tenant-edit"))
                .unwrap_or(false),
            || format!("the push was accepted but the ref is absent: {}", http::cut(&refs.dump(), 200)),
        )?;
        // Put the grant back where the next situation expects it.
        a.post(
            "/api/grants",
            r#"{"principal_id":"tenant-bot","namespace":"alice","repo":"","level":"read"}"#,
        )?;
        Ok(format!("read refused it ({}), write accepted it", http::cut(&refused, 60)))
    });

    go(&mut rows, "revoking-the-grant-closes-git-again", || {
        let (s, _, body) = a.post(
            "/api/grants/revoke",
            r#"{"principal_id":"tenant-bot","namespace":"alice","repo":""}"#,
        )?;
        want((200..300).contains(&s), || format!("revoke answered {s}: {}", http::cut(&body, 150)))?;
        let dir = r.work.join("as-tenant");
        let e = r
            .git_as(&r.tenant_key, &dir, &["clone", "-q", &r.remote_as("tenant-bot", "alice/uitest"), "after-revoke"])
            .err()
            .ok_or("the grant was revoked and git cloned anyway")?;
        Ok(format!("refused again: {}", http::cut(&e, 80)))
    });

    go(&mut rows, "the-console-unmakes-the-key-and-the-principal", || {
        let (_, k) = a.get("/api/keys")?;
        let fp = k
            .path("items")
            .and_then(|i| i.arr().iter().find(|e| e.text("principal_id") == "tenant-bot").cloned())
            .map(|e| e.text("fingerprint"))
            .ok_or("the tenant key is not listed")?;
        let (s, body) = a.delete(&format!("/api/keys/{}", fp.replace(':', "%3A").replace('+', "%2B").replace('/', "%2F")))?;
        want((200..300).contains(&s), || format!("key delete answered {s}: {}", http::cut(&body, 150)))?;
        let (s, body) = a.delete("/api/principals/tenant-bot")?;
        want((200..300).contains(&s), || format!("principal delete answered {s}: {}", http::cut(&body, 150)))?;
        let (_, p) = a.get("/api/principals")?;
        want(!items_have(&p, "id", "tenant-bot"), || "still listed".into())?;
        Ok("key and principal both gone".into())
    });

    go(&mut rows, "the-console-makes-and-unmakes-a-namespace", || {
        let (s, _, body) = a.post("/api/namespaces", r#"{"name":"tenant-kim"}"#)?;
        want((200..300).contains(&s), || format!("create answered {s}: {}", http::cut(&body, 150)))?;
        let (_, j) = a.get("/api/namespaces")?;
        want(items_have(&j, "name", "tenant-kim"), || {
            format!("not listed after create: {}", http::cut(&j.dump(), 200))
        })?;
        let (s, body) = a.delete("/api/namespaces/tenant-kim")?;
        want((200..300).contains(&s), || format!("delete answered {s}: {}", http::cut(&body, 150)))?;
        let (_, j) = a.get("/api/namespaces")?;
        want(!items_have(&j, "name", "tenant-kim"), || "still listed after delete".into())?;
        Ok("created, listed, deleted, gone".into())
    });

    go(&mut rows, "a-public-repository-opens-the-anonymous-door", || {
        // The anonymous door means offering NO key at all — not offering
        // an unknown one, which ssh refuses at publickey auth before any
        // repository ACL is consulted. The control is the same anonymous clone
        // three times, with only the visibility changing: shut, open, shut.
        let dir = r.work.join("as-stranger");
        std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
        let refused = r
            .git_anon(&dir, &["clone", "-q", &r.remote_as("anonymous", "alice/uitest"), "while-private"])
            .err()
            .ok_or("a private repository answered an anonymous clone")?;
        let h = [
            ("Authorization", format!("Bearer {}", r.token)),
            ("Content-Type", "application/json".to_owned()),
        ];
        let patch = |v: &str| -> Result<u16, String> {
            let resp = http::req(
                &r.ui_addr(),
                "PATCH",
                "/api/repos/alice/uitest",
                &h,
                Some(&format!(r#"{{"visibility":{}}}"#, esc(v))),
            )?;
            Ok(resp.status)
        };
        let s1 = patch("public")?;
        want((200..300).contains(&s1), || format!("making it public answered {s1}"))?;
        want(a.get("/api/repos/alice/uitest")?.1.text("visibility") == "public", || {
            "the console did not record it as public".into()
        })?;
        r.git_anon(&dir, &["clone", "-q", &r.remote_as("anonymous", "alice/uitest"), "while-public"])
            .map_err(|e| format!("public, and the anonymous door stayed shut: {}", http::cut(&e, 200)))?;
        want(dir.join("while-public/README").exists(), || "cloned but empty".into())?;
        let s2 = patch("private")?;
        want((200..300).contains(&s2), || format!("making it private answered {s2}"))?;
        r.git_anon(&dir, &["clone", "-q", &r.remote_as("anonymous", "alice/uitest"), "private-again"])
            .err()
            .ok_or("the door stayed open after it was made private again")?;
        Ok(format!("shut ({}), open, shut again", http::cut(&refused, 45)))
    });

    // ── pull requests ─────────────────────────────────────────────────────
    //
    // The branch these propose is the one the identities rows pushed, so this
    // reviews a change that a different principal really made.
    let pull = std::cell::RefCell::new(String::new());
    go(&mut rows, "a-pull-request-is-opened-and-listed", || {
        let (s, j, body) = a.post(
            "/api/repos/alice/uitest/pulls",
            r#"{"title":"a reader's edit, reviewed","description":"opened by uitest",
                "source_ref":"refs/heads/tenant-edit","target_ref":"refs/heads/main"}"#,
        )?;
        want((200..300).contains(&s), || format!("create answered {s}: {}", http::cut(&body, 200)))?;
        let number = j
            .path("number")
            .and_then(|n| n.as_f64())
            .map(|n| format!("{}", n as i64))
            .ok_or_else(|| format!("no number in {}", http::cut(&j.dump(), 200)))?;
        *pull.borrow_mut() = number.clone();
        let (_, list) = a.get("/api/repos/alice/uitest/pulls")?;
        let found = list.arr().iter().any(|p| {
            p.path("number").and_then(|n| n.as_f64()).map(|n| format!("{}", n as i64)) == Some(number.clone())
        }) || list
            .path("items")
            .map(|i| i.arr().iter().any(|p| p.text("title").contains("reviewed")))
            .unwrap_or(false);
        want(found, || format!("not listed: {}", http::cut(&list.dump(), 200)))?;
        Ok(format!("pull #{number}"))
    });

    // ── THE REVIEW GATE, through the console ──────────────────────────────
    //
    // gunnar refuses a merge unless a VERIFIED approval names the commit the
    // source branch is at RIGHT NOW. The three rows below are the same three
    // arms `gunnar/crates/gunnar-cli/tests/forge_merge.rs` asserts against the
    // control plane, driven end-to-end through the console instead — because
    // the console had a Merge button and no way to approve at all, and that
    // combination is exactly what a happy-path-only row cannot see.

    go(&mut rows, "merging-without-an-approval-is-refused", || {
        let n = pull.borrow().clone();
        want(!n.is_empty(), || "no pull request was opened".into())?;
        let (s, rev) = a.get(&format!("/api/repos/alice/uitest/pulls/{n}/review"))?;
        want(s == 200, || format!("review answered {s}: {}", http::cut(&rev.dump(), 200)))?;
        let head = rev.text("head");
        want(!head.is_empty(), || "the review does not say where the branch is".into())?;
        want(
            rev.path("approvals").map(|v| v.arr().is_empty()).unwrap_or(false),
            || format!("something has already approved it: {}", http::cut(&rev.dump(), 200)),
        )?;
        want(rev.path("head_is_approved").and_then(|v| v.as_bool()) == Some(false), || {
            "the console claims an unapproved commit is approved".into()
        })?;

        let before = a.get("/api/repos/alice/uitest/head")?.1.text("oid");
        let (s, _, body) = a.post(&format!("/api/repos/alice/uitest/pulls/{n}/merge"), "{}")?;
        want(s == 400, || format!("an unapproved merge answered {s}: {}", http::cut(&body, 200)))?;
        want(body.contains("no verified approval names"), || {
            format!("refused, but not by the review gate: {}", http::cut(&body, 200))
        })?;
        want(body.contains("Nobody has approved this pull request"), || {
            format!("refused without saying nobody had approved it: {}", http::cut(&body, 200))
        })?;
        // The whole point: a refusal that moved the branch anyway is a refusal
        // that did nothing, and it reads identically from the caller's side.
        let after = a.get("/api/repos/alice/uitest/head")?.1.text("oid");
        want(after == before, || "the merge was refused and moved main anyway".into())?;
        let ls = r.git(&r.work.join("wt"), &["ls-remote", &r.remote("alice/uitest"), "refs/heads/main"])?;
        want(ls.contains(&before), || {
            format!("the console says main is unmoved but the server advertises {}", http::cut(&ls, 80))
        })?;
        Ok(format!("refused at {}", &head[..12.min(head.len())]))
    });

    go(&mut rows, "an-approval-does-not-follow-a-branch-that-moved", || {
        // THE ARM THAT MATTERS. Approve the tip, then push one more commit onto
        // the same branch — the ordinary, innocent-looking case. The approval
        // still names the commit that was reviewed, so it is not an approval of
        // what would now ship, and the merge has to refuse. A mutable
        // `approved: bool` cannot express this, and a matrix that only proved
        // the happy path would let it back in.
        let n = pull.borrow().clone();
        want(!n.is_empty(), || "no pull request was opened".into())?;
        let reviewed = a.get(&format!("/api/repos/alice/uitest/pulls/{n}/review"))?.1.text("head");
        want(!reviewed.is_empty(), || "the review does not say where the branch is".into())?;

        let (s, rev, body) = a.post(
            &format!("/api/repos/alice/uitest/pulls/{n}/approve"),
            &format!(r#"{{"commit":{}}}"#, esc(&reviewed)),
        )?;
        want((200..300).contains(&s), || format!("approve answered {s}: {}", http::cut(&body, 250)))?;
        want(rev.path("head_is_approved").and_then(|v| v.as_bool()) == Some(true), || {
            format!("approved, and the console still says not approved: {}", http::cut(&rev.dump(), 250))
        })?;
        // Verified against gunnar's key table, not merely present. An approval by
        // a key gunnar cannot check is a claim, not evidence, and the two look
        // the same in a count.
        want(
            rev.path("approvals")
                .map(|v| v.arr().iter().any(|e| {
                    e.path("verified").and_then(|x| x.as_bool()) == Some(true)
                        && e.text("commit") == reviewed
                }))
                .unwrap_or(false),
            || format!("no verified approval names the reviewed commit: {}", http::cut(&rev.dump(), 250)),
        )?;

        // Now the branch grows a commit nobody looked at.
        let wt = r.work.join("wt");
        r.git(&wt, &["fetch", "-q", &r.remote("alice/uitest"), "refs/heads/tenant-edit"])?;
        r.git(&wt, &["checkout", "-q", "-B", "after-the-review", "FETCH_HEAD"])?;
        std::fs::write(wt.join("src/unreviewed.rs"), "pub fn nobody_looked_at_this() {}\n")
            .map_err(|e| e.to_string())?;
        r.git(&wt, &["add", "src/unreviewed.rs"])?;
        r.git(&wt, &["commit", "-qm", "a commit that arrived after the review"])?;
        r.git(&wt, &["push", "-q", &r.remote("alice/uitest"), "HEAD:refs/heads/tenant-edit"])?;
        r.git(&wt, &["checkout", "-q", "main"])?;

        let (_, rev) = a.get(&format!("/api/repos/alice/uitest/pulls/{n}/review"))?;
        let moved = rev.text("head");
        want(moved != reviewed, || "the branch did not move, so this proves nothing".into())?;
        want(rev.path("head_is_approved").and_then(|v| v.as_bool()) == Some(false), || {
            format!("the approval followed the branch: {}", http::cut(&rev.dump(), 250))
        })?;
        // And the earlier approval is still THERE and still verified — it is
        // about another commit, which is a different thing from being gone.
        want(
            rev.path("approvals")
                .map(|v| v.arr().iter().any(|e| {
                    e.text("commit") == reviewed
                        && e.path("verified").and_then(|x| x.as_bool()) == Some(true)
                        && e.path("covers_head").and_then(|x| x.as_bool()) == Some(false)
                }))
                .unwrap_or(false),
            || format!("the approval of the reviewed commit vanished: {}", http::cut(&rev.dump(), 250)),
        )?;

        let before = a.get("/api/repos/alice/uitest/head")?.1.text("oid");
        let (s, _, body) = a.post(&format!("/api/repos/alice/uitest/pulls/{n}/merge"), "{}")?;
        want(s == 400, || format!("the merge answered {s}: {}", http::cut(&body, 250)))?;
        want(body.contains("no verified approval names") && body.contains(&moved[..12]), || {
            format!("refused without naming the commit that would ship: {}", http::cut(&body, 250))
        })?;
        want(body.contains("What is approved:") && body.contains(&reviewed[..12]), || {
            format!("refused without saying what IS approved: {}", http::cut(&body, 250))
        })?;
        let after = a.get("/api/repos/alice/uitest/head")?.1.text("oid");
        want(after == before, || "the merge was refused and moved main anyway".into())?;
        Ok(format!("{} approved, {} would ship", &reviewed[..8], &moved[..8]))
    });

    go(&mut rows, "merging-a-pull-request-moves-the-branch", || {
        let n = pull.borrow().clone();
        want(!n.is_empty(), || "no pull request was opened".into())?;
        // The reviewer looks at the NEW commit and approves that one — the
        // remedy the refusal above points at, and the only way past the gate.
        let head = a.get(&format!("/api/repos/alice/uitest/pulls/{n}/review"))?.1.text("head");
        want(!head.is_empty(), || "the review does not say where the branch is".into())?;
        let (s, rev, body) = a.post(
            &format!("/api/repos/alice/uitest/pulls/{n}/approve"),
            &format!(r#"{{"commit":{}}}"#, esc(&head)),
        )?;
        want((200..300).contains(&s), || format!("approve answered {s}: {}", http::cut(&body, 250)))?;
        want(rev.path("head_is_approved").and_then(|v| v.as_bool()) == Some(true), || {
            format!("approved and still not approved: {}", http::cut(&rev.dump(), 250))
        })?;
        let before = a.get("/api/repos/alice/uitest/head")?.1.text("oid");
        let (s, _, body) = a.post(&format!("/api/repos/alice/uitest/pulls/{n}/merge"), "{}")?;
        want((200..300).contains(&s), || format!("merge answered {s}: {}", http::cut(&body, 200)))?;
        let after = a.get("/api/repos/alice/uitest/head")?.1.text("oid");
        want(after != before, || format!("main did not move: still {}", &before[..12.min(before.len())]))?;
        // And the move is real on the wire, not just in the console's answer.
        let wt = r.work.join("wt");
        let ls = r.git(&wt, &["ls-remote", &r.remote("alice/uitest"), "refs/heads/main"])?;
        want(ls.contains(&after), || {
            format!("the console says main is {} but the server advertises {}", &after[..12.min(after.len())], http::cut(&ls, 80))
        })?;
        Ok(format!("main {} → {}", &before[..8.min(before.len())], &after[..8.min(after.len())]))
    });

    go(&mut rows, "a-pull-request-can-be-closed-without-merging", || {
        let wt = r.work.join("wt");
        std::fs::write(wt.join("src/third.rs"), "pub fn third() {}\n").map_err(|e| e.to_string())?;
        r.git(&wt, &["checkout", "-q", "-b", "not-wanted"])?;
        r.git(&wt, &["add", "src/third.rs"])?;
        r.git(&wt, &["commit", "-qm", "a change nobody wants"])?;
        r.git(&wt, &["push", "-q", &r.remote("alice/uitest"), "not-wanted"])?;
        r.git(&wt, &["checkout", "-q", "main"])?;
        let (s, j, body) = a.post(
            "/api/repos/alice/uitest/pulls",
            r#"{"title":"not wanted","description":"","source_ref":"refs/heads/not-wanted","target_ref":"refs/heads/main"}"#,
        )?;
        want((200..300).contains(&s), || format!("create answered {s}: {}", http::cut(&body, 200)))?;
        let n = j.path("number").and_then(|x| x.as_f64()).map(|x| x as i64).ok_or("no number")?;
        let before = a.get("/api/repos/alice/uitest/head")?.1.text("oid");
        let (s, _, body) = a.post(&format!("/api/repos/alice/uitest/pulls/{n}/close"), "{}")?;
        want((200..300).contains(&s), || format!("close answered {s}: {}", http::cut(&body, 200)))?;
        let (_, got) = a.get(&format!("/api/repos/alice/uitest/pulls/{n}"))?;
        let state = got.text("state");
        want(state != "open" && !state.is_empty(), || format!("state is {state:?} after closing"))?;
        // Closing must not merge: the branch has to be exactly where it was.
        let after = a.get("/api/repos/alice/uitest/head")?.1.text("oid");
        want(after == before, || "closing the pull request moved main".into())?;
        Ok(format!("#{n} is {state}, and main did not move"))
    });

    // ── issues ────────────────────────────────────────────────────────────
    //
    // The OTHER half of the forge, driven end-to-end through the console the
    // same way the review gate above is. Every row asserts the FOLD gunnar
    // returns — the state the page renders is nothing but this fold, so
    // asserting the fold IS asserting what the browser draws. Open → comment →
    // close, and then the red-first arm: a comment on a closed issue is refused
    // and the fold is left untouched.
    let issue = std::cell::RefCell::new(String::new());
    go(&mut rows, "an-issue-is-opened-and-listed", || {
        let (s, j, body) = a.post(
            "/api/repos/alice/uitest/issues",
            r#"{"title":"the boot is dark","body":"PID 1 prints to a dead ttyS0"}"#,
        )?;
        want((200..300).contains(&s), || format!("open answered {s}: {}", http::cut(&body, 200)))?;
        let number = j
            .path("number")
            .and_then(|n| n.as_f64())
            .map(|n| format!("{}", n as i64))
            .ok_or_else(|| format!("no number in {}", http::cut(&j.dump(), 200)))?;
        want(j.path("open").and_then(|v| v.as_bool()) == Some(true), || {
            "a fresh issue is not open".into()
        })?;
        *issue.borrow_mut() = number.clone();
        // Listed, and the fold carries its title and its signer as author.
        let (_, list) = a.get("/api/repos/alice/uitest/issues")?;
        let found = list.arr().iter().any(|i| {
            i.path("number").and_then(|n| n.as_f64()).map(|n| format!("{}", n as i64))
                == Some(number.clone())
                && i.text("title") == "the boot is dark"
        });
        want(found, || format!("not listed: {}", http::cut(&list.dump(), 250)))?;
        Ok(format!("issue #{number}, open"))
    });

    go(&mut rows, "an-issue-takes-a-comment", || {
        let n = issue.borrow().clone();
        want(!n.is_empty(), || "no issue was opened".into())?;
        let (s, j, body) = a.post(
            &format!("/api/repos/alice/uitest/issues/{n}/comment"),
            r#"{"body":"seen on the serial console too"}"#,
        )?;
        want((200..300).contains(&s), || format!("comment answered {s}: {}", http::cut(&body, 200)))?;
        // The fold read back shows one comment on an issue still open.
        want(j.path("comments").and_then(|v| v.as_f64()) == Some(1.0), || {
            format!("the comment did not land in the fold: {}", http::cut(&j.dump(), 200))
        })?;
        want(j.path("open").and_then(|v| v.as_bool()) == Some(true), || {
            "commenting closed the issue".into()
        })?;
        Ok(format!("#{n} has 1 comment"))
    });

    go(&mut rows, "an-issue-closes-and-the-fold-shows-it-shut", || {
        let n = issue.borrow().clone();
        want(!n.is_empty(), || "no issue was opened".into())?;
        let (s, j, body) = a.post(&format!("/api/repos/alice/uitest/issues/{n}/close"), "{}")?;
        want((200..300).contains(&s), || format!("close answered {s}: {}", http::cut(&body, 200)))?;
        want(j.path("open").and_then(|v| v.as_bool()) == Some(false), || {
            format!("closed, and the fold still says open: {}", http::cut(&j.dump(), 200))
        })?;
        // And GET agrees — the state is the fold, read fresh, not the POST's
        // own optimistic answer.
        let (_, got) = a.get(&format!("/api/repos/alice/uitest/issues/{n}"))?;
        want(got.path("open").and_then(|v| v.as_bool()) == Some(false), || {
            format!("GET disagrees with the close: {}", http::cut(&got.dump(), 200))
        })?;
        Ok(format!("#{n} is closed"))
    });

    go(&mut rows, "a-comment-on-a-closed-issue-is-refused", || {
        // The red-first arm: an event on a closed issue is evidence about
        // nothing, so gunnar refuses it — and the fold is left exactly as the
        // close left it. A console that let this through would show a comment
        // count nobody signed a still-open issue for.
        let n = issue.borrow().clone();
        want(!n.is_empty(), || "no issue was opened".into())?;
        let before = a
            .get(&format!("/api/repos/alice/uitest/issues/{n}"))?
            .1
            .path("comments")
            .and_then(|v| v.as_f64())
            .unwrap_or(-1.0);
        let (s, _, body) = a.post(
            &format!("/api/repos/alice/uitest/issues/{n}/comment"),
            r#"{"body":"reopening by comment?"}"#,
        )?;
        want(s == 400, || format!("a comment on a closed issue answered {s}: {}", http::cut(&body, 200)))?;
        want(body.to_lowercase().contains("closed"), || {
            format!("refused, but not for being closed: {}", http::cut(&body, 200))
        })?;
        let after = a
            .get(&format!("/api/repos/alice/uitest/issues/{n}"))?
            .1
            .path("comments")
            .and_then(|v| v.as_f64())
            .unwrap_or(-1.0);
        want(after == before, || {
            format!("the refused comment landed in the fold anyway: {before} → {after}")
        })?;
        Ok(format!("refused, still {after:.0} comment(s)"))
    });

    // ── invites ───────────────────────────────────────────────────────────
    // Thin layer L: invites are gunnar's, and gunnar refuses an ADMIN one
    // ("an invite never makes an admin"); the account made for it is taken
    // back. A name of its own, so no later row meets a principal it made.
    go(&mut rows, "an-invite-cannot-grant-admin", || {
        let (s, _, body) = a.post("/auth/passkey/invites", r#"{"user":"zed","role":"admin"}"#)?;
        want(s == 400, || format!("status {s}: {}", http::cut(&body, 120)))?;
        want(body.contains("never makes an admin"), || format!("refused, but for another reason: {}", http::cut(&body, 120)))?;
        Ok(http::cut(&body, 90))
    });

    // IDENTITY-HOST: the open invites are the identity core's, listed and withdrawn AS a
    // passkey session (an admin's: every one). The break-glass token holds no session, so
    // it mints (gunnar's `Access.EnrolmentLink`) and the admin's session lists and withdraws
    // what it minted: the browser rows `an-invite-is-listed-open-to-the-admin` and
    // `a-revoked-invite-stops-being-open`.
    go(&mut rows, "the-break-glass-token-mints-an-invite-link", || {
        let (s, j, body) = a.post("/auth/passkey/invites", r#"{"user":"kim","role":"viewer"}"#)?;
        want(s == 200, || format!("status {s}: {}", http::cut(&body, 150)))?;
        let id = j.text("id");
        let url = j.text("url");
        want(!id.is_empty() && url.contains("/login?invite="), || format!("{}", j.dump()))?;
        want(j.text("role") == "viewer" && j.text("user") == "kim", || format!("{}", http::cut(&j.dump(), 200)))?;
        Ok(format!("invite {} for kim, viewer", &id[..8.min(id.len())]))
    });

    // ── branch protection: the gate, and the push it has to reject ────────
    //
    // The asymmetry these close, measured on this repo: a merge has required a
    // verified signed approval naming the exact commit since `7f605beb`, and a
    // DIRECT push to the same branch was refused by nothing at all. Not because
    // gunnar had no gate — it has served all six `Policy.*` verbs out of
    // `refs/gunnar/policy` since `gunnar_core::PolicyStore` landed — but because
    // the surface that turns the gate on was reachable from neither the CLI nor
    // the console, and `docs/coverage.md` said the whole service was refused.
    //
    // They run on their own repository rather than on `alice/uitest`, so a
    // rule left behind by a failing row cannot silently change what a later row
    // is allowed to push.
    let guard = r.work.join("guarded");
    let guarded_remote = r.remote("alice/guarded");

    let made = go(&mut rows, "a-repository-to-protect-exists-and-takes-a-push", || {
        let (s, _, body) = a.post("/api/repos", r#"{"namespace":"alice","name":"guarded"}"#)?;
        want((200..300).contains(&s), || format!("create answered {s}: {}", http::cut(&body, 150)))?;
        std::fs::create_dir_all(&guard).map_err(|e| e.to_string())?;
        r.git(&guard, &["init", "-q", "--object-format=sha256", "-b", "main", "."])?;
        r.git(&guard, &["config", "user.name", "uitest"])?;
        r.git(&guard, &["config", "user.email", "info@example.invalid"])?;
        std::fs::write(guard.join("README"), "one\n").map_err(|e| e.to_string())?;
        r.git(&guard, &["add", "README"])?;
        r.git(&guard, &["commit", "-qm", "one"])?;
        r.git(&guard, &["push", "-q", &guarded_remote, "main"])?;
        Ok("alice/guarded, one commit on main".into())
    });

    // **The control, and it comes first.** "Refused after the rule" is satisfied
    // by a repository that refuses this push anyway — a wrong hash, a missing
    // grant, a broken remote. Unless the very same command is seen to SUCCEED
    // with no rule in place, the red leg below proves nothing at all.
    let force_push_landed = made
        && go(&mut rows, "with-no-rule-a-force-push-to-main-lands", || {
            std::fs::write(guard.join("README"), "two\n").map_err(|e| e.to_string())?;
            r.git(&guard, &["add", "README"])?;
            r.git(&guard, &["commit", "-q", "--amend", "-m", "rewritten"])?;
            r.git(&guard, &["push", "--force", "-q", &guarded_remote, "main"])
                .map_err(|e| format!("a force-push must be SERVABLE before a rule forbidding it \
                                      means anything: {}", http::cut(&e, 200)))?;
            Ok("history rewritten on main, unguarded".into())
        });

    let protected = force_push_landed
        && go(&mut rows, "the-console-sets-branch-protection-and-reads-it-back", || {
            let (s, _, body) = a.post(
                "/api/repos/alice/guarded/protection",
                r#"{"ref_pattern":"refs/heads/main","require_signature":false}"#,
            )?;
            want((200..300).contains(&s), || {
                format!("SetProtection answered {s}: {}", http::cut(&body, 250))
            })?;
            // Read it back over the console's own GET, not out of the POST's
            // echo: an endpoint that returns what it was handed and stores
            // nothing passes an echo assertion perfectly.
            let (s, j) = a.get("/api/repos/alice/guarded/protection")?;
            want(s == 200, || format!("GetProtection answered {s}"))?;
            want(items_have(&j, "ref_pattern", "refs/heads/main")
                    || j.arr().iter().any(|e| e.text("ref_pattern") == "refs/heads/main"), || {
                format!("the rule did not read back: {}", http::cut(&j.dump(), 250))
            })?;
            // And the CLI agrees, off the same document. Two surfaces onto one
            // store is the whole point; two stores that agree today is not.
            let listed = r.control(&["policy", "list", "alice/guarded"])?;
            want(listed.contains("refs/heads/main"), || {
                format!("`gunnar control policy list` does not see the console's rule: {}",
                        http::cut(&listed, 200))
            })?;
            Ok("refs/heads/main protected, visible from console and CLI".into())
        });

    // **THE ROW.** A protection nobody has watched reject a push is not a
    // protection; it is a row in a database. Everything above is shape.
    if protected {
        go(&mut rows, "a-protected-branch-refuses-a-direct-force-push", || {
            std::fs::write(guard.join("README"), "three\n").map_err(|e| e.to_string())?;
            r.git(&guard, &["add", "README"])?;
            r.git(&guard, &["commit", "-q", "--amend", "-m", "rewritten again"])?;
            let refused = r
                .git(&guard, &["push", "--force", &guarded_remote, "main"])
                .err()
                .ok_or("THE FORCE-PUSH LANDED. A rule was written over the console, read back \
                        from the console and from the CLI, and the pre-receive gate did not \
                        consult it — so branch protection in this build is a value that goes \
                        somewhere to be displayed")?;
            // The server must still be holding the old tip. A push that failed
            // AFTER applying the ref would report an error and have rewritten
            // main anyway, and only the ref advertisement can tell the two
            // apart.
            let (_, refs) = a.get("/api/repos/alice/guarded/refs")?;
            let head = r.git(&guard, &["rev-parse", "HEAD"])?.trim().to_owned();
            let on_server = refs
                .path("items")
                .and_then(|i| i.arr().iter().find(|e| e.text("ref_name") == "refs/heads/main").cloned())
                .ok_or("refs/heads/main is gone from the server entirely")?;
            // `oid` is the field this endpoint serves. This line used to read
            // `target` and `object_id` — neither exists on a RefTarget — so
            // both came back empty and the inequality could NEVER fail
            // (found 2026-08-31 when a sibling row copied the same mistake
            // and its equality check could never pass).
            let on_main = on_server.text("oid");
            want(!on_main.is_empty(), || {
                format!("refs/heads/main has no oid to compare: {}", http::cut(&on_server.dump(), 200))
            })?;
            want(on_main != head, || {
                format!("the push was reported refused and main moved to the rewritten commit \
                         anyway: {}", http::cut(&on_server.dump(), 200))
            })?;
            Ok(format!("refused: {}", http::cut(&refused, 90)))
        });

        go(&mut rows, "a-protected-branch-refuses-a-deletion", || {
            let refused = r
                .git(&guard, &["push", &guarded_remote, "--delete", "main"])
                .err()
                .ok_or("main was DELETED through a rule whose allow_delete is false")?;
            let (_, refs) = a.get("/api/repos/alice/guarded/refs")?;
            want(items_have(&refs, "ref_name", "refs/heads/main"), || {
                "the deletion was reported refused and the branch is gone anyway".into()
            })?;
            Ok(format!("refused: {}", http::cut(&refused, 90)))
        });

        // The other direction, and the one that keeps the two rows above from
        // passing against a repository that had simply stopped accepting
        // pushes: with the rule REMOVED, the identical command lands again.
        go(&mut rows, "removing-the-protection-lets-the-same-push-through-again", || {
            let (s, body) = a.delete(
                "/api/repos/alice/guarded/protection?ref_pattern=refs%2Fheads%2Fmain",
            )?;
            want((200..300).contains(&s), || {
                format!("DeleteProtection answered {s}: {}", http::cut(&body, 200))
            })?;
            r.git(&guard, &["push", "--force", "-q", &guarded_remote, "main"])
                .map_err(|e| format!("the rule was removed and the push is STILL refused, so \
                                      the two rows above were not measuring the rule: {}",
                                     http::cut(&e, 200)))?;
            Ok("rule gone, the identical force-push lands".into())
        });

    }

    // ── self-approval, the merge-side half of the same document ───────────
    //
    // The console's approvals are all the CONSOLE's: one key, one principal,
    // and the console principal also opens every pull request here — so a
    // `forbid_self_approval` rule on main refuses the console's own
    // approve-then-merge, which is exactly the posture an administrator asked
    // for by writing the rule. The rule is the only variable: the SAME merge
    // is driven once with it (refused, naming the rule) and once without it
    // (lands). The rule-off control also already ran above as
    // `merging-a-pull-request-moves-the-branch`.
    //
    // Its own repository, for the same reason the force-push rows have one —
    // measured on the first run of this row: it failed mid-way (the red this
    // row exists for), its cleanup never ran, and the rule it left on
    // `alice/guarded` flipped `the-gate-list-reports-what-is-actually-
    // configured` to FAIL. A rule a failing row leaks must land where no
    // other row looks.
    go(&mut rows, "a-rule-forbidding-self-approval-refuses-the-consoles-own-merge", || {
        let (s, _, body) = a.post("/api/repos", r#"{"namespace":"alice","name":"reviewed"}"#)?;
        want((200..300).contains(&s), || format!("create answered {s}: {}", http::cut(&body, 150)))?;
        let dir = r.work.join("reviewed");
        let remote = r.remote("alice/reviewed");
        std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
        r.git(&dir, &["init", "-q", "--object-format=sha256", "-b", "main", "."])?;
        r.git(&dir, &["config", "user.name", "uitest"])?;
        r.git(&dir, &["config", "user.email", "info@example.invalid"])?;
        std::fs::write(dir.join("README"), "one\n").map_err(|e| e.to_string())?;
        r.git(&dir, &["add", "README"])?;
        r.git(&dir, &["commit", "-qm", "one"])?;
        r.git(&dir, &["push", "-q", &remote, "main"])?;

        // Where main is, read off the server's own ref advertisement — the
        // same source the force-push rows trust.
        let main_tip = || -> Result<String, String> {
            let (_, refs) = a.get("/api/repos/alice/reviewed/refs")?;
            let row = refs
                .path("items")
                .and_then(|i| i.arr().iter().find(|e| e.text("ref_name") == "refs/heads/main").cloned())
                .ok_or("refs/heads/main is gone from the server")?;
            let oid = row.text("oid");
            // Empty must be an error, not a value: every comparison against
            // an empty tip is a comparison that can never fail.
            want(!oid.is_empty(), || format!("refs/heads/main has no oid: {}", http::cut(&row.dump(), 120)))?;
            Ok(oid)
        };

        r.git(&dir, &["checkout", "-q", "-b", "review-me"])?;
        std::fs::write(dir.join("CHANGE"), "please review\n").map_err(|e| e.to_string())?;
        r.git(&dir, &["add", "CHANGE"])?;
        r.git(&dir, &["commit", "-qm", "a change that needs another pair of eyes"])?;
        r.git(&dir, &["push", "-q", &remote, "review-me"])?;
        r.git(&dir, &["checkout", "-q", "main"])?;

        let (s, j, body) = a.post(
            "/api/repos/alice/reviewed/pulls",
            r#"{"title":"needs another pair of eyes","description":"",
                "source_ref":"refs/heads/review-me","target_ref":"refs/heads/main"}"#,
        )?;
        want((200..300).contains(&s), || format!("create answered {s}: {}", http::cut(&body, 200)))?;
        let n = j.path("number").and_then(|x| x.as_f64()).map(|x| x as i64).ok_or("no number")?;

        let head = a.get(&format!("/api/repos/alice/reviewed/pulls/{n}/review"))?.1.text("head");
        want(!head.is_empty(), || "the review does not say where the branch is".into())?;
        let (s, rev, body) = a.post(
            &format!("/api/repos/alice/reviewed/pulls/{n}/approve"),
            &format!(r#"{{"commit":{}}}"#, esc(&head)),
        )?;
        want((200..300).contains(&s), || format!("approve answered {s}: {}", http::cut(&body, 250)))?;
        want(rev.path("head_is_approved").and_then(|v| v.as_bool()) == Some(true), || {
            format!("approved and still not approved: {}", http::cut(&rev.dump(), 250))
        })?;

        // The rule, written AFTER the approval so the approval itself is
        // beyond suspicion, and the merge is what changes.
        let (s, _, body) = a.post(
            "/api/repos/alice/reviewed/protection",
            r#"{"ref_pattern":"refs/heads/main","forbid_self_approval":true}"#,
        )?;
        want((200..300).contains(&s), || format!("SetProtection answered {s}: {}", http::cut(&body, 250)))?;

        let before = main_tip()?;
        let (s, _, body) = a.post(&format!("/api/repos/alice/reviewed/pulls/{n}/merge"), "{}")?;
        want(s == 400, || format!("a self-approved merge answered {s} under forbid_self_approval: {}", http::cut(&body, 250)))?;
        want(body.contains("forbids self-approval"), || {
            format!("refused, but not by the self-approval rule: {}", http::cut(&body, 250))
        })?;
        want(main_tip()? == before, || "the merge was refused and moved main anyway".into())?;

        // The other direction: rule gone, the IDENTICAL merge lands — so the
        // refusal above was the rule and nothing else.
        let (s, body) = a.delete(
            "/api/repos/alice/reviewed/protection?ref_pattern=refs%2Fheads%2Fmain",
        )?;
        want((200..300).contains(&s), || format!("DeleteProtection answered {s}: {}", http::cut(&body, 200)))?;
        let (s, _, body) = a.post(&format!("/api/repos/alice/reviewed/pulls/{n}/merge"), "{}")?;
        want((200..300).contains(&s), || {
            format!("the rule was removed and the identical merge is STILL refused, so the \
                     row above was not measuring the rule: {s} {}", http::cut(&body, 250))
        })?;
        want(main_tip()? == head, || "the merge landed and main is not at the approved tip".into())?;
        Ok(format!("refused under the rule, merged without it, main at {}", &head[..8.min(head.len())]))
    });

    go(&mut rows, "the-gate-list-reports-what-is-actually-configured", || {
        let (s, j) = a.get("/api/repos/alice/guarded/gates")?;
        want(s == 200, || format!("ListGates answered {s}"))?;
        let gates = j.path("items").cloned().unwrap_or(j.clone());
        let branch = gates
            .arr()
            .iter()
            .find(|g| g.text("name") == "branch-protection")
            .cloned()
            .ok_or_else(|| format!("no branch-protection gate: {}", http::cut(&gates.dump(), 250)))?;
        // The rule was just removed, so the gate must report itself OFF. A
        // gate list that says "on" because the code is compiled in tells an
        // operator a guard exists where none does.
        want(branch.path("enabled").and_then(|v| v.as_bool()) == Some(false), || {
            format!("branch-protection reports enabled with no rule configured: {}",
                    http::cut(&branch.dump(), 200))
        })?;
        Ok(format!("{} gates, branch-protection off with no rule", gates.arr().len()))
    });

    // ── the chain: `gunnar verify`, served, with the holes kept honest ────
    //
    // Three rows, three answers. The signed fixture proves the happy path with
    // a REAL `git commit -S` (ssh signing, stock git, the rig's registered
    // admin key); the unsigned fixture proves the hole renders as a named hole
    // and not as a pass; and the tree head between them is what turns the log
    // link from "arithmetic the server did about itself" into evidence.
    go(&mut rows, "a-signed-push-and-a-tree-head-set-the-chain-up", || {
        let wt = r.work.join("wt-signed");
        std::fs::create_dir_all(&wt).map_err(|e| e.to_string())?;
        r.git(&wt, &["init", "-q", "--object-format=sha256", "-b", "main", "."])?;
        r.git(&wt, &["config", "user.name", "uitest"])?;
        r.git(&wt, &["config", "user.email", "info@example.invalid"])?;
        r.git(&wt, &["config", "gpg.format", "ssh"])?;
        let key = r.work.join("admin");
        r.git(&wt, &["config", "user.signingkey", key.to_str().ok_or("key path")?])?;
        std::fs::write(wt.join("src.txt"), "the provable line\n").map_err(|e| e.to_string())?;
        r.git(&wt, &["add", "src.txt"])?;
        r.git(&wt, &["commit", "-qm", "a signed commit", "-S"])?;
        let head = r.git(&wt, &["rev-parse", "HEAD"])?.trim().to_owned();
        r.git(&wt, &["push", "-q", &r.remote("alice/chain-signed"), "main"])?;
        r.attest_head()?;
        Ok(format!("signed head over the log; head commit {}", &head[..12]))
    });

    go(&mut rows, "the-chain-over-a-signed-commit-is-unbroken", || {
        let (s, j) = a.get("/api/repos/alice/chain-signed/chain?rev=refs%2Fheads%2Fmain&line=src.txt%3A1")?;
        want(s == 200, || format!("the chain endpoint answered {s}: {}", http::cut(&j.dump(), 250)))?;
        want(j.text("summary") == "unbroken", || {
            format!("a fully proven chain reported {:?}: {}", j.text("summary"), http::cut(&j.dump(), 400))
        })?;
        let links = j.path("links").cloned().unwrap_or(Json::Null);
        want(links.arr().len() == 4, || format!("{} links", links.arr().len()))?;
        for link in links.arr() {
            want(link.text("verdict") == "proven", || {
                format!("link {} is {}: {}", link.text("id"), link.text("verdict"), link.text("detail"))
            })?;
        }
        let sig = links.arr().iter().find(|l| l.text("id") == "signature").cloned()
            .ok_or("no signature link")?;
        // "signed" without "by whom" is not an answer. The rig's admin key is
        // registered to the bootstrapped principal, and the verdict names it.
        want(sig.text("detail").contains("is signed by"), || {
            format!("the signature verdict does not say who signed: {}", sig.text("detail"))
        })?;
        Ok(format!("4 proven links; {}", http::cut(&sig.text("detail"), 100)))
    });

    go(&mut rows, "the-chain-over-an-unsigned-commit-names-the-hole", || {
        let (s, j) = a.get("/api/repos/alice/uitest/chain?rev=refs%2Fheads%2Fmain&line=README%3A1")?;
        want(s == 200, || format!("the chain endpoint answered {s}"))?;
        // Not "broken": nothing failed. Not "unbroken": a hole is not a pass.
        want(j.text("summary") == "incomplete", || {
            format!("an unsigned commit's chain reported {:?}: {}", j.text("summary"), http::cut(&j.dump(), 400))
        })?;
        let links = j.path("links").cloned().unwrap_or(Json::Null);
        let sig = links.arr().iter().find(|l| l.text("id") == "signature").cloned()
            .ok_or("no signature link")?;
        want(sig.text("verdict") == "unproven", || {
            format!("the unsigned commit's signature link is {:?}", sig.text("verdict"))
        })?;
        want(sig.text("detail").contains("not signed"), || {
            format!("the hole is not named in words: {}", sig.text("detail"))
        })?;
        // …and the rest of the chain still proves, so the ONE hole is the
        // signature and this row cannot pass on a chain that never got walked.
        for id in ["line", "push", "log"] {
            let link = links.arr().iter().find(|l| l.text("id") == id).cloned()
                .ok_or_else(|| format!("no {id} link"))?;
            want(link.text("verdict") == "proven", || {
                format!("link {id} is {}: {}", link.text("verdict"), link.text("detail"))
            })?;
        }
        Ok(format!("incomplete, with the hole named: {}", http::cut(&sig.text("detail"), 100)))
    });

    // ── blame: which commit wrote each line, and is it signed ─────────────
    //
    // Built on the same two walks the chain view uses — `Refs.Blame` for the
    // commit, `Admin.VerifyChain` for the signature — so the signed fixture's
    // line names its commit AND its signer, and the unsigned fixture's line is a
    // named hole: the oid is real, the verdict is not proven. Each row asserts
    // the ACTUAL attributed oid, never that a row merely came back.
    go(&mut rows, "blame-names-the-commit-and-signer-of-a-signed-line", || {
        let head = a.get("/api/repos/alice/chain-signed/head")?.1.text("oid");
        want(head.len() == 64, || format!("no head oid: {head:?}"))?;
        let (s, j) = a.get("/api/repos/alice/chain-signed/blame?rev=refs%2Fheads%2Fmain&path=src.txt")?;
        want(s == 200, || format!("blame answered {s}: {}", http::cut(&j.dump(), 200)))?;
        let lines = j.path("lines").cloned().unwrap_or(Json::Null);
        want(!lines.arr().is_empty(), || "blame returned no lines".into())?;
        let first = lines.arr()[0].clone();
        // The exact commit, not "some row exists": the line is attributed to the
        // head this fixture just signed and pushed.
        want(first.text("commit") == head, || {
            format!("line 1 is attributed to {:?}, not the head {head}", first.text("commit"))
        })?;
        want(first.text("signature") == "proven", || {
            format!("a signed line is not proven: {}", http::cut(&first.dump(), 200))
        })?;
        want(first.text("signer").contains("is signed by"), || {
            format!("the signer is not named: {:?}", first.text("signer"))
        })?;
        Ok(format!("line 1 → {} · signed", &head[..12]))
    });

    go(&mut rows, "blame-over-an-unsigned-line-is-a-named-hole", || {
        // The commit that last touched README, from the log, so the hole is
        // asserted against the ACTUAL oid and not just "unproven, whatever it is".
        let log = a.get("/api/repos/alice/uitest/log?path=README&limit=1")?.1;
        let items = log.path("items").or_else(|| log.path("commits")).cloned().unwrap_or(Json::Null);
        let readme_commit = items.arr().first().map(|c| c.text("oid")).unwrap_or_default();
        let (s, j) = a.get("/api/repos/alice/uitest/blame?rev=refs%2Fheads%2Fmain&path=README")?;
        want(s == 200, || format!("blame answered {s}: {}", http::cut(&j.dump(), 200)))?;
        let first = j.path("lines").and_then(|l| l.arr().first().cloned())
            .ok_or("blame returned no lines")?;
        want(first.text("commit").len() == 64, || "no commit oid on the line".into())?;
        if readme_commit.len() == 64 {
            want(first.text("commit") == readme_commit, || {
                format!("line 1 is attributed to {:?}, not README's commit {readme_commit}", first.text("commit"))
            })?;
        }
        // A hole: not "broken" (nothing failed), and not "proven" (no signature).
        want(first.text("signature") == "unproven", || {
            format!("an unsigned line did not read unproven: {}", http::cut(&first.dump(), 200))
        })?;
        Ok(format!("line 1 → {} · a named hole", &first.text("commit")[..12]))
    });

    // ── search: a literal string across the latest revision ───────────────
    //
    // Not a hollow ">= 0 results". The seeded line is "the provable line" in
    // src.txt, so the query for "provable" must come back with THAT file at
    // THAT line — the specific hit is the whole assertion.
    go(&mut rows, "search-returns-the-file-and-line-of-a-known-string", || {
        let (s, j) = a.get("/api/repos/alice/chain-signed/search?q=provable")?;
        want(s == 200, || format!("search answered {s}: {}", http::cut(&j.dump(), 200)))?;
        let matches = j.path("matches").cloned().unwrap_or(Json::Null);
        let hit = matches.arr().iter().find(|m| m.text("path") == "src.txt").cloned()
            .ok_or_else(|| format!("no hit in src.txt: {}", http::cut(&matches.dump(), 250)))?;
        want(hit.path("line").and_then(|v| v.as_f64()) == Some(1.0), || {
            format!("the hit is not on line 1: {}", http::cut(&hit.dump(), 200))
        })?;
        want(hit.text("text").contains("provable"), || {
            format!("the matching line does not contain the string: {:?}", hit.text("text"))
        })?;
        Ok(format!("src.txt:1 — {}", http::cut(&hit.text("text"), 60)))
    });

    go(&mut rows, "search-is-empty-for-a-string-that-is-not-there", || {
        // The other half: a string nobody wrote comes back with no matches, so
        // the row above cannot be passing on a search that matches everything.
        let (s, j) = a.get("/api/repos/alice/chain-signed/search?q=zzz_not_present_anywhere_zzz")?;
        want(s == 200, || format!("search answered {s}"))?;
        want(j.path("matches").map(|m| m.arr().is_empty()).unwrap_or(false), || {
            format!("a string nobody wrote still matched: {}", http::cut(&j.dump(), 200))
        })?;
        Ok("no matches, as there should be none".into())
    });

    // ── inline review comments: a comment that follows the code ───────────
    //
    // A comment anchored to (path, line, blob_oid). The blob is the point: the
    // comment is filed against the exact file version the reviewer saw, so it
    // follows the code across a rebase that leaves the file alone. Here it is
    // filed through the console, read back with its anchor intact, and (in the
    // browser rows below) rendered on its line in the diff.
    go(&mut rows, "an-inline-topic-and-a-pull-to-review", || {
        let dir = r.work.join("inline");
        std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
        r.git(&dir, &["clone", "-q", &r.remote("alice/uitest"), "wt"])?;
        let wt = dir.join("wt");
        r.git(&wt, &["config", "user.name", "uitest"])?;
        r.git(&wt, &["config", "user.email", "info@example.invalid"])?;
        r.git(&wt, &["checkout", "-q", "-b", "inline-topic"])?;
        std::fs::write(wt.join("src/lib.rs"), "pub fn hello() {}\npub fn added_line() {}\n")
            .map_err(|e| e.to_string())?;
        r.git(&wt, &["add", "src/lib.rs"])?;
        r.git(&wt, &["commit", "-qm", "add a line to comment on"])?;
        r.git(&wt, &["push", "-q", &r.remote("alice/uitest"), "inline-topic"])?;
        // Re-attest after this push. The transparency-log link of the chain
        // view proves a push is covered by a SIGNED head; a push that lands
        // after the last attest leaves the current head unsigned, which would
        // turn `chain-signed`'s already-proven chain INCOMPLETE for the browser
        // rows that walk it later. The signed-chain fixture re-attests after its
        // own push for exactly this reason — so must this one.
        r.attest_head()?;
        let (s, j, body) = a.post(
            "/api/repos/alice/uitest/pulls",
            r#"{"title":"inline comment demo","description":"for the inline-comment rows",
                "source_ref":"refs/heads/inline-topic","target_ref":"refs/heads/main"}"#,
        )?;
        want((200..300).contains(&s), || format!("create answered {s}: {}", http::cut(&body, 200)))?;
        let number = j.path("number").and_then(|n| n.as_f64()).map(|n| format!("{}", n as i64))
            .ok_or_else(|| format!("no number: {}", http::cut(&j.dump(), 200)))?;
        *inline_pull.borrow_mut() = number.clone();
        // The blob of src/lib.rs on the NEW side, from the diff — exactly what
        // the diff page will render the comment against.
        let (_, diff) = a.get(
            "/api/repos/alice/uitest/diff?from=refs%2Fheads%2Fmain&to=refs%2Fheads%2Finline-topic",
        )?;
        let change = diff.path("changes").and_then(|c| {
            c.arr().iter().find(|ch| ch.text("path") == "src/lib.rs").cloned()
        }).ok_or_else(|| format!("no change for src/lib.rs: {}", http::cut(&diff.dump(), 250)))?;
        let blob = change.text("new_oid");
        want(blob.len() == 64, || format!("no new_oid on the change: {}", http::cut(&change.dump(), 200)))?;
        *inline_blob.borrow_mut() = blob.clone();
        Ok(format!("pull #{number}, src/lib.rs @ {}", &blob[..12]))
    });

    go(&mut rows, "an-inline-comment-anchors-to-the-blob-and-line", || {
        let n = inline_pull.borrow().clone();
        let blob = inline_blob.borrow().clone();
        want(!n.is_empty() && !blob.is_empty(), || "the inline pull was not set up".into())?;
        let (s, _, body) = a.post(
            &format!("/api/repos/alice/uitest/pulls/{n}/comments"),
            &format!(r#"{{"path":"src/lib.rs","line":2,"blob_oid":{},"body":"this new fn needs a test"}}"#, esc(&blob)),
        )?;
        want((200..300).contains(&s), || format!("comment answered {s}: {}", http::cut(&body, 250)))?;
        // Read back: the anchor is intact — the SAME blob and line, not the pull
        // number standing in for them. That is what lets it follow the code.
        let (_, list) = a.get(&format!("/api/repos/alice/uitest/pulls/{n}/comments"))?;
        let mine = list.arr().iter().find(|c| c.text("body").contains("needs a test")).cloned()
            .ok_or_else(|| format!("the comment did not read back: {}", http::cut(&list.dump(), 250)))?;
        want(mine.text("path") == "src/lib.rs", || format!("wrong path: {:?}", mine.text("path")))?;
        want(mine.path("line").and_then(|v| v.as_f64()) == Some(2.0), || {
            format!("wrong line: {}", http::cut(&mine.dump(), 200))
        })?;
        want(mine.text("blob_oid") == blob, || {
            format!("the comment is not anchored to the blob it was made on: {}", http::cut(&mine.dump(), 200))
        })?;
        Ok(format!("#{n} src/lib.rs:2 @ {}", &blob[..12]))
    });

    // ── the first admin is the operator's account (2026-10-04) ─────────────
    //
    // test2.gunnar.rs, the fresh install of 2026-10-04: at 07:37 a first push
    // with the installer's admin key into `rickard/t2-scratch` conjured the
    // namespace `rickard`, with no owner. At 08:15 the owner registered his
    // YubiKey with the break-glass token as `rickard`; the console asked gunnar
    // `AddPrincipal rickard`, gunnar refused it ("namespace already exists"), and
    // the passkey bound to nothing — "gunnar has no principal bound to this
    // session", no Account page, no Add SSH key. Here the same morning, with
    // this rig's names: alice (the admin key) pushes `bob/t2-scratch` first, and
    // the browser below registers `bob` with the token.
    go(&mut rows, "a-first-push-conjures-the-operators-namespace-first", || {
        let wt = r.work.join("conjure");
        std::fs::create_dir_all(&wt).map_err(|e| e.to_string())?;
        // SHA-1 on purpose: gunnar's first-push door infers SHA-1, as stock
        // `git init` makes it, and it is the door that conjures a namespace.
        r.git(&wt, &["init", "-q", "-b", "main", "."])?;
        r.git(&wt, &["config", "user.name", "uitest"])?;
        r.git(&wt, &["config", "user.email", "info@example.invalid"])?;
        std::fs::write(wt.join("SCRATCH"), "pushed before anybody registered\n").map_err(|e| e.to_string())?;
        r.git(&wt, &["add", "SCRATCH"])?;
        r.git(&wt, &["commit", "-qm", "scratch"])?;
        r.git(&wt, &["push", "-q", &r.remote("bob/t2-scratch"), "main"])?;
        // Re-attest after this push, as every fixture push before a browser
        // chain row does: a push after the last signed head leaves the log's
        // head unsigned, and `chain-signed`'s proven chain reads INCOMPLETE in
        // the browser rows that walk it later.
        r.attest_head()?;
        let spaces = r.control(&["namespace", "list"])?;
        want(spaces.lines().any(|l| l.trim() == "bob"), || {
            format!("the first push did not conjure `bob`: {}", http::cut(&spaces, 200))
        })?;
        let users = r.control(&["user", "list"])?;
        want(!users.lines().any(|l| l.split('\t').next() == Some("bob")), || {
            format!("a principal `bob` already exists, so this is not test2's morning: {}", http::cut(&users, 200))
        })?;
        Ok("bob/t2-scratch pushed by the admin key: namespace `bob` exists, no principal `bob`".into())
    });

    // ── the installer's word: the operator, and the break-glass token as his
    // first invite (Stage L) ────────────────────────────────────────────────
    //
    // On an estate the installer feeds gunnar `operator_name` and the token's
    // hash (`operator_invite_sha256`); this rig is a box no installer fed, so
    // it says the same through the control plane — the token from a file,
    // hashed by gunnar-cli, never on a command line. gunnar claims `bob` over
    // the namespace the push above conjured (ADOPTED, never refused) and keeps
    // the hash as the invite for his first passkey; the browser's
    // registration below is `Access.WebRegister` with the token, decided by
    // gunnar. A gunnar from before Stage L has no such verb: the row says so,
    // and the console's old ceremony runs.
    go(&mut rows, "the-installer-claims-the-operator-and-his-first-invite", || {
        let file = r.work.join("break-glass.token");
        std::fs::write(&file, format!("{}\n", r.token)).map_err(|e| e.to_string())?;
        let said = r.control(&["account", "claim-operator", "bob", "--bootstrap-file", &file.display().to_string()])?;
        let _ = std::fs::remove_file(&file);
        want(said.contains("operator bob") && said.contains("adopted"), || {
            format!("gunnar did not claim `bob` over the conjured namespace: {}", http::cut(&said, 200))
        })?;
        want(said.contains("first invite"), || format!("no first invite was recorded: {}", http::cut(&said, 200)))?;
        let users = r.control(&["user", "list"])?;
        want(users.lines().any(|l| l.split('\t').next() == Some("bob")), || {
            format!("gunnar holds no principal `bob`: {}", http::cut(&users, 200))
        })?;
        Ok(format!("{} — gunnar holds `bob` and the token's hash as his first invite", said.trim()))
    });

    go(&mut rows, "no-passkey-exists-yet", || {
        let (_, j) = a.get("/auth/passkey/credentials")?;
        want(j.path("credentials").map(|c| c.arr().is_empty()).unwrap_or(false), || {
            format!("{}", http::cut(&j.dump(), 200))
        })?;
        want(j.path("enabled_admins").and_then(|v| v.as_f64()) == Some(0.0), || "admins already exist".into())?;
        Ok("0 credentials, 0 admins".into())
    });

    let Some(chrome_bin) = chrome_bin else {
        return rows;
    };
    browser(
        r,
        &mut rows,
        chrome_bin,
        authenticator,
        &inline_pull.borrow(),
        &inline_blob.borrow(),
    );
    rows
}

/// The browser half: the ceremonies a bearer token can never reach.
/// **A stranger clicks "Log in" and must land on the server's login page.**
///
/// A stranger on a public page (the repository list, or a public repository's
/// graph) is offered exactly one door: the "Log in" button in the top bar. It
/// is a plain anchor to `/login`, which the SERVER renders — the console has no
/// `/login` route of its own. A same-origin anchor inside the console is the
/// router's to intercept, and an intercepted click on a route the console does
/// not have moves the URL bar and nothing else. Measured on gunnar.rs
/// 2026-09-10 from /repo/alice/linux/graph: "Log in" did nothing. So this
/// clicks the button the way a person does and asks the BROWSER where it ended
/// up and whether the login page's own element is there — not the URL alone,
/// which the router changes either way.
/// **The login page's one button is the passkey sign-in** (LOGIN-1, owner 2026-10-27: `/login`
/// IS the login, its one button says "Log in" and starts the passkey ceremony). A console from
/// before LOGIN-1 named the passkey on it, which a `--live` deployment may still serve.
fn is_the_login_button(label: &str) -> bool {
    label.trim() == "Log in" || label.to_lowercase().contains("passkey")
}

/// **A passkey registered through an invite link** in `sess`, named `label`. Since the owner's
/// 2026-10-21 order a link's registration signs the person in at once while the browser still
/// holds the press (the page then leaves `/login`, so its `#msg` is gone), and otherwise says
/// it registered and leaves "Log in" as the way on. Either way the page's word is checked, and
/// the answer says which happened: `true` when the person is signed in already.
pub(crate) fn register_through_link(c: &mut Cdp, sess: &str, url: &str, label: &str) -> Result<bool, String> {
    c.navigate(sess, url)?;
    c.wait_for(sess, "!!document.getElementById('register')", 30_000)?;
    c.eval(
        sess,
        &format!(
            "(() => {{ document.getElementById('label').value = {};
               document.getElementById('register').click(); return 'clicked'; }})()",
            esc(label)
        ),
    )?;
    const DONE: &str = "fetch('/auth/status').then(r => r.json()).then(j => {
        const m = document.getElementById('msg');
        return j.authenticated === true || !!m?.classList.contains('err')
          || (!!m?.classList.contains('ok') && m.textContent.includes('registered') && !m.textContent.includes('Signing you in'));
      })";
    c.wait_for(sess, DONE, 30_000).map_err(|e| {
        let at = c.eval_str(sess, "location.pathname + location.search.replace(/invite=[^&]*/, 'invite=…')").unwrap_or_default();
        let said = c.eval_str(sess, "document.getElementById('msg')?.textContent ?? '(no #msg)'").unwrap_or_default();
        format!("the registration never settled at {at}, the page said {said:?} ({e})")
    })?;
    let st = c.eval(sess, "fetch('/auth/status').then(r => r.json())")?;
    if st.path("authenticated").and_then(|v| v.as_bool()) == Some(true) {
        return Ok(true);
    }
    let said = c.eval_str(sess, "document.getElementById('msg')?.textContent ?? ''")?;
    want(said.contains("registered"), || format!("the page said {said:?}"))?;
    Ok(false)
}

fn a_stranger_clicks_log_in(c: &mut Cdp, sess: &str, origin: &str) -> Result<String, String> {
    c.navigate(sess, &format!("{origin}/repos"))?;
    let _ = c.drain_page_errors();
    // A `public-site` console with its switch off shows a guest only `/` (GUN-GUEST,
    // `auth::middleware::GuestsOff`): the server sends the stranger to the login page at once,
    // with `next`. That is the same arrival without the click (GUN-FIELD-TEST, 2026-10-31).
    c.wait_for(
        sess,
        "!!document.querySelector('a.btn.primary[href^=\"/login\"]') || (location.pathname === '/login' && !!document.getElementById('login'))",
        30_000,
    )
    .map_err(|e| format!("neither a Log in button nor the login page for a stranger on /repos ({e})"))?;
    if c.eval_str(sess, "location.pathname")? == "/login" {
        let label = c.eval_str(sess, "document.getElementById('login')?.textContent ?? ''")?;
        want(is_the_login_button(&label), || format!("the login page shows no passkey button: {}", http::cut(&label, 100)))?;
        let next = c.eval_str(sess, "new URLSearchParams(location.search).get('next') ?? ''")?;
        want(next == "/repos", || format!("the login page was not told where to return to: next={next:?}"))?;
        return Ok("guests see only / here: sent to the server's login page at once, next=/repos".into());
    }
    c.wait_for(sess, "!!document.querySelector('a.btn.primary[href^=\"/login\"]')", 30_000)
        .map_err(|e| {
            let at = c.eval_str(sess, "location.pathname + location.search").unwrap_or_default();
            let header = c.eval_str(sess, "document.querySelector('header')?.innerText ?? document.body.innerText").unwrap_or_default();
            format!("no Log in button for a stranger on /repos: at {at}, the page says {} ({e})", http::cut(&header.replace('\n', " "), 300))
        })?;
    c.eval(sess, "document.querySelector('a.btn.primary[href^=\"/login\"]').click(); true")?;
    c.wait_for(sess, "location.pathname === '/login' && !!document.getElementById('login')", 15_000)
        .map_err(|e| {
            let path = c.eval_str(sess, "location.pathname").unwrap_or_default();
            let login = c.eval(sess, "!!document.getElementById('login')").map(|v| v.dump()).unwrap_or_default();
            format!("after clicking Log in the browser is at {path} with a login element: {login} ({e})")
        })?;
    let label = c.eval_str(sess, "document.getElementById('login')?.textContent ?? ''")?;
    want(is_the_login_button(&label), || format!("the login page shows no passkey button: {}", http::cut(&label, 100)))?;
    let next = c.eval_str(sess, "new URLSearchParams(location.search).get('next') ?? ''")?;
    want(next == "/repos", || format!("the login page was not told where to return to: next={next:?}"))?;
    Ok("clicked Log in, landed on the server's login page, next=/repos".into())
}

/// GUN-MERGE-ENGINE (2026-11-01): make `branch` off the server's main of
/// `alice/pr-web` with `file`, then move the server's main with `main_file`, open
/// the pull request in the console, approve its head in the browser, and answer
/// its number.
fn pr_behind_main(
    r: &Rig,
    c: &mut Cdp,
    sess: &str,
    branch: &str,
    file: (&str, &str),
    main_file: (&str, &str),
) -> Result<String, String> {
    let dir = r.work.join("pr-web");
    let remote = r.remote("alice/pr-web");
    r.git(&dir, &["fetch", "-q", &remote, "main"])?;
    r.git(&dir, &["checkout", "-q", "-B", "main", "FETCH_HEAD"])?;
    r.git(&dir, &["checkout", "-q", "-b", branch])?;
    std::fs::write(dir.join(file.0), file.1).map_err(|e| e.to_string())?;
    r.git(&dir, &["add", file.0])?;
    r.git(&dir, &["commit", "-qm", &format!("{branch}: a change made while main moved on")])?;
    r.git(&dir, &["push", "-q", &remote, branch])?;
    r.git(&dir, &["checkout", "-q", "main"])?;
    std::fs::write(dir.join(main_file.0), main_file.1).map_err(|e| e.to_string())?;
    r.git(&dir, &["add", main_file.0])?;
    r.git(&dir, &["commit", "-qm", &format!("main moves on past {branch}")])?;
    r.git(&dir, &["push", "-q", &remote, "main"])?;
    let opened = c.eval(
        sess,
        &format!(
            "fetch('/api/repos/alice/pr-web/pulls', {{method:'POST',
                 headers:{{'Content-Type':'application/json'}},
                 body: JSON.stringify({{title:'{branch} behind main', description:'',
                     source_ref:'refs/heads/{branch}', target_ref:'refs/heads/main'}})}})
               .then(r => r.json())"
        ),
    )?;
    let number = opened
        .path("number")
        .and_then(|n| n.as_f64())
        .map(|n| format!("{}", n as i64))
        .ok_or_else(|| format!("no pull number: {}", http::cut(&opened.dump(), 200)))?;
    c.navigate(sess, &format!("{}/repo/alice/pr-web/pulls/{number}", r.origin()))?;
    c.wait_for(
        sess,
        "[...document.querySelectorAll('button')].some(e => e.textContent.trim() === 'Approve this commit')",
        30_000,
    )?;
    c.eval_str(
        sess,
        "[...document.querySelectorAll('button')].find(e => e.textContent.trim() === 'Approve this commit').click()",
    )?;
    c.wait_for(
        sess,
        "(() => { const t=(document.querySelector('main')||document.body).innerText;
           return t.includes('approved at this commit') && !t.includes('not approved at this commit'); })()",
        20_000,
    )
    .map_err(|e| format!("the review badge did not flip to approved: {e}"))?;
    Ok(number)
}

fn browser(
    r: &Rig,
    rows: &mut Vec<Row>,
    chrome_bin: &str,
    authenticator: bool,
    inline_pull: &str,
    inline_blob: &str,
) {
    let port = match std::net::TcpListener::bind("127.0.0.1:0").and_then(|l| l.local_addr()) {
        Ok(a) => a.port(),
        Err(e) => {
            go(rows, "chrome-starts-headless", || Err(e.to_string()));
            return;
        }
    };
    let profile = r.work.join("chrome");
    let mut chrome = match Chrome::launch(chrome_bin, Path::new(&profile), port) {
        Ok(c) => {
            go(rows, "chrome-starts-headless", || Ok(format!("debugging on {port}")));
            c
        }
        Err(e) => {
            go(rows, "chrome-starts-headless", || Err(e));
            return;
        }
    };
    let mut c = match Cdp::attach(port) {
        Ok(c) => c,
        Err(e) => {
            go(rows, "devtools-protocol-attaches", || Err(e));
            chrome.kill();
            return;
        }
    };
    let sess = match c.page() {
        Ok(s) => {
            go(rows, "devtools-protocol-attaches", || Ok("page attached".into()));
            s
        }
        Err(e) => {
            go(rows, "devtools-protocol-attaches", || Err(e));
            chrome.kill();
            return;
        }
    };

    let auth = std::cell::RefCell::new(String::new());
    if !go(rows, "a-virtual-authenticator-is-installed", || {
        if !authenticator {
            // --no-authenticator: the suite's own red. Every passkey situation
            // below must fail without this, and if any of them still passes it
            // was never testing the ceremony: a guard that cannot fail is worth
            // nothing. Run it after changing anything in this file.
            return Err("skipped deliberately (--no-authenticator)".into());
        }
        let id = c.add_authenticator(&sess)?;
        want(!id.is_empty(), || "no authenticatorId".into())?;
        *auth.borrow_mut() = id.clone();
        Ok(format!("authenticator {id}"))
    }) && authenticator
    {
        chrome.kill();
        return;
    }
    let auth = auth.into_inner();

    go(rows, "the-login-page-offers-a-passkey", || {
        c.navigate(&sess, &format!("{}/login", r.origin()))?;
        let label = c.eval_str(&sess, "document.getElementById('login').textContent")?;
        want(is_the_login_button(&label), || format!("the button says {label:?}"))?;
        let supported = c.eval(&sess, "!!window.PublicKeyCredential")?;
        want(supported.as_bool() == Some(true), || "the browser reports no WebAuthn".into())?;
        Ok(label)
    });

    go(rows, "the-authenticator-starts-with-no-credential", || {
        let creds = c.credentials(&sess, &auth)?;
        want(creds.is_empty(), || format!("{} credentials already", creds.len()))?;
        Ok("0 credentials".into())
    });

    let registered = go(rows, "a-passkey-registers-with-the-admin-token", || {
        c.eval(
            &sess,
            &format!(
                "(() => {{ document.getElementById('reg').open = true;
                   document.getElementById('token').value = {};
                   document.getElementById('label').value = 'virtual';
                   document.getElementById('register').click(); return 'clicked'; }})()",
                esc(&r.token)
            ),
        )?;
        c.wait_for(
            &sess,
            "document.getElementById('msg').classList.contains('ok') || document.getElementById('msg').classList.contains('err')",
            30_000,
        )?;
        let msg = c.eval_str(&sess, "document.getElementById('msg').textContent")?;
        want(msg.contains("registered"), || format!("the page said {msg:?}"))?;
        // The page's word is not the evidence. The authenticator's is.
        let creds = c.credentials(&sess, &auth)?;
        want(creds.len() == 1, || format!("the authenticator holds {} credentials", creds.len()))?;
        want(creds[0].text("rpId") == "localhost", || format!("rpId {:?}", creds[0].text("rpId")))?;
        want(creds[0].path("isResidentCredential").and_then(|v| v.as_bool()).is_some(), || {
            format!("no resident flag: {}", http::cut(&creds[0].dump(), 200))
        })?;
        Ok(format!("1 credential for rp {}", creds[0].text("rpId")))
    });

    if registered {
        go(rows, "the-admin-token-retires-once-an-admin-passkey-exists", || {
            // The situation this whole ordering exists for: the break-glass
            // token worked in every row above, and must stop working here.
            // `/api/storage` and not `/api/repos`: the repository list is
            // served to a stranger as the guest, so a spent token there reads
            // 200 with the public subset and proves nothing about the token.
            // Storage is on no anonymous allow-list; only a credential opens it.
            let h = [("Authorization", format!("Bearer {}", r.token))];
            let resp = http::req(&r.ui_addr(), "GET", "/api/storage", &h, None)?;
            want(resp.status == 401, || {
                format!("the token still opens /api/storage: {} {}", resp.status, http::cut(&resp.body, 120))
            })?;
            Ok("401 — the break-glass token is spent".into())
        });
    }

    go(rows, "the-passkey-logs-in", || {
        // The registration's answer carried the core's session already, and /login sends a
        // signed-in browser on: out first, so this row is the passkey's own login, after the
        // logout's second (GHUNT-12).
        c.eval(&sess, "fetch('/auth/logout', {method:'POST'}).then(r => r.status)")?;
        std::thread::sleep(std::time::Duration::from_millis(1_100));
        c.navigate(&sess, &format!("{}/login", r.origin()))?;
        c.wait_for(&sess, "!!document.getElementById('login')", 30_000)?;
        c.eval(&sess, "document.getElementById('login').click()")?;
        c.wait_for(&sess, SETTLED, 30_000)?;
        let st = c.eval(&sess, "fetch('/auth/status').then(r => r.json())")?;
        want(st.path("authenticated").and_then(|v| v.as_bool()) == Some(true), || {
            format!("not authenticated: {}", http::cut(&st.dump(), 200))
        })?;
        want(st.text("user") == "bob", || format!("logged in as {:?}", st.text("user")))?;
        want(st.text("role") == "admin", || format!("role {:?}", st.text("role")))?;
        Ok(format!("{} as {}", st.text("user"), st.text("role")))
    });

    // ── the first admin passkey IS the operator's account (2026-10-04) ────
    //
    // Owner: "a YubiKey is used to register me as an admin user. I can THEN
    // select to ADD ssh keys to it", and "the UI shall not DO anything such as
    // create a user — it shall CALL gunnar-server". The token's registration
    // above made the console ask gunnar `Accounts.ClaimOperator`; gunnar made
    // the account (over the namespace the first push conjured) and the passkey
    // is bound to it. Every assertion is gunnar's word or the page's DOM, never
    // the console's own claim about itself.
    go(rows, "the-first-admin-passkey-is-the-operators-account", || {
        let users = r.control(&["user", "list"])?;
        want(users.lines().any(|l| l.split('\t').next() == Some("bob")), || {
            format!("gunnar holds no principal `bob` after the first admin registered — test2's morning: {}", http::cut(&users, 200))
        })?;
        let repos = r.control(&["repo", "list"])?;
        want(repos.contains("bob/t2-scratch"), || {
            format!("the conjured namespace was not ADOPTED with what was pushed into it: {}", http::cut(&repos, 200))
        })?;
        let key = c.eval(&sess, "fetch('/api/account/console-key').then(r => r.json())")?;
        want(key.text("principal_id") == "bob", || {
            format!("the session is bound to {:?}, not the operator's account: {}", key.text("principal_id"), http::cut(&key.dump(), 200))
        })?;
        want(key.text("state") == "enrolled", || {
            format!("the session is {:?}, not enrolled (no attestation is owed on a fresh account): {}", key.text("state"), http::cut(&key.dump(), 240))
        })?;
        // IDENTITY-HOST (gunnar-ui 7a0c3a8): the person's calls carry the identity core's
        // session, never a key the console signs with, so the console enrols NO SSH key of
        // its own on the operator. His keys are the ones he adds (the next row).
        let held = r.control(&["user", "keys", "bob"])?;
        want(held.lines().filter(|l| l.starts_with("bob\t")).count() == 0, || {
            format!("gunnar holds a key on bob the operator never added (the console enrols none since IDENTITY-HOST): {}", http::cut(&held, 240))
        })?;
        Ok("bob is a principal, bob/t2-scratch adopted, the passkey bound to his account, no key enrolled for him".into())
    });

    go(rows, "the-account-page-reads-as-the-operator", || {
        c.navigate(&sess, &format!("{}/account", r.origin()))?;
        c.wait_for(&sess, "!!document.getElementById('own-key')", 30_000).map_err(|e| {
            let body = c.eval_str(&sess, "document.body ? document.body.innerText : ''").unwrap_or_default();
            format!("You → Account drew no SSH keys panel ({e}): {}", http::cut(&body, 240))
        })?;
        // One more round trip for the console-key panel to resolve, then the
        // page must hold neither of the two panels the owner saw.
        std::thread::sleep(std::time::Duration::from_millis(1_500));
        let body = c.eval_str(&sess, "document.body.innerText")?;
        for refused in ["This console needs your word", "This console cannot read as you", "no principal bound to this session", "an operator creates it"] {
            want(!body.contains(refused), || format!("the account page says {refused:?}: {}", http::cut(&body, 240)))?;
        }
        Ok("SSH keys panel drawn; no attestation panel, no unbound-session refusal".into())
    });

    go(rows, "the-operator-adds-an-ssh-key-on-his-account-page", || {
        let laptop = r.work.join("operator-laptop");
        let _ = std::fs::remove_file(&laptop);
        let _ = std::fs::remove_file(laptop.with_extension("pub"));
        let made = std::process::Command::new("ssh-keygen")
            .args(["-q", "-t", "ed25519", "-N", "", "-C", "bob-laptop", "-f"])
            .arg(&laptop)
            .status()
            .map_err(|e| format!("ssh-keygen: {e}"))?;
        want(made.success(), || "ssh-keygen failed".into())?;
        let line = std::fs::read_to_string(laptop.with_extension("pub")).map_err(|e| e.to_string())?.trim().to_owned();
        c.eval(
            &sess,
            &format!(
                "(() => {{ const i = document.getElementById('own-key');
                   i.value = {};
                   i.dispatchEvent(new Event('input', {{bubbles:true}}));
                   const b = [...document.querySelectorAll('button')].find(b => b.textContent.trim() === 'Add key');
                   if (!b) return 'no Add key button'; b.click(); return 'clicked'; }})()",
                esc(&line)
            ),
        )?;
        // gunnar's word first: the key is on bob.
        let fingerprint = std::process::Command::new("ssh-keygen")
            .arg("-lf")
            .arg(laptop.with_extension("pub"))
            .output()
            .map(|o| String::from_utf8_lossy(&o.stdout).split_whitespace().nth(1).unwrap_or_default().to_owned())
            .map_err(|e| format!("ssh-keygen -l: {e}"))?;
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
        let mut held = String::new();
        while std::time::Instant::now() < deadline {
            held = r.control(&["user", "keys", "bob"])?;
            if held.contains(&fingerprint) {
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(500));
        }
        let refusal = c.eval_str(&sess, "[...document.querySelectorAll('.failure, [role=alert]')].map(e => e.innerText).join(' | ')").unwrap_or_default();
        want(held.contains(&fingerprint), || {
            format!("gunnar does not hold the added key {fingerprint} on bob after 30 s (the page said: {refusal:?}): {}", http::cut(&held, 240))
        })?;
        let n = held.lines().filter(|l| l.starts_with("bob\t")).count();
        want(n == 1, || format!("gunnar should hold the laptop key alone on bob (the console enrols none of its own), holds {n}: {}", http::cut(&held, 240)))?;
        // Then the page: the SSH keys table lists it.
        c.wait_for(&sess, &format!("document.body.innerText.includes({})", esc(&fingerprint)), 30_000).map_err(|e| {
            let body = c.eval_str(&sess, "document.body ? document.body.innerText : ''").unwrap_or_default();
            let at = body.find("SSH keys").unwrap_or(0);
            format!("the added key {fingerprint} never appeared on the page ({e}): {}", http::cut(&body[at..], 400))
        })?;
        // And it is HIS key now: git over SSH with it is answered as bob.
        let dir = r.work.join("as-bob");
        std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
        let _ = std::fs::remove_dir_all(dir.join("scratch"));
        r.git_as(&laptop, &dir, &["clone", "-q", &r.remote_as("bob", "bob/t2-scratch"), "scratch"])?;
        Ok("Add key on You → Account: gunnar holds it on bob, and bob clones his own repository with it".into())
    });

    go(rows, "the-session-lists-the-credential-it-logged-in-with", || {
        let j = c.eval(&sess, "fetch('/auth/passkey/credentials').then(r => r.json())")?;
        let creds = j.path("credentials").map(|c| c.arr().to_vec()).unwrap_or_default();
        want(creds.len() == 1, || format!("{} credentials", creds.len()))?;
        want(creds[0].text("label") == "virtual", || format!("label {:?}", creds[0].text("label")))?;
        want(creds[0].text("user") == "bob", || format!("user {:?}", creds[0].text("user")))?;
        want(j.path("enabled_admins").and_then(|v| v.as_f64()) == Some(1.0), || {
            format!("enabled_admins {}", j.text("enabled_admins"))
        })?;
        Ok(format!("{} ({})", creds[0].text("label"), creds[0].text("role")))
    });

    // The invite the break-glass token minted (`the-break-glass-token-mints-an-invite-link`),
    // as the identity core lists it to the admin's session, then withdrawn by that session.
    let token_invite = std::cell::RefCell::new(String::new());
    go(rows, "an-invite-is-listed-open-to-the-admin", || {
        let list = c.eval(&sess, "fetch('/auth/passkey/invites').then(r => r.json())")?;
        let kims = list
            .path("invites")
            .and_then(|i| i.arr().iter().find(|e| e.text("user") == "kim").cloned())
            .ok_or_else(|| format!("kim's invite is not listed to the admin: {}", http::cut(&list.dump(), 200)))?;
        want(kims.path("open").and_then(|o| o.as_bool()) == Some(true), || "listed but not open".into())?;
        want(kims.text("role") == "viewer", || format!("role {}", kims.text("role")))?;
        let id = kims.text("id");
        want(!id.is_empty(), || "listed with no id".into())?;
        *token_invite.borrow_mut() = id.clone();
        Ok(format!("invite {} for kim, viewer, open", &id[..8.min(id.len())]))
    });

    go(rows, "a-revoked-invite-stops-being-open", || {
        let id = token_invite.borrow().clone();
        want(!id.is_empty(), || "no invite was listed".into())?;
        let j = c.eval(
            &sess,
            &format!(
                "fetch('/auth/passkey/invites/revoke', {{method:'POST', headers:{{'Content-Type':'application/json'}},
                     body: JSON.stringify({{id: {}}})}}).then(async r => ({{status: r.status, body: await r.text()}}))",
                esc(&id)
            ),
        )?;
        want(j.path("status").and_then(|s| s.as_f64()) == Some(200.0), || {
            format!("revoke answered {}: {}", j.text("status"), http::cut(&j.text("body"), 120))
        })?;
        // The core lists OPEN invites only: a withdrawn one is gone from the list.
        let list = c.eval(&sess, "fetch('/auth/passkey/invites').then(r => r.json())")?;
        let still = list.path("invites").is_some_and(|i| i.arr().iter().any(|e| e.text("id") == id));
        want(!still, || format!("still open after revoke: {}", http::cut(&list.dump(), 200)))?;
        Ok("withdrawn at the identity core, and no longer open".into())
    });

    go(rows, "the-logged-in-session-reads-the-api-with-no-token", || {
        let j = c.eval(&sess, "fetch('/api/repos').then(r => r.json())")?;
        want(
            j.path("items").map(|i| i.arr().iter().any(|e| e.text("path") == "alice/uitest")).unwrap_or(false),
            || format!("the console does not list the pushed repo: {}", http::cut(&j.dump(), 200)),
        )?;
        Ok("the cookie is the credential now".into())
    });

    go(rows, "the-console-renders-the-pushed-repository", || {
        c.navigate(&sess, &r.origin())?;
        // The landing page is the Overview; the repositories are one view
        // away. Clicking the console's own navigation is the point of driving
        // a browser at all — a direct URL would skip what a user actually does.
        // Wait for the console to have drawn its navigation before clicking it.
        // Without this the click races the SPA's first paint and the row fails
        // as "no Repositories link" perhaps one run in three — a flake that
        // would have been read as a product bug.
        c.wait_for(
            &sess,
            "[...document.querySelectorAll('a,button')].some(e => e.textContent.trim() === 'Repositories')",
            30_000,
        )?;
        let nav = c.eval_str(
            &sess,
            "(() => { const hit = [...document.querySelectorAll('a,button')]
                 .find(e => e.textContent.trim() === 'Repositories');
               if (!hit) return 'no nav item'; hit.click(); return 'clicked'; })()",
        )?;
        want(nav == "clicked", || format!("the console has no Repositories link"))?;
        // A timeout that only says "the text never appeared" cannot be acted
        // on, so this polls itself and fails with what the page DID hold.
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
        let mut seen = String::new();
        while std::time::Instant::now() < deadline {
            seen = c.eval_str(&sess, "document.body ? document.body.innerText : ''")?;
            if seen.contains("uitest") {
                return Ok(http::cut(&seen, 110));
            }
            std::thread::sleep(std::time::Duration::from_millis(250));
        }
        let url = c.eval_str(&sess, "location.href")?;
        let title = c.eval_str(&sess, "document.title")?;
        Err(format!(
            "after 30 s at {url} (title {title:?}) the page held: {}",
            http::cut(&seen, 220)
        ))
    });

    // ── the chain page: every verdict in words, and a hole IS a verdict ───
    //
    // The API rows above proved what gunnar answers; these prove what a person
    // SEES. The one thing a provenance page must never do is render a hole as
    // anything quieter than a hole, so the unsigned arm asserts the words, not
    // just the row count.
    go(rows, "the-chain-page-shows-every-link-proven", || {
        c.navigate(
            &sess,
            &format!(
                "{}/repo/alice/chain-signed/chain?rev=refs%2Fheads%2Fmain&line=src.txt%3A1",
                r.origin()
            ),
        )?;
        c.wait_for(
            &sess,
            "document.querySelectorAll('tr[data-chain-link]').length >= 4",
            30_000,
        )?;
        let verdict = c.eval_str(
            &sess,
            "document.querySelector('[data-chain-summary]')?.closest('.panel')?.innerText ?? ''",
        )?;
        want(verdict.contains("UNBROKEN") && verdict.contains("Every link is proven"), || {
            format!("the summary does not say UNBROKEN: {}", http::cut(&verdict, 200))
        })?;
        let holes = c.eval_str(
            &sess,
            "String(document.querySelectorAll('tr[data-chain-verdict=broken], \
             tr[data-chain-verdict=unproven]').length)",
        )?;
        want(holes.trim() == "0", || format!("{} link(s) not proven", holes.trim()))?;
        let sig = c.eval_str(
            &sess,
            "document.querySelector('tr[data-chain-link=signature]')?.innerText ?? ''",
        )?;
        want(sig.contains("proven") && sig.contains("is signed by"), || {
            format!("the signature row does not name the signer: {}", http::cut(&sig, 200))
        })?;
        Ok(format!("UNBROKEN; {}", http::cut(&sig.replace('\n', " · "), 110)))
    });

    go(rows, "the-chain-page-renders-the-unsigned-hole-in-words", || {
        c.navigate(
            &sess,
            &format!(
                "{}/repo/alice/uitest/chain?rev=refs%2Fheads%2Fmain&line=README%3A1",
                r.origin()
            ),
        )?;
        c.wait_for(
            &sess,
            "document.querySelectorAll('tr[data-chain-link]').length >= 4",
            30_000,
        )?;
        let sig = c.eval_str(
            &sess,
            "document.querySelector('tr[data-chain-link=signature]')?.innerText ?? ''",
        )?;
        want(sig.contains("unproven"), || {
            format!("the unsigned commit's signature row does not read unproven: {}", http::cut(&sig, 200))
        })?;
        want(sig.contains("not signed"), || {
            format!("the hole is a colour with no words: {}", http::cut(&sig, 200))
        })?;
        let verdict = c.eval_str(
            &sess,
            "document.querySelector('[data-chain-summary]')?.closest('.panel')?.innerText ?? ''",
        )?;
        want(verdict.contains("INCOMPLETE") && verdict.contains("hole"), || {
            format!("the summary does not call the hole a hole: {}", http::cut(&verdict, 200))
        })?;
        Ok(format!("the hole in words: {}", http::cut(&sig.replace('\n', " · "), 110)))
    });

    // ── the blame page: each line coloured by its commit, signer named ────
    //
    // The API row proved what gunnar answers; this proves what a person SEES.
    // It asserts the ACTUAL oid the first line is coloured by — not that a row
    // rendered — and that the signature reads proven, so it cannot pass on a
    // page that drew a grey unattributed line.
    go(rows, "the-blame-page-colours-each-line-by-its-commit", || {
        let head = c.eval_str(
            &sess,
            "fetch('/api/repos/alice/chain-signed/head').then(r => r.json()).then(h => h.oid)",
        )?;
        want(head.len() == 64, || format!("could not read the head: {head:?}"))?;
        c.navigate(
            &sess,
            &format!(
                "{}/repo/alice/chain-signed/blame?rev=refs%2Fheads%2Fmain&path=src.txt",
                r.origin()
            ),
        )?;
        c.wait_for(&sess, "document.querySelector('tr[data-blame-commit]')", 30_000)?;
        let commit = c.eval_str(
            &sess,
            "document.querySelector('tr[data-blame-commit]')?.getAttribute('data-blame-commit') ?? ''",
        )?;
        want(commit == head, || {
            format!("line 1 is coloured for {commit}, not the head {head}")
        })?;
        let sig = c.eval_str(
            &sess,
            "document.querySelector('tr[data-blame-commit]')?.getAttribute('data-blame-signature') ?? ''",
        )?;
        want(sig == "proven", || format!("the signed line does not read proven: {sig:?}"))?;
        Ok(format!("line 1 → {} · {sig}", &head[..12]))
    });

    // ── the search page: the file and line of a known string ──────────────
    go(rows, "the-search-page-lists-the-file-and-line-of-a-hit", || {
        c.navigate(
            &sess,
            &format!("{}/repo/alice/chain-signed/search?q=provable", r.origin()),
        )?;
        c.wait_for(&sess, "document.querySelector('tr[data-search-path]')", 30_000)?;
        let path = c.eval_str(
            &sess,
            "document.querySelector('tr[data-search-path]')?.getAttribute('data-search-path') ?? ''",
        )?;
        want(path == "src.txt", || format!("the hit is not in src.txt: {path:?}"))?;
        let line = c.eval_str(
            &sess,
            "document.querySelector('tr[data-search-path]')?.getAttribute('data-search-line') ?? ''",
        )?;
        want(line.trim() == "1", || format!("the hit is not on line 1: {line:?}"))?;
        Ok(format!("src.txt:{}", line.trim()))
    });

    // ── the diff page: the inline comment lands on its line ───────────────
    //
    // The comment was filed against src/lib.rs line 2 on a specific blob. The
    // diff page renders it UNDER that line, and this asserts the row's line AND
    // its blob — so it cannot pass on a comment that rendered somewhere generic,
    // and the blob is what proves it followed the code and not the pull number.
    go(rows, "the-diff-page-shows-the-inline-comment-on-its-line", || {
        want(!inline_pull.is_empty(), || "the inline pull was never set up".into())?;
        c.navigate(
            &sess,
            &format!(
                "{}/repo/alice/uitest/diff?from=refs%2Fheads%2Fmain&to=refs%2Fheads%2Finline-topic&pull={}",
                r.origin(),
                inline_pull,
            ),
        )?;
        c.wait_for(&sess, "document.querySelector('tr.inline-comment')", 30_000)?;
        let line = c.eval_str(
            &sess,
            "document.querySelector('tr.inline-comment')?.getAttribute('data-inline-comment-line') ?? ''",
        )?;
        want(line.trim() == "2", || format!("the comment landed on line {line:?}, not 2"))?;
        let blob = c.eval_str(
            &sess,
            "document.querySelector('tr.inline-comment')?.getAttribute('data-inline-comment-blob') ?? ''",
        )?;
        want(blob == inline_blob, || {
            format!("the comment is not anchored to the blob it was made on: {blob}")
        })?;
        let body = c.eval_str(
            &sess,
            "document.querySelector('tr.inline-comment')?.innerText ?? ''",
        )?;
        want(body.contains("needs a test"), || format!("the comment body is missing: {}", http::cut(&body, 120)))?;
        Ok(format!("on line 2 @ {}", &blob[..blob.len().min(12)]))
    });

    go(rows, "logging-out-ends-the-session", || {
        // Assert the BEFORE state first. Without it this situation passes when
        // there was never a session to end — which is exactly how it behaved
        // under --no-authenticator, and exactly the hollow guard that measuring
        // rather than asserting is about: "logged out" is only a fact if
        // "logged in" was one.
        let before = c.eval(&sess, "fetch('/auth/status').then(r => r.json())")?;
        want(before.path("authenticated").and_then(|v| v.as_bool()) == Some(true), || {
            format!("there was no session to end: {}", http::cut(&before.dump(), 150))
        })?;
        let out = c.eval(
            &sess,
            "fetch('/auth/logout', {method:'POST'}).then(r => r.status)",
        )?;
        want(out.as_f64().map(|s| (200.0..400.0).contains(&s)) == Some(true), || {
            format!("logout answered {}", out.dump())
        })?;
        let st = c.eval(&sess, "fetch('/auth/status').then(r => r.json())")?;
        want(st.path("authenticated").and_then(|v| v.as_bool()) == Some(false), || {
            format!("still authenticated: {}", http::cut(&st.dump(), 150))
        })?;
        // The session is gone; what remains is the stranger's view, served as
        // the grantless guest. The property is NON-DISCLOSURE, not a status
        // code: `/api/repos` may answer a stranger, and it must not name the
        // private repository this rig holds. (Until 2026-09-10 the rig ran
        // with no guest and asserted 401 here, which would have gone green on
        // the confused deputy too — the whole list, admin's eyes, 200.)
        let repos = c.eval(&sess, "fetch('/api/repos').then(r => r.text())")?;
        let repos = repos.as_str().unwrap_or_default().to_owned();
        want(!repos.contains("\"alice/uitest\""), || {
            format!("a stranger's /api/repos names the PRIVATE repository: {}", http::cut(&repos, 200))
        })?;
        Ok("logged out, and the private repository is invisible again".into())
    });

    go(rows, "a-stranger-clicks-log-in-and-reaches-the-login-page", || {
        a_stranger_clicks_log_in(&mut c, &sess, &r.origin())
    });

    go(rows, "the-same-passkey-logs-in-again", || {
        // GHUNT-12: Gunnar's logout ends the person's sessions everywhere, and a floor voids
        // every session minted AT its second, so the core refuses a login in that very second
        // by name ("sign in again in a moment"). A person is never that fast; the row waits
        // the second out, as the page's words ask.
        std::thread::sleep(std::time::Duration::from_millis(1_100));
        c.navigate(&sess, &format!("{}/login", r.origin()))?;
        c.wait_for(&sess, "!!document.getElementById('login')", 30_000)?;
        c.eval(&sess, "document.getElementById('login').click()")?;
        c.wait_for(&sess, SETTLED, 30_000)?;
        let st = c.eval(&sess, "fetch('/auth/status').then(r => r.json())")?;
        let msg = c.eval_str(&sess, "document.getElementById('msg')?.textContent ?? ''").unwrap_or_default();
        want(st.path("authenticated").and_then(|v| v.as_bool()) == Some(true), || {
            format!("not authenticated (the page said {msg:?}): {}", http::cut(&st.dump(), 150))
        })?;
        Ok(format!("back in as {}", st.text("user")))
    });

    go(rows, "every-view-in-the-console-renders", || {
        // The closest thing to "drive all the functions": walk the console's
        // own navigation, and require each view both to draw something of its
        // own AND to leave the page free of errors. A WASM console that panics
        // renders an empty div and still answers questions about itself
        // cheerfully, so neither half is sufficient alone.
        c.navigate(&sess, &r.origin())?;
        c.wait_for(&sess, "document.body && document.body.innerText.length > 40", 30_000)?;
        let _ = c.drain_page_errors();
        // The top bar since the final navigation (2026-10-02, `nav::menu`):
        // Repositories; You ▾ Account, Storage (the first "Storage" in the
        // document), Activity; Administration ▾ Overview, Identities. A link
        // inside a closed menu is still a link to click.
        let views = ["Repositories", "Overview", "Storage", "Activity", "Identities", "Account"];
        let mut drawn = Vec::new();
        for v in views {
            let clicked = c.eval_str(
                &sess,
                &format!(
                    "(() => {{ const hit = [...document.querySelectorAll('a,button')]
                         .find(e => e.textContent.trim() === {});
                       if (!hit) return 'missing'; hit.click(); return 'clicked'; }})()",
                    esc(v)
                ),
            )?;
            want(clicked == "clicked", || format!("the console has no {v} link"))?;
            // Something of this view's own, not merely the chrome around it.
            c.wait_for(
                &sess,
                &format!(
                    "(() => {{ const m = document.querySelector('main') || document.body;
                       return m && m.innerText.trim().length > 60; }})()"
                ),
                20_000,
            )
            .map_err(|e| format!("{v} drew nothing: {e}"))?;
            let len = c
                .eval(&sess, "(document.querySelector('main') || document.body).innerText.trim().length")?
                .as_f64()
                .unwrap_or(0.0);
            drawn.push(format!("{v} {len:.0}"));
        }
        let errs = c.drain_page_errors();
        want(errs.is_empty(), || {
            format!("{} page error(s) while walking the views: {}", errs.len(), http::cut(&errs.join(" | "), 250))
        })?;
        Ok(drawn.join(", "))
    });

    go(rows, "the-issues-page-renders-the-folded-log", || {
        // The console shows an issue from gunnar's fold, never a store of its
        // own. This asserts exactly that: the fold is fetched in the browser,
        // and every issue it reports must appear on the rendered page — title
        // and state. The issue the API section opened and CLOSED is issue #1,
        // so "the boot is dark" must render AND read as closed; a page drawing
        // a stale or invented store would not match the fold it just fetched.
        c.navigate(&sess, &format!("{}/repo/alice/uitest/issues", r.origin()))?;
        c.wait_for(
            &sess,
            "(document.querySelector('main')||document.body).innerText.includes('Issues')",
            30_000,
        )?;
        // Wait for the list rows themselves to draw, not just the heading.
        c.wait_for(
            &sess,
            "(document.querySelector('main')||document.body).innerText.includes('the boot is dark')",
            20_000,
        )
        .map_err(|e| format!("the issue never rendered from the fold: {e}"))?;
        let folded = c.eval_str(
            &sess,
            "fetch('/api/repos/alice/uitest/issues').then(r=>r.json())\
             .then(a=>a.map(i=>i.number+'|'+i.title+'|'+(i.open?'open':'closed')).join('\\n'))",
        )?;
        want(!folded.trim().is_empty(), || "the fold is empty; nothing to render".into())?;
        let text = c.eval_str(&sess, "(document.querySelector('main')||document.body).innerText")?;
        let mut checked = 0;
        for line in folded.lines() {
            let title = line.split('|').nth(1).unwrap_or("");
            want(!title.is_empty() && text.contains(title), || {
                format!("the page does not render issue {line:?}: {}", http::cut(&text, 250))
            })?;
            checked += 1;
        }
        // And the closed issue reads as closed on the page, not open — the
        // state IS the fold, and the badge must agree with it.
        want(text.contains("closed"), || {
            format!("no issue renders as closed, but the fold has one: {}", http::cut(&text, 250))
        })?;
        let errs = c.drain_page_errors();
        want(errs.is_empty(), || format!("page error(s) on the issues page: {}", http::cut(&errs.join(" | "), 200)))?;
        Ok(format!("{checked} issue(s) rendered from the fold"))
    });

    go(rows, "the-overview-hides-the-indexing-panel-when-nothing-is-indexing", || {
        // The panel is ABSENT when idle, not showing a zero — a row that reads
        // "0 indexing" every day is one an operator learns to skip past, and
        // then misses the day it says something.
        //
        // Absence alone would pass on a page that failed to render at all, so
        // this asserts the Overview DREW first and only then that the panel is
        // not on it.
        // The Overview is Administration → Overview since 2026-10-02.
        c.navigate(&sess, &format!("{}/admin/overview", r.origin()))?;
        // Waited for by the panel itself: the Machine card above it loads
        // first (the estate's topology), and a page caught between the two
        // is still drawing, not failing to.
        let drew = c.wait_for(&sess, "document.body && document.body.innerText.includes('The server')", 30_000);
        let text = c.eval_str(&sess, "(document.querySelector('main') || document.body).innerText")?;
        want(drew.is_ok() && text.contains("The server"), || {
            format!("the Overview did not draw: {}", http::cut(&text, 160))
        })?;
        want(!text.contains("Indexing"), || {
            format!("an idle box is showing an Indexing panel: {}", http::cut(&text, 200))
        })?;
        Ok("the Overview drew, and no Indexing panel with it".into())
    });

    go(rows, "the-indexing-panel-draws-when-there-is-work", || {
        // The absence situation above would pass just as happily against a
        // panel that never renders at all, so this is its other half.
        //
        // It proves the COMPONENT, not the server: the page's own `fetch` is
        // wrapped so that `/api/stats` answers with one index row, and the
        // console is then required to draw the repository, the phase, the
        // percentage and an actual progress bar. That the SERVER really fills
        // that field is a separate situation
        // (`stats-carry-the-index-progress-rows`) — deliberately separate,
        // because a repository small enough for a test indexes faster than a
        // browser can look, and a situation that has to win a race is a
        // situation that will lie eventually.
        let stub = r#"{"namespaces":1,"repos":1,"principals":2,"keys":3,"grants":2,
            "event_subscribers":1,"events_published":2,"events_dropped":0,
            "event_buffer_capacity":1024,
            "index":[{"repo":"alice/uitest","pack_id":1,"objects_done":4700000,
              "objects_total":13948244,"bytes_done":2900000000,"bytes_total":8731794676,
              "phase":"resolve","started_at_unix_ms":0,"resumed":false,"percent":34,
              "reinflations":12,"pauses":2}]}"#;
        let script = format!(
            // `u` is a Request OBJECT here, not a string: the console fetches
            // through gloo-net, so `String(u)` is "[object Request]" and a
            // naive URL test never matches. Ask the object for its url first.
            "(() => {{ const real = window.fetch;
               window.fetch = (u, o) => {{
                 const url = (u && typeof u === 'object' && u.url) ? u.url : String(u);
                 return url.includes('/api/stats')
                   ? Promise.resolve(new Response({}, {{status:200,
                       headers:{{'Content-Type':'application/json'}}}}))
                   : real(u, o);
               }}; }})()",
            esc(&stub.split_whitespace().collect::<Vec<_>>().join(" ").replace(", ", ","))
        );
        let added = c.call(
            Some(&sess),
            "Page.addScriptToEvaluateOnNewDocument",
            &format!(r#"{{"source":{}}}"#, esc(&script)),
        )?;
        let ident = added.text("identifier");
        c.navigate(&sess, &format!("{}/admin/overview", r.origin()))?;
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
        let mut seen = String::new();
        let mut found = None;
        while std::time::Instant::now() < deadline {
            seen = c.eval_str(&sess, "(document.querySelector('main') || document.body).innerText")?;
            if seen.contains("Indexing") {
                found = Some(());
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(200));
        }
        let outcome = (|| -> Result<String, String> {
            want(found.is_some(), || {
                format!("the panel never drew; the page held: {}", http::cut(&seen, 200))
            })?;
            let pct = c.eval_str(&sess, "document.querySelector('.indexing .pct')?.textContent ?? ''")?;
            want(pct.trim() == "34%", || format!("the percentage reads {pct:?}, not 34%"))?;
            let width = c.eval_str(
                &sess,
                "document.querySelector('.indexing .bar span')?.style.width ?? '(no bar)'",
            )?;
            want(width.trim() == "34%", || format!("the bar is {width:?}, not 34%"))?;
            want(seen.contains("alice/uitest"), || "the row does not name the repository".into())?;
            want(seen.contains("resolve"), || "the row does not show the phase".into())?;
            // The strain numbers are the memory ceiling being enforced, and the
            // row is where an operator would ever see them.
            want(seen.contains("re-inflated"), || {
                format!("re-inflations are not shown: {}", http::cut(&seen, 200))
            })?;
            Ok(format!("{} at {} with a {} bar", "alice/uitest", pct.trim(), width.trim()))
        })();
        // Put the page back the way it was, whatever happened above.
        if !ident.is_empty() {
            let _ = c.call(
                Some(&sess),
                "Page.removeScriptToEvaluateOnNewDocument",
                &format!(r#"{{"identifier":{}}}"#, esc(&ident)),
            );
        }
        c.navigate(&sess, &r.origin())?;
        outcome
    });

    go(rows, "the-storage-page-shows-what-a-deletion-still-costs", || {
        // Real data, not a stub: a repository really was soft-deleted earlier
        // in this walk, so the panel has something true to draw.
        c.navigate(&sess, &r.origin())?;
        // The machine's storage is Administration → Storage since 2026-10-02:
        // the admin clicks THAT link in the top bar's Administration menu.
        c.wait_for(&sess, "!!document.querySelector('header.topbar a[href=\"/admin/storage\"]')", 30_000)?;
        c.eval(&sess, "document.querySelector('header.topbar a[href=\"/admin/storage\"]').click()")?;
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
        let mut seen = String::new();
        while std::time::Instant::now() < deadline {
            seen = c.eval_str(&sess, "(document.querySelector('main') || document.body).innerText")?;
            if seen.contains("Deleted, not yet purged") {
                want(seen.contains("made-by-console"), || {
                    format!("the panel drew but names no repository: {}", http::cut(&seen, 250))
                })?;
                // Case-insensitively: the console uppercases metric labels in
                // CSS, so `innerText` reads READ CACHE, not "Read cache". A
                // case-sensitive match here fails against a page that is
                // perfectly correct.
                want(seen.to_lowercase().contains("read cache"), || {
                    format!("the engine panel does not show the read cache: {}", http::cut(&seen, 250))
                })?;
                return Ok("the deleted repository, its bytes, and the read cache".into());
            }
            std::thread::sleep(std::time::Duration::from_millis(250));
        }
        Err(format!("no tombstone panel after 30 s; the page held: {}", http::cut(&seen, 250)))
    });

    go(rows, "a-standby-that-has-never-synced-is-shown-as-never", || {
        // The one alarm this panel exists for. gunnar sends 0 for "never", and
        // rendering that as 1970 — or as "synced 56 years ago" — would turn the
        // most important value in the message into a joke. A standby that has
        // never taken a byte is not behind; it is a durability promise that
        // does not exist.
        //
        // Stubbed at the page's own fetch, like the indexing row: standing up a
        // second gunnar and breaking replication between them would test
        // gunnar, not this panel.
        let stub = r#"{"namespaces":1,"repos":1,"principals":2,"keys":3,"grants":2,
            "event_subscribers":1,"events_published":2,"events_dropped":0,
            "event_buffer_capacity":1024,"index":[],
            "tombstoned_repos":0,"tombstoned_bytes":0,
            "replication_configured":true,"replication_synced":0,"replication_failed":7,
            "replication_enqueued":3,"replication_bus_gaps":0,
            "standbys":[{"name":"backup-b","remote":"ssh://gunnar@backup:2222","pending":12,
              "consecutive_failures":7,"last_error":"connection refused",
              "last_success_unix_ms":null,"needs_full_resync":false,"stale":true}]}"#;
        let script = format!(
            "(() => {{ const real = window.fetch;
               window.fetch = (u, o) => {{
                 const url = (u && typeof u === 'object' && u.url) ? u.url : String(u);
                 return url.includes('/api/stats')
                   ? Promise.resolve(new Response({}, {{status:200,
                       headers:{{'Content-Type':'application/json'}}}}))
                   : real(u, o);
               }}; }})()",
            esc(&stub.split_whitespace().collect::<Vec<_>>().join(" "))
        );
        let added = c.call(
            Some(&sess),
            "Page.addScriptToEvaluateOnNewDocument",
            &format!(r#"{{"source":{}}}"#, esc(&script)),
        )?;
        let ident = added.text("identifier");
        // `/admin` since 2026-09-18: the Replication panel was moved off the
        // Overview with every other instrument that acts on the box.
        c.navigate(&sess, &format!("{}/admin", r.origin()))?;
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
        let mut seen = String::new();
        while std::time::Instant::now() < deadline {
            seen = c.eval_str(&sess, "(document.querySelector('main') || document.body).innerText")?;
            if seen.contains("Replication") {
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(200));
        }
        let outcome = (|| -> Result<String, String> {
            want(seen.contains("Replication"), || {
                format!("no Replication panel; the page held: {}", http::cut(&seen, 200))
            })?;
            want(seen.contains("never synced"), || {
                format!("a standby with no successful sync is not called out: {}", http::cut(&seen, 250))
            })?;
            want(seen.contains("backup-b"), || "the standby is not named".into())?;
            want(seen.contains("connection refused"), || {
                "the last error is not shown, so nobody can act on it".into()
            })?;
            // And it must NOT have invented a date out of gunnar's zero.
            want(!seen.contains("1970"), || {
                format!("`never` was rendered as 1970: {}", http::cut(&seen, 200))
            })?;
            Ok("never synced, named, with its error and no invented date".into())
        })();
        if !ident.is_empty() {
            let _ = c.call(
                Some(&sess),
                "Page.removeScriptToEvaluateOnNewDocument",
                &format!(r#"{{"identifier":{}}}"#, esc(&ident)),
            );
        }
        c.navigate(&sess, &r.origin())?;
        outcome
    });

    go(rows, "the-admin-page-hides-replication-when-none-is-configured", || {
        // Absence, asserted only after the page is shown to have drawn — an
        // operator who has not set up replication does not need a panel telling
        // them so every day.
        //
        // The page under it changed on 2026-09-18 and so did the proof that it
        // drew: "The server" is the Overview's panel and is not on `/admin`, so
        // the marker is the banner every Administration page carries. Asserting
        // absence against a page that never rendered is the one way this row
        // can pass while meaning nothing.
        c.navigate(&sess, &format!("{}/admin", r.origin()))?;
        c.wait_for(&sess, "document.body && document.body.innerText.includes('Administration')", 30_000)?;
        let text = c.eval_str(&sess, "(document.querySelector('main') || document.body).innerText")?;
        want(text.contains("Settings for the whole site."), || {
            format!("the Administration page did not draw: {}", http::cut(&text, 160))
        })?;
        want(!text.contains("Replication"), || {
            format!("replication is not configured, yet a panel is shown: {}", http::cut(&text, 200))
        })?;
        Ok("the Administration page drew, and no Replication panel with it".into())
    });

    go(rows, "the-live-feed-carries-a-change-as-it-happens", || {
        // The dashboard is driven by this feed, and until now the only check on
        // it was grepping the front's log for the ABSENCE of "cannot
        // subscribe" — which stays quiet whether the feed works or is merely
        // silent. This subscribes, makes a change, and requires the change to
        // arrive: named events on the `feed` channel, so `onmessage` alone
        // would never fire.
        //
        // Robust to a reconnecting pump. An EARLIER situation (`attest head`,
        // in the chain setup) restarts gunnar, and the BFF's single upstream
        // subscription takes RECONNECT_DELAY to come back. If this situation
        // happens to land inside that window, the change we make is published
        // while the BFF is between subscriptions and is NOT replayed to it —
        // that is deliberate (bus.rs: a cursor from a dead epoch is void, and
        // the console re-reads its lists rather than being sold events nobody
        // can place; event_bus.rs pins it, feed.rs acts on it). What the feed
        // carries in that case is a `restart`/`gap` line, on which the console
        // re-reads. So: if a continuity break arrives, the pump is settled now
        // — make the change AGAIN and require THAT to arrive live. A genuine
        // drop still fails, because a live change that never arrives times out.
        //
        // **The probe name carries a nonce, and that is load-bearing now.** A
        // feed opens with the BFF's backlog of what happened before this
        // connection, so matching a bare `feed-probe` would accept a line left
        // by an earlier run of this very row against the same console — a row
        // named "as it happens" passing on something that happened yesterday.
        // The nonce is minted per run, so only a repository this promise
        // created can satisfy it, whether it arrives live or not at all.
        let got = c.eval(
            &sess,
            "new Promise(resolve => {
                const tag = 'feed-probe-' + Math.random().toString(36).slice(2, 10);
                const es = new EventSource('/api/events');
                const seen = [];
                let probe = 0;
                const change = () => { probe += 1; fetch('/api/repos', {method:'POST',
                    headers:{'Content-Type':'application/json'},
                    body: JSON.stringify({namespace:'alice', name: tag + '-' + probe})}); };
                const done = (ok, why) => { clearTimeout(t); es.close(); resolve({ok, why, seen}); };
                const t = setTimeout(() => done(false, 'nothing arrived in 20 s'), 20000);
                es.addEventListener('feed', e => {
                    const data = String(e.data).slice(0, 120);
                    seen.push(data);
                    if (data.includes(tag)) { done(true, 'the change arrived'); return; }
                    // A break in continuity: the pump reconnected under us. It is
                    // subscribed again now, so a fresh change will arrive live.
                    if (data.includes('\"kind\":\"restart\"') || data.includes('\"kind\":\"gap\"')) change();
                });
                es.onerror = () => done(false, 'the event source errored');
                es.onopen = () => change();
             })",
        )?;
        want(got.path("ok").and_then(|v| v.as_bool()) == Some(true), || {
            format!(
                "{} (saw {} events: {})",
                got.text("why"),
                got.path("seen").map(|s| s.arr().len()).unwrap_or(0),
                http::cut(&got.path("seen").map(|s| s.dump()).unwrap_or_default(), 200)
            )
        })?;
        Ok(format!(
            "{} after {} event(s)",
            got.text("why"),
            got.path("seen").map(|s| s.arr().len()).unwrap_or(0)
        ))
    });

    go(rows, "a-restarted-gunnar-is-reported-as-a-restart-not-as-silence", || {
        // The defect this closes. gunnar's event sequence begins again at 1 on
        // every boot, so a console holding sequence 900 that reconnects to a
        // restarted server sees sequence 1. The old BFF computed a gap by
        // subtracting the two — and after a restart the NEW number is lower, so
        // it computed nothing at all and the feed carried on as if the hole
        // were not there. Silence is the failure mode being tested for.
        //
        // Shape: park a collector in the page, restart gunnar underneath it,
        // then read back what the console was told. The collector cannot be a
        // promise the driver awaits, because the restart has to happen while it
        // is listening.
        c.eval_str(
            &sess,
            "(() => {
                window.__restartFeed = [];
                if (window.__restartEs) { window.__restartEs.close(); }
                const es = new EventSource('/api/events');
                window.__restartEs = es;
                es.addEventListener('feed', e => {
                    try { window.__restartFeed.push(JSON.parse(e.data)); }
                    catch (_) { window.__restartFeed.push({kind:'unparsed', summary:String(e.data)}); }
                });
                return 'listening';
             })()",
        )?;

        // Give the console a cursor worth losing: something must have happened
        // in the run that is about to end, or "the epoch changed" would be
        // reported about a console that had never been told any sequence
        // number and the situation would prove nothing.
        //
        // **The guard names the repository rather than counting lines.** A feed
        // now opens with the BFF's backlog, so a non-zero length proves only
        // that something happened at some point — including on a feed that has
        // been dead since. This repository cannot be in any backlog: it did not
        // exist when the collector connected. Its position is also the cut
        // between the replayed history and this run, which is what the
        // assertions below need in order to be about THIS restart.
        r.control(&["repo", "create", "alice/before-the-restart"])?;
        std::thread::sleep(std::time::Duration::from_millis(1500));
        let before = c.eval(&sess, "window.__restartFeed")?;
        let cut = before
            .arr()
            .iter()
            .rposition(|i| i.text("repo") == "alice/before-the-restart")
            .ok_or_else(|| {
                format!(
                    "the repository created after the collector connected never reached the \
                     feed, so this situation would be measuring a dead feed rather than a \
                     restart. It saw: {}",
                    http::cut(&before.dump(), 400)
                )
            })?;

        r.restart_server()?;
        // The BFF waits 3 s before re-subscribing (RECONNECT_DELAY), so this
        // has to outlast that with room for the reconnect itself.
        std::thread::sleep(std::time::Duration::from_millis(6000));
        r.control(&["repo", "create", "alice/after-the-restart"])?;
        std::thread::sleep(std::time::Duration::from_millis(2000));

        let seen = c.eval(&sess, "window.__restartFeed")?;
        // From the cut onwards: everything before it is either the backlog the
        // feed opened with or the line that established the cut, and a
        // `restart` found in there would be an EARLIER restart — the chain
        // setup performs one — answering for this one.
        let items: Vec<&Json> = seen.arr().iter().skip(cut + 1).collect();
        let restarts: Vec<&Json> = items
            .iter()
            .copied()
            .filter(|i| i.text("kind") == "restart")
            .collect();
        want(!restarts.is_empty(), || {
            format!(
                "gunnar restarted and the console was never told; the feed carried on as if \
                 nothing had happened. It saw: {}",
                http::cut(&seen.dump(), 400)
            )
        })?;
        let line = restarts[0];
        want(line.text("summary").contains("restarted"), || {
            format!("the line does not say what happened: {}", line.dump())
        })?;
        // The count must be ABSENT. Nothing buffered the events on the other
        // side of a restart, so a number here would be invented — which is
        // exactly what the synthesised gap used to do.
        want(line.path("dropped").is_none_or(|d| *d == Json::Null), || {
            format!(
                "the restart line reports a count of missed events, which nothing could have \
                 measured: {}",
                line.dump()
            )
        })?;
        // …and the feed recovered: the change made AFTER the restart arrives.
        want(
            items.iter().any(|i| i.text("summary").contains("created")
                || i.text("kind") == "repo_created"),
            || {
                format!(
                    "the feed did not resume after the restart: {}",
                    http::cut(&seen.dump(), 400)
                )
            },
        )?;
        let _ = c.eval_str(&sess, "(window.__restartEs && window.__restartEs.close(), 'closed')");
        Ok(format!(
            "the console was told the feed restarted, with no invented count, and recovered \
             ({} line(s))",
            items.len()
        ))
    });

    // ── pull requests, driven through the browser ─────────────────────────
    //
    // The API rows proved gunnar's merge gate. These prove the PAGE a reviewer
    // uses: the Review panel, the Approve button, the Merge button behind a
    // verified approval, and the honest note that an approval made here is the
    // console's key and not the operator's. The console shipped a Merge button
    // with no way to approve at all until REVIEW/MERGEGATE — the one pairing a
    // happy-path row cannot catch.
    let pr_web = std::cell::RefCell::new(String::new());
    go(rows, "the-pull-request-page-refuses-a-merge-with-no-approval", || {
        // Plumbing through gunnar directly, so the browser's only job is the
        // review: a repository, a base commit, and a branch to propose.
        r.control(&["repo", "create", "alice/pr-web"])?;
        let dir = r.work.join("pr-web");
        std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
        r.git(&dir, &["init", "-q", "--object-format=sha256", "-b", "main", "."])?;
        r.git(&dir, &["config", "user.name", "uitest"])?;
        r.git(&dir, &["config", "user.email", "info@example.invalid"])?;
        std::fs::write(dir.join("README"), "base\n").map_err(|e| e.to_string())?;
        r.git(&dir, &["add", "README"])?;
        r.git(&dir, &["commit", "-qm", "base"])?;
        r.git(&dir, &["push", "-q", &r.remote("alice/pr-web"), "main"])?;
        r.git(&dir, &["checkout", "-q", "-b", "feature"])?;
        std::fs::write(dir.join("FEATURE"), "the change under review\n").map_err(|e| e.to_string())?;
        r.git(&dir, &["add", "FEATURE"])?;
        r.git(&dir, &["commit", "-qm", "a change that needs review"])?;
        r.git(&dir, &["push", "-q", &r.remote("alice/pr-web"), "feature"])?;
        r.git(&dir, &["checkout", "-q", "main"])?;

        // Opened through the logged-in console — the admin token has retired by
        // now, so this could not go over a bearer even if it wanted to.
        let opened = c.eval(
            &sess,
            "fetch('/api/repos/alice/pr-web/pulls', {method:'POST',
                 headers:{'Content-Type':'application/json'},
                 body: JSON.stringify({title:'a change that needs review', description:'',
                     source_ref:'refs/heads/feature', target_ref:'refs/heads/main'})})
               .then(r => r.json())",
        )?;
        let number = opened
            .path("number")
            .and_then(|n| n.as_f64())
            .map(|n| format!("{}", n as i64))
            .ok_or_else(|| format!("no pull number: {}", http::cut(&opened.dump(), 200)))?;
        *pr_web.borrow_mut() = number.clone();

        c.navigate(&sess, &format!("{}/repo/alice/pr-web/pulls/{number}", r.origin()))?;
        // Wait for the review to have LOADED, not merely for the panel title:
        // the badge text ("approved at this commit" / "not approved at this
        // commit") only renders once the review resource resolves, and reading
        // while it still says "Loading …" was a hollow wait that raced the fetch.
        c.wait_for(
            &sess,
            "document.body && document.body.innerText.includes('approved at this commit')",
            30_000,
        )?;
        let panel = c.eval_str(&sess, "(document.querySelector('main')||document.body).innerText")?;
        // BEFORE: a fresh pull must read NOT approved. A tick here would be the
        // exact lie the commit-not-pull design exists to prevent.
        want(panel.contains("not approved at this commit"), || {
            format!("a fresh pull request is not shown as unapproved: {}", http::cut(&panel, 250))
        })?;
        // The honest note, now that a passkey reviewer signs as THEMSELVES:
        // the page tells bob his approval is his OWN key, not the console's.
        // Red-first — the old page disclosed "signed by the console's own key".
        want(panel.contains("your own key") && panel.contains("not the console"), || {
            format!("the page does not disclose the approval is the reviewer's own key: {}", http::cut(&panel, 300))
        })?;
        // With nothing approved the page offers NO Merge button and shows gunnar's own
        // refusal, asked ahead through PullRequests.CheckMerge (GUN-FIELD-TEST,
        // 2026-11-01: a button that could only be refused is not offered). main stays
        // where it was.
        let before = server_main(r, "alice/pr-web")?;
        want(!before.is_empty(), || "the server has no main tip to watch".into())?;
        c.wait_for(
            &sess,
            "(() => { const t=(document.querySelector('main')||document.body).innerText;
               return t.includes('Not mergeable yet') && t.includes('no verified approval'); })()",
            20_000,
        )
        .map_err(|e| format!("the page does not say in gunnar's words why it cannot merge: {e}"))?;
        let offered = c.eval_str(
            &sess,
            "String([...document.querySelectorAll('button')].some(e => /^(Merge|Fast-forward)/.test(e.textContent.trim())))",
        )?;
        want(offered == "false", || "the page offers a Merge button gunnar would refuse".into())?;
        let after = server_main(r, "alice/pr-web")?;
        want(after == before, || format!("main moved with nothing approved: {before} → {after}"))?;
        Ok(format!("#{number}: unapproved, no Merge button, gunnar's reason shown, main unmoved"))
    });

    go(rows, "approving-in-the-browser-then-merging-moves-the-branch", || {
        let n = pr_web.borrow().clone();
        want(!n.is_empty(), || "the pull request was never opened".into())?;
        c.navigate(&sess, &format!("{}/repo/alice/pr-web/pulls/{n}", r.origin()))?;
        c.wait_for(
            &sess,
            "[...document.querySelectorAll('button')].some(e => e.textContent.trim() === 'Approve this commit')",
            30_000,
        )?;
        // BEFORE — still not approved.
        let before_panel = c.eval_str(&sess, "(document.querySelector('main')||document.body).innerText")?;
        want(before_panel.contains("not approved at this commit"), || {
            format!("not in the unapproved state before approving: {}", http::cut(&before_panel, 200))
        })?;
        // Click Approve and require the badge to FLIP — the applied output, not
        // that a POST returned 200.
        c.eval_str(
            &sess,
            "[...document.querySelectorAll('button')].find(e => e.textContent.trim() === 'Approve this commit').click()",
        )?;
        c.wait_for(
            &sess,
            "(() => { const t=(document.querySelector('main')||document.body).innerText;
               return t.includes('approved at this commit') && !t.includes('not approved at this commit'); })()",
            20_000,
        )
        .map_err(|e| format!("the review badge did not flip to approved after clicking Approve: {e}"))?;
        // gunnar itself agrees the head is approved now.
        let review = c.eval(&sess, &format!("fetch('/api/repos/alice/pr-web/pulls/{n}/review').then(r => r.json())"))?;
        want(review.path("head_is_approved").and_then(|v| v.as_bool()) == Some(true), || {
            format!("the page says approved but gunnar does not: {}", http::cut(&review.dump(), 200))
        })?;
        // THE FIX, in the applied output: the approval is attributed to the
        // REVIEWER's own principal — `bob` himself since the thin layer (signed
        // with his session key; no `console-*` reviewer principal exists any
        // more, and gunnar refuses one) — verified against the key table and
        // covering the tip, and NOT to the console (`uitest-console`).
        // GHUNT-19: bob's browser session holds no key of his, so gunnar signs
        // his approval itself after his fresh passkey touch, and the verdict
        // says so: "attested for session step-up at <time>".
        let names_reviewer = review.path("approvals").map(|v| v.arr().iter().any(|e| {
            e.text("actor") == "bob"
                && e.path("verified").and_then(|x| x.as_bool()) == Some(true)
                && e.path("covers_head").and_then(|x| x.as_bool()) == Some(true)
                && e.text("verdict").starts_with("attested for session step-up at ")
        })).unwrap_or(false);
        want(names_reviewer, || {
            format!("the approval does not name the reviewer bob: {}", http::cut(&review.dump(), 300))
        })?;
        let names_console = review.path("approvals")
            .map(|v| v.arr().iter().any(|e| e.text("actor") == "uitest-console"))
            .unwrap_or(false);
        want(!names_console, || {
            format!("the approval is the console's after all: {}", http::cut(&review.dump(), 300))
        })?;
        // Now Merge, and the branch moves on the SERVER.
        let before = server_main(r, "alice/pr-web")?;
        c.eval_str(
            &sess,
            "[...document.querySelectorAll('button')].find(e => /^(Merge|Fast-forward)/.test(e.textContent.trim())).click()",
        )?;
        c.wait_for(&sess, "(document.querySelector('main')||document.body).innerText.includes('merged')", 20_000)
            .map_err(|e| format!("the pull request did not become merged in the UI: {e}"))?;
        let after = server_main(r, "alice/pr-web")?;
        want(after != before && !after.is_empty(), || format!("main did not move: {before} → {after}"))?;
        // The pull is merged in gunnar too, not merely in the console's word.
        let pull = c.eval(&sess, &format!("fetch('/api/repos/alice/pr-web/pulls/{n}').then(r => r.json())"))?;
        want(pull.text("state") == "merged", || format!("gunnar's pull state is {:?}", pull.text("state")))?;
        Ok(format!("approved in the UI, merged, main {} → {}", &before[..8.min(before.len())], &after[..8.min(after.len())]))
    });

    // ── the merge engine, through the browser (GUN-MERGE-ENGINE, 2026-11-01) ──
    //
    // The owner hit it on gunnar.rs: a pull request whose branch did not descend
    // from main could not be merged at all. These two rows drive the page: a
    // branch behind main is offered "Merge" (a merge commit, not "Fast-forward"),
    // and the press writes a two-parent commit a real git client accepts; a branch
    // that conflicts with main shows gunnar's conflict list and no button.
    // `pr_behind_main` sets each one up.
    go(rows, "a-pull-request-behind-main-is-merged-with-a-merge-commit-in-the-browser", || {
        let n = pr_web.borrow().clone();
        want(!n.is_empty(), || "the first pull request was never opened".into())?;
        let number = pr_behind_main(r, &mut c, &sess, "behind", ("BEHIND", "made on the branch\n"), ("AHEAD", "made on main\n"))?;
        let dir = r.work.join("pr-web");
        let head = r.git(&dir, &["rev-parse", "behind"])?.trim().to_owned();
        let before = server_main(r, "alice/pr-web")?;
        // gunnar's check says a merge commit: the button reads "Merge", never
        // "Fast-forward" (which would drop main's new commit).
        c.wait_for(
            &sess,
            "[...document.querySelectorAll('button')].some(e => e.textContent.trim() === 'Merge')",
            20_000,
        )
        .map_err(|e| format!("a branch behind main is not offered 'Merge': {e}"))?;
        let ff = c.eval_str(
            &sess,
            "String([...document.querySelectorAll('button')].some(e => e.textContent.trim() === 'Fast-forward'))",
        )?;
        want(ff == "false", || "a branch behind main is offered a fast-forward".into())?;
        c.eval_str(
            &sess,
            "[...document.querySelectorAll('button')].find(e => e.textContent.trim() === 'Merge').click()",
        )?;
        c.wait_for(&sess, "(document.querySelector('main')||document.body).innerText.includes('merged')", 20_000)
            .map_err(|e| format!("the pull request did not become merged in the UI: {e}"))?;
        let after = server_main(r, "alice/pr-web")?;
        want(after != before && after != head, || format!("main is not a new merge commit: {before} → {after}"))?;
        // A real git client: both parents, a clean fsck, a merge in the graph.
        r.git(&dir, &["fetch", "-q", &r.remote("alice/pr-web"), "main"])?;
        let parents = r.git(&dir, &["rev-list", "--parents", "-n1", "FETCH_HEAD"])?;
        let parents: Vec<&str> = parents.split_whitespace().collect();
        want(parents == [after.as_str(), before.as_str(), head.as_str()], || {
            format!("the merge commit's parents are {parents:?}, not [{after}, {before}, {head}]")
        })?;
        r.git(&dir, &["fsck", "--full", "--strict", "--no-dangling"])?;
        let graph = r.git(&dir, &["log", "--graph", "--oneline", "-4", "FETCH_HEAD"])?;
        want(graph.contains("|\\"), || format!("git log --graph draws no merge:\n{graph}"))?;
        let pull = c.eval(&sess, &format!("fetch('/api/repos/alice/pr-web/pulls/{number}').then(r => r.json())"))?;
        want(pull.text("state") == "merged" && pull.text("merge_oid") == after, || {
            format!("gunnar's record: {}", http::cut(&pull.dump(), 200))
        })?;
        Ok(format!("#{number}: Merge (not Fast-forward), main {} → merge {}", &before[..8], &after[..8]))
    });

    go(rows, "a-conflicting-pull-request-lists-its-paths-in-the-browser", || {
        let n = pr_web.borrow().clone();
        want(!n.is_empty(), || "the first pull request was never opened".into())?;
        let number = pr_behind_main(r, &mut c, &sess, "clash", ("README", "the branch's README\n"), ("README", "main's README\n"))?;
        let before = server_main(r, "alice/pr-web")?;
        c.wait_for(
            &sess,
            "(() => { const t=(document.querySelector('main')||document.body).innerText;
               return t.includes('changed on both sides') && t.includes('README (both sides changed the same lines)'); })()",
            20_000,
        )
        .map_err(|e| format!("the page does not list gunnar's conflicting path: {e}"))?;
        let offered = c.eval_str(
            &sess,
            "String([...document.querySelectorAll('button')].some(e => /^(Merge|Fast-forward)/.test(e.textContent.trim())))",
        )?;
        want(offered == "false", || "a conflicting pull request is offered a merge button".into())?;
        let after = server_main(r, "alice/pr-web")?;
        want(after == before, || format!("main moved on a conflict: {before} → {after}"))?;
        Ok(format!("#{number}: README listed as a conflict, no button, main unmoved"))
    });

    go(rows, "a-self-approved-merge-is-refused-in-the-browser", || {
        // The merge-side of forbid_self_approval, driven through the Merge
        // button. bob BOTH opens the pull request and approves it, so — now
        // that a web approval carries the human's own principal — the author and
        // the approver are the SAME real reviewer (`bob` himself, his session
        // key — thin layer), and the
        // rule refuses his approve-then-merge. This is a genuine human
        // self-approval, not the console's: before the fix, author and approver
        // were both `uitest-console` and the refusal named the console. The rule
        // is the only variable: the SAME Merge click is refused with it and
        // lands without it.
        r.control(&["repo", "create", "alice/pr-web-self"])?;
        let dir = r.work.join("pr-web-self");
        std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
        r.git(&dir, &["init", "-q", "--object-format=sha256", "-b", "main", "."])?;
        r.git(&dir, &["config", "user.name", "uitest"])?;
        r.git(&dir, &["config", "user.email", "info@example.invalid"])?;
        std::fs::write(dir.join("README"), "base\n").map_err(|e| e.to_string())?;
        r.git(&dir, &["add", "README"])?;
        r.git(&dir, &["commit", "-qm", "base"])?;
        r.git(&dir, &["push", "-q", &r.remote("alice/pr-web-self"), "main"])?;
        r.git(&dir, &["checkout", "-q", "-b", "review-me"])?;
        std::fs::write(dir.join("CHANGE"), "please review\n").map_err(|e| e.to_string())?;
        r.git(&dir, &["add", "CHANGE"])?;
        r.git(&dir, &["commit", "-qm", "a change that needs another pair of eyes"])?;
        r.git(&dir, &["push", "-q", &r.remote("alice/pr-web-self"), "review-me"])?;
        r.git(&dir, &["checkout", "-q", "main"])?;

        let opened = c.eval(
            &sess,
            "fetch('/api/repos/alice/pr-web-self/pulls', {method:'POST',
                 headers:{'Content-Type':'application/json'},
                 body: JSON.stringify({title:'needs another pair of eyes', description:'',
                     source_ref:'refs/heads/review-me', target_ref:'refs/heads/main'})})
               .then(r => r.json())",
        )?;
        let n = opened.path("number").and_then(|x| x.as_f64()).map(|x| x as i64)
            .ok_or_else(|| format!("no pull number: {}", http::cut(&opened.dump(), 200)))?;
        // Opened FROM the browser, so gunnar records bob's own principal as
        // the author — not the console. This is what makes the refusal below a
        // real self-approval; red-first, the old path authored it `uitest-console`.
        want(opened.text("author") == "bob", || {
            format!("a browser-opened pull request is authored by {:?}, not the reviewer", opened.text("author"))
        })?;

        // bob approves his own change. Write the rule AFTER, so only the
        // merge is under test.
        let review = c.eval(&sess, &format!("fetch('/api/repos/alice/pr-web-self/pulls/{n}/review').then(r => r.json())"))?;
        let head = review.text("head");
        want(!head.is_empty(), || "the review does not say where the branch is".into())?;
        // As the page sends it: a `"code":"stepup"` answer (bob's touch is past
        // gunnar's 10-minute window) is a fresh touch of his key, then the same
        // call once more (GHUNT-19).
        let approved = c.eval(
            &sess,
            &stepped_post(
                &format!("/api/repos/alice/pr-web-self/pulls/{n}/approve"),
                &format!("JSON.stringify({{commit:{}}})", esc(&head)),
            ),
        )?;
        want(approved.as_f64().map(|s| (200.0..300.0).contains(&s)) == Some(true), || {
            format!("the approval did not take: {}", approved.dump())
        })?;
        // The approver is bob too — the same real principal as the author,
        // which is exactly what forbid-self-approval turns on. Under the old
        // path this was the console approving the console.
        let approved_review = c.eval(&sess, &format!("fetch('/api/repos/alice/pr-web-self/pulls/{n}/review').then(r => r.json())"))?;
        want(approved_review.path("approvals").map(|v| v.arr().iter().any(|e| e.text("actor") == "bob")).unwrap_or(false), || {
            format!("the approval is not the reviewer's own: {}", http::cut(&approved_review.dump(), 300))
        })?;
        let ruled = c.eval(
            &sess,
            "fetch('/api/repos/alice/pr-web-self/protection', {method:'POST',
                 headers:{'Content-Type':'application/json'},
                 body: JSON.stringify({ref_pattern:'refs/heads/main', forbid_self_approval:true})})
               .then(r => r.status)",
        )?;
        want(ruled.as_f64().map(|s| (200.0..300.0).contains(&s)) == Some(true), || {
            format!("the rule did not store: {}", ruled.dump())
        })?;

        // Under the rule the page offers no Merge button and says why in gunnar's words
        // (CheckMerge, GUN-FIELD-TEST 2026-11-01); main unmoved.
        c.navigate(&sess, &format!("{}/repo/alice/pr-web-self/pulls/{n}", r.origin()))?;
        let before = server_main(r, "alice/pr-web-self")?;
        c.wait_for(
            &sess,
            "(() => { const t=(document.querySelector('main')||document.body).innerText;
               return t.includes('Not mergeable yet') && t.includes('self-approval'); })()",
            30_000,
        )
        .map_err(|e| format!("the self-approved merge was not refused in words in the UI: {e}"))?;
        let offered = c.eval_str(
            &sess,
            "String([...document.querySelectorAll('button')].some(e => /^(Merge|Fast-forward)/.test(e.textContent.trim())))",
        )?;
        want(offered == "false", || "the page offers a Merge button the rule refuses".into())?;
        want(server_main(r, "alice/pr-web-self")? == before, || "main moved under the rule".into())?;

        // The control: rule gone, the IDENTICAL Merge click lands — so the
        // refusal above was the rule and nothing else.
        let unruled = c.eval(
            &sess,
            "fetch('/api/repos/alice/pr-web-self/protection?ref_pattern=refs%2Fheads%2Fmain', {method:'DELETE'})
               .then(r => r.status)",
        )?;
        want(unruled.as_f64().map(|s| (200.0..300.0).contains(&s)) == Some(true), || {
            format!("removing the rule answered {}", unruled.dump())
        })?;
        c.navigate(&sess, &format!("{}/repo/alice/pr-web-self/pulls/{n}", r.origin()))?;
        c.wait_for(&sess, "[...document.querySelectorAll('button')].some(e => /^(Merge|Fast-forward)/.test(e.textContent.trim()))", 30_000)?;
        c.eval_str(&sess, "[...document.querySelectorAll('button')].find(e => /^(Merge|Fast-forward)/.test(e.textContent.trim())).click()")?;
        c.wait_for(&sess, "(document.querySelector('main')||document.body).innerText.includes('merged')", 20_000)
            .map_err(|e| format!("the rule was removed and the identical UI merge still did not land: {e}"))?;
        want(server_main(r, "alice/pr-web-self")? != before, || "the merge landed but main did not move".into())?;
        Ok("not offered under the rule (gunnar's words shown), offered and merged without it".into())
    });

    // ── branch protection, set and removed from the UI ────────────────────
    //
    // The API rows proved a viewer cannot set protection and that gunnar
    // enforces it. These prove the OPERATOR's panel: the form writes a rule the
    // page then renders in its forbids-words, gunnar's own CLI sees the same
    // document, and the Remove button takes it away again.
    go(rows, "branch-protection-set-from-the-ui-is-stored-by-gunnar", || {
        r.control(&["repo", "create", "alice/ui-guarded"])?;
        // A real commit, so the operator is protecting a repository that has
        // content — and the detail page renders its history rather than the
        // "does not exist" state a bare namespace entry shows.
        let dir = r.work.join("ui-guarded");
        std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
        r.git(&dir, &["init", "-q", "--object-format=sha256", "-b", "main", "."])?;
        r.git(&dir, &["config", "user.name", "uitest"])?;
        r.git(&dir, &["config", "user.email", "info@example.invalid"])?;
        std::fs::write(dir.join("README"), "guard me\n").map_err(|e| e.to_string())?;
        r.git(&dir, &["add", "README"])?;
        r.git(&dir, &["commit", "-qm", "one"])?;
        r.git(&dir, &["push", "-q", &r.remote("alice/ui-guarded"), "main"])?;

        // The Settings tab since the final navigation (2026-10-02).
        c.navigate(&sess, &format!("{}/repo/alice/ui-guarded/settings", r.origin()))?;
        c.wait_for(&sess, "document.getElementById('prot-pattern') !== null", 30_000)?;
        // Wait for the protection list to have ANSWERED, not merely for the
        // form control beside it: the pattern input is in the page's markup
        // and renders at once, while the list is a fetch. Reading between the
        // two is a race this row lost under load on 2026-09-12 — "the panel
        // did not start empty", with `Loading …` in the text it printed.
        //
        // Either outcome ends the wait, so the assertion below still decides:
        // if a rule IS already there, this returns and the row fails saying
        // so, rather than timing out with a message about the wait.
        //
        // The list's OWN marks, not a word the panel prints anyway: the help
        // line and the pattern's hint both say `refs/heads/…`, so
        // `includes('refs/heads/')` was true the moment the form rendered and
        // this wait waited for nothing — the 2026-09-12 race still open, lost
        // again on 2026-10-04 with `Loading …` in the text. The empty state,
        // the rules table's caption, or a failure.
        c.wait_for(
            &sess,
            "(() => { const t=(document.querySelector('main')||document.body).innerText;
               return t.includes('Nothing is protected')
                 || [...document.querySelectorAll('caption')].some(c => c.textContent.includes('Branch protection rules'))
                 || !!document.querySelector('.failure'); })()",
            30_000,
        )?;
        // BEFORE: nothing protected, and gunnar holds no rule. gunnar's empty
        // help LISTS `refs/heads/main` as an example, so the empty state is read
        // off its own sentence rather than off the pattern.
        let before = c.eval_str(&sess, "(document.querySelector('main')||document.body).innerText")?;
        want(before.contains("Nothing is protected"), || {
            format!("the panel did not start empty: {}", http::cut(&before, 250))
        })?;
        let cli_before = r.control(&["policy", "list", "alice/ui-guarded"])?;
        want(cli_before.contains("NOTHING is protected"), || {
            format!("gunnar already holds a rule: {}", http::cut(&cli_before, 200))
        })?;
        // Type the pattern into the Leptos-controlled input (setting `.value`
        // then dispatching `input`, which is what the `on:input` handler reads)
        // and Protect. The refuse-force and refuse-delete switches start on, so
        // this stores a real protection.
        c.eval_str(
            &sess,
            "(() => { const p=document.getElementById('prot-pattern');
               p.value='refs/heads/main';
               p.dispatchEvent(new Event('input', {bubbles:true}));
               return p.value; })()",
        )?;
        let clicked = c.eval_str(
            &sess,
            "(() => { const b=[...document.querySelectorAll('button')].find(e => e.textContent.trim() === 'Protect');
               if (!b) return 'no button'; b.click(); return 'clicked'; })()",
        )?;
        want(clicked == "clicked", || format!("no Protect button: {clicked}"))?;
        // AFTER: the rule row renders, in the forbids-words the panel promises.
        c.wait_for(
            &sess,
            "(() => { const t=(document.querySelector('main')||document.body).innerText;
               return t.includes('force-push refused') && t.includes('refs/heads/main'); })()",
            20_000,
        )
        .map_err(|e| format!("the rule did not render after Protect: {e}"))?;
        let after = c.eval_str(&sess, "(document.querySelector('main')||document.body).innerText")?;
        want(!after.contains("Nothing is protected"), || "the empty state is still shown".into())?;
        want(after.contains("delete refused"), || {
            format!("the delete-refused chip is missing: {}", http::cut(&after, 250))
        })?;
        // gunnar stored it — the CLI is a different surface onto the same
        // policy document, which is the whole point of the two agreeing. "N
        // rule(s) on" appears only when a rule really exists, so it cannot be
        // satisfied by the empty help's example pattern.
        let cli = r.control(&["policy", "list", "alice/ui-guarded"])?;
        want(cli.contains("rule(s) on") && cli.contains("refs/heads/main") && !cli.contains("NOTHING is protected"), || {
            format!("gunnar did not store the UI's rule: {}", http::cut(&cli, 250))
        })?;
        Ok("protected from the UI, stored in gunnar".into())
    });

    go(rows, "required-approvals-set-from-the-ui-then-removed", || {
        // A second rule on the same repository (a concrete staging branch),
        // exercising the number field and the self-approval switch, then its own
        // Remove button — so a rule that renders is one that can be taken away
        // again without touching the main rule beside it.
        // The Settings tab since the final navigation (2026-10-02).
        c.navigate(&sess, &format!("{}/repo/alice/ui-guarded/settings", r.origin()))?;
        c.wait_for(&sess, "document.getElementById('prot-required') !== null", 30_000)?;
        let ready = c.eval_str(
            &sess,
            "(() => { const p=document.getElementById('prot-pattern');
               p.value='refs/heads/staging';
               p.dispatchEvent(new Event('input', {bubbles:true}));
               const n=document.getElementById('prot-required');
               n.value='2'; n.dispatchEvent(new Event('input', {bubbles:true}));
               const lab=[...document.querySelectorAll('label.switch')].find(l => l.textContent.includes('self-approval'));
               const cb=lab && lab.querySelector('input');
               if (cb && !cb.checked) cb.click();
               return (cb && cb.checked) ? 'ready' : 'switch-missing'; })()",
        )?;
        want(ready == "ready", || format!("could not set up the staging rule form: {ready}"))?;
        c.eval_str(&sess, "[...document.querySelectorAll('button')].find(e => e.textContent.trim() === 'Protect').click()")?;

The file goes on; the console shows all of it.