crates/gunnar-server/tests/slow_disk_placement.rs
//! **The placement table, proved against a real `ServerStores`.**
//!
//! `gunnar_server::slow`'s unit tests prove the rules in isolation; this file
//! proves that a server opened on a data root actually PUTS things where the
//! layout config says, and that what it then reports about them is honest in
//! each of the three states.
//!
//! # The config drives everything, and the config is a file
//!
//! Each test writes a layout config of its own into a temp directory, sealed
//! exactly as `korp_installer::layout::seal` seals it with no signing key — an
//! empty `auth.signature` and the note saying why — and points
//! `gunnar_server::layout_config::CONFIG_ENV` at it. That is the same channel
//! the installer uses to tell gunnar anything: a boot parameter.
//!
//! # Why the environment variable is taken under a lock
//!
//! `GUNNAR_LAYOUT_CONFIG` is process-global and these tests run on threads of
//! one binary. Every test holds `ENV` for as long as it needs the value to be
//! true, so two of them can never be inside `ServerStores::open` with each
//! other's config. Without it this file would be the flakiest thing in the
//! repository.
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard};
use gunnar_server::layout_config::{self, Authenticity};
use gunnar_server::slow::{self, Claim};
use gunnar_server::stores::{ServerStores, IDENTITY_MIRROR};
/// Serialises every test that touches `GUNNAR_LAYOUT_CONFIG`.
static ENV: Mutex<()> = Mutex::new(());
/// A machine: a data root, a `/serialdat` stand-in, and a layout config that
/// points at both.
struct Machine {
_guard: MutexGuard<'static, ()>,
dir: tempfile::TempDir,
data: PathBuf,
slow: PathBuf,
config: PathBuf,
}
impl Machine {
/// The installer's own shape: `meta` is a second copy on `/serialdat` with
/// no `/data` twin, and the other kinds keep a `/data` path that is a
/// symlink onto the other spindle. A temp directory has no second device to
/// point at, so the "symlink" is not laid — which is exactly the honest
/// thing for a test to assert: gunnar writes the CONFIGURED path, and what
/// that path is on is the installer's business and the config's claim.
fn dir_of(&self, name: &str) -> PathBuf {
match name {
slow::META_DIR => self.slow.join(name),
_ => self.data.join(name),
}
}
}
impl Drop for Machine {
fn drop(&mut self) {
std::env::remove_var(layout_config::CONFIG_ENV);
}
}
/// Seal a body with no signing key, exactly as the installer does today.
fn seal_unsigned(body: &str) -> String {
format!(
"{body}auth.domain=korp.layout-config.v1\nauth.blake3={}\nauth.signature=\n\
auth.pubkey=\nauth.note=NOT SIGNED: no ed25519 root key was enrolled at build time \
(KORP_ROOT_SIGNING_KEY unset), so this config carries INTEGRITY (blake3) and NO \
AUTHENTICITY.\n",
blake3::hash(body.as_bytes()).to_hex()
)
}
/// A machine whose config says the off-stripe-requiring directories are (or
/// are not) satisfied. `None` writes no config at all — the appliance
/// installed before the config existed.
fn machine(satisfied: Option<bool>) -> Machine {
let guard = ENV.lock().unwrap_or_else(|e| e.into_inner());
let dir = tempfile::tempdir().expect("a temp dir");
let data = dir.path().join("data");
let slow = dir.path().join("serialdat");
let config = dir.path().join("layout.conf");
std::fs::create_dir_all(&data).expect("mkdir data");
std::fs::create_dir_all(&slow).expect("mkdir serialdat");
if let Some(satisfied) = satisfied {
let yn = if satisfied { "yes" } else { "no" };
let class = if satisfied { "p3" } else { "fallback" };
let device = if satisfied {
"/dev/vda3".to_owned()
} else {
data.display().to_string()
};
let mut body = format!(
"korp-layout-config 1\n\
install.unix=1789000000\n\
install.product=gunnar\n\
install.mode=fresh\n\
boot.dev=/dev/vda\n\
boot.why=largest-group-remainder\n\
data.mountpoint={data}\n\
data.shape=raid0\n\
data.striped=yes\n\
data.count=4\n\
data.0.dev=/dev/vdb\n\
unused.count=1\n\
unused.0.name=vdf\n\
unused.0.why=unequal, not used\n\
serialdat.mountpoint={slowdir}\n\
serialdat.class={class}\n\
serialdat.dev={device}\n\
serialdat.off_stripe={yn}\n\
dir.users_dir=repos\n\
threshold.rootfs_measured_bytes=12163481\n",
data = data.display(),
slowdir = slow.display(),
);
for p in slow::PLACEMENT {
let path = match p.dir {
slow::META_DIR => slow.join(p.dir),
_ => data.join(p.dir),
};
let required = if p.requires_off_stripe { "yes" } else { "no" };
let sat = if p.requires_off_stripe { yn } else { "yes" };
body.push_str(&format!(
"dir.{d}.path={path}\ndir.{d}.device=serialdat\n\
dir.{d}.off_stripe_required={required}\n\
dir.{d}.off_stripe_satisfied={sat}\n\
dir.{d}.what={what}\n",
d = p.dir,
path = path.display(),
what = p.what,
));
}
std::fs::write(&config, seal_unsigned(&body)).expect("write the config");
}
// Set either way: with no file at that path, `load` answers ABSENT and
// names it — which is the case the third state is about.
std::env::set_var(layout_config::CONFIG_ENV, &config);
Machine {
_guard: guard,
dir,
data,
slow,
config,
}
}
fn open(m: &Machine) -> ServerStores {
ServerStores::open(&m.data).expect("the stores open")
}
/// **Every path gunnar uses is the one the config gave it** — no path is
/// computed, and no second placement mechanism sits on top of the installer's
/// symlinks.
#[test]
fn every_path_is_the_configured_one() {
let m = machine(Some(true));
let stores = open(&m);
let reality = stores.storage_reality();
assert_eq!(reality.placement.len(), slow::PLACEMENT.len());
for row in &reality.placement {
let p = slow::PLACEMENT
.iter()
.find(|p| p.what == row.what)
.expect("every row comes from the table");
assert_eq!(
Path::new(&row.path),
m.dir_of(p.dir),
"{} was not placed where the config said",
p.what
);
}
}
/// **The metadata mirror is written at open**, into `dir.meta.path`.
///
/// Not a directory that exists: a file with the principals and the enrolled
/// keys in it, before anybody has typed anything.
#[test]
fn the_identity_mirror_lands_where_the_config_says() {
let m = machine(Some(true));
let _stores = open(&m);
let mirror = m.slow.join(slow::META_DIR).join(IDENTITY_MIRROR);
let text = std::fs::read_to_string(&mirror)
.unwrap_or_else(|err| panic!("no {}: {err}", mirror.display()));
let doc: serde_json::Value = serde_json::from_str(&text).expect("the mirror is JSON");
assert!(doc.get("principals").is_some(), "{text}");
assert!(doc.get("keys").is_some(), "{text}");
// Public material only. A mirror that carried secrets would make the other
// spindle a second place to steal them from.
assert!(
!text.contains("PRIVATE KEY"),
"the identity mirror must never carry a private key:\n{text}"
);
}
/// **The roster is mirrored too** — the cheapest thing on the list, and the
/// only one that changes what happens when something breaks.
///
/// Gated with the roster itself: `gunnar_server::mirror` is the `replication`
/// feature, and naming a type a default build does not compile would take the
/// whole file down with it.
#[cfg(feature = "replication")]
#[test]
fn the_replication_roster_is_mirrored() {
let m = machine(Some(true));
let stores = open(&m);
gunnar_server::ReplicationConfig::default()
.save_mirrored(stores.root(), stores.slow())
.expect("the roster saves");
let primary = m.data.join(gunnar_server::mirror::REPLICATION_CONFIG);
let mirror = m
.slow
.join(slow::META_DIR)
.join(gunnar_server::mirror::REPLICATION_CONFIG);
assert!(
primary.is_file(),
"the primary copy is still on the data root"
);
assert_eq!(
std::fs::read(&primary).expect("primary"),
std::fs::read(&mirror)
.unwrap_or_else(|err| panic!("no mirror at {}: {err}", mirror.display())),
"the mirror must be the same bytes, not a summary of them"
);
}
/// **The audit trail is adopted into `dir.log.path`, once.**
#[test]
fn the_audit_trail_moves_into_the_configured_log_directory() {
let m = machine(Some(true));
let stores = open(&m);
// A chain from a build that predates the layout config.
let old = stores.control_dir().join(gunnar_core::AUDIT_FILE);
std::fs::write(&old, b"{\"seq\":0}\n").expect("write the old chain");
let used = stores.audit_path();
assert_eq!(
used,
m.data.join(slow::LOG_DIR).join(gunnar_core::AUDIT_FILE)
);
assert_eq!(std::fs::read(&used).expect("adopted"), b"{\"seq\":0}\n");
assert!(!old.exists(), "the old copy is gone");
// Idempotent: a second call does not re-adopt and does not clobber.
assert_eq!(stores.audit_path(), used);
}
/// ★ **The assertion this whole lane exists for.**
///
/// On a box the installer could not give a second device, the mirror, the
/// crash records and the audit trail are on the same device as the thing they
/// protect. They are STILL WRITTEN — one code path, never a branch on whether
/// the slow area is real — and they are never reported as satisfied.
///
/// A health check that says "mirror OK" for a mirror that dies with its
/// original is worse than no mirror, because somebody will plan a recovery
/// around it.
#[test]
fn a_same_device_layout_is_written_and_never_reported_as_satisfied() {
let m = machine(Some(false));
let stores = open(&m);
// Written. The placement does not change.
assert!(
m.slow.join(slow::META_DIR).join(IDENTITY_MIRROR).is_file(),
"the mirror is written anyway: that is the one code path"
);
// And not claimed.
assert_eq!(stores.slow().claim(), Claim::SameDevice);
assert!(!stores.slow().claim().satisfied());
let reality = stores.storage_reality();
assert_eq!(reality.off_stripe, "same-device");
assert_eq!(reality.backing_class, "fallback");
assert_eq!(reality.off_stripe_health.len(), 3);
for line in &reality.off_stripe_health {
let lower = line.to_lowercase();
assert!(lower.contains("present"), "never `missing`: {line}");
assert!(lower.contains("not off-stripe"), "{line}");
assert!(!lower.contains(" ok"), "never `OK`: {line}");
}
// Every row that requires another failure domain carries the verdict, and
// no row anywhere says it is satisfied.
for row in reality.placement.iter().filter(|r| r.requires_off_stripe) {
assert_eq!(row.off_stripe, "same-device", "{}", row.what);
}
// A row that does not require it carries no verdict at all — painting
// "off-stripe" on git's own objects would suggest a guarantee nobody made.
for row in reality.placement.iter().filter(|r| !r.requires_off_stripe) {
assert_eq!(row.off_stripe, "", "{}", row.what);
}
}
/// The three states change the CLAIM and not one byte of the LAYOUT.
#[test]
fn the_three_states_change_only_the_claim() {
let mut layouts = Vec::new();
for (satisfied, expected) in [
(Some(true), "off-stripe"),
(Some(false), "same-device"),
// No config: nothing has said either way, and that is its own answer.
(None, "absent"),
] {
let m = machine(satisfied);
let stores = open(&m);
let reality = stores.storage_reality();
assert_eq!(reality.off_stripe, expected, "satisfied={satisfied:?}");
layouts.push(
reality
.placement
.iter()
.map(|r| {
Path::new(&r.path)
.strip_prefix(m.dir.path())
.expect("under the machine")
.display()
.to_string()
})
.collect::<Vec<_>>(),
);
}
assert_eq!(layouts[0], layouts[1], "the verdict must not move a path");
// With no config every directory falls back under the data root — the
// documented default, and NOT a `/serialdat` gunnar went looking for.
assert!(
layouts[2].iter().all(|p| p.starts_with("data/")),
"the fallback is the data root and nothing else: {:?}",
layouts[2]
);
}
/// **An appliance installed before the layout config existed must start**, and
/// must SAY the config is missing rather than have its installation described
/// from the mount table.
#[test]
fn an_absent_config_is_reported_as_absent_and_nothing_is_inferred() {
let m = machine(None);
let stores = open(&m);
let reality = stores.storage_reality();
assert!(!reality.record_present);
assert!(
reality.record_fields.is_empty(),
"not one field may be invented: {:?}",
reality.record_fields
);
assert!(!reality.record_signature_verified);
assert_eq!(reality.record_version, 0);
assert_eq!(
reality.backing_class, "",
"a class that was not read is not reported"
);
assert_eq!(
reality.slow_root, "",
"a mountpoint that was not read is not reported"
);
assert_eq!(reality.off_stripe, "absent");
let why = &reality.record_absent_reason;
assert!(why.contains("no layout config at"), "{why}");
assert!(
why.contains(&m.config.display().to_string()),
"the reason names where gunnar looked: {why}"
);
}
/// ★ **`INTEGRITY ONLY`, in the installer's own two words.**
///
/// No `KORP_ROOT_SIGNING_KEY` is enrolled on this estate, so `auth.signature`
/// is empty and the config says so in its own bytes. gunnar says the same
/// thing, and never presents it as verified.
#[test]
fn an_unsigned_config_is_read_and_reported_as_integrity_only() {
let m = machine(Some(true));
let stores = open(&m);
let reality = stores.storage_reality();
assert!(reality.record_present);
assert_eq!(reality.record_version, 1);
assert_eq!(reality.record_signature, Authenticity::IntegrityOnly.word());
assert_eq!(reality.record_signature, "INTEGRITY ONLY");
assert!(
!reality.record_signature_verified,
"an unsigned config must NEVER be reported as verified"
);
assert!(
reality.record_signature_note.contains("NOT SIGNED"),
"the installer's own sentence travels with it: {}",
reality.record_signature_note
);
let field = |key: &str| {
reality
.record_fields
.iter()
.find(|f| f.key == key)
.map(|f| f.value.as_str())
};
// The only place the REASONING survives.
assert_eq!(field("boot.why"), Some("largest-group-remainder"));
assert_eq!(field("data.shape"), Some("raid0"));
assert_eq!(field("unused.0.why"), Some("unequal, not used"));
// The measured rootfs, read rather than guessed: 11.6 MiB, not gigabytes.
assert_eq!(field("threshold.rootfs_measured_bytes"), Some("12163481"));
// And nothing that was not written is answered.
assert_eq!(field("data.bytes"), None);
}
/// **A config that does not hold together is ABSENT, not trusted** — it says
/// where the forge's data lives, so a forged one redirects its storage.
#[test]
fn a_tampered_config_is_treated_as_absent() {
let m = machine(Some(true));
let sealed = std::fs::read_to_string(&m.config).expect("read");
std::fs::write(&m.config, sealed.replace("raid0", "plain")).expect("tamper");
let stores = open(&m);
let reality = stores.storage_reality();
assert!(
!reality.record_present,
"not one value may be read out of it"
);
let why = &reality.record_absent_reason;
assert!(why.contains("does not verify"), "{why}");
assert!(why.contains("altered"), "{why}");
assert!(why.contains("rather than trusted"), "{why}");
// And the server still came up.
assert_eq!(reality.off_stripe, "absent");
}
/// Layer 2: only the configured paths are measured — and a measurement that
/// failed reports no number at all rather than a zero somebody reads as full.
#[test]
fn the_filesystem_rows_are_measured_or_say_they_were_not() {
let m = machine(Some(true));
let stores = open(&m);
let reality = stores.storage_reality();
assert!(!reality.filesystems.is_empty());
for fs in &reality.filesystems {
assert!(
Path::new(&fs.path).starts_with(m.dir.path()),
"only paths gunnar was told to use are measured: {}",
fs.path
);
if fs.measured {
assert!(fs.total_bytes > 0, "{fs:?}");
assert!(fs.error.is_empty(), "{fs:?}");
} else {
assert_eq!(fs.total_bytes, 0);
assert_eq!(fs.free_bytes, 0);
assert!(!fs.error.is_empty(), "a failure has to say why: {fs:?}");
}
}
assert_eq!(reality.data_root, m.data.display().to_string());
assert_eq!(reality.slow_root, m.slow.display().to_string());
assert!(
reality.slow_root_source.contains("layout config"),
"gunnar was TOLD, and says so: {}",
reality.slow_root_source
);
}
// ── the trail survives a restore (lane T11, 2026-09-21; the D6 finding) ──────
/// **The audit trail where the layout puts it IS the replicated trail.**
///
/// D6: `log/` was a per-box path, so no twin ever held the audit trail and
/// `gunnar control twin replay` brought a box back with none — silently,
/// reporting success. This asserts the two halves that make that impossible:
/// the sink names the chain `log/audit-chain.jsonl` (the one path a restore
/// resolves), and every append lands in this box's transaction log.
///
/// **Fails before the fix**: restore `"log"` to the `matches!` list in
/// `gunnar_core::txlog::is_per_box_path` — `open_framed` then still succeeds
/// (the path resolves) but the first `append` is refused as a per-box path,
/// and the `head_seq` assertion never runs.
#[test]
fn the_audit_trail_is_the_replicated_trail() {
let m = machine(Some(true));
let stores = open(&m);
let path = stores.audit_path();
assert_eq!(
path,
m.data.join(slow::LOG_DIR).join(gunnar_core::AUDIT_FILE)
);
// The sink names it by the ONE relative path a restore knows.
let (_log, rel) =
gunnar_core::txlog::sink::log_for(&path).expect("the trail is under the data root");
assert_eq!(rel, gunnar_core::txlog::AUDIT_REL);
let before = stores.txlog().head_seq();
let chain = gunnar_core::AuditChain::open_framed(&path).expect("the trail is framable");
assert!(chain.is_framed());
chain
.append(
gunnar_core::AuditEvent {
method: "Access.Grant",
principal: "alice",
fingerprint: "SHA256:abc",
scope: "server",
level: "admin",
auth_mode: "required",
decision: "allow",
reason: "",
},
1,
)
.expect("append");
assert_eq!(
stores.txlog().head_seq(),
before + 1,
"the decision must be a transaction, or the twin never sees it"
);
let e = stores.txlog().read(before + 1).expect("the frame");
assert_eq!(e.kind(), Some(gunnar_core::txlog::Kind::Append));
match e.meta().expect("meta") {
gunnar_core::txlog::Meta::Append { log } => assert_eq!(log, gunnar_core::txlog::AUDIT_REL),
other => panic!("the audit decision was framed as {other:?}"),
}
}
/// **A twin keeps its OWN trail, in its own file.**
///
/// A twin serves a control plane of its own and every decision it makes is
/// its decision. Writing those into the chain it is HOLDING for the primary
/// would put a second writer into a hash chain whose entire value is that it
/// has one — every `prev` after the splice would break, and the restored copy
/// would be refused at open. So the two are different files and only the
/// primary's is framed.
///
/// **Fails before the fix**: remove the `if self.is_twin() || …` early return
/// from `ServerStores::audit_path` and a twin writes into the primary's trail.
#[test]
fn a_twin_writes_its_own_audit_trail_and_not_the_primarys() {
let m = machine(Some(true));
let stores = open(&m);
let primary_trail = stores.audit_path();
stores.mark_twin();
let own = stores.audit_path();
assert_ne!(
own, primary_trail,
"a twin must not append to the trail it is holding"
);
assert_eq!(
own,
m.data.join(slow::LOG_DIR).join(
std::path::Path::new(gunnar_core::txlog::AUDIT_LOCAL_REL)
.file_name()
.expect("a name")
),
);
// And its own trail is never a transaction: it stays on the twin.
let rel = gunnar_core::txlog::sink::log_for(&own).map(|(_, rel)| rel);
assert!(
rel.as_deref().is_none_or(gunnar_core::txlog::never_framed),
"a twin's own decisions must never be framed into the primary's log: {rel:?}"
);
}
/// **The site's hub declaration becomes a transaction, once per change** (lane T11).
///
/// `wg/hub.conf` is laid into the data root by `korp-installer` through the ESP data stage and
/// is never written by gunnar, so it had no sink call site: the twin did not hold it and a
/// restored main came up with no way onto the tunnel. It is framed on open instead — and the
/// digest of what was framed is kept beside it, so a box that reboots a thousand times adds
/// one frame and not a thousand.
///
/// **Fails before the fix**: delete the `adopt_hub_conf(&root, &txlog);` call from
/// `ServerStores::open` and the log holds no `File` frame naming `wg/hub.conf`.
#[test]
fn the_hub_declaration_is_framed_once_per_change() {
let m = machine(Some(true));
let wg = m.data.join("wg");
std::fs::create_dir_all(&wg).expect("wg dir");
std::fs::write(
wg.join("hub.conf"),
b"[Interface]\nAddress = 10.100.0.7/32\n",
)
.expect("hub.conf");
// The machine's own identity sits beside it and must never travel.
std::fs::write(wg.join("identity.key"), b"PRIVATE").expect("identity.key");
let named = |stores: &ServerStores| -> Vec<String> {
let log = stores.txlog();
let (first, head) = (log.first_seq(), log.head_seq());
if head < first {
return Vec::new();
}
log.range(first, head)
.expect("range")
.into_iter()
.filter_map(|e| match e.meta() {
Ok(gunnar_core::txlog::Meta::File { path, .. }) => Some(path),
_ => None,
})
.collect()
};
let stores = open(&m);
let after_first = named(&stores);
assert_eq!(
after_first
.iter()
.filter(|p| *p == gunnar_core::txlog::HUB_CONF_REL)
.count(),
1,
"the hub declaration must be exactly one transaction: {after_first:?}"
);
assert!(
!after_first.iter().any(|p| p.contains("identity.key")),
"the machine's own WireGuard private key must never be a transaction: {after_first:?}"
);
drop(stores);
// A reboot that changes nothing frames nothing.
let stores = open(&m);
assert_eq!(
named(&stores)
.iter()
.filter(|p| *p == gunnar_core::txlog::HUB_CONF_REL)
.count(),
1,
"a boot that changed nothing must not add a frame"
);
drop(stores);
// A hub declaration that CHANGED is framed again — it is the current one that matters.
std::fs::write(
wg.join("hub.conf"),
b"[Interface]\nAddress = 10.100.0.8/32\n",
)
.expect("hub.conf");
let stores = open(&m);
assert_eq!(
named(&stores)
.iter()
.filter(|p| *p == gunnar_core::txlog::HUB_CONF_REL)
.count(),
2,
"a changed hub declaration is a new transaction"
);
}