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

nordisk/holger

xtask/tests/installer_iso_fleet_kvm.rs

//! **The FLEET proof — ONE per-site ISO → N machines, each minting its OWN
//! identity** (appliance brief §4's closing sentence, run literally: "each
//! machine installs, mints its OWN key on P3, and prints its public key for
//! the hub").
//!
//! draupnir is the PLANNER: `plan_fleet` fans one `BootSpec` into N members —
//! the same spec shape whose Redfish arm burns real metal (the fleet
//! wire-shape proof in draupnir's redfish.rs pins those requests). The BOOT
//! leg rides korp-installer's `kvmproof` harness, because draupnir's KVM
//! backend cannot boot an ISO (`ImageSource::suits`: Iso → Redfish only) —
//! the wall is named in draupnir/.nornir/iso-to-fleet.md, per §4's own
//! don't-break-working-proofs clause.
//!
//! Per member: blank disk → install boot (INSTALL-OK) → **FIRST BOOT of the
//! installed disk** (bare-metal semantics: no cdrom, the firmware walks to
//! the ESP's BOOTX64.EFI) → korp-start mounts P3, MINTS, prints the canonical
//! line. The fleet-level assert is the one a single-machine proof cannot
//! make: the N public keys are DISTINCT — an ISO that accidentally baked an
//! identity would hand the whole fleet one key, and this is the test that
//! catches it.
//!
//! Heavy, opt-in:
//! ```text
//! KORP_TEST_KERNEL=/path/to/bzImage \
//!   cargo test -p xtask --test installer_iso_fleet_kvm -- --ignored --nocapture
//! ```

use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;

use draupnir::{plan_fleet, BmcEndpoint, BootSpec};
use korp_installer::isoproof::{
    attach_slirp_nic, free_port, identity_line_count, minted_pubkey, FirstBootProof,
    IsoInstallProof, KvmPreflight, INSTALL_OK,
};
use korp_installer::kvmproof::{boot_disk_until_verdict, gpt_entry, read_fat_file};
use wg_appliance_core::hubfixture::HubFixture;

/// THIS product, everywhere the shared harness needs to know whose ceremony it
/// is running: the rollup row's product, and the `product=` field on the
/// canonical identity line korp-start prints (from the installed cmdline's
/// `korp.entry`). The line's FORMAT is wg-appliance-core's, read back through
/// the harness — never a private prefix constant that can drift from it.
const PRODUCT: &str = "holger";

fn workspace_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .unwrap()
        .to_path_buf()
}

