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/machine_named.rs

//! **Which installer rows name their machine, which do not, and the MEASURED
//! reason the ones that do not cannot yet.**
//!
//! # What this file said until 2026-09-08, and what the base did about it
//!
//! For a month four rows carried `preset: None` and this file measured why.
//! The block had two halves and a coda:
//!
//! 1. `upcloud`'s `MediumBring::IsoCdrom` derived `target.buses ∋ ide-cdrom`,
//!    which put `CONFIG_ATA=y` on the shared donor `tunnr/tunnr-kernel` — the
//!    `nuked` preset, whose fragment says `# CONFIG_ATA is not set` — and
//!    `KernelBuild::apply_requirements` refuses a positive requirement over a
//!    preset veto instead of merging it. The build did not get a bigger kernel;
//!    it stopped.
//! 2. `CONFIG_SERIAL_8250` and `CONFIG_SERIAL_EARLYCON` flipped `y → n`,
//!    because `target.consoles` owned the DRIVER and UpCloud's only console is
//!    VNC — on a medium whose own `boot_params` say `console=ttyS0,115200`.
//! 3. and naming the machine would not even silence the composer's gap
//!    sentence: `cloud_medium_gap` answered "no writer in `image_writer`
//!    produces that container" for a NAMED ISO-attach machine, against its own
//!    doc and against korp-installer's composer, which both already said such a
//!    row owes no import container.
//!
//! **All three were base defects and all three are fixed** (edda `84a281a`,
//! `0625555`, `1f86a1f`):
//!
//! * `MediumBring::kernel_medium_bus()` separates the PLATFORM fact (how the
//!   medium is handed over) from the KERNEL fact (which driver must read it).
//!   Every arm is `None` today, and UpCloud is why: the medium rides TWICE —
//!   as an IDE cdrom the SeaBIOS firmware boots, and as a virtio disk Linux and
//!   `korp-installer` read the ISO-9660 payload off — so the kernel needs no
//!   ATA at all;
//! * `target.uarts` owns `SERIAL_8250` + `SERIAL_EARLYCON` (a COM port is
//!   hardware); `target.consoles` keeps `SERIAL_8250_CONSOLE`. UpCloud HAS
//!   uarts — ttyS1/ttyS2 at 0x2f8/0x3e8 — and only ttyS0 is absent, so the old
//!   veto was written for "no port" and implemented as "no console";
//! * `cloud_medium_gap` answers `None` for a named ISO-attach machine.
//!
//! # So the block is gone — except it is not, and the remaining half is NEW
//!
//! `Target::preset` is the ONE writer of the whole group, so naming a machine
//! replaces every field of the declaration with the machine's derivation. Three
//! of those fields are kernel facts and one is not:
//!
//! ```text
//! firmware: SeaBios · consoles: [Vga] · uarts: [Com2, Com3]
//! buses: [VirtioBlk] · nic: Virtio · nics: 2 · cpu: Epyc
//! ```
//!
//! `firmware` is not a Kconfig fact at all. It is what
//! `InstallerBuild::installer_iso` turns into `IsoFormat`, and
//! `korp-installer::isocompose` lays the EFI El-Torito section only when
//! `firmware.lays_uefi()`. `both → seabios` therefore does not narrow a kernel;
//! **it deletes a section of the medium.** MEASURED below: the ISO format of
//! all three UpCloud rows goes `Uefi → Bios`.
//!
//! That is not a hypothetical rig. `bench/runs/mother-2026-09-05T02-02` booted
//! `.iso-out/gunnar-installer-open.iso` under OVMF 4M three days ago;
//! the install guide boots the same medium `--target
//! qemu-local`, which is UEFI/OVMF; `.nornir/gunnar-quickstart.md` tells a
//! reader whose VM is BIOS to switch it to UEFI; and the SEALED row is the
//! medium that gets MAILED, to UEFI clouds included. The repo's own recorded
//! ruling for this exact field is one sentence in
//! the reasoning archive: *"UpCloud (SeaBIOS, IDE cdrom, no
//! UART, 2 NICs) is the shape this open medium is deployed to; `both` keeps the
//! OVMF path for the rig."*
//!
//! **THE RULING (2026-09-08): the four rows stay silent, for a DIFFERENT and
//! smaller reason than before.** The kernel is no longer the obstacle — the
//! donor accepts the machine and the shipped bzImage does not move by a byte
//! (measured two ways below). What is left is that `target.preset` overwrites
//! `target.firmware`, and `target.firmware` is a fact about the MEDIUM, not
//! about the machine. Trading a UEFI boot for a log sentence is the same bad
//! bargain the old ruling refused, so the answer is the same and the reason is
//! new: this is the third instance of one category error — a platform attach
//! read as a kernel driver, an unwatched port read as a missing one, and now a
//! machine's firmware read as a medium's sections — and it belongs upstream
//! beside the other two, not worked around here.
//!
//! Each test below fails the day its half of that stops being true, and says
//! what to do.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use gunnar_recipe::GunnarBuildThing;
use nornir_build_thing::iso::IsoFormat;
use nornir_build_thing::kernel::KernelBuild;
use nornir_build_thing::plugin::BuildThing;
use nornir_build_thing::target::Target;
use nornir_build_thing::variant::BuildVariantSet;

/// The machine the three UpCloud rows WOULD name. Named once so the blocks
/// below and the tripwires cannot drift.
const WOULD_BE: &str = "upcloud";

/// The machine the GCE row would name — a DIFFERENT question with a different
/// answer, which is why it is a separate const and a separate test.
const GCE_WOULD_BE: &str = "gce-virtio";

