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

nordisk/gunnar-ui

crates/gunnar-ui-server/src/anonymous/console_address.rs

//! ★ **GUN-GUEST (owner 2026-10-30): the console is THE page, for everyone, at one address.**
//!
//! * `/{ns}/{name}`, `…/tree/…`, `…/blob/…`, `…/commits`, `…/issues…` are the console's
//!   routes; the old `/repo/{ns}/{name}…` answers 301 to them.
//! * Every such address answers the console's own `index.html` to everyone, signed in or
//!   not, and a browser boots the console there (a guest reads public repositories
//!   read-only, through gunnar's anonymous reads: files, commits, the README, issues).
//! * With the public site on (`public-site`), the same document carries the page's content
//!   as plain HTML in `<main id="prerender">`, so a crawler that runs no WebAssembly reads
//!   it; the same bytes for everybody (no user-agent is read). Off, a guest sees only `/`.

use super::*;

/// The console's `index.html` as a test bundle has it: the bootstrap's module named, so a
/// `contains` tells the console's document from any other answer.
const SHELL: &str = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Gunnar</title>\n<link rel=\"modulepreload\" href=\"/gunnar-ui-web-0123abcd.js\">\n</head>\n<body></body>\n</html>\n";
pub(super) const SHELL_MARK: &str = "/gunnar-ui-web-0123abcd.js";

/// A console with a built bundle (its `index.html` is [`SHELL`]) and a guest key.
pub(super) async fn console_with_shell(fake: FakeGunnar) -> (axum::Router, Arc<crate::auth::Webauthn>, crate::sse::EventHub, tempfile::TempDir) {
    let ui = tempfile::tempdir().expect("a bundle dir");
    std::fs::write(ui.path().join("index.html"), SHELL).expect("the bundle's page");
    let (app, passkeys, feed) = console_waiting(fake, Some(GUEST_SEED), None, None, None, None, Some(ui.path().to_owned())).await;
    (app, passkeys, feed, ui)
}

/// GET, with the answer's status, headers and body.
pub(super) async fn get_full(app: &axum::Router, uri: &str, cookie: Option<&str>) -> (http::StatusCode, http::HeaderMap, String) {
    use tower::ServiceExt;
    let mut r = http::Request::get(uri);
    if let Some(c) = cookie {
        r = r.header(http::header::COOKIE, format!("{}={c}", crate::auth::SESSION_COOKIE));
    }
    let answer = app.clone().oneshot(r.body(axum::body::Body::empty()).unwrap()).await.unwrap();
    let (status, headers) = (answer.status(), answer.headers().clone());
    let body = axum::body::to_bytes(answer.into_body(), 4 << 20).await.unwrap();
    (status, headers, String::from_utf8_lossy(&body).into_owned())
}

/// ★ **The old address answers 301 to the new one**, the same for a visitor and a signed-in
/// person: `files` is `tree` (its `?path=` the path), `graph` is `commits`, every other leaf
/// keeps its name, and the rest of the query stays.
#[tokio::test]
async fn the_old_repo_address_answers_301_to_the_new_one() {
    let (app, passkeys, _feed, _ui) = console_with_shell(FakeGunnar::new()).await;
    let cookie = passkeys.make_session("root", crate::role::Role::Admin, true);
    for (old, new) in [
        (format!("/repo/{NS}/{PUBLIC}"), format!("/{NS}/{PUBLIC}")),
        (format!("/repo/{NS}/{PUBLIC}/files"), format!("/{NS}/{PUBLIC}/tree")),
        (format!("/repo/{NS}/{PUBLIC}/files?path=docs/v2/README.md"), format!("/{NS}/{PUBLIC}/tree/docs/v2/README.md")),
        (format!("/repo/{NS}/{PUBLIC}/files?rev=feature%2Fone&path=a%20b.md"), format!("/{NS}/{PUBLIC}/tree/a%20b.md?rev=feature%2Fone")),
        (format!("/repo/{NS}/{PUBLIC}/graph"), format!("/{NS}/{PUBLIC}/commits")),
        (format!("/repo/{NS}/{PUBLIC}/issues"), format!("/{NS}/{PUBLIC}/issues")),
        (format!("/repo/{NS}/{PUBLIC}/issues/3"), format!("/{NS}/{PUBLIC}/issues/3")),
        (format!("/repo/{NS}/{PUBLIC}/blame?path=src/lib.rs"), format!("/{NS}/{PUBLIC}/blame?path=src/lib.rs")),
        (format!("/repo/{NS}/{PRIVATE}/settings"), format!("/{NS}/{PRIVATE}/settings")),
        // The two shorter old addresses the console's own redirect knows (gunnar-ui-web `addr`).
        ("/repo".to_owned(), "/repos".to_owned()),
        (format!("/repo/{NS}"), format!("/{NS}")),
    ] {
        for who in [None, Some(cookie.as_str())] {
            let (status, headers, _) = get_full(&app, &old, who).await;
            assert_eq!(status, http::StatusCode::MOVED_PERMANENTLY, "{old} ({}): not a 301", if who.is_some() { "signed in" } else { "a visitor" });
            assert_eq!(headers[http::header::LOCATION], new.as_str(), "{old}");
        }
    }
}