/// **Pack a FRESH `app.znippy` from the tree being tested** — build the static
/// musl `holger-mvp`, then `appliance-pack` it. Called by every proof in this
/// file BEFORE `installer-iso`.
///
/// Exists because of a measured lie (2026-08-15→17): `installer-iso` READS
/// `.znippy-out/holger-app.znippy` and reuses it silently, with no freshness
/// check. The durability fix in `raw.rs::write_relpath` was committed 17:12;
/// the znippy on disk had been packed 16:41 — so the "re-run after the fix"
/// power-cycle proof drove a PRE-FIX binary and reported the fix insufficient.
/// A proof that masters its ISO from a payload it did not pack is measuring
/// whatever happened to be in an output directory, not the tree — the same
/// disease gunnar's ISO cell recorded on 2026-08-05 ("a build input declared
/// inside the output directory"), one level up.
/// ⚠ **AND THE ARGV IS THE DECLARATION'S, NOT THIS FILE'S** (2026-09-21, lane H2).
///
/// This function used to hand-spell `--no-default-features --features
/// mvp-server,appliance`, while the declared `mvp-server-musl` row carries
/// `mvp-server, appliance, auth-passkey, sign, wasm-plugins, meter-guests,
/// fmt-raw, fmt-rust, jobs`. A second writer of a build argv, drifted from the
/// one declaration — the disease the doc above names, one level further in.
///
/// What it cost, MEASURED by running this proof: the payload served **no
/// formats at all**, so every appliance installed from it died at first boot,
///
/// ```text
/// Error: [holger-mvp-1] repository 'bundles' is `ron_repo_type: "raw"` but this
/// server was built without the `fmt-raw` feature; the formats this binary
/// serves are [] — rebuild with `--features fmt-raw` (or `fmt-all`)
/// Kernel panic - not syncing: Attempted to kill init!
/// ```
///
/// — "boots but does not serve", inside the proof that exists to catch exactly
/// that. `Row::cargo_args()` is the same call `gate::rows_check` makes, so the
/// features, profile and target are whatever the declaration says and this file
/// names none of them.
fn pack_fresh_app_znippy(root: &Path) {
    let set = holger_build_space::build_variant_set().expect("build space loads");
    let row = set
        .variants
        .iter()
        .find(|v| v.name == "mvp-server-musl")
        .expect("the declared static appliance payload");
    let build = Command::new(env!("CARGO"))
        .args(row.cargo_args())
        .current_dir(root)
        .output()
        .expect("spawn cargo build (musl holger-mvp)");
    assert!(
        build.status.success(),
        "the musl holger-mvp must build from THIS tree:\n{}",
        String::from_utf8_lossy(&build.stderr)
    );
    let target_dir = std::env::var_os("CARGO_TARGET_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|| root.join("target"));
    let bin = target_dir.join("x86_64-unknown-linux-musl/release/holger-mvp");
    assert!(
        bin.is_file(),
        "the musl build left no binary at {}",
        bin.display()
    );
    let pack = Command::new(env!("CARGO"))
        .args([
            "run",
            "--quiet",
            "--manifest-path",
            "xtask/Cargo.toml",
            "--",
            "appliance-pack",
            "--bin",
        ])
        .arg(&bin)
        .current_dir(root)
        .output()
        .expect("spawn cargo xtask appliance-pack");
    assert!(
        pack.status.success(),
        "appliance-pack must pack the binary this tree just built:\n{}",
        String::from_utf8_lossy(&pack.stderr)
    );
}

#[test]
#[ignore = "heavy: masters one per-site ISO, then per fleet member installs under KVM and FIRST-BOOTS the installed disk — asserts N distinct minted identities"]
fn one_iso_fans_to_n_machines_each_minting_its_own_identity() {
    let root = workspace_root();
    let Some(preflight) = KvmPreflight::detect("holger fleet, N distinct identities (iso-kvm)")
    else {
        return;
    };

    let scratch = root.join("target/fleet-proof");
    let _ = std::fs::remove_dir_all(&scratch);
    std::fs::create_dir_all(&scratch).unwrap();

    // ── 1. ONE per-site ISO through THE VERB (the declared cell) ──
    // THE HUB'S IDENTITY EXISTS BEFORE THE FLEET IS BURNED — reserve keypair
    // + port now, bake hub.conf from the ONE renderer, activate the pump per
    // appliance key after its first boot. Endpoint 10.0.2.2 = QEMU slirp's
    // host alias (guest→host); the fixture binds 0.0.0.0:<port> on the host.
    let hub = HubFixture::reserve().expect("reserve the hub identity");
    let hub_conf = hub.hub_conf(
        std::net::Ipv4Addr::new(10, 0, 2, 2),
        "10.77.0.0/24",
        5,
        Some((std::net::Ipv4Addr::new(10, 77, 0, 7), 24)),
    );
    let hub_path = scratch.join("fleet-hub.conf");
    std::fs::write(&hub_path, &hub_conf).unwrap();
    let ron_path = root.join("xtask/tests/fixtures/site-holger.ron");
    pack_fresh_app_znippy(&root);
    let out = Command::new(env!("CARGO"))
        .args([
            "run",
            "--quiet",
            "--manifest-path",
            "xtask/Cargo.toml",
            "--",
            "installer-iso",
            "--param",
            "site=fleet-site-1",
            "--param",
        ])
        .arg(format!("hub_conf=@{}", hub_path.display()))
        .arg("--param")
        .arg(format!("holger_ron=@{}", ron_path.display()))
        .args(["--param", "underlay=10.0.2.15/24@eth0"])
        .args(["--root-key", "dev"])
        .current_dir(&root)
        .output()
        .expect("spawn installer-iso");
    assert!(
        out.status.success(),
        "the verb must master the fleet ISO:\n{}\n{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let iso = root.join(".iso-out/holger-installer.iso");
    assert!(iso.is_file());

    // The declared cell (disk size + attachment shape — the one writer).
    let set = holger_build_space::build_variant_set().expect("build space loads");
    let cell = set
        .installers
        .iter()
        .find(|i| i.name == "holger-installer")
        .expect("the declared holger-installer cell")
        .clone();

    // ── 2. draupnir PLANS the fleet: one spec, N members ──
    // The Redfish-shaped spec is the SAME one whose request shapes draupnir's
    // fleet wire-shape test pins against three distinct BMCs; here its
    // members name the KVM stand-ins for those machines. The URL is what a
    // depot would serve — the KVM leg boots the local file the URL names.
    let template = BootSpec::redfish_iso(
        "fleet-site-1-holger",
        "https://depot.fleet-site-1.example/holger-installer.iso",
        BmcEndpoint {
            host: "https://bmc-template.example".into(),
            username: "admin".into(),
            system_id: "System.Embedded.1".into(),
        },
    );
    let fleet = plan_fleet(&template, 2);
    assert_eq!(fleet.len(), 2);

    // ── 3. per member: install, then FIRST BOOT off the installed disk ──
    let mut pubkeys: Vec<String> = Vec::new();
    for member in &fleet {
        let mdir = scratch.join(&member.name);
        std::fs::create_dir_all(&mdir).unwrap();
        let disk = mdir.join("disk.raw");
        let install_dir = mdir.join("install");

        // ── the SHARED install ceremony, with holger's values as inputs ──
        // Gap #15's install-time leg rides `expect_serial`: the enrolled root
        // verifies at INSTALL time — PASS, not NOT-ENROLLED (the measured
        // pre-enrollment state), and the policy token is on the installer's own
        // cmdline so a NOT-ENROLLED install would have REFUSED here instead of
        // shipping integrity-only.
        IsoInstallProof {
            product: PRODUCT,
            check: "install_ok",
            iso: &iso,
            disk: &disk,
            disk_bytes: cell.target_disk_bytes(),
            disk_args: cell.kvm_disk_args(&disk.display().to_string()),
            workdir: &install_dir,
            marker: INSTALL_OK,
            timeout: Duration::from_secs(180),
            expect_serial: &["signature-over-root=PASS"],
            expect_cmdline: &["korp.entry=holger", "korp.site=fleet-site-1"],
            expect_data_files: &[],
            detail: "fleet member: per-site ISO → KVM boot → INSTALL-OK with the enrolled root verified",
        }
        .run(&preflight);

        // ── FIRST BOOT: the disk alone, bare-metal semantics — the firmware
        //    walks to \EFI\BOOT\BOOTX64.EFI on the ESP the installer wrote ──
        // The marker is the SERVING line, not the mint: a mint with no app
        // behind it is the exact "boots but does not serve" the argv gap
        // measured (help text -> PID 1 died -> kernel panic). The harness
        // returns the WHOLE capture, so the identity asserts below read it.
        let boot_dir = mdir.join("firstboot");
        let booted = FirstBootProof {
            product: PRODUCT,
            disk: &disk,
            disk_args: cell.kvm_disk_args(&disk.display().to_string()),
            workdir: &boot_dir,
            // A real bind (TcpListener::bind fails loud before this prints),
            // never a startup banner.
            marker: "HTTP/OCI gateway listening on 0.0.0.0:8443",
            hostfwd: None,
            timeout: Duration::from_secs(120),
            expect_serial: &[
                // the first boot MINTS (never a pre-baked key)
                "MINTED",
                // the DECLARED argv reached the exec — the cell's cmd, via korp.args
                r#"exec argv: ["holger", "start", "--config", "/data/holger.ron"]"#,
                // Gap #15 on the BOOT side: every boot re-verifies the
                // enrollment (korp-start's signature leg against the ESP's
                // root.pub) — the require_signed policy rode the carry to the
                // installed cmdline.
                "signature-over-root=PASS",
                // ENV SEAM + UNDERLAY: the declared environment reached the
                // exec, the underlay address applied, and the tunnel INITIATED
                // toward the baked hub endpoint.
                "wg-appliance underlay: 10.0.2.15/24 on eth0",
                "wg-appliance address: 10.77.0.7/24 on wg0",
                "wg-appliance tunnel: initiating interface=wg0",
                // Gap #12: the boot narration is DURABLE — the log rides P3,
                // named on serial by the boot itself.
                "log: boot narration teed to /data/log/boot.log",
            ],
            refuse_serial: &[
                // PID 1 must not die (the literal shape of the argv gap).
                "Kernel panic",
                // The APP must actually take over — minting an identity on a
                // machine that then fails to run the product is not an
                // installed appliance. Proven RED by measurement (2026-08-13):
                // with a glibc-linked payload this failed with `fexecve …
                // ENOENT` (the absent PT_INTERP), AFTER a green mint. A static
                // payload is what the appliance shape requires and this is the
                // refusal that enforces it.
                "FAILED exec",
            ],
        }
        .run(&preflight);
        let serial = booted.serial;
        // holger prints the canonical line ITSELF (the ceremony's load half):
        // the SAME key korp-start minted — the line appears (at least) twice in
        // one boot and both spell the same identity.
        assert!(
            identity_line_count(&serial, PRODUCT) >= 2,
            "member {}: korp-start AND holger both print the canonical line:\n{serial}",
            member.name
        );
        let (p3_first_log, p3_last_log) = gpt_entry(&disk, 2);
        let logged = read_fat_file(&disk, p3_first_log, p3_last_log, "log/boot.log")
            .unwrap_or_else(|| panic!("member {} keeps log/boot.log on P3", member.name));
        let logged = String::from_utf8_lossy(&logged);
        assert!(
            logged.contains("wg-appliance identity: product=holger"),
            "member {}: the durable log carries the canonical line (RAM-copy replay):\n{logged}",
            member.name
        );
        let key = booted.pubkey.clone().unwrap_or_else(|| {
            panic!(
                "member {} prints the canonical identity line:\n{serial}",
                member.name
            )
        });

        // #16 pubkey-identity: what the machine PRINTED must be what it
        // KEPT — read identity.pub straight off the installed P3 and compare.
        // A machine that prints one key and stores another would hand the hub
        // a credential that never authenticates.
        let (p3_first, p3_last) = gpt_entry(&disk, 2);
        let stored = read_fat_file(&disk, p3_first, p3_last, "wg/identity.pub")
            .unwrap_or_else(|| panic!("member {} keeps identity.pub on P3", member.name));
        assert_eq!(
            String::from_utf8_lossy(&stored).trim(),
            key,
            "member {}: the stored identity must be the printed one",
            member.name
        );

        // #18 second reboot: boot the SAME disk again — the key must survive
        // and MINTED must NOT reappear (a second mint would silently replace
        // the identity the hub already registered).
        let reboot_dir = mdir.join("reboot2");
        std::fs::create_dir_all(&reboot_dir).unwrap();
        // THE TUNNEL BOOT (#17): the hub pump activates on the RESERVED port
        // with THIS member's minted key, the guest boots and initiates, the
        // hub responds, traffic seals, and the hub pings the appliance's
        // tunnel address — the reply crosses back through the tunnel. The
        // boot is HELD past the serving marker until the echo lands.
        let key_bytes = wg_appliance_core::decode_key(&key).expect("the minted key decodes");
        let fixture = HubFixture::spawn_on(hub.port, hub.hub_private, key_bytes)
            .expect("activate the hub pump on the reserved port");
        // The ping is sent AFTER the handshake responds (the hub can neither
        // seal into a session that does not exist nor knows the appliance's
        // NAT address until it hears it) — the hold closure watches for the
        // response and fires the ping exactly once.
        let pinged = std::cell::Cell::new(false);
        // The SAME attachment shape the first boot used — the declared cell's
        // disk argv plus the harness's virtio NIC (one writer for both).
        let mut disk_only = cell.kvm_disk_args(&disk.display().to_string());
        attach_slirp_nic(&mut disk_only, None);
        let serial2 = boot_disk_until_verdict(
            &preflight.qemu,
            &preflight.ovmf_code,
            &preflight.ovmf_vars,
            &disk_only,
            &reboot_dir,
            "HTTP/OCI gateway listening on",
            Duration::from_secs(120),
            || {
                let v = fixture.verdict();
                if v.responses > 0 && !pinged.get() {
                    fixture.send_ping(
                        std::net::Ipv4Addr::new(10, 77, 0, 1),
                        std::net::Ipv4Addr::new(10, 77, 0, 7),
                        7,
                    );
                    pinged.set(true);
                }
                v.echo_replies > 0
            },
            Duration::from_secs(60),
        );
        let verdict = fixture.verdict();
        drop(fixture); // release the port for the next member's activation
        assert!(
            verdict.initiations > 0 && verdict.responses > 0,
            "member {}: the installed machine HANDSHOOK the hub: {verdict:?}",
            member.name
        );
        assert!(
            verdict.sealed_packets > 0,
            "member {}: traffic crossed the tunnel: {verdict:?}",
            member.name
        );
        assert!(
            verdict.echo_replies > 0,
            "member {}: the hub's ping came back THROUGH the tunnel from the \
             installed machine — tunnel e2e on metal: {verdict:?}",
            member.name
        );
        assert!(
            !serial2.contains("MINTED"),
            "member {}'s SECOND boot must load, never re-mint:\n{serial2}",
            member.name
        );
        assert_eq!(
            minted_pubkey(&serial2, PRODUCT).as_deref(),
            Some(key.as_str()),
            "member {}: the identity survives reboot #2",
            member.name
        );
        // Gap #12, generation two: the SECOND boot rotated — the first
        // boot's narration is now prev.log (containing the MINTED line this
        // boot must not print), and boot.log is the new boot's.
        let prev = read_fat_file(&disk, p3_first_log, p3_last_log, "log/prev.log")
            .unwrap_or_else(|| panic!("member {} rotated to log/prev.log", member.name));
        assert!(
            String::from_utf8_lossy(&prev).contains("MINTED"),
            "member {}: the previous generation is the FIRST boot's narration",
            member.name
        );

        pubkeys.push(key);
    }

    // ── 4. the fleet-level truth: every machine minted its OWN identity ──
    assert_eq!(pubkeys.len(), 2);
    assert_ne!(
        pubkeys[0], pubkeys[1],
        "two machines from ONE ISO must hold DISTINCT identities — a shared \
         key means the ISO carries a baked-in identity, the exact defect the \
         per-machine mint exists to prevent"
    );

    // The rollup's fleet cell — an extra proof column (floor-not-cap), through
    // the harness's ONE component writer so it lands on `iso-kvm`, never the
    // form-less `iso` a KVM green could hide behind.
    korp_installer::isoproof::emit_kvm_row(
        PRODUCT,
        "fleet_distinct_identities",
        true,
        &format!(
            "one ISO -> {} machines, distinct minted pubkeys",
            pubkeys.len()
        ),
    );
}

// ─────────────────────────────────────────────────────────────────────────
// The ISO bar's last leg: "…reboot AGAIN → still the same key AND THE REPO
// IS STILL THERE" (henriks_prompt.txt §PROOFS REQUIRED).
//
// The identity half was already proven above. What was NOT proven is the
// half that costs a customer their data: that the SERVED CONTENT written
// through the gateway survives a power cycle. A P3 that remounts empty, or
// a filesystem that loses writes on an unclean stop, passes every identity
// assertion in this file while quietly discarding the customer's artifacts.
//
// WHY THIS RIDES A HOSTFWD AND NOT THE TUNNEL, stated because the brief asks
// for the round trip "over the tunnel": the host-side hub is a raw-packet
// fixture — `HubFixture` seals datagrams and crafts ICMP echo requests
// (`craft_icmp_echo_request`), and it deliberately owns no tun and no root.
// There is no TCP stack behind it, so an HTTP round trip cannot cross it
// from the test host without writing one, which is precisely the twin the
// brief forbids. The two halves are therefore proven separately and neither
// is faked: `one_iso_fans_to_n_machines_each_minting_its_own_identity` proves
// traffic CROSSES the tunnel with NO hostfwd at all (the only path in is the
// tunnel), and this proves content SURVIVES the reboot. Read together they
// cover step 7; read alone neither claims the other.
// ─────────────────────────────────────────────────────────────────────────

/// One HTTP/1.1 request over a fresh connection, hand-rolled on `std::net`
/// so this proof adds no dependency. Returns `(status, body)`; `None` when
/// the guest is not listening yet — the caller RETRIES rather than failing,
/// because "the port is not up in this poll" is not the same as "the server
/// refused", and conflating them is how a flaky proof gets written.
fn http(port: u16, method: &str, path: &str, body: Option<&[u8]>) -> Option<(u16, Vec<u8>)> {
    use std::io::{Read, Write};
    let mut s = std::net::TcpStream::connect(("127.0.0.1", port)).ok()?;
    s.set_read_timeout(Some(Duration::from_secs(10))).ok()?;
    let mut req = format!("{method} {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n");
    if let Some(b) = body {
        req.push_str(&format!("Content-Length: {}\r\n", b.len()));
        req.push_str("Content-Type: application/octet-stream\r\n");
    }
    req.push_str("\r\n");
    s.write_all(req.as_bytes()).ok()?;
    if let Some(b) = body {
        s.write_all(b).ok()?;
    }
    s.flush().ok()?;
    let mut raw = Vec::new();
    s.read_to_end(&mut raw).ok()?;
    let split = raw.windows(4).position(|w| w == b"\r\n\r\n")?;
    let head = String::from_utf8_lossy(&raw[..split]).to_string();
    let status: u16 = head.split_whitespace().nth(1)?.parse().ok()?;
    Some((status, raw[split + 4..].to_vec()))
}

#[test]
#[ignore = "heavy: installs a machine, PUTs a blob through the gateway, power-cycles it, and GETs the same bytes back off the installed disk"]
fn served_content_survives_a_power_cycle() {
    let root = workspace_root();
    let Some(preflight) =
        KvmPreflight::detect("holger served content survives a power cycle (iso-kvm)")
    else {
        return;
    };

    let scratch = root.join("target/content-persist-proof");
    let _ = std::fs::remove_dir_all(&scratch);
    std::fs::create_dir_all(&scratch).unwrap();

    // The cell REQUIRES a hub declaration, so reserve one; this proof never
    // activates the pump — the tunnel is the other test's subject.
    let hub = HubFixture::reserve().expect("reserve the hub identity");
    let hub_conf = hub.hub_conf(
        std::net::Ipv4Addr::new(10, 0, 2, 2),
        "10.77.0.0/24",
        5,
        Some((std::net::Ipv4Addr::new(10, 77, 0, 7), 24)),
    );
    let hub_path = scratch.join("persist-hub.conf");
    std::fs::write(&hub_path, &hub_conf).unwrap();
    let ron_path = root.join("xtask/tests/fixtures/site-holger.ron");
    pack_fresh_app_znippy(&root);
    let out = Command::new(env!("CARGO"))
        .args([
            "run",
            "--quiet",
            "--manifest-path",
            "xtask/Cargo.toml",
            "--",
            "installer-iso",
            "--param",
            "site=persist-site",
            "--param",
        ])
        .arg(format!("hub_conf=@{}", hub_path.display()))
        .arg("--param")
        .arg(format!("holger_ron=@{}", ron_path.display()))
        .args(["--param", "underlay=10.0.2.15/24@eth0"])
        .args(["--root-key", "dev"])
        .current_dir(&root)
        .output()
        .expect("spawn installer-iso");
    assert!(
        out.status.success(),
        "the verb must master the ISO:\n{}",
        String::from_utf8_lossy(&out.stderr)
    );
    let iso = root.join(".iso-out/holger-installer.iso");
    assert!(iso.is_file());

    let set = holger_build_space::build_variant_set().expect("build space loads");
    let cell = set
        .installers
        .iter()
        .find(|i| i.name == "holger-installer")
        .expect("the declared cell")
        .clone();

    // ── install, through the SHARED ceremony ──
    let disk = scratch.join("disk.raw");
    let idir = scratch.join("install");
    IsoInstallProof {
        product: PRODUCT,
        check: "install_ok",
        iso: &iso,
        disk: &disk,
        disk_bytes: cell.target_disk_bytes(),
        disk_args: cell.kvm_disk_args(&disk.display().to_string()),
        workdir: &idir,
        marker: INSTALL_OK,
        timeout: Duration::from_secs(180),
        expect_serial: &[],
        expect_cmdline: &["korp.entry=holger", "korp.site=persist-site"],
        expect_data_files: &[],
        detail: "content-persistence arm: per-site ISO → KVM boot → INSTALL-OK",
    }
    .run(&preflight);

    // The blob: path-addressed into the declared `bundles` raw repo, whose
    // data dir is /data/store/bundles — ON P3, which is the whole point.
    const BLOB_PATH: &str = "/bundles/persist/proof.bin";
    let blob: Vec<u8> = (0u16..4096).map(|i| (i % 251) as u8).collect();

    // Boot args WITH a host forward: the gateway must be reachable as a real
    // TCP peer, not merely announced on serial. The serving assertion in the
    // fleet proof reads a serial LINE; this one reads bytes off a socket.
    let port = free_port();
    let boot_args = |disk: &std::path::Path| {
        let mut a = cell.kvm_disk_args(&disk.display().to_string());
        attach_slirp_nic(&mut a, Some((port, 8443)));
        a
    };

    // ── boot 1: PUT the blob while the guest is held alive ──
    let b1 = scratch.join("boot1");
    std::fs::create_dir_all(&b1).unwrap();
    let put_ok = std::cell::Cell::new(false);
    let serial1 = boot_disk_until_verdict(
        &preflight.qemu,
        &preflight.ovmf_code,
        &preflight.ovmf_vars,
        &boot_args(&disk),
        &b1,
        "HTTP/OCI gateway listening on",
        Duration::from_secs(180),
        || {
            if put_ok.get() {
                return true;
            }
            match http(port, "PUT", BLOB_PATH, Some(&blob)) {
                Some((s, _)) if (200..300).contains(&s) => {
                    put_ok.set(true);
                    true
                }
                _ => false,
            }
        },
        Duration::from_secs(90),
    );
    assert!(serial1.contains("MINTED"), "first boot mints:\n{serial1}");
    assert!(
        put_ok.get(),
        "the blob was STORED through the gateway (no 2xx seen):\n{serial1}"
    );
    let key1 =
        minted_pubkey(&serial1, PRODUCT).expect("first boot prints the canonical identity line");

    // ── boot 2: the SAME disk, power-cycled. GET it back. ──
    let b2 = scratch.join("boot2");
    std::fs::create_dir_all(&b2).unwrap();
    let got = std::cell::RefCell::new(None::<Vec<u8>>);
    let serial2 = boot_disk_until_verdict(
        &preflight.qemu,
        &preflight.ovmf_code,
        &preflight.ovmf_vars,
        &boot_args(&disk),
        &b2,
        "HTTP/OCI gateway listening on",
        Duration::from_secs(180),
        || {
            if got.borrow().is_some() {
                return true;
            }
            match http(port, "GET", BLOB_PATH, None) {
                Some((200, body)) => {
                    *got.borrow_mut() = Some(body);
                    true
                }
                _ => false,
            }
        },
        Duration::from_secs(90),
    );

    // THE ASSERTION THIS TEST EXISTS FOR: same bytes, off the installed disk,
    // after a power cycle. Length first so a truncation reports as a length.
    let body = got.borrow().clone().unwrap_or_else(|| {
        panic!(
            "the blob was NOT served after the power cycle — the repo did not survive:\n{serial2}"
        )
    });
    assert_eq!(
        body.len(),
        blob.len(),
        "the stored blob came back TRUNCATED after the reboot"
    );
    assert_eq!(
        body, blob,
        "the stored blob came back CORRUPTED after the reboot"
    );

    // …and the identity half still holds on the same boot, so a data home
    // that survived by being REFORMATTED cannot pass: a wipe would re-mint.
    assert!(
        !serial2.contains("MINTED"),
        "the second boot must LOAD, never re-mint:\n{serial2}"
    );
    assert_eq!(
        minted_pubkey(&serial2, PRODUCT).as_deref(),
        Some(key1.as_str()),
        "the identity survives the same power cycle the content did"
    );

    korp_installer::isoproof::emit_kvm_row(
        PRODUCT,
        "installed_content_survives_reboot",
        true,
        &format!(
            "{} bytes PUT through the gateway, byte-identical after a power cycle",
            blob.len()
        ),
    );
}

/// The instrument, checked against a real socket. `http` is what decides
/// whether the blob came back, so a bug in IT would report as "the repo did
/// not survive" — a false red on the product for a defect in the test. This
/// drives it against a listener that answers a known body and asserts the
/// status, the length and the bytes are read back exactly.
#[test]
fn the_http_client_reads_status_and_body_off_a_real_socket() {
    use std::io::{Read, Write};
    let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let port = l.local_addr().unwrap().port();
    let body: Vec<u8> = (0u16..300).map(|i| (i % 251) as u8).collect();
    let expect = body.clone();
    let h = std::thread::spawn(move || {
        let (mut s, _) = l.accept().unwrap();
        // Read the request head so the client's write side is exercised too.
        let mut buf = [0u8; 2048];
        let n = s.read(&mut buf).unwrap();
        let head = String::from_utf8_lossy(&buf[..n]).to_string();
        s.write_all(
            format!(
                "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n",
                expect.len()
            )
            .as_bytes(),
        )
        .unwrap();
        s.write_all(&expect).unwrap();
        head
    });
    let (status, got) = http(port, "GET", "/bundles/persist/proof.bin", None).expect("a reply");
    let head = h.join().unwrap();
    assert_eq!(status, 200, "the status line is parsed, not guessed");
    assert_eq!(
        got.len(),
        body.len(),
        "the whole body is read, not one packet"
    );
    assert_eq!(got, body, "the bytes survive the read");
    assert!(
        head.starts_with("GET /bundles/persist/proof.bin HTTP/1.1"),
        "{head}"
    );
    // A closed port is None, NOT a panic — the caller retries on it, and
    // conflating "not up yet" with "refused" is how a flaky proof is written.
    assert!(
        http(free_port(), "GET", "/x", None).is_none(),
        "a dead port yields None"
    );
}