/// The donor the three UpCloud rows share, and its preset — `tunnr/.nornir/
/// build-variants.toml`, `[[build.kernel]] name = "tunnr-kernel"`, and the pin
/// beside the built bzImage says the same.
const DONOR: (&str, &str) = ("tunnr-kernel", "nuked");

/// `fragment_sha256` from `tunnr/.kernel-out/tunnr-kernel/tunnr-kernel.kernel-pin.json`
/// — the merged Kconfig fragment the LIVE bzImage was actually built from.
///
/// Re-pinned 2026-10-27 (lane OX-RED) from [`PRE_ONE_USER_MOVE_FRAGMENT_SHA256`]: nornir-build-thing
/// 0.2.6 moved `# CONFIG_MULTIUSER is not set` from the `nuked` preset into the appliance-boot
/// contract. The fragment has the same lines in another order, so the built config and the
/// bzImage do not move ([`PINNED_CONFIG_SHA256`] and [`PINNED_BZIMAGE_SHA256`] stay); a kernel
/// built from today's recipe records this value in its pin.
const PINNED_FRAGMENT_SHA256: &str =
    "16a1ec4c265bad8b981ff10e78c923714bf65a7b118d855e6262d80c3c8689e2";

/// The fragment the 2026-09-22 kernel was built from, before nornir-build-thing 0.2.6 moved the
/// one-user line (see `the_pin_moved_only_by_the_one_user_line_nornir_build_thing_0_2_6_moved`).
const PRE_ONE_USER_MOVE_FRAGMENT_SHA256: &str =
    "43bfafdfe8d054ee5f137a777889d69aa5553686769cd7d9d56bbba91c4d8e46";

/// Re-pinned 2026-09-22 to the installer-CD kernel (tunnr agent/installer-reads-cd 3f7c2ff:
/// ATA+PIIX+AHCI+SR in the donor closure, and `variants/kernel/tunnr-hardening.config`, which
/// the fragment test merges last). Built through `--build kernel` for `gunnar-installer-metered`
/// with the pin's own toggles (`--opt dhcp=on --opt wireguard=off`, the set `kernel.why.toml`
/// beside the pin records) in the pinned toolchain 3f632569 / sha256:78eaa55a, which reproduces
/// again since edda ad4dfe7 hands podman the recipe's SOURCE_DATE_EPOCH. bzImage 2 651 136 ->
/// 2 778 112 B (+126 976).
/// The two hashes the same pin records for the artifacts. Nothing here
/// may move either one; the tests below are how that is checked without
/// building anything.
const PINNED_CONFIG_SHA256: &str =
    "d9af650d6b1b65d3eb90ca2d1b92d05b91902d0a5ba5d7c1a4a80c3b9a856483";
const PINNED_BZIMAGE_SHA256: &str =
    "42e3257e20f3eea62a4bf930067da5cac280423d31a33878bd966787dd07b5df";

/// **The EXACT delta naming `upcloud` puts on the donor's merged fragment.**
///
/// Two lines, both added, none removed and none flipped — and both are already
/// `=y` in the shipped `kernel.config` as `kconfig-derived`, so
/// `merge_config.sh -m` + `olddefconfig` produce the same file. That is the
/// whole proof that the kernel does not move, and it is asserted twice: against
/// the shipped config's own text, and against the pinned fragment reproduced
/// byte-for-byte.
const MACHINE_ADDS: &[&str] = &["CONFIG_CPU_SUP_AMD=y", "CONFIG_VIRTIO_PCI_LEGACY=y"];

/// The rows that carry `preset: None` today, and the one sentence each.
const BLOCKED: &[(&str, &str)] = &[
    (
        gunnar_recipe::SEALED_INSTALLER,
        "the MAILED medium: `both` is what lets one ISO boot under OVMF and SeaBIOS alike",
    ),
    (
        gunnar_recipe::OPEN_INSTALLER,
        "the keyless twin of that medium — and the one the OVMF bench rig actually booted \
         (bench/runs/mother-2026-09-05T02-02)",
    ),
    (
        gunnar_recipe::GCE_INSTALLER,
        "hand-spelled virtio-scsi; `gce-virtio` is UEFI-only, and UEFI-only + \
         `medium_boot = hybrid` is a composer refusal BY NAME",
    ),
    (
        gunnar_recipe::METERED_INSTALLER,
        "the row live on UpCloud, and the row the composer named — the same medium as the \
         open row, so the same EFI section",
    ),
    (
        gunnar_recipe::FRONT_INSTALLER,
        "vm1's own medium, derived from the open row: the same machine and the same EFI \
         section as the gunnar medium it stands beside (2026-09-25)",
    ),
    (
        gunnar_recipe::FRONT_METERED_INSTALLER,
        "the paid front medium, the open front row with the metered payload: the same \
         machine and EFI section (2026-09-25)",
    ),
    // The twin appliance had a row here from 2026-09-15 to 2026-09-17. It has
    // none now: it installs the row of the primary it twins, and gets its role over the wire, so its
    // machine IS that row's machine and there is nothing separate to name.
];

fn set() -> BuildVariantSet {
    GunnarBuildThing.variants()
}

fn row(set: &BuildVariantSet, name: &str) -> nornir_build_thing::installer::InstallerBuild {
    set.installers
        .iter()
        .find(|i| i.name == name)
        .unwrap_or_else(|| panic!("gunnar declares an installer row {name:?}"))
        .clone()
}

