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

nordisk/gunnar

crates/gunnar-recipe/tests/airgap_rows.rs

//! **AIRGAP: the release rows turn on, by name, exactly the outbound surfaces each live site
//! needs** (owner rule 2026-10-25: the default build is the airgap install, and a live or
//! sold box keeps today's behaviour at its next release).
//!
//! Every outbound surface that left a default build is listed here with the rows that name it
//! and the medium switch that turns it on. A row gaining one, or losing one, is a decision, so
//! this test names it: change the list here in the same commit as the row.

use gunnar_recipe::GunnarBuildThing;
use nornir_build_thing::plugin::BuildThing as _;

/// `(feature, the rows that name it)`. Rows not listed must not name it.
const OUTBOUND: &[(&str, &[&str])] = &[
    // Forge import: GitHub, a GitLab (gunnar-ui-server `forges`).
    (
        "forges",
        &[
            "gunnar-front",
            "gunnar-front-narrate",
            "gunnar-front-metered",
            "gunnar-front-metered-narrate",
        ],
    ),
    // estate-clock's HTTPS Date sources (gunnar-server and gunnar-front `clock-fetch`).
    (
        "clock-fetch",
        &[
            "gunnar-server",
            "gunnar-server-narrate",
            "gunnar-server-thin",
            "gunnar-server-metered",
            "gunnar-server-metered-narrate",
            "gunnar-server-metered-thin",
            "gunnar-front",
            "gunnar-front-narrate",
            "gunnar-front-metered",
            "gunnar-front-metered-narrate",
        ],
    ),
    // The boot's DNS question for github.com (gunnar-server and gunnar-front `resolver-probe`).
    (
        "resolver-probe",
        &[
            "gunnar-server",
            "gunnar-server-narrate",
            "gunnar-server-thin",
            "gunnar-server-metered",
            "gunnar-server-metered-narrate",
            "gunnar-server-metered-thin",
            "gunnar-front",
            "gunnar-front-narrate",
            "gunnar-front-metered",
            "gunnar-front-metered-narrate",
        ],
    ),
    // The pull channel to the vendor's monetize (gunnar-server `fleet-pull`): estate rows only.
    (
        "fleet-pull",
        &[
            "gunnar-server",
            "gunnar-server-narrate",
            "gunnar-server-thin",
            "gunnar-server-metered",
            "gunnar-server-metered-narrate",
            "gunnar-server-metered-thin",
        ],
    ),
    // The front's ACME client (gunnar-front `acme`).
    (
        "acme",
        &[
            "gunnar-front",
            "gunnar-front-narrate",
            "gunnar-front-metered",
            "gunnar-front-metered-narrate",
        ],
    ),
    // The front's egress door for its boxes (gunnar-front `egress-door`).
    (
        "egress-door",
        &[
            "gunnar-front",
            "gunnar-front-narrate",
            "gunnar-front-metered",
            "gunnar-front-metered-narrate",
        ],
    ),
    // The console's invite mail (gunnar-front `mail`; the relay's configuration is its switch).
    (
        "mail",
        &[
            "gunnar-front",
            "gunnar-front-narrate",
            "gunnar-front-metered",
            "gunnar-front-metered-narrate",
        ],
    ),
    // gunnar.rs's public face (IndexNow behind GUNNAR_UI_PUBLIC_INDEXNOW_KEY).
    (
        "public-site",
        &["gunnar-front-metered", "gunnar-front-metered-narrate"],
    ),
    // Outbound webhooks: no row ships them.
    ("webhooks", &[]),
    // The MCP connector: no row ships it yet.
    ("mcp", &[]),
];

/// `(medium, the switches its boot parameters must carry)`.
const SWITCHES: &[(&str, &[&str])] = &[
    (
        "gunnar-front-installer-open",
        &[
            "korp.env.GUNNAR_UI_FORGES=on",
            "korp.env.GUNNAR_FRONT_ACME=on",
            "korp.env.GUNNAR_FRONT_EGRESS=on",
        ],
    ),
    (
        "gunnar-front-installer-metered",
        &[
            "korp.env.GUNNAR_UI_FORGES=on",
            "korp.env.GUNNAR_FRONT_ACME=on",
            "korp.env.GUNNAR_FRONT_EGRESS=on",
        ],
    ),
];

