crates/gunnar-recipe/tests/dev_narrate_symbols.rs
//! **A RELEASE build contains no narrate sender at all — read off the symbol
//! table, and held by the declaration** (DEV-NARRATE, owner + coordinator
//! 2026-09-30).
//!
//! The dev medium streams a box's boot log, sealed, over udp/51822 (Skidbladnir
//! `nordisk-narrate`). The promise to the owner is that the channel is a DEV
//! tool: a release binary does not DECLINE to narrate, it does not CONTAIN a
//! narrator. Two halves, as `metered_symbols.rs` proves for entitlement:
//!
//! 1. **The artifact.** `gunnar-server` built with the release rows' switches
//! has ZERO `nordisk_narrate` symbols; built with `dev-narrate` it has the
//! sender — or the clean table above proves nothing. Same reader as
//! `OptionAudit` (`linkage::demangled_symbols`); a stripped binary is
//! UNMEASURED and fails.
//! 2. **The declaration.** Only the four `…-narrate` rows name the feature; no
//! crate's `default` list does; every medium's `dev-narrate` option defaults
//! OFF and forbids `nordisk_narrate` (so the composer's own symbol audit
//! refuses an OFF medium that carries it).
use std::path::PathBuf;
use std::process::Command;
use gunnar_recipe::recipe::{DEV_NARRATE_FEATURE, DEV_NARRATE_FORBIDDEN, DEV_NARRATE_OPTION};
use nornir_build_thing::linkage::{count_families, demangled_symbols};
use nornir_build_thing::plugin::BuildThing;
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("two below the root")
.to_path_buf()
}
/// Build `gunnar-server` with exactly `features`; its path from cargo's own
/// `compiler-artifact` message (never a guessed target dir).
fn build_server(features: &str) -> PathBuf {
let out = Command::new(env!("CARGO"))
.current_dir(workspace_root())
.args([
"build",
"-q",
"--message-format=json-render-diagnostics",
"-p",
"gunnar-server",
"--bin",
"gunnar-server",
"--no-default-features",
"--features",
features,
])
.output()
.expect("cargo runs");
assert!(
out.status.success(),
"cargo build --features {features} failed ({}):\n{}",
out.status,
String::from_utf8_lossy(&out.stderr)
.lines()
.rev()
.take(40)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n")
);
let stdout = String::from_utf8_lossy(&out.stdout);
let bin = stdout
.lines()
.filter(|l| l.contains(r#""compiler-artifact""#) && l.contains(r#""name":"gunnar-server""#))
.filter_map(|l| {
const KEY: &str = r#""executable":""#;
let i = l.find(KEY)? + KEY.len();
let rest = &l[i..];
Some(PathBuf::from(&rest[..rest.find('"')?]))
})
.next_back()
.unwrap_or_else(|| {
panic!("cargo reported no executable for gunnar-server --features {features}")
});
assert!(bin.is_file(), "no binary at {}", bin.display());
bin
}
fn narrate_symbols(features: &str) -> Vec<(String, usize)> {
let bin = build_server(features);
let symbols = demangled_symbols(&bin).unwrap_or_else(|| {
panic!(
"{} has no readable symbol table: UNMEASURED is not a pass",
bin.display()
)
});
assert!(
symbols.iter().any(|s| s.contains("gunnar_server::")),
"{} names no gunnar_server code: a clean read would be vacuous",
bin.display()
);
let c = count_families(
&symbols,
&[DEV_NARRATE_FORBIDDEN, "gunnar_server::dev_narrate"],
);
println!(
"symbol audit --features {features}: {} symbols; found {:?}; glue-only {:?}",
symbols.len(),
c.found,
c.glue_only
);
let mut all = c.found;
all.extend(c.glue_only);
all
}
#[test]
fn the_release_server_has_zero_narrate_symbols_and_the_dev_one_has_the_sender() {
// The release rows' switches that matter here (the tunnel and DHCP the
// sender rides beside), host target, dev profile — sequential, one path.
let release = narrate_symbols("server,engine-znippy,dhcp-client,wg-client");
assert!(release.is_empty(), "a RELEASE gunnar-server CONTAINS narrate code: {release:?}. Off means never compiled in, not declining to send");
let dev = narrate_symbols("server,engine-znippy,dhcp-client,wg-client,dev-narrate");
let names: Vec<&str> = dev.iter().map(|(f, _)| f.as_str()).collect();
for family in [DEV_NARRATE_FORBIDDEN, "gunnar_server::dev_narrate"] {
assert!(names.contains(&family), "the DEV gunnar-server has no `{family}` symbols, so the release table's emptiness proves nothing: {dev:?}");
}
}
#[test]
fn only_the_narrate_rows_name_the_feature_and_every_medium_defaults_it_off() {
let set = gunnar_recipe::GunnarBuildThing.variants();
let mut narrate_rows = Vec::new();
for v in &set.variants {
if v.features.iter().any(|f| f == DEV_NARRATE_FEATURE) {
assert!(
v.name.ends_with("-narrate") && !v.is_default,
"`{}` names `{DEV_NARRATE_FEATURE}` and is not a `…-narrate` dev row",
v.name
);
narrate_rows.push(v.name.clone());
} else {
assert!(
!v.name.ends_with("-narrate"),
"`{}` is called a narrate row and does not build the feature",
v.name
);
}
}
narrate_rows.sort();
assert_eq!(
narrate_rows,
[
"gunnar-front-metered-narrate",
"gunnar-front-narrate",
"gunnar-server-metered-narrate",
"gunnar-server-narrate"
]
);
let mut media = Vec::new();
for i in &set.installers {
for o in i.options.iter().filter(|o| o.name == DEV_NARRATE_OPTION) {
assert!(!o.default, "`{}` turns DEV-NARRATE on by default", i.name);
assert_eq!(o.feature, DEV_NARRATE_FEATURE);
assert!(o.forbid_symbols.iter().any(|s| s == DEV_NARRATE_FORBIDDEN), "`{}`'s option does not forbid `{DEV_NARRATE_FORBIDDEN}` when off, so the composer's audit cannot refuse a release medium that carries it", i.name);
media.push(i.name.clone());
}
}
media.sort();
assert_eq!(
media,
[
"gunnar-front-installer-metered",
"gunnar-front-installer-open",
"gunnar-installer-metered",
"gunnar-installer-open"
],
"the estate's four media carry the option, and no other"
);
// No crate's `default` list reaches for it: a `cargo build` that says
// nothing must build no sender.
for krate in [
"crates/gunnar-server/Cargo.toml",
"crates/gunnar-front/Cargo.toml",
] {
let text = std::fs::read_to_string(workspace_root().join(krate)).unwrap();
let default = text
.lines()
.find(|l| l.trim_start().starts_with("default ="))
.unwrap_or("");
assert!(
!default.contains("dev-narrate"),
"{krate}: `{default}` puts DEV-NARRATE in every default build"
);
}
}
/// **ON and OFF both resolve to a DECLARED build point** — the medium's option
/// switched ON builds exactly its payload's `…-narrate` row, OFF exactly the
/// payload row itself, and the payload keeps `dev-narrate` as a SWITCH (open),
/// never a decided feature. MEASURED release-3 test2 mock 2026-09-30 (gunnar
/// 3e374eb): `--opt dev-narrate=on` failed the compose with "feature
/// 'dev-narrate' is both enabled and open" — the resolution left the feature
/// on the open axis after adding it (fixed in edda nornir-build-thing
/// `apply_to_set`, the compose's copy); this holds the recipe's half.
#[test]
fn the_dev_narrate_option_on_and_off_resolves_to_a_declared_row() {
use std::collections::{BTreeMap, BTreeSet};
let set = gunnar_recipe::GunnarBuildThing.variants();
for i in set
.installers
.iter()
.filter(|i| i.options.iter().any(|o| o.name == DEV_NARRATE_OPTION))
{
let app = set
.variants
.iter()
.find(|v| v.name == i.variant)
.unwrap_or_else(|| panic!("{}: no payload row {}", i.name, i.variant));
assert!(
app.open.contains(DEV_NARRATE_FEATURE)
&& !app.features.iter().any(|f| f == DEV_NARRATE_FEATURE),
"{}: `{}` must keep dev-narrate a switch",
i.name,
app.name
);
let narrate = set
.variants
.iter()
.find(|v| v.name == format!("{}-narrate", app.name))
.unwrap_or_else(|| panic!("{}: no `{}-narrate` row", i.name, app.name));
for on in [true, false] {
let supplied: BTreeMap<String, bool> = [
("dhcp".to_string(), true),
("wireguard".to_string(), true),
(DEV_NARRATE_OPTION.to_string(), on),
]
.into_iter()
.filter(|(n, _)| i.options.iter().any(|o| &o.name == n))
.collect();
let r = nornir_build_thing::options::resolve_options(&i.options, &i.params, &supplied)
.unwrap_or_else(|e| panic!("{} dev-narrate={on}: {e:#}", i.name));
let got: BTreeSet<String> = r.app_features(app).into_iter().collect();
let want: BTreeSet<String> = if on {
narrate.features.iter().cloned().collect()
} else {
app.features.iter().cloned().collect()
};
assert_eq!(
got, want,
"{} dev-narrate={on} resolves to no declared row",
i.name
);
}
}
}