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

nordisk/gunnar

crates/gunnar-cli/tests/plain_cargo.rs

//! **The two plain-cargo builds, guarded from the resolved graph.**
//!
//! Owner order 2026-10-12: the `gunnar` client and the core `gunnar-server`
//! must each build with an ordinary `cargo build` / `cargo install`, as well as
//! be cells of the Build Thing (`.nornir/build-variants.toml`). The cells pass
//! exactly these cargo arguments, so guarding the cargo graph guards both:
//!
//! ```text
//!   cargo build --release -p gunnar-cli --no-default-features --features client   → gunnar
//!   cargo build --release -p gunnar-server                                         → gunnar-server
//! ```
//!
//! Three things are held here, each from `cargo tree -e normal` (what a shipped
//! binary links; dev- and build-dependencies never reach it):
//!
//! 1. the client stays SMALL — a crate-count ceiling just above what it measured
//!    after the 2026-10-12 cut (see `crates/gunnar-cli/README.md`, "Size");
//! 2. the client links NO server crate (store, server, wire, ssh server, gRPC,
//!    monetize, UI);
//! 3. core `gunnar-server` (its default features) links no monetize, no UI and
//!    no shop.
//!
//! Every "absent" assertion has a non-vacuity partner: the graph must contain
//! the crate that makes it a real graph, because an empty `cargo tree` output
//! contains no forbidden crate either.

use std::collections::BTreeSet;
use std::process::Command;

/// Ceiling on the `gunnar` client's distinct normal-dependency packages
/// (name + version), counted as [`packages`] counts them.
///
/// MEASURED 2026-10-12: 442 before the cut, 389 after (nornir-git asked for gix
/// with its default features and the reqwest/rustls https transport the client
/// never reaches). Raise it only together with the README's "Size" table, and
/// say there what came in and why.
const CLIENT_CEILING: usize = 395;

fn tree(package: &str, extra: &[&str]) -> String {
    let manifest = concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml");
    let out = Command::new(env!("CARGO"))
        .args(["tree", "--manifest-path", manifest, "-p", package])
        .args(extra)
        .args(["-e", "normal", "--prefix", "none"])
        .output()
        .expect("cargo tree runs");
    assert!(
        out.status.success(),
        "cargo tree failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let text = String::from_utf8_lossy(&out.stdout).into_owned();
    assert!(
        text.lines().any(|l| l.starts_with(&format!("{package} v"))),
        "cargo tree printed no {package} at all, so every 'not found' below would be vacuous:\n{text}"
    );
    text
}

/// Distinct `name version` pairs in a `--prefix none` tree.
fn packages(tree: &str) -> BTreeSet<String> {
    tree.lines()
        .filter_map(|l| {
            let mut it = l.split_whitespace();
            Some(format!("{} {}", it.next()?, it.next()?))
        })
        .collect()
}

fn names(tree: &str) -> BTreeSet<String> {
    packages(tree)
        .into_iter()
        .map(|p| p.split(' ').next().unwrap().to_string())
        .collect()
}

fn client() -> String {
    tree(
        "gunnar-cli",
        &["--no-default-features", "--features", "client"],
    )
}

#[test]
fn the_client_stays_under_its_crate_ceiling() {
    let all = packages(&client());
    assert!(
        all.len() > 200,
        "only {} packages: this is not the client graph, the count below would mean nothing",
        all.len()
    );
    assert!(
        all.len() <= CLIENT_CEILING,
        "the `gunnar` client links {} packages, past its ceiling of {CLIENT_CEILING}. Something \
         came in; find it with `cargo tree -p gunnar-cli --no-default-features --features client \
         -e normal -i <crate>`, and either cut it or raise the ceiling with the README's Size \
         table. Packages:\n{}",
        all.len(),
        all.into_iter().collect::<Vec<_>>().join("\n")
    );
}

#[test]
fn the_client_links_no_server_crate() {
    let names = names(&client());
    // Non-vacuity: this IS the client — git, the transport, the crypto.
    for needed in [
        "gunnar-client",
        "gunnar-crypt",
        "nornir-git",
        "russh",
        "gix",
        "age",
    ] {
        assert!(
            names.contains(needed),
            "the client graph lacks {needed}: {names:?}"
        );
    }
    let forbidden: Vec<&String> = names
        .iter()
        .filter(|n| {
            matches!(
                n.as_str(),
                "gunnar-server"
                    | "gunnar-store"
                    | "gunnar-core"
                    | "gunnar-wire"
                    | "gunnar-ssh"
                    | "gunnar-grpc"
                    | "gunnar-http"
                    | "gunnar-front"
                    | "gunnar-policy"
                    | "gunnar-pairing-door"
                    | "gunnar-monetize-guest"
                    | "tonic"
                    // the https transport nornir-git used to drag in unasked
                    | "reqwest"
                    | "hyper"
                    | "rustls"
            ) || n.starts_with("monetize")
                || n.starts_with("gunnar-ui")
                || n.starts_with("znippy")
                || n.starts_with("storage-git")
        })
        .collect();
    assert!(
        forbidden.is_empty(),
        "the `gunnar` client links server-side or unneeded crates: {forbidden:?}"
    );
}

#[test]
fn core_gunnar_server_links_no_monetize_ui_or_shop() {
    let names = names(&tree("gunnar-server", &[]));
    // Non-vacuity: a git server with its storage engine and SSH door.
    for needed in ["gunnar-store", "gunnar-ssh", "gunnar-wire"] {
        assert!(
            names.contains(needed),
            "core gunnar-server lacks {needed}: {names:?}"
        );
    }
    let forbidden: Vec<&String> = names
        .iter()
        .filter(|n| {
            n.starts_with("monetize")
                || n.starts_with("gunnar-ui")
                || n.contains("shop")
                || matches!(
                    n.as_str(),
                    "gunnar-front" | "gunnar-monetize-guest" | "plugin-door-wire"
                )
        })
        .collect();
    assert!(
        forbidden.is_empty(),
        "`cargo build -p gunnar-server` (default features) links money, UI or shop crates: \
         {forbidden:?}. Those belong to gunnar-front / the `metered` feature, never to the core \
         server."
    );
}