/// **The CLI's gRPC hole is own-network unless the SITE says `public`** (RA1-1, then RA1-14):
/// no medium carries the hole's reach any more. It rode the metered front medium, which every
/// sold estate's front is built from too, so they all opened it. Now the ONE box feeds
/// `front_cli_hole = public` to the one site whose declaration says `cli_hole_public`
/// (gunnar.rs), and gunnar-front reads it off the feed (`web::hole::FEED_CLI_HOLE`).
#[test]
fn no_medium_opens_the_hole_the_site_feed_does() {
    let set = GunnarBuildThing.variants();
    for m in &set.installers {
        let hole: Vec<&String> = m
            .boot_params
            .iter()
            .filter(|p| p.contains("GUNNAR_FRONT_HOLE"))
            .collect();
        assert!(
            hole.is_empty(),
            "{} sets the hole on its medium: {hole:?}",
            m.name
        );
    }
    let front = std::fs::read_to_string(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../gunnar-front/src/web/hole.rs"
    ))
    .expect("gunnar-front's hole");
    assert!(
        front.contains(r#"pub const FEED_CLI_HOLE: &str = "front_cli_hole";"#)
            && front.contains(r#"pub const REACH_PUBLIC: &str = "public";"#),
        "gunnar-front reads another word"
    );
    assert!(
        !front.contains("std::env::var(HOLE_ENV)"),
        "the reach is the site's feed, not the medium's environment"
    );
}

#[test]
fn every_row_names_exactly_its_outbound_features() {
    let set = GunnarBuildThing.variants();
    for (feature, rows) in OUTBOUND {
        for v in &set.variants {
            let names = v.features.iter().any(|f| f == feature);
            let listed = rows.contains(&v.name.as_str());
            assert_eq!(
                names,
                listed,
                "row {} {} `{feature}`: {:?}",
                v.name,
                if names { "names" } else { "lacks" },
                v.features
            );
        }
        for r in *rows {
            assert!(set.variants.iter().any(|v| v.name == *r), "no row {r}");
        }
    }
}

#[test]
fn every_front_medium_switches_on_what_its_row_builds() {
    let set = GunnarBuildThing.variants();
    for (medium, switches) in SWITCHES {
        let m = set
            .installers
            .iter()
            .find(|i| i.name == *medium)
            .unwrap_or_else(|| panic!("no medium {medium}"));
        for s in *switches {
            assert!(
                m.boot_params.iter().any(|p| p == s),
                "{medium} does not carry {s}: {:?}",
                m.boot_params
            );
        }
    }
}

#[test]
fn no_box_medium_switches_forge_import_on() {
    let set = GunnarBuildThing.variants();
    for m in &set.installers {
        if SWITCHES.iter().any(|(n, _)| *n == m.name) {
            continue;
        }
        assert!(
            !m.boot_params.iter().any(|p| p.contains("GUNNAR_UI_FORGES")
                || p.contains("GUNNAR_FRONT_ACME")
                || p.contains("GUNNAR_FRONT_EGRESS")),
            "{} sets a front switch",
            m.name
        );
    }
}

#[test]
fn gunnar_front_forwards_forges_and_leaves_it_off_by_default() {
    let m = std::fs::read_to_string(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../gunnar-front/Cargo.toml"
    ))
    .expect("gunnar-front's manifest");
    assert!(
        m.lines()
            .any(|l| l.trim() == r#"forges = ["console", "gunnar-ui-server/forges"]"#),
        "gunnar-front has no forges feature"
    );
    let default = m
        .lines()
        .find(|l| l.starts_with("default = "))
        .expect("a default line");
    assert!(!default.contains("forges"), "{default}");
}