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

nordisk/gunnar

crates/gunnar-server/tests/appliance_matrix_rows.rs

// The pairing loop and its door are `wg-client` code; the gix engine arm builds without it.
#![cfg(feature = "wg-client")]
//! **The host-form appliance proof row, asserted on the bytes the matrix reads.**
//!
//! `edda/crates/nornir-testmatrix/src/appliance.rs` folds every
//! `component = "<product>-appliance"` row into the (product × proof) grid. Until
//! this test existed gunnar emitted NOTHING into it: measured 2026-08-15, holger
//! had two emitters and gunnar and korp had zero, so eight of gunnar's cells were
//! `Absent` — a visible hole, correctly.
//!
//! What is asserted here is the APPLIED OUTPUT, not a struct round-trip: the
//! emitter is driven through `wg::bring_up` (the real ceremony lane, the real
//! `appliance::run`), with `NORNIR_TESTMATRIX_OUT` pointed at a JSONL sink —
//! the SAME cross-process file `nornir_testmatrix::drain_functional_file` reads
//! back when the matrix harness runs a leaf repo's `cargo test --features
//! testmatrix` (`functional.rs:97`). The row is then read off disk and folded
//! through `ApplianceRollup::from_rows`, so what is proven is that the grid
//! actually turns green for gunnar — not that a function was called.
//!
//! **Both directions.** A row that cannot go red proves nothing, so the second
//! arm corrupts `wg/identity.key` and asserts the SAME cell goes `Red` with the
//! refusal text in the row's message. That is the whole reason
//! `ceremony_lane` matches on the ceremony's `Result` instead of `?`-ing it.
//!
//! ```text
//! CARGO_TARGET_DIR=~/scratch/cargo/gunnar-appliance \
//!   cargo test -p gunnar-server --features testmatrix --test appliance_matrix_rows -- --nocapture
//! ```

#[cfg(feature = "testmatrix")]
use std::path::PathBuf;

/// Scratch on DISK under the test binary's own target dir — never `std::env::
/// temp_dir()`, which is RAM on this box (and never shared between arms: the
/// second arm needs the FIRST arm's minted key to corrupt).
#[cfg(feature = "testmatrix")]
fn scratch(tag: &str) -> PathBuf {
    // `…/target/<profile>/deps/<test-bin>` → `…/target/appliance-matrix-rows`.
    let exe = std::env::current_exe().expect("the test binary knows where it is");
    let target = exe
        .ancestors()
        .nth(3)
        .expect("target/<profile>/deps/<bin> has three parents")
        .to_path_buf();
    let dir = target.join("appliance-matrix-rows").join(tag);
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).expect("scratch dir");
    dir
}

#[test]
fn gunnars_host_form_emits_identity_ready_and_it_can_go_red() {
    #[cfg(not(feature = "testmatrix"))]
    {
        // SKIP LOUDLY BY NAME. Without the feature `functional_status` is an
        // `#[inline]` no-op, so a "passing" run here would prove nothing at all.
        eprintln!(
            "SKIP appliance_matrix_rows: gunnar-server was built WITHOUT \
             --features testmatrix, so the appliance emitter is compiled out — \
             the gunnar-appliance/identity_ready row is NOT proven here"
        );
    }
    #[cfg(feature = "testmatrix")]
    proven();
}

#[cfg(feature = "testmatrix")]
fn proven() {
    use nornir_testmatrix::appliance::{ApplianceRollup, ProofVerdict};
    use nornir_testmatrix::functional::drain_functional_file;
    use nornir_testmatrix::model::status;
    use wg_appliance_core::ceremony::IDENTITY_KEY_FILE;

    let work = scratch("host");
    let sink = work.join("rows.jsonl");
    let root = work.join("data");
    std::fs::create_dir_all(&root).unwrap();

    // The gate is the appliance's own declaration (`[[build.installer]]` puts
    // `GUNNAR_APPLIANCE=1` on the kernel command line); here it is the env this
    // one test process sets. This test file is its own binary, so nothing else
    // races these two envs.
    std::env::set_var("GUNNAR_APPLIANCE", "1");
    std::env::set_var("NORNIR_TESTMATRIX_OUT", &sink);
    // Leave no inherited seeded-lane declaration to trip `seeded_lane_conflict`.
    std::env::remove_var("GUNNAR_WG");
    std::env::remove_var("GUNNAR_APPLIANCE_DATA");

    // ── green: the ceremony ran, so the cell is proven ───────────────────
    let tunnel = gunnar_server::wg::bring_up(&root).expect("an empty root is first boot");
    assert!(tunnel.is_none(), "no hub declared, so no tunnel to raise");

    let rows = drain_functional_file(&sink);
    assert!(
        !rows.is_empty(),
        "the ceremony must have APPENDED a row to {}",
        sink.display()
    );
    let row = rows
        .iter()
        .find(|r| r.suite == "gunnar-appliance" && r.test_name == "identity_ready")
        .unwrap_or_else(|| panic!("no gunnar-appliance/identity_ready row on disk; got {rows:#?}"));
    assert_eq!(
        row.status,
        status::PASS,
        "a ceremony that ran is a green cell: {row:#?}"
    );
    assert_eq!(row.aspect, "functional");
    assert!(
        row.message.starts_with("pubkey=") && row.message.contains("minted=true"),
        "the detail must carry the ceremony's OWN result, not a constant: {}",
        row.message
    );
    // The key in the row is the key on disk — the no-log readback.
    let on_disk = std::fs::read_to_string(gunnar_server::appliance::identity_pub_path(&root))
        .expect("identity.pub is written next to the key");
    assert!(
        row.message.contains(on_disk.trim()),
        "the row's pubkey must be the one in identity.pub: row={} disk={}",
        row.message,
        on_disk.trim()
    );

    // …and it folds onto the canonical grid as gunnar's identity_ready.
    let grid = ApplianceRollup::from_rows(&rows);
    assert_eq!(
        grid.verdict("gunnar", "identity_ready"),
        ProofVerdict::Green,
        "the rollup must see gunnar, not a hole:\n{}",
        grid.render()
    );

    // ── red: corrupt the identity, the SAME cell must go Red ─────────────
    // The refusal is the shared crate's (refuse, never re-mint); what is proven
    // here is that gunnar REPORTS it instead of dying silently.
    let key = gunnar_server::appliance::wg_dir(&root).join(IDENTITY_KEY_FILE);
    std::fs::write(&key, "this is not a base64 x25519 key\n").expect("corrupt it");

    let err = gunnar_server::wg::bring_up(&root)
        .expect_err("a corrupt identity must refuse, not re-mint");
    let red = drain_functional_file(&sink);
    let row = red
        .iter()
        .find(|r| r.suite == "gunnar-appliance" && r.test_name == "identity_ready")
        .unwrap_or_else(|| panic!("the refusal must ALSO emit a row; got {red:#?}"));
    assert_eq!(
        row.status,
        status::FAIL,
        "a refused ceremony is a RED cell: {row:#?}"
    );
    assert!(
        row.message.contains(&key.display().to_string()),
        "the red row names the file the operator goes to look at: {}",
        row.message
    );
    assert!(
        format!("{err:#}").contains(&key.display().to_string()),
        "and so does the error the boot dies on"
    );
    let grid = ApplianceRollup::from_rows(&red);
    assert_eq!(
        grid.verdict("gunnar", "identity_ready"),
        ProofVerdict::Red,
        "red must reach the grid:\n{}",
        grid.render()
    );

    std::env::remove_var("GUNNAR_APPLIANCE");
    std::env::remove_var("NORNIR_TESTMATRIX_OUT");
}