/// The same set with ONE row's `target` replaced by a bare machine name — what
/// "name the preset" would literally be, and nothing else.
fn named(subject: &str, machine: &str) -> BuildVariantSet {
    let mut set = set();
    for i in set.installers.iter_mut() {
        if i.name == subject {
            i.target = Target {
                preset: Some(machine.to_string()),
                ..Target::default()
            };
        }
    }
    set
}

/// The three rows whose machine IS UpCloud: the sealed medium, its keyless twin
/// and the metered twin of that. One medium, three doors.
const UPCLOUD_ROWS: &[&str] = &[
    gunnar_recipe::SEALED_INSTALLER,
    gunnar_recipe::OPEN_INSTALLER,
    gunnar_recipe::METERED_INSTALLER,
];

/// **The roster of named and un-named rows, BY NAME.**
///
/// A count would pass a repo that swapped one row for another. The names make
/// the diff say which row changed, and adding an installer variant fails here
/// with the new name in the output — the lesson `appliance_reachable.rs` learned
/// when `left: 4 / right: 3` told two days' worth of readers nothing.
#[test]
fn every_installer_row_either_names_its_machine_or_is_on_the_measured_block_list() {
    let set = set();
    let mut named: Vec<(&str, String)> = Vec::new();
    let mut silent: Vec<&str> = Vec::new();
    for i in &set.installers {
        match &i.target.preset {
            Some(p) => named.push((i.name.as_str(), p.clone())),
            None => silent.push(i.name.as_str()),
        }
    }
    named.sort();
    silent.sort();

    let mut expect_silent: Vec<&str> = BLOCKED.iter().map(|(n, _)| *n).collect();
    expect_silent.sort();
    assert_eq!(
        silent, expect_silent,
        "a row without a machine must be on the block list with its reason, and a row that \
         LEFT the list must have gained a preset — see this file's header"
    );

    // The two clouds gunnar has never booted on are the two that name a
    // machine. Until 2026-09-07 the difference was `ide-cdrom`; it is not any
    // more (the base stopped deriving a kernel bus from a platform attach).
    // What still separates them is the FIRMWARE: both are UEFI machines, so
    // naming them keeps `lays_uefi()` and costs the medium no section.
    assert_eq!(
        named
            .iter()
            .map(|(n, p)| (*n, p.as_str()))
            .collect::<Vec<_>>(),
        vec![
            (gunnar_recipe::AWS_INSTALLER, "aws-nitro"),
            (gunnar_recipe::AZURE_INSTALLER, "azure-gen2"),
            (gunnar_recipe::METERED_AWS_INSTALLER, "aws-nitro"),
            (gunnar_recipe::METERED_AZURE_INSTALLER, "azure-gen2"),
        ],
        "the named rows and the machines they name"
    );
    for (name, machine) in &named {
        let p = nornir_build_thing::target::presets::preset(machine)
            .unwrap_or_else(|| panic!("{name} names {machine}, which is not in the roster"));
        let t = p
            .target()
            .unwrap_or_else(|_| panic!("{machine} has a spellable target group"));
        assert!(
            t.firmware.lays_uefi(),
            "{name} names {machine}, whose firmware lays no EFI section — that row would hit \
             the same block the four silent ones do"
        );
    }
    assert!(
        !named.is_empty() && !silent.is_empty(),
        "if either half emptied, this test stopped distinguishing anything"
    );
}

