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