/// ★ **Every repository address is the console's document, for a visitor too**, at any
/// depth and whatever the file is called (a path ending `.js` is a repository's file here,
/// never a missing asset).
#[tokio::test]
async fn every_repository_address_is_the_consoles_document_for_a_visitor() {
    let (app, passkeys, _feed, _ui) = console_with_shell(FakeGunnar::new().with_docs()).await;
    guests_on(&app, &passkeys).await;
    for leaf in ["", "/tree", "/tree/docs", "/blob/README.md", "/blob/docs/v2/README.md", "/blob/docs/app.js", "/commits", "/issues", "/issues/1", "/pulls", "/blame?path=README.md"] {
        let uri = format!("/{NS}/{PUBLIC}{leaf}");
        let (status, _, body) = get_full(&app, &uri, None).await;
        assert_eq!(status, http::StatusCode::OK, "{uri}: {body}");
        assert!(body.contains(SHELL_MARK), "{uri} is not the console's document: {body}");
    }
    // The console's own routes are not a repository's: a visitor is still sent to log in.
    for page in ["/admin/storage", "/account/activity", "/usage/nordisk"] {
        let (status, _, body) = get_full(&app, page, None).await;
        assert!(!body.contains(SHELL_MARK) || status != http::StatusCode::OK, "{page} was handed to a visitor: {status}");
    }
}

/// ★ **A guest reads a public repository's issues** through gunnar's anonymous read
/// (`Forge.ListIssues` / `GetIssue` take Read, and Read on a public repository is
/// everybody's); a private one is refused, as its files are.
#[tokio::test]
async fn a_guest_reads_a_public_repositorys_issues_and_not_a_private_ones() {
    let fake = FakeGunnar::new();
    seed_issue(&fake, &format!("{NS}/{PUBLIC}"), "The boot is dark");
    seed_issue(&fake, &format!("{NS}/{PRIVATE}"), "payroll-issue-nobody-may-read");
    let (app, _passkeys, _feed, _ui) = console_with_shell(fake).await;
    let (status, list) = anon(&app, &format!("/api/repos/{NS}/{PUBLIC}/issues")).await;
    assert_eq!(status, http::StatusCode::OK, "{list}");
    assert!(list.contains("The boot is dark"), "{list}");
    let (status, one) = anon(&app, &format!("/api/repos/{NS}/{PUBLIC}/issues/1")).await;
    assert_eq!(status, http::StatusCode::OK, "{one}");
    for uri in [format!("/api/repos/{NS}/{PRIVATE}/issues"), format!("/api/repos/{NS}/{PRIVATE}/issues/1")] {
        let (status, body) = anon(&app, &uri).await;
        assert_ne!(status, http::StatusCode::OK, "{uri}: {body}");
        assert!(!body.contains("payroll-issue"), "{uri}: {body}");
    }
    // Nobody is named to a guest: the issue's filer is a signed-in person's to see.
    for body in [&list, &one] {
        assert!(!body.contains("the-filers-principal-id"), "a guest was told who filed: {body}");
    }
    let (_, admins) = admin(&app, &format!("/api/repos/{NS}/{PUBLIC}/issues")).await;
    assert!(admins.contains("the-filers-principal-id"), "a signed-in person still sees who filed: {admins}");
    // Filing, commenting and closing stay a signed-in person's.
    for uri in [format!("/api/repos/{NS}/{PUBLIC}/issues"), format!("/api/repos/{NS}/{PUBLIC}/issues/1/comment"), format!("/api/repos/{NS}/{PUBLIC}/issues/1/close")] {
        let (status, _) = send(&app, http::Method::POST, &uri, None).await;
        assert_ne!(status, http::StatusCode::OK, "a visitor's POST {uri} was taken");
    }
}