/// **HALF ONE OF THE OLD BLOCK IS GONE, and this is the measurement.**
///
/// The `nuked` donor now ACCEPTS `preset = "upcloud"`, and the closure it
/// accepts differs from today's by exactly [`MACHINE_ADDS`] — two additions,
/// zero removals, zero flips. `CONFIG_ATA` does not appear on either side, and
/// `CONFIG_SERIAL_8250` stays `Y` on both, which are the two symbols the old
/// ruling was written about.
///
/// This test is the reason the header's ruling is about firmware and nothing
/// else. It goes RED if the donor starts refusing again (the old defect
/// returning) or if the delta grows (a machine fact reaching the kernel that
/// was not measured here), and either way the header is what to re-read.
#[test]
fn the_donor_accepts_the_machine_now_and_the_closure_delta_is_exactly_two_symbols() {
    let subject = gunnar_recipe::METERED_INSTALLER;
    let before = kconfig_rows(&set(), subject);
    let after = kconfig_rows(&named(subject, WOULD_BE), subject);

    // The two symbols the old block was argued over, on BOTH sides.
    assert!(
        !before.contains_key("CONFIG_ATA") && !after.contains_key("CONFIG_ATA"),
        "the ide-cdrom derivation is gone: a platform attach is not a kernel driver \
         (before: {before:#?}, after: {after:#?})"
    );
    for side in [&before, &after] {
        assert_eq!(
            side.get("CONFIG_SERIAL_8250").map(String::as_str),
            Some("Y"),
            "UpCloud HAS uarts (ttyS1/ttyS2 at 0x2f8/0x3e8) — only ttyS0 is absent, and the \
             ports own the driver now: {side:#?}"
        );
    }

    // The whole delta, as a set difference, so a new symbol on either side
    // names itself in the failure instead of hiding in a count.
    let added: Vec<String> = after
        .iter()
        .filter(|(k, v)| before.get(*k) != Some(*v))
        .map(|(k, v)| format!("{k}={v}"))
        .collect();
    let removed: Vec<String> = before
        .iter()
        .filter(|(k, v)| after.get(*k) != Some(*v))
        .map(|(k, v)| format!("{k}={v}"))
        .collect();
    assert_eq!(
        added,
        vec![
            "CONFIG_CPU_SUP_AMD=Y".to_string(),
            "CONFIG_VIRTIO_PCI_LEGACY=Y".to_string()
        ],
        "the machine adds its AMD vendor half and virtio-pci's legacy half, and nothing else"
    );
    // ── THE ONE REQUIREMENT THAT LEAVES, AND WHY IT IS NOT A LOST SYMBOL ────
    //
    // `ApplianceBootContract` steps aside for every symbol a DECLARED `target`
    // group can name (the vocabulary rule), and `SERIAL_8250_CONSOLE` is in
    // that vocabulary. UpCloud's `consoles = [Vga]` then declines to speak
    // inside it — VNC is the only console there — so the contract's
    // `"printk to ttyS0"` row has nobody to re-state it and the REQUIREMENT
    // disappears.
    //
    // The base's own writer says that is the one place silence is correct:
    // "`SERIAL_8250_CONSOLE` on a machine no `console=` points at a UART on,
    // where base.config's `=y` is inert". It is load-bearing that
    // `kconfig/base.config` really does carry the line, because THIS medium's
    // `boot_params` say `console=ttyS0,115200` and the KVM rigs read the
    // `INSTALLER:` narration off it — a kernel that dropped the symbol would
    // boot dark on every rig while UpCloud, which never reads it, stayed fine.
    // `the_pinned_fragment_reproduces_…` is where that is proven: the merged
    // fragment keeps the line, so the requirement leaving costs the artifact
    // nothing.
    //
    // Pinned as the EXACT list rather than waved past, so a SECOND symbol
    // going quiet the same way names itself here instead of arriving as a dark
    // boot on a rig nobody ran that week.
    assert_eq!(
        removed,
        vec!["CONFIG_SERIAL_8250_CONSOLE=Y".to_string()],
        "exactly one requirement steps aside, and only because base.config states it anyway"
    );

    // And the donor's own verdict, by name, on the real closure.
    let reqs = nornir_build_thing::kernel_requires::requirements_of_variant(
        &named(subject, WOULD_BE),
        "gunnar",
        subject,
        &BTreeMap::new(),
    )
    .expect("the closure resolves")
    .expect("the row has a kernel edge");
    let mut donor = KernelBuild {
        name: DONOR.0.to_string(),
        preset: DONOR.1.to_string(),
        profile: "hardened".to_string(),
        ..Default::default()
    };
    donor.apply_requirements(&reqs).unwrap_or_else(|e| {
        panic!(
            "THE OLD BLOCK IS BACK: the `{}` donor refuses the machine again — {e:#}. Re-read \
             this file's header; the base regression is upstream, not here.",
            DONOR.1
        )
    });
}

/// **HALF TWO — THE BLOCK THAT IS LEFT, and it is about the MEDIUM.**
///
/// `Target::preset` is the one writer of the group, so naming `upcloud`
/// overwrites `firmware: both` with the machine's `seabios`.
/// `InstallerBuild::installer_iso` turns that into `IsoFormat::Bios`, and
/// `korp-installer::isocompose` lays the EFI El-Torito section only when
/// `firmware.lays_uefi()`. The medium loses a section.
///
/// The legacy half SURVIVES (`lays_bios()` stays true either way), which is
/// what makes this a one-way loss rather than a trade: `both` is a superset of
/// `seabios`, so declaring the machine's firmware buys the medium nothing at
/// all and costs it every UEFI rig it is proven on — the OVMF bench run of
/// 2026-09-05, `installtest --target qemu-local`, and every UEFI cloud the
/// SEALED medium is mailed to.
///
/// **The day this goes RED is the day the base separates the two facts** — a
/// machine's firmware is what it CAN start, a medium's sections are what it
/// carries, and a medium is allowed to carry more than one machine needs. When
/// that lands, name the preset on all three rows in [`UPCLOUD_ROWS`] and delete
/// this test.
#[test]
fn naming_the_machine_deletes_the_medias_efi_section_and_that_is_the_block_now() {
    for subject in UPCLOUD_ROWS {
        let today = row(&set(), subject);
        assert_eq!(
            today.installer_iso().format,
            IsoFormat::Uefi,
            "{subject} lays an EFI El-Torito section today"
        );
        assert!(
            today.target.effective().firmware.lays_bios()
                && today.target.effective().firmware.lays_uefi(),
            "{subject} declares firmware `both`, which is both sections"
        );

        let would_be = row(&named(subject, WOULD_BE), subject);
        assert_eq!(
            would_be.installer_iso().format,
            IsoFormat::Bios,
            "THE BLOCK IS GONE: naming {WOULD_BE} on {subject} no longer narrows the medium's \
             El-Torito shape. Read this file's header and name the preset on every row in \
             UPCLOUD_ROWS."
        );
        assert!(
            !would_be.target.effective().firmware.lays_uefi(),
            "{subject}: the EFI section is what the machine's `seabios` word deletes"
        );
        assert!(
            would_be.target.effective().firmware.lays_bios(),
            "{subject}: the legacy half survives — which is why this is a pure loss and not a \
             trade, and why `both` is the honest word for a medium that is mailed"
        );
    }
}

