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

nordisk/edda

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