/// One open issue in `repo`.
fn seed_issue(fake: &FakeGunnar, repo: &str, title: &str) {
    let mut issues = fake.issues.lock().unwrap();
    let number = issues.iter().filter(|i| i.repo == repo).count() as u64 + 1;
    issues.push(pb::IssueState { repo: repo.to_owned(), number, title: title.to_owned(), body: format!("{title}, in words."), author: "the-filers-principal-id".to_owned(), open: true, comments: 0, opened_at_unix_ms: 1_761_264_000_000 });
}

#[cfg(feature = "public-site")]
pub(super) mod prerendered {
    use super::*;

    /// The issue the public repository has, and the comment in its thread.
    pub(in crate::anonymous) const ISSUE: &str = "The boot is dark";
    const COMMENT: &str = "It happens on Mondays only.";

    pub(in crate::anonymous) fn seed_thread(fake: &FakeGunnar, repo: &str) {
        let mut issues = fake.issues.lock().unwrap();
        issues.push(pb::IssueState { repo: repo.to_owned(), number: 1, title: ISSUE.to_owned(), body: "PID 1 prints to a dead ttyS0. <script>alert(2)</script>".to_owned(), author: "the-filers-principal-id".to_owned(), open: true, comments: 1, opened_at_unix_ms: 1_761_264_000_000 });
        let comment = pb::ForgeEvent { repo: repo.to_owned(), pull: 1, actor: "the-commenters-principal-id".to_owned(), at_unix_ms: 1_761_300_000_000, kind: Some(pb::forge_event::Kind::IssueCommented(pb::IssueCommented { body: COMMENT.to_owned() })) };
        fake.issue_threads.lock().unwrap().insert((repo.to_owned(), 1), vec![comment]);
    }

    /// The site on, its first refresh done.
    pub(in crate::anonymous) async fn on(app: &axum::Router, passkeys: &Arc<crate::auth::Webauthn>) {
        use tower::ServiceExt;
        let admin = passkeys.make_session("root", crate::role::Role::Admin, true);
        let put = http::Request::put("/api/public-site")
            .header(http::header::COOKIE, format!("{}={admin}", crate::auth::SESSION_COOKIE))
            .header(http::header::CONTENT_TYPE, "application/json")
            .header(http::header::ORIGIN, passkeys.origin())
            .body(axum::body::Body::from("{\"on\":true}"))
            .unwrap();
        assert_eq!(app.clone().oneshot(put).await.unwrap().status(), http::StatusCode::OK);
        for _ in 0..80 {
            let (status, body) = anon(app, "/llms.txt").await;
            if status == http::StatusCode::OK && body.contains(&format!("{NS}/{PUBLIC}")) {
                return;
            }
            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
        }
        panic!("the public site never built its pages");
    }

    /// What is inside `<main id="prerender">`, or `None`.
    pub(in crate::anonymous) fn prerender(body: &str) -> Option<&str> {
        let open = "<main id=\"prerender\">";
        let at = body.find(open)? + open.len();
        Some(&body[at..at + body[at..].find("</main>")?])
    }