/// **The GCE row is refused from the OTHER side, by its own `medium_boot`.**
///
/// `-gce` is the one row whose machine is not UpCloud, so it gets its own
/// evidence rather than the block list's shared sentence. `gce-virtio` is the
/// honest candidate — GCE Persistent Disk really is virtio-scsi, which is why
/// this row exists at all — and it is refused for the MIRROR of the reason the
/// UpCloud rows are:
///
/// * `gce-virtio.firmware = Uefi`, so `lays_bios()` goes `true → false`;
/// * this row carries `medium_boot = hybrid`, and `korp-installer::isocompose`
///   ensures BY NAME that a hybrid medium has a real legacy-BIOS stage: *"a
///   55 AA boot sector chainloading nothing is worse than none"*. So naming the
///   machine does not degrade the medium here, it REFUSES the compose;
/// * and the machine's `buses = [virtio-scsi]` alone drops `VIRTIO_BLK` +
///   `VIRTIO_PCI` from the closure — the bus the local proof rig and the
///   `-drive` path actually use, which this row hand-spells for that reason.
///
/// `upcloud` on this row is not a candidate at all and is measured anyway, so
/// the file cannot be read as having only considered one machine: it drops the
/// SCSI stack this row exists to carry, which is the MEASURED 2026-08-30
/// failure `INSTALLER: FAILED hwdetect: no usable target disk`.
#[test]
fn the_gce_row_is_refused_by_its_own_hybrid_word_and_by_the_bus_it_exists_for() {
    let subject = gunnar_recipe::GCE_INSTALLER;
    let today = row(&set(), subject);
    assert!(
        today.medium_boot.is_hybrid(),
        "the GCE medium is hybrid — that is what makes a UEFI-only machine a refusal here"
    );
    assert!(today.target.effective().firmware.lays_bios());

    let gce = row(&named(subject, GCE_WOULD_BE), subject);
    assert!(
        !gce.target.effective().firmware.lays_bios(),
        "THE BLOCK IS GONE: {GCE_WOULD_BE} no longer takes the legacy stage off a hybrid \
         medium. Re-read this file's header."
    );

    // The bus half, off the real closure.
    let before = kconfig_rows(&set(), subject);
    let after = kconfig_rows(&named(subject, GCE_WOULD_BE), subject);
    for sym in ["CONFIG_VIRTIO_BLK", "CONFIG_VIRTIO_PCI"] {
        assert_eq!(before.get(sym).map(String::as_str), Some("Y"));
        assert_eq!(
            after.get(sym),
            None,
            "{sym} is the bus the local proof rig attaches; the machine's own group names \
             only virtio-scsi and would take it off: {after:#?}"
        );
    }
    // And the machine that is NOT this row's, for completeness.
    let wrong = kconfig_rows(&named(subject, WOULD_BE), subject);
    for sym in ["CONFIG_SCSI", "CONFIG_BLK_DEV_SD", "CONFIG_SCSI_VIRTIO"] {
        assert_eq!(before.get(sym).map(String::as_str), Some("Y"));
        assert_eq!(
            wrong.get(sym),
            None,
            "{sym} is what this row is FOR; `{WOULD_BE}` is a different cloud's disk and \
             takes it away: {wrong:#?}"
        );
    }
}

/// **The composer's gap sentence, and the half of it the base fixed.**
///
/// `cloud_medium_gap`'s doc says `None` means the row can derive a container
/// "(or that it is an ISO-attach machine and does not need one)", and
/// `korp-installer`'s composer says the same. Until 2026-09-07 the code did
/// neither for a NAMED ISO-attach machine — it answered "no writer in
/// `image_writer` produces that container", so naming `upcloud` swapped one gap
/// for another. It now answers `None`.
///
/// Pinned from both sides: the un-named row still gaps (which is the sentence
/// the composer prints on every real build, and the reason this file exists),
/// and the named row does not. If the second assertion ever fails the base
/// regressed; if the FIRST one fails, the composer stopped asking and the whole
/// block list needs re-reading.
#[test]
fn a_named_iso_attach_machine_now_owes_no_import_container() {
    let set = set();
    let mut r = row(&set, gunnar_recipe::METERED_INSTALLER);

    let today = r
        .cloud_medium_gap()
        .expect("a hybrid medium with no machine cannot have a container derived");
    assert!(today.contains("names no machine"), "{today}");
    assert!(today.contains("Name the preset."), "{today}");

    r.target = Target {
        preset: Some(WOULD_BE.to_string()),
        ..Target::default()
    };
    assert_eq!(
        r.cloud_medium_gap(),
        None,
        "THE BASE REGRESSED: an ISO-attach machine owes no import container — that is what \
         `cloud_medium_gap`'s own doc and korp-installer's composer both say, and what edda \
         0625555 made the code say too"
    );
}

