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}");
}