    /// ★ **The crawler's view**: the raw HTML of each page kind holds its content without
    /// running a line of script, inside the console's own document, with its title and
    /// canonical URL in the head; nothing private and nobody's name is in it.
    #[tokio::test]
    async fn the_raw_html_of_every_page_kind_holds_its_content() {
        let fake = FakeGunnar::new().with_docs();
        seed_thread(&fake, &format!("{NS}/{PUBLIC}"));
        seed_thread(&fake, &format!("{NS}/{PRIVATE}"));
        let (app, passkeys, _feed, _ui) = console_with_shell(fake).await;
        on(&app, &passkeys).await;
        let kinds: Vec<(String, Vec<&str>)> = vec![
            // The repository's page: the README through the console's own render, the files, the newest commit.
            (format!("/{NS}/{PUBLIC}"), vec![README_HEADING, "README.md", "guide.pdf", "docs", "the one commit"]),
            (format!("/{NS}/{PUBLIC}/tree"), vec!["README.md", "guide.pdf", "docs"]),
            (format!("/{NS}/{PUBLIC}/tree/docs"), vec!["v2", "app.js"]),
            (format!("/{NS}/{PUBLIC}/blob/README.md"), vec![README_HEADING]),
            (format!("/{NS}/{PUBLIC}/blob/docs/v2/README.md"), vec![DOCS_HEADING]),
            (format!("/{NS}/{PUBLIC}/blob/docs/app.js"), vec!["console.log(&#39;the docs script&#39;);"]),
            (format!("/{NS}/{PUBLIC}/commits"), vec!["the one commit"]),
            (format!("/{NS}/{PUBLIC}/issues"), vec![ISSUE, "1 open"]),
            (format!("/{NS}/{PUBLIC}/issues/1"), vec![ISSUE, "PID 1 prints to a dead ttyS0.", COMMENT]),
        ];
        for (uri, words) in kinds {
            let (status, headers, body) = get_full(&app, &uri, None).await;
            assert_eq!(status, http::StatusCode::OK, "{uri}: {body}");
            assert!(headers[http::header::CONTENT_TYPE].to_str().unwrap().starts_with("text/html"), "{uri}");
            assert!(body.contains(SHELL_MARK), "{uri} is not the console's document: {body}");
            let part = prerender(&body).unwrap_or_else(|| panic!("{uri} has no pre-rendered part: {body}"));
            for w in words {
                assert!(part.contains(w), "{uri}: the part does not hold {w:?}: {part}");
            }
            assert!(!part.contains("<script"), "{uri}: a script in the part: {part}");
            let head = &body[..body.find("</head>").expect("a head")];
            assert_eq!(head.matches("<title>").count(), 1, "{uri}: one title");
            assert!(head.contains(&format!("<link rel=\"canonical\" href=\"https://console.example{}\">", uri)), "{uri}: {head}");
            for never in [PRIVATE, "payroll", "principal-id", "the-private-blob"] {
                assert!(!body.contains(never), "{uri} names {never}: {body}");
            }
        }
    }

    /// ★ **No cloaking**: a signed-in person gets the same bytes at the same address.
    #[tokio::test]
    async fn a_signed_in_person_gets_the_same_document() {
        let (app, passkeys, _feed, _ui) = console_with_shell(FakeGunnar::new().with_docs()).await;
        on(&app, &passkeys).await;
        let cookie = passkeys.make_session("root", crate::role::Role::Admin, true);
        for leaf in ["", "/blob/docs/v2/README.md", "/commits"] {
            let uri = format!("/{NS}/{PUBLIC}{leaf}");
            let visitor = get_full(&app, &uri, None).await;
            let person = get_full(&app, &uri, Some(&cookie)).await;
            assert_eq!(visitor.0, http::StatusCode::OK, "{uri}");
            assert_eq!((visitor.0, visitor.2), (person.0, person.2), "{uri}: a signed-in person got another document");
        }
    }

    /// ★ **A private repository answers as a missing one**: the console's document with no
    /// part, the same as for a name nobody holds, and nothing of it.
    #[tokio::test]
    async fn a_private_repository_answers_as_a_missing_one() {
        let (app, passkeys, _feed, _ui) = console_with_shell(FakeGunnar::new()).await;
        on(&app, &passkeys).await;
        for leaf in ["", "/tree", "/blob/README.md", "/commits", "/issues"] {
            let private = get_full(&app, &format!("/{NS}/{PRIVATE}{leaf}"), None).await;
            let nobody = get_full(&app, &format!("/{NS}/no-such-repository{leaf}"), None).await;
            assert_eq!((private.0, &private.2), (nobody.0, &nobody.2), "{leaf}: a private repository must answer as none");
            assert!(prerender(&private.2).is_none(), "{leaf}: a private repository was pre-rendered");
            assert!(!private.2.contains("the-private-blob"), "{leaf}");
        }
    }

