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

nordisk/gunnar

xtask/tests/default_tree.rs

//! **The plain default is small and holds one version of each crate** (owner order
//! 2026-10-22).
//!
//! `cargo build --release` at the root (and `cargo install gunnar`) builds the
//! `gunnar` package with its default features: a server that starts and answers
//! SSH and HTTPS (TLS on the HTTP port, `--http-tls-self` or `--http-tls-cert`).
//! Measured on 3aa3acd it resolved 399 crates, 47 of them names in two versions
//! (mostly the RustCrypto 0.10 line beside russh's current one). This holds the
//! dedup: every name still in two versions is listed with the crate that pins
//! the other one, and company SSO (OIDC + SAML) and mimalloc stay opt-in.

use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::process::Command;

fn root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("xtask has a parent")
        .to_path_buf()
}

/// The names allowed in two versions in the default, each with the pin outside
/// our control that keeps the second one.
const KNOWN_PAIRS: &[(&str, &str)] = &[
    ("base64", "tonic 0.14 and jsonwebtoken pin 0.22; ureq 3 and arrow-cast pin 0.23"),
    ("generic-array", "russh 0.63 links 1.x, which re-exports 0.14 itself"),
    ("getrandom", "ring 0.17 is on 0.2, ahash (arrow) on 0.3, the RustCrypto line on 0.4"),
    ("num-bigint", "der-parser 10 (x509-parser 0.18) is on 0.4; russh and arrow on 0.5"),
    ("russh", "the twin's push (replication, in `cargo install gunnar`'s default since GUN-PRODUCTS) dials through nornir-git 0.2.2 on russh 0.63.3 beside gunnar-ssh's 0.64.1; one again once nornir-git 0.2.3 is published (OSSF-NEXT)"),
    ("redb", "znippy-plugin-git links 2 and 4; gunnar-core's 2 -> 4 is its own round (migration + drill)"),
    ("syn", "proc macros still on 2 beside serde/clap/tokio on 3"),
    ("untrusted", "aws-lc-rs is on 0.7, ring and rustls-webpki on 0.9"),
];

/// Never in the plain default.
/// The public site (with IndexNow and its crawler lists), the MCP connector and the forge
/// import are never in it either (owner rule 7, 2026-10-26): they are the console's, behind
/// default-off features (`crates/gunnar-recipe/tests/airgap_default_builds.rs` holds the front).
/// Nor is an outbound HTTP client (A32-18, owner rule 7): `ureq` is the mirror pull's https half
/// (`mirror-http`) and the company-SSO egress (`sso-egress`), both default-off and named by the
/// appliance rows that ship them; and no SMTP client (`lettre`, the resend mail, rides
/// `fleet-pull`).
const OPT_IN: &[&str] = &[
    "nornir-auth-oidc",
    "nornir-auth-saml",
    "jsonwebtoken",
    "mimalloc",
    "monetize-embed",
    "gunnar-public-site",
    "gunnar-mcp",
    "gunnar-forge",
    "ureq",
    "lettre",
];

/// Pure: (name -> versions) out of `cargo tree --prefix none --format {p}` lines.
fn versions(tree: &str) -> BTreeMap<String, BTreeSet<String>> {
    let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
    for line in tree.lines() {
        let mut parts = line.split_whitespace();
        if let (Some(name), Some(version)) = (parts.next(), parts.next()) {
            out.entry(name.to_owned())
                .or_default()
                .insert(version.to_owned());
        }
    }
    out
}

#[test]
fn versions_reads_names_in_two_versions() {
    let v = versions("a v1.0.0\nb v0.1.0 (proc-macro)\na v2.0.0 (*)\nc v0.1.0 (/x/c)\n");
    assert_eq!(v["a"].len(), 2);
    assert_eq!(v["b"].len(), 1);
    assert_eq!(v.len(), 3);
}

#[test]
fn the_plain_default_is_deduplicated_and_has_no_opt_in_crate() {
    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned());
    let out = Command::new(&cargo)
        .current_dir(root())
        .args([
            "tree", "-p", "gunnar", "-e", "normal", "--prefix", "none", "--format", "{p}",
        ])
        .output()
        .expect("running cargo tree");
    assert!(
        out.status.success(),
        "cargo tree -p gunnar: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let v = versions(&String::from_utf8_lossy(&out.stdout));
    assert!(v.len() > 200, "the tree read only {} crates", v.len());
    let known: BTreeSet<&str> = KNOWN_PAIRS.iter().map(|(n, _)| *n).collect();
    let doubled: Vec<String> = v
        .iter()
        .filter(|(n, vs)| vs.len() > 1 && !known.contains(n.as_str()))
        .map(|(n, vs)| format!("{n} {vs:?}"))
        .collect();
    assert!(
        doubled.is_empty(),
        "names in two versions in the plain default, beyond the known pins:\n  {}",
        doubled.join("\n  ")
    );
    let opt_in: Vec<&str> = OPT_IN
        .iter()
        .copied()
        .filter(|n| v.contains_key(*n))
        .collect();
    assert!(
        opt_in.is_empty(),
        "opt-in crates in the plain default: {opt_in:?}"
    );
}

/// The crates company SSO's protocols link (`gunnar-grpc/sso`, edda nornir-identity-sso's
/// `oidc` + `saml`).
const SSO_PROTOCOLS: &[&str] = &["nornir-auth-oidc", "nornir-auth-saml", "jsonwebtoken"];

fn tree_of(args: &[&str]) -> BTreeMap<String, BTreeSet<String>> {
    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned());
    let out = Command::new(&cargo)
        .current_dir(root())
        .args([
            "tree", "-e", "normal", "--prefix", "none", "--format", "{p}",
        ])
        .args(args)
        .output()
        .expect("running cargo tree");
    assert!(
        out.status.success(),
        "cargo tree {args:?}: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    versions(&String::from_utf8_lossy(&out.stdout))
}

/// ★ **Company SSO is its own default-off feature on the server too** (A34-15): gunnar-server's
/// default (and its `server` marker) links no SSO protocol; `sso` turns them on, and
/// `sso-egress`, which every appliance row names, brings `sso` with it.
#[test]
fn the_servers_default_has_no_company_sso_and_sso_turns_it_on() {
    let plain = tree_of(&["-p", "gunnar-server"]);
    let linked: Vec<&str> = SSO_PROTOCOLS
        .iter()
        .copied()
        .filter(|n| plain.contains_key(*n))
        .collect();
    assert!(
        linked.is_empty(),
        "gunnar-server's default links company SSO: {linked:?}"
    );
    let marker = tree_of(&[
        "-p",
        "gunnar-server",
        "--no-default-features",
        "--features",
        "server,engine-znippy",
    ]);
    let linked: Vec<&str> = SSO_PROTOCOLS
        .iter()
        .copied()
        .filter(|n| marker.contains_key(*n))
        .collect();
    assert!(
        linked.is_empty(),
        "gunnar-server's `server` marker turns company SSO on: {linked:?}"
    );
    for features in ["sso", "sso-egress"] {
        let on = tree_of(&["-p", "gunnar-server", "--features", features]);
        let missing: Vec<&str> = SSO_PROTOCOLS
            .iter()
            .copied()
            .filter(|n| !on.contains_key(*n))
            .collect();
        assert!(
            missing.is_empty(),
            "gunnar-server --features {features} lacks {missing:?}"
        );
    }
}