crates/nornir-build-thing/tests/consequence_per_typ.rs
//! **The consequence of a choice is derived per `typ`, bijectively.**
//!
//! `consequence::*` derives what ENTERS and LEAVES when a setting is toggled, in
//! the space the `typ` decides — the cargo feature/dep CLOSURE for an app
//! variant, the `CONFIG_*` symbols for a kernel — and the INVERSE names the
//! toggle back from the delta, so a consequence can never be an orphan nobody
//! can trace to a setting.
//!
//! Proven RED-first (LAW 2). Landed on `lane/toml-is-emitted` 2026-08-27 and
//! carried onto main by lane AG (2026-08-30) when that branch's OTHER half — a
//! `to_toml_str` / `BuildThing::emit_toml` emitter — was superseded by
//! `nornir_build_thing::emit`, the one writer every repo's recipe crate now
//! calls. The emitter tests went with it; these are the half that had no
//! replacement, and deleting the branch without them would have thrown away the
//! only code in this crate that answers "what does turning this off actually
//! remove".
use std::collections::BTreeSet;
use nornir_build_thing::consequence::{
self, feature_consequence, group_of_symbol, kconfig_consequence, thin_kernel_groups,
toggle_of_features, Consequence,
};
use nornir_build_thing::kernel::{BASE_FRAGMENT, HARDENED_FRAGMENT};
use nornir_build_thing::variant::FeatureTable;
/// A feature graph modelling gunnar's measured wireguard edge: `wireguard`
/// enables `boringtun` and `wg_appliance_core`; the appliance base also carries
/// `dhcp`. This is the DECLARED edge (gunnar's `[build] offers` + feature deps),
/// so the closure walk over it is the same one gunnar's own build resolves.
fn wireguard_graph() -> (FeatureTable, Vec<String>) {
let mut features = std::collections::BTreeMap::new();
features.insert(
"wireguard".to_string(),
vec!["boringtun".to_string(), "wg_appliance_core".to_string()],
);
features.insert("boringtun".to_string(), vec![]);
features.insert("wg_appliance_core".to_string(), vec![]);
features.insert("dhcp".to_string(), vec![]);
let ft = FeatureTable { features, default: vec![] };
let base = vec!["wireguard".to_string(), "dhcp".to_string()];
(ft, base)
}
/// **cargo typ**: turning `wireguard` OFF removes exactly `boringtun` and
/// `wg_appliance_core` from the closure — the measured `70 → 0` symbol-family
/// collapse, at the feature-graph level that causes it. RED-first: an empty
/// consequence (the collapse everyone claims but nobody derived) fails the
/// non-vacuity assertion below.
#[test]
fn cargo_consequence_of_dropping_wireguard() {
let (ft, base) = wireguard_graph();
let delta = feature_consequence(&ft, &base, "wireguard", false, true);
assert!(!delta.is_empty(), "dropping wireguard changed nothing — the consequence is vacuous");
assert_eq!(
delta.leaves,
BTreeSet::from(["boringtun".to_string(), "wg_appliance_core".to_string(), "wireguard".to_string()]),
"off must remove wireguard and its whole closure",
);
assert!(delta.enters.is_empty(), "turning a feature off brings nothing in");
// dhcp is untouched — the consequence is SCOPED to the toggle, not the world.
assert!(!delta.leaves.contains("dhcp"), "dhcp is not part of the wireguard closure");
}
/// **The inverse**: given the delta, name the toggle that caused it — `wireguard`
/// and nothing else. This is what "bijective" buys: the consequence traces back
/// to its choice.
#[test]
fn cargo_consequence_names_its_own_toggle() {
let (ft, base) = wireguard_graph();
let candidates = vec!["wireguard".to_string(), "dhcp".to_string()];
let delta = feature_consequence(&ft, &base, "wireguard", false, true);
let recovered = toggle_of_features(&ft, &base, &candidates, false, &delta);
assert_eq!(recovered.as_deref(), Some("wireguard"), "the delta did not name its cause");
// A delta no candidate produces is refused, not mis-attributed.
let invented = Consequence { enters: BTreeSet::new(), leaves: BTreeSet::from(["nonsense".to_string()]) };
assert_eq!(
toggle_of_features(&ft, &base, &candidates, false, &invented),
None,
"an invented delta must resolve to no toggle, not the nearest one",
);
}
/// **kernel typ** — a DIFFERENT consequence space, on purpose. The six groups
/// that made the thin appliance kernel runnable each flip named `CONFIG_*`
/// symbols in/out of the merged `.config` (and thus the `bzImage`). RED-first:
/// if `merge_kconfig` dropped the base fragment, none of these would enter and
/// the non-vacuity assertion fails.
#[test]
fn kernel_consequence_of_the_thin_kernel_groups() {
// The full base contract vs an empty config (tinyconfig leaves all six off).
let delta = kconfig_consequence(&[BASE_FRAGMENT], &[]);
assert!(!delta.is_empty(), "the base fragment enabled no symbols — merge_kconfig is dead");
// Every symbol the six groups name must ENTER when the base fragment is applied.
for (handle, syms) in thin_kernel_groups() {
for s in *syms {
assert!(
delta.enters.contains(*s),
"group '{handle}' names {s}, but it did not enter the merged config",
);
}
}
// The precise six that let the appliance boot — proc-fs and memfd in particular,
// the two whose absence made `/data` invisible and `korp.*` tokens unreadable.
for s in ["CONFIG_PROC_FS", "CONFIG_MEMFD_CREATE", "CONFIG_SHMEM", "CONFIG_TMPFS",
"CONFIG_FILE_LOCKING", "CONFIG_EPOLL", "CONFIG_FUTEX"] {
assert!(delta.enters.contains(s), "{s} did not enter");
}
}
/// **The kernel inverse**: a symbol in the delta names the group it belongs to —
/// the kernel counterpart of naming the cargo toggle. This is law-D shaped: the
/// fragment's consequence traces back to the decision.
#[test]
fn kernel_consequence_names_its_group() {
assert_eq!(group_of_symbol("CONFIG_PROC_FS"), Some("kernel.proc-fs"));
assert_eq!(group_of_symbol("CONFIG_EVENTFD"), Some("kernel.event-syscalls"));
assert_eq!(group_of_symbol("CONFIG_MEMFD_CREATE"), Some("kernel.memfd-create"));
assert_eq!(group_of_symbol("CONFIG_NONSENSE"), None, "an unowned symbol names no group");
}
/// The hardened overlay is its OWN consequence over the base — a second kernel
/// delta, showing the space composes: `merge([base, hardened])` enables strictly
/// more than `merge([base])`, and the extra symbols are the overlay's.
#[test]
fn hardened_overlay_only_adds() {
let base = consequence::enabled_symbols(&nornir_build_thing::kernel::merge_kconfig(&[BASE_FRAGMENT]));
let hard = consequence::enabled_symbols(&nornir_build_thing::kernel::merge_kconfig(&[
BASE_FRAGMENT,
HARDENED_FRAGMENT,
]));
let delta = Consequence::between(&base, &hard);
// The overlay is a real consequence: it changes the merged config. A
// hardening profile may legitimately UNSET a base symbol (that is what
// hardening often is), so `leaves` is allowed to be non-empty — what is not
// allowed is a no-op overlay claiming to harden while adding and removing
// nothing.
assert!(
!delta.is_empty(),
"the hardened overlay changed nothing over base — it is not a consequence at all",
);
assert!(
!delta.enters.is_empty(),
"the hardened overlay enabled no new symbol over base",
);
}