    /// ★ **Switched off, a guest sees only `/`**: no repository page (the login instead),
    /// and robots.txt keeps every crawler out. The anonymous reads are not the switch's
    /// (PUB-IMG: "the switch changes nothing on the raw route"). A signed-in person still has
    /// the console.
    #[tokio::test]
    async fn switched_off_a_guest_sees_only_the_front_page() {
        let (app, passkeys, _feed, _ui) = console_with_shell(FakeGunnar::new()).await;
        let (status, _, robots) = get_full(&app, "/robots.txt", None).await;
        assert_eq!((status, robots.as_str()), (http::StatusCode::OK, "User-agent: *\nDisallow: /\n"));
        for uri in [format!("/{NS}/{PUBLIC}"), format!("/{NS}/{PUBLIC}/blob/README.md"), format!("/{NS}/{PUBLIC}/issues"), "/repos".to_owned()] {
            let (status, _, body) = get_full(&app, &uri, None).await;
            assert!(status != http::StatusCode::OK || !body.contains(SHELL_MARK), "{uri} was handed to a guest with the site off: {status}");
        }
        let cookie = passkeys.make_session("root", crate::role::Role::Admin, true);
        let (status, _, body) = get_full(&app, &format!("/{NS}/{PUBLIC}"), Some(&cookie)).await;
        assert_eq!(status, http::StatusCode::OK);
        assert!(body.contains(SHELL_MARK) && prerender(&body).is_none(), "{body}");
    }

    /// ★ **A push rebuilds the pages at once**: gunnar's `ref_updated` for the repository
    /// reaches the console's feed, and the next visitor reads the new commit.
    #[tokio::test]
    async fn a_push_rebuilds_the_repositorys_pages_at_once() {
        let fake = FakeGunnar::new();
        let (app, passkeys, feed, _ui) = console_with_shell(fake.clone()).await;
        on(&app, &passkeys).await;
        let uri = format!("/{NS}/{PUBLIC}/commits");
        assert!(prerender(&get_full(&app, &uri, None).await.2).is_some_and(|p| p.contains("the one commit")));
        *fake.subject.lock().unwrap() = "the pushed commit".to_owned();
        feed.publish(crate::sse::FeedItem { seq: 9, at_unix_ms: 1, kind: "ref_updated".into(), repo: Some(format!("{NS}/{PUBLIC}")), summary: "refs/heads/main moved".into(), dropped: None });
        for _ in 0..40 {
            if prerender(&get_full(&app, &uri, None).await.2).is_some_and(|p| p.contains("the pushed commit")) {
                return;
            }
            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
        }
        panic!("the push was not on the page within 2 s");
    }

    /// ★ **An issue filed in the console rebuilds the issue list at once.**
    #[tokio::test]
    async fn an_issue_filed_rebuilds_the_issue_list_at_once() {
        use tower::ServiceExt;
        let fake = FakeGunnar::new();
        let (app, passkeys, _feed, _ui) = console_with_shell(fake.clone()).await;
        on(&app, &passkeys).await;
        let uri = format!("/{NS}/{PUBLIC}/issues");
        assert!(prerender(&get_full(&app, &uri, None).await.2).is_some_and(|p| !p.contains("A brand new issue")));
        // The namespace's owner files it, signed as themselves (gunnar decides the write).
        fake.principals.lock().unwrap().insert(NS.to_owned(), pb::Principal { id: NS.to_owned(), name: NS.to_owned(), kind: pb::PrincipalKind::User as i32, created_at_unix_ms: 1 });
        passkeys.plant_bound_credential_for_tests(NS, crate::role::Role::Viewer, NS);
        let cookie = passkeys.make_session(NS, crate::role::Role::Viewer, true);
        let post = http::Request::post(format!("/api/repos/{NS}/{PUBLIC}/issues"))
            .header(http::header::COOKIE, format!("{}={cookie}", crate::auth::SESSION_COOKIE))
            .header(http::header::ORIGIN, "https://console.example")
            .header(http::header::CONTENT_TYPE, "application/json")
            .body(axum::body::Body::from("{\"title\":\"A brand new issue\",\"body\":\"found today\"}"))
            .unwrap();
        let filed = app.clone().oneshot(post).await.unwrap();
        let status = filed.status();
        let said = axum::body::to_bytes(filed.into_body(), 1 << 20).await.unwrap();
        assert_eq!(status, http::StatusCode::OK, "{}", String::from_utf8_lossy(&said));
        assert!(prerender(&get_full(&app, &uri, None).await.2).is_some_and(|p| p.contains("A brand new issue")), "the new issue is not on the list");
    }