/// **THE KERNEL DOES NOT MOVE — proof one, off the shipped `.config` itself.**
///
/// The two symbols [`MACHINE_ADDS`] would add are ALREADY `=y` in the config
/// the live bzImage was built from, as `kconfig-derived` rows (nothing asked
/// for them; `olddefconfig` produced them). A requirement that repeats a line
/// the merge already has changes no byte, so naming the machine cannot move
/// `config_sha256` off [`PINNED_CONFIG_SHA256`] and cannot move the bzImage off
/// [`PINNED_BZIMAGE_SHA256`].
///
/// The three symbols the OLD ruling was argued over are asserted beside them,
/// because they are what says the base fix was the right one: the shipped
/// kernel has no ATA (so the derivation that demanded it described some other
/// medium), it HAS the 8250 (so the derivation that switched it off was wrong
/// about a machine with real ports), and it reads ISO-9660 (off the VIRTIO copy
/// of the medium, since it cannot see an IDE cdrom at all — which is why that
/// symbol belongs to the boot contract and not to a bus row).
///
/// Read from tunnr's checkout beside this one. A fresh clone has no tunnr, so
/// the absence is a PRINTED SKIP naming the path — never a silent pass.
#[test]
fn the_shipped_kernel_already_has_both_symbols_the_machine_would_add() {
    let Some(config) = read_tunnr(&kernel_config_path()) else {
        return;
    };
    for line in MACHINE_ADDS {
        assert!(
            config.lines().any(|l| l.trim() == *line),
            "{line} is already in the shipped config — that is why naming the machine cannot \
             move {PINNED_CONFIG_SHA256} or {PINNED_BZIMAGE_SHA256}"
        );
    }
    // 2026-09-22 (tunnr 3f7c2ff, "the installer's own CD is a block device"): the donor
    // closure now GRANTS ATA — PIIX + AHCI + SR — because the installer reads its own cdrom
    // rather than only the virtio copy. The old row asserted the opposite ("no ATA"), which
    // was true of the kernel before that ruling and is the fact this pin moved.
    for line in [
        "CONFIG_ATA=y",
        "CONFIG_ATA_PIIX=y",
        "CONFIG_SATA_AHCI=y",
        "CONFIG_BLK_DEV_SR=y",
    ] {
        assert!(
            config.lines().any(|l| l.trim() == line),
            "{line}: the installer reads its OWN cdrom since tunnr 3f7c2ff, so the shipped \
             kernel carries the ATA path — the medium no longer rides only as virtio"
        );
    }
    assert!(
        config.lines().any(|l| l.trim() == "CONFIG_SERIAL_8250=y"),
        "and it HAS the 8250, which the old console-owned derivation would have switched off"
    );
    assert!(
        config.lines().any(|l| l.trim() == "CONFIG_ISO9660_FS=y"),
        "and it reads ISO-9660 — the contract's row, not a bus driver's, or every consumer \
         that named a machine would silently lose it"
    );
}

/// **THE KERNEL DOES NOT MOVE — proof two: the pinned fragment, reproduced.**
///
/// This is a PIN CHECK and not a build. It re-derives the merged Kconfig
/// fragment from three declarations — gunnar's recipe (the closure), the donor's
/// preset + profile, and tunnr's committed `variants/kernel/tunnr-hardening.config`
/// merged last — and holds it to [`PINNED_FRAGMENT_SHA256`], the value in the
/// pin beside the live bzImage. Reproducing that hash is what makes the second
/// half admissible: the same construction with `preset = "upcloud"` differs
/// from the pinned bytes by exactly [`MACHINE_ADDS`], as ADDED LINES, with
/// nothing removed.
///
/// The toggles are the ones the shipped kernel was actually built with, and
/// they are not a guess: `kernel.why.toml` beside the pin lists the features
/// `appliance-boot`, `data.fs`, `data.io` and `dhcp` — and no `wireguard`, which
/// is `--opt wireguard=off`. Hashing the fragment for any other option set would
/// reproduce nothing, and a test that cannot reproduce the pin proves nothing
/// about the artifact.
///
/// Why hash the FRAGMENT rather than diff the built config: the fragment is the
/// only input this repo can compute, and it is the input the pin itself keys on
/// (`ledger_key`). A `fragment_sha256` that moves is a REBUILD — which is a real
/// cost and is reported — but a fragment that gains only lines the built config
/// already satisfies rebuilds to the same bytes.
#[test]
fn the_pinned_fragment_reproduces_and_the_machine_adds_exactly_two_lines() {
    use sha2::{Digest, Sha256};

    let Some(user) = read_tunnr(&hardening_closure_path()) else {
        return;
    };
    let subject = gunnar_recipe::METERED_INSTALLER;
    let fragment = |set: &BuildVariantSet| -> String { merged_fragment_of(set, &user) };
    let sha = |s: &str| {
        Sha256::digest(s.as_bytes())
            .iter()
            .map(|b| format!("{b:02x}"))
            .collect::<String>()
    };

    let today = fragment(&set());
    assert_eq!(
        sha(&today),
        PINNED_FRAGMENT_SHA256,
        "this test reproduces the LIVE kernel's fragment or it measures nothing. If this is \
         the only assertion failing, the recipe moved a kernel fact and the appliance's pin \
         is stale — check `tunnr/.kernel-out/tunnr-kernel/tunnr-kernel.kernel-pin.json` \
         before believing anything below it."
    );

    let would_be = fragment(&named(subject, WOULD_BE));
    let added: Vec<&str> = would_be
        .lines()
        .filter(|l| !today.lines().any(|t| t == *l))
        .collect();
    let removed: Vec<&str> = today
        .lines()
        .filter(|l| !would_be.lines().any(|w| w == *l))
        .collect();
    assert_eq!(
        added, MACHINE_ADDS,
        "the machine's whole cost to the fragment"
    );
    assert!(
        removed.is_empty(),
        "nothing may LEAVE the fragment: a line that disappears is a symbol whose value the \
         base config then decides on its own — {removed:?}"
    );
}

/// The donor's merged Kconfig fragment for the metered installer row of `set`, with the pin's
/// own toggles (`--opt dhcp=on --opt wireguard=off`, off the why-index beside the pin) and
/// tunnr's committed hardening file (`user`) merged last.
fn merged_fragment_of(set: &BuildVariantSet, user: &str) -> String {
    let toggles: BTreeMap<String, bool> =
        [("dhcp".to_string(), true), ("wireguard".to_string(), false)]
            .into_iter()
            .collect();
    let reqs = nornir_build_thing::kernel_requires::requirements_of_variant(
        set,
        "gunnar",
        gunnar_recipe::METERED_INSTALLER,
        &toggles,
    )
    .expect("the closure resolves")
    .expect("the row has a kernel edge");
    let mut donor = KernelBuild {
        name: DONOR.0.to_string(),
        preset: DONOR.1.to_string(),
        profile: "hardened".to_string(),
        ..Default::default()
    };
    donor
        .apply_requirements(&reqs)
        .expect("the donor grants the closure");
    nornir_build_thing::kernel::merged_fragment(&donor, Some(user)).expect("the fragment merges")
}

/// **Why the pin moved on 2026-10-27, and that the kernel did not.**
///
/// [`PRE_ONE_USER_MOVE_FRAGMENT_SHA256`] is the fragment the 2026-09-22 kernel was built from.
/// nornir-build-thing 0.2.6 (edda lane HOLGER-MEDIA, 2026-10-23) moved ONE line,
/// `# CONFIG_MULTIUSER is not set`, out of the `nuked` preset into the appliance-boot contract,
/// so a consumer that runs containers can lift it by name. The fragment keeps every line it had
/// and gains none; only that line's place moves (after `# CONFIG_INET6_ESP is not set` in the
/// preset, into the sorted closure block now). `merge_config.sh` sets each symbol once whatever
/// its place, so the built `.config` and bzImage are the same; the pin's `fragment_sha256` (the
/// ledger key) is what moves. Put the line back in its old place and the old pin reproduces.
#[test]
fn the_pin_moved_only_by_the_one_user_line_nornir_build_thing_0_2_6_moved() {
    use sha2::{Digest, Sha256};

    let Some(user) = read_tunnr(&hardening_closure_path()) else {
        return;
    };
    let sha = |s: &str| {
        Sha256::digest(s.as_bytes())
            .iter()
            .map(|b| format!("{b:02x}"))
            .collect::<String>()
    };
    let today = merged_fragment_of(&set(), &user);
    let one_user = "# CONFIG_MULTIUSER is not set";
    assert_eq!(
        today.lines().filter(|l| *l == one_user).count(),
        1,
        "the one-user line, once"
    );

    let mut old: Vec<&str> = today.split('\n').filter(|l| *l != one_user).collect();
    let at = old
        .iter()
        .position(|l| *l == "# CONFIG_INET6_ESP is not set")
        .expect("the preset's IPv6 block")
        + 1;
    old.insert(at, one_user);
    let old = old.join("\n");
    assert_eq!(
        sha(&old),
        PRE_ONE_USER_MOVE_FRAGMENT_SHA256,
        "the one line in its old place is the 2026-09-22 fragment"
    );

    let mut a: Vec<&str> = today.lines().collect();
    let mut b: Vec<&str> = old.lines().collect();
    a.sort_unstable();
    b.sort_unstable();
    assert_eq!(
        a, b,
        "the same lines: no symbol came or went, so the built kernel is the same"
    );
    assert_ne!(
        PINNED_FRAGMENT_SHA256, PRE_ONE_USER_MOVE_FRAGMENT_SHA256,
        "the pin follows the fragment nornir-build-thing 0.2.6 merges"
    );
}

/// The kconfig rows one consumer places on its kernel edge, as `symbol → Y|N`.
fn kconfig_rows(set: &BuildVariantSet, variant: &str) -> BTreeMap<String, String> {
    let closure =
        nornir_build_thing::requires::closure_of_consumer(set, "gunnar", variant, &BTreeMap::new())
            .expect("the closure resolves")
            .expect("the row declares edges");
    let mut out = BTreeMap::new();
    for sc in closure.by_edge.values() {
        let Some(k) = sc.as_kconfig() else { continue };
        for (symbol, rows) in &k.rows {
            for (value, _) in rows {
                out.insert(symbol.clone(), format!("{value:?}"));
            }
        }
    }
    out
}

/// The sibling `tunnr` checkout — the donor's repo, beside this one.
fn tunnr_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(Path::parent)
        .expect("crates/gunnar-recipe has two ancestors")
        .parent()
        .expect("the checkout has a parent directory")
        .join("tunnr")
}

/// `<tunnr>/.kernel-out/tunnr-kernel/kernel.config` — the config of the bzImage
/// the pin beside it names, which is the kernel on the live box.
fn kernel_config_path() -> PathBuf {
    tunnr_root().join(".kernel-out/tunnr-kernel/kernel.config")
}

/// `<tunnr>/variants/kernel/tunnr-hardening.config` — the donor's committed veto
/// file, merged LAST into the fragment (`KernelBuild::kconfig_fragment`).
fn hardening_closure_path() -> PathBuf {
    tunnr_root().join("variants/kernel/tunnr-hardening.config")
}

/// Read a file out of the donor's checkout, or PRINT why the assertion below it
/// did not run. A fresh clone has no tunnr; a silent pass would be a green test
/// that measured nothing, which is the one outcome this file cannot afford.
fn read_tunnr(p: &Path) -> Option<String> {
    match std::fs::read_to_string(p) {
        Ok(s) => Some(s),
        Err(e) => {
            eprintln!(
                "machine_named: NO DONOR CHECKOUT — {} could not be read ({e}), so the \
                 artifact half of this block was NOT measured. The declaration half is \
                 asserted by the tests that need no checkout \
                 (`the_donor_accepts_the_machine_now_…`, \
                 `naming_the_machine_deletes_the_medias_efi_section_…`).",
                p.display()
            );
            None
        }
    }
}