    /// ★ **The issue list is searchable** (OpenSSF `report_archive`): `?q=` keeps the
    /// issues whose words match; the canonical page is the list.
    #[tokio::test]
    async fn the_issue_list_is_searchable() {
        let fake = FakeGunnar::new();
        seed_thread(&fake, &format!("{NS}/{PUBLIC}"));
        let (app, passkeys, _feed, _ui) = console_with_shell(fake).await;
        on(&app, &passkeys).await;
        let found = get_full(&app, &format!("/{NS}/{PUBLIC}/issues?q=ttys0"), None).await.2;
        assert!(prerender(&found).is_some_and(|p| p.contains(ISSUE)), "{found}");
        let none = get_full(&app, &format!("/{NS}/{PUBLIC}/issues?q=tuesdays"), None).await.2;
        assert!(prerender(&none).is_some_and(|p| !p.contains(ISSUE)), "{none}");
        assert!(none.contains(&format!("<link rel=\"canonical\" href=\"https://console.example/{NS}/{PUBLIC}/issues\">")), "{none}");
    }

    /// ★ **An on-demand render has its own, lower budget** (the console's one throttle,
    /// `Throttle::admit_render`): past it a client is answered 429 with `Retry-After` before
    /// anything is rendered or asked of gunnar; a page already kept costs no render; another
    /// address has its own budget.
    #[tokio::test]
    async fn an_on_demand_render_has_its_own_lower_budget() {
        use tower::ServiceExt;
        let mut limits = crate::throttle::tests::wide();
        limits.render.burst = 1;
        crate::throttle::tests::next_router(limits);
        let fake = FakeGunnar::new().with_docs();
        let (app, passkeys, _feed, _ui) = console_with_shell(fake.clone()).await;
        on(&app, &passkeys).await;
        let ask = |peer: &'static str, uri: String| {
            let app = app.clone();
            async move {
                let mut r = http::Request::get(uri).body(axum::body::Body::empty()).unwrap();
                r.extensions_mut().insert(axum::extract::ConnectInfo(peer.parse::<std::net::SocketAddr>().unwrap()));
                let answer = app.oneshot(r).await.unwrap();
                let wait = answer.headers().get(http::header::RETRY_AFTER).cloned();
                let body = axum::body::to_bytes(answer.into_body(), 4 << 20).await.unwrap();
                (wait, String::from_utf8_lossy(&body).into_owned())
            }
        };
        let first = format!("/{NS}/{PUBLIC}/blob/README.md");
        let (_, page) = ask("198.51.100.20:4000", first.clone()).await;
        assert!(prerender(&page).is_some(), "the first render is in the budget: {page}");
        let before = fake.fingerprints().len();
        let (wait, refused) = ask("198.51.100.20:4000", format!("/{NS}/{PUBLIC}/blob/docs/v2/README.md")).await;
        assert!(wait.is_some() && !refused.contains(DOCS_HEADING), "a second render past the budget is refused with Retry-After: {refused}");
        assert_eq!(fake.fingerprints().len(), before, "a refused render asked gunnar something");
        let (_, kept) = ask("198.51.100.20:4000", first).await;
        assert!(prerender(&kept).is_some(), "a kept page costs no render");
        let (_, other) = ask("198.51.100.21:4000", format!("/{NS}/{PUBLIC}/blob/docs/v2/README.md")).await;
        assert!(prerender(&other).is_some_and(|p| p.contains(DOCS_HEADING)), "another address renders: {other}");
    }

    /// ★ **In a real browser, with the console's real bundle** (headless Chromium, the
    /// browser step): a guest opens a deep link (`…/blob/docs/v2/README.md`) and the console
    /// draws it in place of the pre-rendered part; a signed-in person opens the same address
    /// and gets the full console; both pages settle (one document each, no reload loop).
    ///
    /// The bundle is the one `trunk` builds (`dist-metered`), named by `GUNNAR_UI_BUNDLE`;
    /// without one the test says NOT RUN (the toolchain image carries no trunk).
    #[cfg(feature = "metered")]
    mod in_the_browser {
        use std::time::Duration;

        use serde_json::{json, Value};

        use super::*;
        use crate::passkeys_at_gunnar::first_touch::{chrome, Chromium, ONE_BROWSER};

        /// Every document this tab loads, kept in its `sessionStorage`: its path, and whether
        /// it carried the pre-rendered part as it was parsed (seen by an observer from the
        /// document's first byte: the console may take it away before `DOMContentLoaded`).
        const LOADS: &str = r#"(() => {
          let saw = false, seen = [];
          new MutationObserver((records) => { for (const r of records) for (const n of r.addedNodes) { if (n.id === 'prerender') saw = true; if (n.nodeType === 1 && seen.length < 400) seen.push(n.nodeName + (n.id ? '#' + n.id : '')); } }).observe(document, { childList: true, subtree: true });
          window.__seen = seen;
          addEventListener('DOMContentLoaded', () => {
            let all = [];
            try { all = JSON.parse(sessionStorage.getItem('__loads') || '[]'); } catch (e) {}
            all.push({ path: location.pathname, prerender: saw || !!document.getElementById('prerender'), seen: seen.join(' '), at: Date.now() });
            try { sessionStorage.setItem('__loads', JSON.stringify(all)); } catch (e) {}
          });
        })();"#;

        fn loads(c: &Chromium) -> Vec<Value> {
            c.js("JSON.parse(sessionStorage.getItem('__loads') || '[]')").as_array().cloned().unwrap_or_default()
        }

        /// The console's built bundle, where one is given.
        fn bundle() -> Option<std::path::PathBuf> {
            let dir = std::path::PathBuf::from(std::env::var("GUNNAR_UI_BUNDLE").ok().filter(|d| !d.is_empty())?);
            dir.join("index.html").is_file().then_some(dir)
        }

        #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
        async fn a_guest_and_a_signed_in_person_open_a_deep_link_in_the_console() {
            let Some(chrome) = chrome() else { return };
            let Some(dist) = bundle() else {
                eprintln!("NOT RUN: no built console bundle (set GUNNAR_UI_BUNDLE to a trunk dist with index.html)");
                return;
            };
            let (app, passkeys, _feed) = console_waiting(FakeGunnar::new().with_docs(), Some(GUEST_SEED), None, None, None, None, Some(dist)).await;
            on(&app, &passkeys).await;
            let session = passkeys.make_session("root", crate::role::Role::Admin, true);
            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("a loopback port");
            let base = format!("http://localhost:{}", listener.local_addr().expect("its address").port());
            let served = app.clone();
            tokio::spawn(async move { axum::serve(listener, served.into_make_service_with_connect_info::<std::net::SocketAddr>()).await });
            let deep = format!("{base}/{NS}/{PUBLIC}/blob/docs/v2/README.md");
            tokio::task::spawn_blocking(move || {
                let _one = ONE_BROWSER.lock().unwrap_or_else(|p| p.into_inner());
                let c = Chromium::launch(&chrome);
                c.on_page("Page.addScriptToEvaluateOnNewDocument", json!({ "source": LOADS }));
                let mut red: Vec<String> = Vec::new();
                let drawn = "(() => !document.getElementById('prerender') && !!document.querySelector('header.topbar') && (document.body.textContent || '').includes('Edition'))()";

                // ── a guest: the deep link, drawn by the console itself ──
                c.open(&deep);
                c.until("the console drew the file for a guest", drawn);
                let first = loads(&c);
                let raw = c.js("fetch(location.href).then(r => r.text())");
                if !raw.as_str().is_some_and(|t| t.contains("<main id=\"prerender\">") && t.contains("Edition")) {
                    red.push(format!("the address's document does not carry the pre-rendered part: {raw}"));
                }
                if first.first().map(|l| l["prerender"].clone()) != Some(json!(true)) {
                    red.push(format!("the guest's document did not carry the pre-rendered part as it loaded: {first:?}"));
                }
                std::thread::sleep(Duration::from_secs(5));
                if loads(&c).len() != 1 {
                    red.push(format!("the guest's page did not settle: {:?}", loads(&c)));
                }
                if c.js("location.pathname") != json!(format!("/{NS}/{PUBLIC}/blob/docs/v2/README.md")) {
                    red.push(format!("the guest was moved off the address: {}", c.js("location.href")));
                }
                let guest_text = c.text();

                // ── signed in: the same address, the full console ──
                c.on_page("Network.setCookie", json!({ "name": crate::auth::SESSION_COOKIE, "value": session, "url": format!("{base}/"), "path": "/", "secure": true, "httpOnly": true, "sameSite": "Strict" }));
                c.js("(() => { sessionStorage.removeItem('__loads'); return true; })()");
                c.open(&deep);
                c.until("the console drew the file signed in", drawn);
                c.until("the full console (Administration) for the signed-in person", "(document.body.innerText || '').includes('Administration')");
                std::thread::sleep(Duration::from_secs(5));
                if loads(&c).len() != 1 {
                    red.push(format!("the signed-in page did not settle: {:?}", loads(&c)));
                }
                if (guest_text.contains("Administration"), true) != (false, true) {
                    red.push("the guest was shown the administration".to_owned());
                }
                assert!(red.is_empty(), "GUN-GUEST in the browser:\n- {}\n--- the page says ---\n{}", red.join("\n- "), c.text());
            })
            .await
            .expect("the browser half");
        }
    }

    /// ★ **No public site page of its own is left**: the old plain-HTML pages are gone; the
    /// site answers only its files (llms.txt, llms-full.txt, sitemap.xml, robots.txt and each
    /// repository's readme.md, tree.txt and issues.md), and the sitemap names the console's
    /// addresses.
    #[tokio::test]
    async fn the_site_serves_only_its_files_and_the_sitemap_names_the_consoles_addresses() {
        let (app, passkeys, _feed, _ui) = console_with_shell(FakeGunnar::new().with_docs()).await;
        on(&app, &passkeys).await;
        for (uri, kind) in [("/llms.txt", "text/plain"), ("/llms-full.txt", "text/markdown"), ("/sitemap.xml", "application/xml"), ("/robots.txt", "text/plain")] {
            let (status, headers, _) = get_full(&app, uri, None).await;
            assert_eq!(status, http::StatusCode::OK, "{uri}");
            assert!(headers[http::header::CONTENT_TYPE].to_str().unwrap().starts_with(kind), "{uri}");
        }
        for leaf in ["readme.md", "tree.txt", "issues.md"] {
            let (status, headers, _) = get_full(&app, &format!("/{NS}/{PUBLIC}/{leaf}"), None).await;
            assert_eq!(status, http::StatusCode::OK, "{leaf}");
            assert!(!headers[http::header::CONTENT_TYPE].to_str().unwrap().starts_with("text/html"), "{leaf}");
        }
        let map = get_full(&app, "/sitemap.xml", None).await.2;
        assert!(map.contains(&format!("/{NS}/{PUBLIC}/blob/docs/v2/README.md</loc>")), "the docs are in the sitemap: {map}");
        assert!(!map.contains("/repo/"), "{map}");
    }
}

/// ★ The new addresses are pages a visitor may be handed (the console's document), and none
/// of the console's or the server's own routes is mistaken for a repository.
#[test]
fn the_new_addresses_are_a_visitors_pages_and_the_consoles_own_routes_are_not() {
    use crate::auth::middleware::{is_anonymous_page, is_anonymous_read};
    let get = http::Method::GET;
    for p in ["/nordisk/gunnar", "/nordisk/gunnar/tree", "/nordisk/gunnar/tree/docs", "/nordisk/gunnar/blob/docs/app.js", "/nordisk/gunnar/commits", "/nordisk/gunnar/issues", "/nordisk/gunnar/issues/1", "/nordisk/gunnar.rs"] {
        assert!(is_anonymous_page(&get, p), "{p}");
    }
    for p in ["/admin/storage", "/admin/audit", "/account/activity", "/usage/nordisk", "/mirrors/import", "/twin-door/restore", "/api/repos/a/b", "/auth/sso/x", "/scim/v2", "/oauth/token", "/login/device", "/signup/x", "/welcome/x", "/private/x", "/health/x", "/mcp/x", "/download/x"] {
        assert!(!is_anonymous_page(&get, p), "{p} is the console's or the server's own");
    }
    #[cfg(feature = "metered")]
    for p in ["/paid/estate/acme", "/cancel/x"] {
        assert!(!is_anonymous_page(&get, p), "{p} is the buy flow's");
    }
    assert!(!is_anonymous_page(&http::Method::POST, "/nordisk/gunnar"));
    // GUN-FIELD-TEST (2026-11-01): a repository's Settings is not a visitor's page: a
    // guest is sent to sign in, and the page draws nothing of it.
    assert!(!is_anonymous_page(&get, "/nordisk/gunnar/settings"), "Settings is not a visitor's page");
    assert!(is_anonymous_read(&get, "/api/repos/nordisk/gunnar/issues"));
    assert!(is_anonymous_read(&get, "/api/repos/nordisk/gunnar/issues/7"));
    for p in ["/api/repos/nordisk/gunnar/issues/7/comment", "/api/repos/nordisk/gunnar/issues/x", "/api/repos/nordisk/gunnar/pulls"] {
        assert!(!is_anonymous_read(&get, p), "{p}");
    }
    assert!(!is_anonymous_read(&http::Method::POST, "/api/repos/nordisk/gunnar/issues"));
}