/// ★ **A metered medium carries a renew URL, or it does not boot.**
///
/// MEASURED 2026-09-08, as an outage. `gunnar-server` dropped its hardcoded
/// `https://gunnar.rs/billing` default — correctly, because the OPEN product
/// must not ship one operator's commercial URL. But the metered row was
/// not updated with it, so the next medium carried `--monetize-pubkey` and no
/// `--billing-url`. gunnar refuses that pairing at boot, by design; gunnar is
/// PID 1; the refusal was a kernel panic:
///
/// ```text
/// Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000100
/// ```
///
/// Neither half was wrong on its own. What was missing was anything that knew
/// they belong together — the two live in different repos' heads and nothing
/// held the pair. This does.
///
/// It asserts the PAIRING, not the URL: a deployment may put any renew page it
/// likes there, and a self-hoster building the open medium reaches this row not
/// at all. What it must never do is name a key and stay silent about where a
/// refused tenant goes next.
///
/// **It is quantified over the key since 2026-09-20**, because the key stopped
/// being a constant that day: `gunnar_recipe::metered_env` builds both halves
/// from whatever key it is handed, and the pairing has to hold for EVERY key a
/// front can mint, not only for the one that used to be spelled in this file.
/// The pair also moved from the `korp.args` argv to `korp.env.*` — still the
/// kernel command line, still the only channel into a booting gunnar on a
/// keep-upgrade, but now a token a composing door can replace. See
/// `gunnar_recipe::metered_env` for why no other field could carry it.
#[test]
fn metered_env_pairs_the_key_with_a_renew_url() {
    for key in [
        gunnar_recipe::MONETIZE_PUBKEY,
        // A fresh front's key: a different 64 hex characters, which is exactly
        // what `monetize-server` mints every time an estate is laid again.
        "9d8759c3a83f0e4c1b6a2d5f8e07c94b3a61d2f0e8c57b49a3d6120fe4b8c7a51",
    ] {
        let env = gunnar_recipe::metered_env(key);
        assert_eq!(
            env.get(gunnar_recipe::MONETIZE_PUBKEY_ENV)
                .map(String::as_str),
            Some(key),
            "a metered medium built for key {key} must carry that key and no other; \
             a stale one makes the appliance refuse every fact the front signs"
        );
        assert_eq!(
            env.get(gunnar_recipe::BILLING_URL_ENV).map(String::as_str),
            Some(gunnar_recipe::recipe::METERED_BILLING_URL),
            "the metered medium names a monetize key but no renew URL. gunnar refuses \
             that pairing at boot and gunnar is PID 1, so this composes a medium that \
             KERNEL PANICS on first boot — measured 2026-09-08. Both halves come out \
             of ONE writer so that cannot happen: {env:?}"
        );
        assert_eq!(
            env.get(gunnar_recipe::ADMIT_LIMIT_ENV).map(String::as_str),
            Some("none"),
            "a metered medium composed without `admit_limit` must admit as today (PassThrough)"
        );
    }
}

/// ★ **The front's key REACHES the medium — the whole point of the parameter.**
///
/// The declared row carries [`gunnar_recipe::MONETIZE_PUBKEY`] as its default,
/// and `--param monetize_pubkey=<hex>` must beat it. What makes that an
/// override rather than a second opinion is `effective_boot_params`' per-key
/// idempotence: a landed `korp.env.GUNNAR_MONETIZE_PUBKEY=` token is already in
/// `boot_params` when the declared `env` is rendered, so the DEFAULT IS NEVER
/// EMITTED and the medium carries exactly one key token. Asserted on the
/// rendered cmdline, because a medium with two of them is a medium with two
/// answers — and `korp_installer::install::carry_site_tokens` carries the FIRST
/// of a duplicate pair to the installed disk, which would be the default.
#[test]
fn a_supplied_monetize_key_replaces_the_baked_default() {
    let fresh = "9d8759c3a83f0e4c1b6a2d5f8e07c94b3a61d2f0e8c57b49a3d6120fe4b8c7a51";
    let set = gunnar_recipe::emit_recipe::declared();
    let mut cell = set
        .installers
        .iter()
        .find(|i| i.name == gunnar_recipe::METERED_INSTALLER)
        .expect("the metered installer row")
        .clone();

    let supplied: std::collections::BTreeMap<String, String> = [(
        gunnar_recipe::MONETIZE_PUBKEY_PARAM.to_string(),
        fresh.to_string(),
    )]
    .into_iter()
    .collect();
    cell.apply_cli_params(&supplied)
        .expect("the row declares monetize_pubkey");

    let want = format!("korp.env.{}={fresh}", gunnar_recipe::MONETIZE_PUBKEY_ENV);
    let params = cell.effective_boot_params();
    let carried: Vec<&String> = params
        .iter()
        .filter(|p| p.starts_with(&format!("korp.env.{}=", gunnar_recipe::MONETIZE_PUBKEY_ENV)))
        .collect();
    assert_eq!(
        carried,
        vec![&want],
        "exactly ONE monetize key token, and it is the supplied one — the baked \
         default must not also be emitted: {params:?}"
    );
    assert!(
        params.iter().any(|p| p
            == &format!(
                "korp.env.{}={}",
                gunnar_recipe::BILLING_URL_ENV,
                gunnar_recipe::recipe::METERED_BILLING_URL
            )),
        "the renew URL rides beside ANY key, the supplied one included: {params:?}"
    );

    // And with nothing supplied, the declared default is what the medium gets.
    let mut bare = set
        .installers
        .iter()
        .find(|i| i.name == gunnar_recipe::METERED_INSTALLER)
        .expect("the metered installer row")
        .clone();
    bare.apply_cli_params(&std::collections::BTreeMap::new())
        .expect("no parameter is required");
    assert!(
        bare.effective_boot_params().iter().any(|p| p
            == &format!(
                "korp.env.{}={}",
                gunnar_recipe::MONETIZE_PUBKEY_ENV,
                gunnar_recipe::MONETIZE_PUBKEY
            )),
        "a build that names no key still gets the declared default"
    );
}