src/vmsuite.rs
//! **VM-SYSTEMD-SUITE** — assert what systemd *resolved*, inside a real KVM guest.
//!
//! ## Why this exists
//!
//! Skidbladnir owns service creation, so a defect in what it installs is a defect in
//! every product that installs through it. But a unit renderer tested by
//! string-comparing its own output is the emptiest kind of green: it asserts what it
//! *wrote*, never what systemd *did* with it.
//!
//! The defect that forced this module, measured on host oden **2026-08-03**:
//! `korp-server --install --system` rendered `User=korp`, printed
//! `✓ installed korp-server systemd service (user korp)` — and the service then ran
//! as **`alice`**, because five stale drop-ins in
//! `/etc/systemd/system/korp-server.service.d/` (dated 25–31 July) overrode it, one
//! of them setting `User=alice`. Read back from `/proc/<pid>/environ`. **No
//! unit-rendering test could ever have caught that** — only asking a live systemd
//! what it resolved after the merge.
//!
//! ## The shape
//!
//! * The **guest** (`skidbladnir-scenario`, a static-musl binary in `probe/`)
//! executes a host-authored [plan](crate::vmsuite::render_plan) against the guest's
//! real init and reports one [`Facts`] line per case: `systemctl show`'s resolved
//! properties, `systemctl cat` (fragment **plus** drop-ins), `/proc/<MainPID>/environ`
//! and the live process's uid, and the unit's on-disk footprint.
//! * The **host** grades. Every verdict is a pure `fn(&Check, &Facts) -> Verdict`
//! ([`judge`]) with no IO, so each guard can be driven RED on the host with
//! synthetic facts.
//!
//! ## Every guard is proven falsifiable, twice
//!
//! A VM harness "passes" most easily by asserting the VM booted. Two independent
//! defences, because either alone can rot:
//!
//! 1. **Host unit tests** feed each judge synthetic facts and assert it returns
//! `Fail` (see this module's `tests`). A judge that cannot say no is caught here.
//! 2. **Negative cases in the guest.** Several cases carry
//! [`Expect::expect_pass`]` == false`: the suite plants a contradicting drop-in and
//! requires the guard to go RED against a *live* systemd. A judge stuck on Pass
//! fails those; a judge stuck on Fail fails the positive cases. Neither hollow
//! direction survives.
//!
//! ## What a host-only test could not have caught
//!
//! [`SuiteCase::host_only_blind_spot`] records, per case, why. The load-bearing ones
//! are `dropin-user-override` and `reinstall-inherits-the-landmine` (a merge only a
//! live systemd performs) and `boot-ordering` (an activation order only a fresh boot
//! produces).
use std::path::PathBuf;
#[cfg(feature = "matrix")]
use std::path::Path;
use std::time::Duration;
#[cfg(feature = "matrix")]
use std::time::Instant;
#[cfg(feature = "matrix")]
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[cfg(feature = "matrix")]
use crate::machine::kvm::KvmController;
#[cfg(feature = "matrix")]
use crate::machine::{ImageRef, MachineController, MachineSpec, NetMode, Provision, Readiness};
use crate::service::{Backend, RenderedService, Scope, ServiceSpec, renderer_for};
/// The unit the suite installs and abuses.
pub const UNIT: &str = "skidtest.service";
/// The dedicated non-root system user the spec declares. Non-root on purpose: a
/// service already running as root cannot demonstrate a `User=` override.
pub const SVC_USER: &str = "skidsvc";
/// The home `useradd --create-home` gives [`SVC_USER`]; systemd puts it in the
/// service's `HOME`, which is exactly what korp's defect got wrong.
pub const SVC_HOME: &str = "/var/lib/skidsvc";
/// The injected test service binary.
pub const EXEC: &str = "/usr/local/bin/skidbladnir-testsvc";
/// The in-guest scenario interpreter.
pub const SCENARIO: &str = "/usr/local/bin/skidbladnir-scenario";
/// The port the test service binds.
pub const TEST_PORT: u16 = 7070;
/// The host-readable virtio-serial report port.
pub const REPORT_PORT: &str = "/dev/virtio-ports/org.skidbladnir.report";
/// The deliberately-early control unit: a `sysinit.target` oneshot that CANNOT be
/// ordered after the network. It exists so the boot-ordering guard has a real red.
pub const EARLY_UNIT: &str = "skidearly.service";
// ─────────────────────────────── the facts wire ──────────────────────────────
/// One case's applied state, as the in-guest scenario reported it.
///
/// Mirrors `skidbladnir_probe::facts::Facts`. The two live in different cargo
/// workspaces (the guest crate may depend on nothing but `std`) and deliberately
/// share no type; the format is kept flat — strings and arrays of strings only — so
/// the hand-rolled emitter on one side and serde on the other cannot disagree about
/// anything subtler than a field name. [`tests::the_guest_facts_line_deserializes`]
/// pins a real emitted line.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Facts {
pub case: String,
/// `"<exit> <argv>"` per command the case ran, in order.
#[serde(default)]
pub exits: Vec<String>,
/// `KEY=VALUE` lines from `systemctl show`, plus the synthetic
/// `NetworkOnlineActiveEnterTimestampMonotonic=`.
#[serde(default)]
pub show: Vec<String>,
/// `systemctl cat <unit>` verbatim — fragment AND drop-ins.
#[serde(default)]
pub cat: String,
/// `/proc/<MainPID>/environ`, as `KEY=VALUE`.
#[serde(default)]
pub proc_env: Vec<String>,
/// The username the live process's real uid resolves to.
#[serde(default)]
pub proc_user: String,
/// Unit-related paths that exist on disk right now.
#[serde(default)]
pub files: Vec<String>,
/// `"<key>=<stdout>"` per [`PlanStep::Capture`] — the **bytes a command produced**
/// in the guest, which is what an applied-output assertion needs and what an exit
/// code cannot give (LAW 2: a malformed command's empty output is shaped exactly
/// like a clean result).
#[serde(default)]
pub captures: Vec<String>,
#[serde(default)]
pub at: String,
}
impl Facts {
/// The value of a `systemctl show` property, or `None` when the property was
/// never captured.
///
/// `None` and `Some("")` are **different**: systemd prints `Group=` for a unit
/// with no group, and that empty string is an answer. A judge that conflated the
/// two would read "I never asked" as "the answer is empty" — the exact shape of a
/// check that cannot fail.
pub fn show_value(&self, key: &str) -> Option<&str> {
self.show
.iter()
.find_map(|l| l.strip_prefix(key).and_then(|r| r.strip_prefix('=')))
}
/// A `/proc/<MainPID>/environ` entry.
pub fn env_value(&self, key: &str) -> Option<&str> {
self.proc_env
.iter()
.find_map(|l| l.strip_prefix(key).and_then(|r| r.strip_prefix('=')))
}
/// A [`PlanStep::Capture`]'s stdout.
///
/// `None` (never captured) and `Some("")` (ran and printed nothing) are different
/// answers and every judge must treat them so — reading "I never asked" as "the
/// answer is empty" is the exact shape of a check that cannot fail.
pub fn capture(&self, key: &str) -> Option<&str> {
self.captures
.iter()
.find_map(|l| l.strip_prefix(key).and_then(|r| r.strip_prefix('=')))
}
/// The `"<code> WAITFOR <waited>ms/<timeout>ms <argv…>"` audit line whose argv
/// contains `needle`, as `(exit_code, waited_ms)`.
///
/// This is how a case reports **how long an outcome took to arrive on the
/// service's own schedule** — the number that separates "the server noticed by
/// itself" from "the test slept long enough".
pub fn waited(&self, needle: &str) -> Option<(i32, u64)> {
self.exits
.iter()
.find(|l| l.contains(" WAITFOR ") && l.contains(needle))
.and_then(|l| {
let mut it = l.split_whitespace();
let code = it.next()?.parse::<i32>().ok()?;
it.next()?; // "WAITFOR"
let waited = it.next()?.split_once("ms/")?.0.parse::<u64>().ok()?;
Some((code, waited))
})
}
/// Every drop-in systemd merged, from BOTH sources: the resolved `DropInPaths`
/// property and the on-disk listing of `<unit>.d/`.
///
/// Both, because they fail in opposite directions. `DropInPaths` is empty when the
/// unit is not loaded — which is precisely the state a half-done uninstall leaves,
/// so trusting it alone would call a machine with a live landmine clean. The disk
/// listing is empty for a `/run` or `/usr/lib` drop-in, which `DropInPaths` names.
pub fn drop_ins(&self) -> Vec<&str> {
let mut v: Vec<&str> = self
.show_value("DropInPaths")
.unwrap_or("")
.split_whitespace()
.collect();
for f in &self.files {
if f.contains(".service.d/") && !f.ends_with('/') && !v.contains(&f.as_str()) {
v.push(f);
}
}
v.sort_unstable();
v.dedup();
v
}
/// `After=` as a list.
pub fn after(&self) -> Vec<&str> {
self.show_value("After")
.unwrap_or("")
.split_whitespace()
.collect()
}
/// A monotonic-microsecond property, `None` when absent or unparseable, and
/// `Some(0)` only when systemd genuinely reported 0 (= never activated).
pub fn monotonic(&self, key: &str) -> Option<u64> {
self.show_value(key).and_then(|v| v.trim().parse().ok())
}
}
// ───────────────────────────────── the judges ────────────────────────────────
/// A guard's answer. `Fail` always carries the observed value, because a red whose
/// message does not say what it saw costs an extra VM boot to diagnose.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Verdict {
Pass(String),
Fail(String),
}
impl Verdict {
pub fn is_pass(&self) -> bool {
matches!(self, Verdict::Pass(_))
}
pub fn detail(&self) -> &str {
match self {
Verdict::Pass(s) | Verdict::Fail(s) => s,
}
}
}
/// What to assert about a case's applied state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Check {
/// systemd resolved `User=` to this, AND the live process really runs as it.
ResolvedUser(String),
/// The only drop-ins merged are these (usually none).
NoForeignDropIns(Vec<String>),
/// `true` ⇒ `ActiveState == active`. `false` ⇒ genuinely STOPPED
/// (`inactive`/`failed`/not loaded) — NOT merely "not active", which a
/// still-draining `deactivating` unit would satisfy. See [`judge_active`].
Active(bool),
/// No trace of the unit is left: not loaded, no fragment, nothing under
/// `/etc/systemd/system/<unit>*`.
CleanSlate,
/// The live process's environment carries `key=value`.
ProcEnv(String, String),
/// The unit declares `After=network-online.target` AND actually activated at or
/// after network-online.target did, in this boot.
OrderedAfterNetworkOnline,
}
/// Grade `check` against `facts`. **Pure** — no IO, no clock, no environment — which
/// is what lets every one of these be driven RED from a host unit test.
///
/// The governing rule: a fact that was never captured is a **Fail**, never a Pass.
/// Absence of evidence read as evidence of correctness is how a guard stops being
/// able to fail at all.
pub fn judge(check: &Check, f: &Facts) -> Verdict {
match check {
Check::ResolvedUser(want) => judge_resolved_user(want, f),
Check::NoForeignDropIns(allowed) => judge_no_foreign_drop_ins(allowed, f),
Check::Active(want) => judge_active(*want, f),
Check::CleanSlate => judge_clean_slate(f),
Check::ProcEnv(k, v) => judge_proc_env(k, v, f),
Check::OrderedAfterNetworkOnline => judge_ordered_after_network_online(f),
}
}
fn judge_resolved_user(want: &str, f: &Facts) -> Verdict {
let Some(resolved) = f.show_value("User") else {
return Verdict::Fail(
"systemd reported no `User` property at all — the unit is not loaded, so nothing was \
resolved and there is nothing to pass"
.into(),
);
};
if resolved != want {
return Verdict::Fail(format!(
"systemd RESOLVED User={resolved:?}, the spec declares {want:?} (drop-ins merged: {:?})",
f.drop_ins()
));
}
// The resolved property is only half the answer: the process is the other half.
if !f.proc_user.is_empty() && f.proc_user != want {
return Verdict::Fail(format!(
"systemd resolved User={want:?} but the LIVE process runs as {:?} (/proc/<MainPID>/status)",
f.proc_user
));
}
if let Some(env_user) = f.env_value("USER") {
if env_user != want {
return Verdict::Fail(format!(
"systemd resolved User={want:?} but the live process's environ says USER={env_user:?}"
));
}
}
Verdict::Pass(format!(
"User={want} resolved, process uid={}, environ USER={}",
if f.proc_user.is_empty() {
"-"
} else {
&f.proc_user
},
f.env_value("USER").unwrap_or("-"),
))
}
fn judge_no_foreign_drop_ins(allowed: &[String], f: &Facts) -> Verdict {
let found = f.drop_ins();
let foreign: Vec<&str> = found
.iter()
.copied()
.filter(|p| !allowed.iter().any(|a| a == p))
.collect();
if foreign.is_empty() {
Verdict::Pass(format!("drop-ins merged: {found:?} (all declared)"))
} else {
Verdict::Fail(format!(
"{} undeclared drop-in(s) are overriding this unit: {foreign:?}",
foreign.len()
))
}
}
/// **Grade `ActiveState` — and note that the two directions are NOT each other's
/// negation.**
///
/// `want = true` is `ActiveState == "active"`. `want = false` is *genuinely stopped* —
/// [`crate::systemd::state_is_stopped`], i.e. `inactive`, `failed`, or not loaded.
/// A unit in `deactivating` or `activating` satisfies NEITHER: it is transitional, and
/// grading it as either is a false green in the harness whose whole job is to catch
/// false greens.
///
/// It used to be `is_active == want`, so `want = false` was satisfied by
/// `deactivating` — a unit whose processes are all still running. The suite case that
/// grades it is `systemctl stop` → `Sleep(1500)` → collect, and a service that drains
/// for longer than 1.5 s therefore graded PASS on "it stopped" while it had not.
/// MEASURED on t14s 2026-08-25: a real service sat in `deactivating` for 6.4 s after
/// `StopUnit`. Same defect as the one fixed in the installer's stop wait (35ab55c),
/// found by sweeping every reader of `ActiveState` afterwards.
///
/// `failed` counts as stopped deliberately: a unit SIGKILLed after its stop timeout
/// settles there and never reaches `inactive`, so demanding `inactive` would go red on
/// exactly the services slowest to die.
fn judge_active(want: bool, f: &Facts) -> Verdict {
let Some(state) = f.show_value("ActiveState") else {
return Verdict::Fail("systemd reported no `ActiveState` — the unit is not loaded".into());
};
let satisfied = if want {
state == "active"
} else {
crate::systemd::state_is_stopped(Some(state))
};
if satisfied {
Verdict::Pass(format!(
"ActiveState={state} (SubState={})",
f.show_value("SubState").unwrap_or("-")
))
} else {
Verdict::Fail(format!(
"ActiveState={state}, expected {}; Result={}, ExecMainStatus={}",
if want {
"active"
} else {
"stopped (inactive/failed) — a transitional state is not stopped"
},
f.show_value("Result").unwrap_or("-"),
f.show_value("ExecMainStatus").unwrap_or("-"),
))
}
}
fn judge_clean_slate(f: &Facts) -> Verdict {
let mut problems: Vec<String> = Vec::new();
match f.show_value("LoadState") {
Some("not-found") | Some("") | None => {}
Some(other) => problems.push(format!("LoadState={other} (systemd still knows this unit)")),
}
if let Some(frag) = f.show_value("FragmentPath") {
if !frag.is_empty() {
problems.push(format!("FragmentPath={frag} still on disk"));
}
}
let leftovers: Vec<&String> = f
.files
.iter()
.filter(|p| p.starts_with("/etc/systemd/system/skid"))
.collect();
if !leftovers.is_empty() {
problems.push(format!(
"{} leftover path(s): {leftovers:?}",
leftovers.len()
));
}
if problems.is_empty() {
Verdict::Pass("no fragment, no drop-in dir, unit unknown to systemd".into())
} else {
Verdict::Fail(format!(
"uninstall left a landmine — {}",
problems.join("; ")
))
}
}
fn judge_proc_env(key: &str, want: &str, f: &Facts) -> Verdict {
match f.env_value(key) {
Some(v) if v == want => Verdict::Pass(format!("{key}={v}")),
Some(v) => Verdict::Fail(format!(
"the live process has {key}={v:?}, expected {want:?}"
)),
None => Verdict::Fail(format!(
"the live process's environ has no {key} at all (MainPID={}, {} vars captured) — \
nothing was read, so nothing can pass",
f.show_value("MainPID").unwrap_or("-"),
f.proc_env.len()
)),
}
}
fn judge_ordered_after_network_online(f: &Facts) -> Verdict {
const NET: &str = "network-online.target";
if !f.after().contains(&NET) {
return Verdict::Fail(format!(
"the unit does not declare After={NET} (After={:?}) — systemd was never asked to order it",
f.after()
));
}
let (Some(unit_t), Some(net_t)) = (
f.monotonic("ActiveEnterTimestampMonotonic"),
f.monotonic("NetworkOnlineActiveEnterTimestampMonotonic"),
) else {
return Verdict::Fail(
"no activation timestamps were captured — an ordering claim needs two clocks".into(),
);
};
if net_t == 0 {
return Verdict::Fail(format!(
"{NET} never became active in this boot, so there is nothing to be ordered after \
(unit activated at {unit_t}µs monotonic)"
));
}
if unit_t == 0 {
return Verdict::Fail("the unit never became active in this boot".into());
}
if unit_t < net_t {
return Verdict::Fail(format!(
"the unit activated {}µs BEFORE {NET} (unit {unit_t}µs, network {net_t}µs monotonic)",
net_t - unit_t
));
}
Verdict::Pass(format!(
"unit activated {}µs after {NET} ({unit_t} ≥ {net_t})",
unit_t - net_t
))
}
// ────────────────────────────── the case model ───────────────────────────────
/// A step in the plan wire (host-side mirror of `skidbladnir_probe::plan::Step`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlanStep {
Run(Vec<String>),
Write {
path: String,
mode: u32,
contents: String,
},
Rm(String),
RmTree(String),
Sleep(u64),
Collect {
unit: String,
exec: String,
},
/// Keep a command's **stdout** under `key` for the next `Collect` to carry.
Capture {
key: String,
argv: Vec<String>,
},
/// Poll `argv` until it exits 0 or `timeout_ms` elapses; the waited-for latency is
/// recorded in `exits`.
WaitFor {
timeout_ms: u64,
poll_ms: u64,
argv: Vec<String>,
},
PowerOff,
}
/// Convenience: `RUN systemctl <args…>`.
fn systemctl(args: &[&str]) -> PlanStep {
let mut v = vec!["systemctl".to_string()];
v.extend(args.iter().map(|s| s.to_string()));
PlanStep::Run(v)
}
/// One expectation on a case's facts.
#[derive(Debug, Clone)]
pub struct Expect {
/// Row name in the test matrix (`<case>::<name>`).
pub name: String,
pub check: Check,
/// `false` marks a **deliberate red**: the case engineers a broken machine and the
/// suite requires the guard to notice. This is what stops a judge from being
/// hollow — see the module docs.
pub expect_pass: bool,
}
impl Expect {
pub fn pass(name: &str, check: Check) -> Self {
Expect {
name: name.into(),
check,
expect_pass: true,
}
}
/// A guard that MUST go red here.
pub fn red(name: &str, check: Check) -> Self {
Expect {
name: name.into(),
check,
expect_pass: false,
}
}
}
/// One case of the suite.
pub struct SuiteCase {
pub id: String,
pub steps: Vec<PlanStep>,
pub checks: Vec<Expect>,
/// Why a host-only (no-VM) test could not have caught what this case catches, or
/// `""` for a setup case that asserts nothing. Data, not a comment, so the test
/// can print it and a reviewer can audit the claim per case.
pub host_only_blind_spot: &'static str,
}
impl SuiteCase {
fn setup(id: &str, steps: Vec<PlanStep>) -> Self {
SuiteCase {
id: id.into(),
steps,
checks: Vec::new(),
host_only_blind_spot: "",
}
}
}
/// Render the plan wire the guest interpreter parses.
///
/// Hex payloads and whitespace-split argv: see `skidbladnir_probe::plan` for the
/// grammar and why file bodies must not be escapable.
pub fn render_plan(cases: &[SuiteCase]) -> String {
let mut s = String::from("# skidbladnir vm-suite plan\n");
for c in cases {
s.push_str(&format!("CASE {}\n", c.id));
for step in &c.steps {
match step {
PlanStep::Run(argv) => s.push_str(&format!("RUN {}\n", argv.join(" "))),
PlanStep::Write {
path,
mode,
contents,
} => s.push_str(&format!(
"WRITE {path} {mode:04o} {}\n",
hex_encode(contents.as_bytes())
)),
PlanStep::Rm(p) => s.push_str(&format!("RM {p}\n")),
PlanStep::RmTree(p) => s.push_str(&format!("RMTREE {p}\n")),
PlanStep::Sleep(ms) => s.push_str(&format!("SLEEP {ms}\n")),
PlanStep::Collect { unit, exec } => s.push_str(&format!("COLLECT {unit} {exec}\n")),
PlanStep::Capture { key, argv } => {
s.push_str(&format!("CAPTURE {key} {}\n", argv.join(" ")))
}
PlanStep::WaitFor {
timeout_ms,
poll_ms,
argv,
} => s.push_str(&format!(
"WAITFOR {timeout_ms} {poll_ms} {}\n",
argv.join(" ")
)),
PlanStep::PowerOff => s.push_str("POWEROFF\n"),
}
}
s.push_str("END\n");
}
s
}
/// Lowercase hex (mirror of `skidbladnir_probe::hex_encode`; the guest crate cannot
/// be depended on from here — separate workspace, `publish = false`).
fn hex_encode(bytes: &[u8]) -> String {
const D: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push(D[(b >> 4) as usize] as char);
s.push(D[(b & 0x0f) as usize] as char);
}
s
}
// ─────────────────────────── the service under test ──────────────────────────
/// The [`ServiceSpec`] the suite installs — built through the SAME public builder
/// every product uses, and rendered by the SAME [`renderer_for`] renderer, so the
/// thing under test is Skidbladnir's real unit path and not a fixture that resembles
/// it.
pub fn suite_service_spec() -> ServiceSpec {
ServiceSpec::new("skidtest", "Skidbladnir VM systemd suite service", EXEC)
.arg("--port")
.arg(TEST_PORT.to_string())
.arg("--name")
.arg("skidtest")
.user(SVC_USER)
.scope(Scope::System)
.after("network-online.target")
.needs("network-online.target")
.env("SKID_MARK", "vm-suite")
}
/// The rendered unit for [`suite_service_spec`].
pub fn suite_rendered() -> RenderedService {
renderer_for(Backend::SystemdSystem)
.expect("the systemd-system renderer is always present")
.render(&suite_service_spec())
.expect("the fixed suite spec renders")
}
/// The hostile drop-in the suite plants — the reproduction of korp's 2026-08-03
/// defect: a stale `.d/` file that silently wins over a correctly rendered unit.
pub const HOSTILE_DROP_IN: &str = "/etc/systemd/system/skidtest.service.d/99-stale-conflict.conf";
fn hostile_drop_in_body() -> String {
"# A stale drop-in of exactly the kind that survived five korp uninstalls.\n\
[Service]\n\
User=root\n\
Group=root\n\
Environment=\"SKID_MARK=hijacked\"\n"
.to_string()
}
/// The deliberately-early control unit: `DefaultDependencies=no` + `Before=sysinit.target`
/// makes systemd activate it long before any network target, so
/// [`Check::OrderedAfterNetworkOnline`] MUST go red on it. `Type=oneshot` +
/// `ExecStart=/bin/true` so it cannot delay or wedge the boot.
fn early_unit_body() -> String {
"[Unit]\n\
Description=Skidbladnir boot-ordering NEGATIVE control (activates before the network)\n\
DefaultDependencies=no\n\
Before=sysinit.target\n\
[Service]\n\
Type=oneshot\n\
RemainAfterExit=yes\n\
ExecStart=/bin/true\n\
[Install]\n\
WantedBy=sysinit.target\n"
.to_string()
}
/// The oneshot unit that runs the boot-B plan late in the second boot.
fn boot_report_unit_body(plan_path: &str) -> String {
format!(
"[Unit]\n\
Description=Skidbladnir VM suite boot-B reporter\n\
After=multi-user.target network-online.target {UNIT}\n\
[Service]\n\
Type=oneshot\n\
ExecStart={SCENARIO} --plan {plan_path} --report {REPORT_PORT} --also /var/log/skid-report-b.jsonl\n\
[Install]\n\
WantedBy=multi-user.target\n"
)
}
/// Where the boot-B plan is staged inside the guest.
pub const PLAN_B_PATH: &str = "/var/lib/skid/plan-b.txt";
fn write_rendered(r: &RenderedService) -> Vec<PlanStep> {
r.files
.iter()
.map(|f| PlanStep::Write {
path: if f.path.starts_with('/') {
f.path.clone()
} else {
format!("/{}", f.path)
},
mode: f.mode,
contents: f.contents.clone(),
})
.collect()
}
/// **Boot A** — the lifecycle and drop-in suite: eight assertable cases in ONE boot.
///
/// Ordered as a story, because the interesting bugs are sequential: a clean install,
/// a stale drop-in hijacking it, the drop-in removed and the guard recovering, a
/// naive uninstall leaving that drop-in behind, and a *fresh, correct* install
/// silently inheriting it — which is korp's defect, reproduced from first principles.
pub fn plan_boot_a() -> Vec<SuiteCase> {
let rendered = suite_rendered();
let install = write_rendered(&rendered);
let no_drop_ins = Check::NoForeignDropIns(Vec::new());
let right_user = || Check::ResolvedUser(SVC_USER.to_string());
let mut cases: Vec<SuiteCase> = Vec::new();
cases.push(SuiteCase::setup(
"prepare",
vec![
PlanStep::Run(vec![
"/usr/sbin/useradd".into(),
"--system".into(),
"--home-dir".into(),
SVC_HOME.into(),
"--create-home".into(),
"--shell".into(),
"/usr/sbin/nologin".into(),
SVC_USER.into(),
]),
systemctl(&["daemon-reload"]),
],
));
let bringup = |extra_sleep: u64| -> Vec<PlanStep> {
vec![
systemctl(&["daemon-reload"]),
systemctl(&["enable", UNIT]),
systemctl(&["start", UNIT]),
PlanStep::Sleep(extra_sleep),
]
};
// 1 ── a clean install resolves to the declared user.
let mut steps = install.clone();
steps.extend(bringup(3000));
steps.push(PlanStep::Collect {
unit: UNIT.into(),
exec: EXEC.into(),
});
cases.push(SuiteCase {
id: "clean-install".into(),
steps,
checks: vec![
Expect::pass("resolved-user", right_user()),
Expect::pass("no-foreign-dropins", no_drop_ins.clone()),
Expect::pass("active", Check::Active(true)),
Expect::pass("proc-env-user", Check::ProcEnv("USER".into(), SVC_USER.into())),
Expect::pass("proc-env-home", Check::ProcEnv("HOME".into(), SVC_HOME.into())),
Expect::pass("proc-env-declared", Check::ProcEnv("SKID_MARK".into(), "vm-suite".into())),
],
host_only_blind_spot:
"`User=`/`HOME`/`Environment=` as the LIVE process received them come from \
/proc/<MainPID>/environ of a process systemd forked. Nothing on the host reads that \
for a unit the host never started.",
});
// 2 ── THE RED: a contradicting drop-in silently wins.
cases.push(SuiteCase {
id: "dropin-user-override".into(),
steps: vec![
PlanStep::Write {
path: HOSTILE_DROP_IN.into(),
mode: 0o644,
contents: hostile_drop_in_body(),
},
systemctl(&["daemon-reload"]),
systemctl(&["restart", UNIT]),
PlanStep::Sleep(3000),
PlanStep::Collect { unit: UNIT.into(), exec: EXEC.into() },
],
checks: vec![
Expect::red("resolved-user", right_user()),
Expect::red("no-foreign-dropins", no_drop_ins.clone()),
Expect::red("proc-env-user", Check::ProcEnv("USER".into(), SVC_USER.into())),
Expect::red("proc-env-declared", Check::ProcEnv("SKID_MARK".into(), "vm-suite".into())),
// Still running — which is what makes the bug so quiet in production.
Expect::pass("active", Check::Active(true)),
],
host_only_blind_spot:
"This is korp's 2026-08-03 defect: the unit body still says User=skidsvc; only \
systemd's own merge of fragment + drop-ins produces the wrong answer. A renderer \
test reads the fragment and sees nothing wrong.",
});
// 3 ── the guard recovers: not stuck on fail.
cases.push(SuiteCase {
id: "dropin-removed".into(),
steps: vec![
PlanStep::Rm(HOSTILE_DROP_IN.into()),
PlanStep::RmTree(format!("/etc/systemd/system/{UNIT}.d")),
systemctl(&["daemon-reload"]),
systemctl(&["restart", UNIT]),
PlanStep::Sleep(3000),
PlanStep::Collect { unit: UNIT.into(), exec: EXEC.into() },
],
checks: vec![
Expect::pass("resolved-user", right_user()),
Expect::pass("no-foreign-dropins", no_drop_ins.clone()),
Expect::pass("active", Check::Active(true)),
],
host_only_blind_spot:
"Proves the two guards above are not stuck on red — the same live systemd, the same \
two judges, opposite verdict.",
});
// 4 ── stop: the Active guard proven in both directions.
cases.push(SuiteCase {
id: "stopped".into(),
steps: vec![
systemctl(&["stop", UNIT]),
PlanStep::Sleep(1500),
PlanStep::Collect { unit: UNIT.into(), exec: EXEC.into() },
],
checks: vec![
Expect::pass("inactive", Check::Active(false)),
Expect::red("active", Check::Active(true)),
],
host_only_blind_spot:
"An ActiveState transition of a real system unit, driven by a real systemctl stop.",
});
// 5 ── a naive uninstall: unit file gone, drop-in dir left behind.
cases.push(SuiteCase {
id: "uninstall-naive-leaves-a-landmine".into(),
steps: vec![
PlanStep::Write {
path: HOSTILE_DROP_IN.into(),
mode: 0o644,
contents: hostile_drop_in_body(),
},
systemctl(&["disable", UNIT]),
PlanStep::Rm(format!("/etc/systemd/system/{UNIT}")),
systemctl(&["daemon-reload"]),
PlanStep::Collect { unit: UNIT.into(), exec: EXEC.into() },
],
checks: vec![
Expect::red("clean-slate", Check::CleanSlate),
Expect::red("no-foreign-dropins", no_drop_ins.clone()),
],
host_only_blind_spot:
"The unit is now UNLOADED, so systemd reports DropInPaths= (empty). A guard trusting \
only the resolved property would call this machine clean; only the on-disk \
/etc/systemd/system/<unit>.d listing shows the landmine.",
});
// 6 ── the payload: a fresh, CORRECT install inherits the stale override.
let mut steps = install.clone();
steps.extend(bringup(3000));
steps.push(PlanStep::Collect {
unit: UNIT.into(),
exec: EXEC.into(),
});
cases.push(SuiteCase {
id: "reinstall-inherits-the-landmine".into(),
steps,
checks: vec![
Expect::red("resolved-user", right_user()),
Expect::red("no-foreign-dropins", no_drop_ins.clone()),
Expect::red("proc-env-user", Check::ProcEnv("USER".into(), SVC_USER.into())),
Expect::pass("active", Check::Active(true)),
],
host_only_blind_spot:
"korp's defect END TO END: an install that rendered the right unit and reported \
success, running as the wrong user. The install is correct; the machine is not.",
});
// 7 ── a thorough uninstall really is clean.
cases.push(SuiteCase {
id: "uninstall-thorough".into(),
steps: vec![
systemctl(&["disable", "--now", UNIT]),
PlanStep::Rm(format!("/etc/systemd/system/{UNIT}")),
PlanStep::RmTree(format!("/etc/systemd/system/{UNIT}.d")),
systemctl(&["daemon-reload"]),
systemctl(&["reset-failed", UNIT]),
PlanStep::Sleep(500),
PlanStep::Collect { unit: UNIT.into(), exec: EXEC.into() },
],
checks: vec![Expect::pass("clean-slate", Check::CleanSlate)],
host_only_blind_spot:
"Proves the clean-slate guard is not stuck on red, against the same live systemd \
that reported the landmine one case earlier.",
});
// 8 ── install → uninstall → install round-trips with no residue.
let mut steps = install.clone();
steps.extend(bringup(3000));
steps.push(PlanStep::Collect {
unit: UNIT.into(),
exec: EXEC.into(),
});
cases.push(SuiteCase {
id: "reinstall-clean-roundtrip".into(),
steps,
checks: vec![
Expect::pass("resolved-user", right_user()),
Expect::pass("no-foreign-dropins", no_drop_ins),
Expect::pass("active", Check::Active(true)),
Expect::pass("proc-env-user", Check::ProcEnv("USER".into(), SVC_USER.into())),
],
host_only_blind_spot:
"After a thorough uninstall, the SAME spec installs to the SAME resolved state — the \
round-trip property, asserted against systemd's merge rather than a rendered string.",
});
// 9 ── arm the second boot, then power off.
cases.push(SuiteCase::setup(
"arm-boot-b",
vec![
PlanStep::Write {
path: format!("/etc/systemd/system/{EARLY_UNIT}"),
mode: 0o644,
contents: early_unit_body(),
},
PlanStep::Write {
path: PLAN_B_PATH.into(),
mode: 0o644,
contents: render_plan(&plan_boot_b()),
},
PlanStep::Write {
path: "/etc/systemd/system/skid-bootreport.service".into(),
mode: 0o644,
contents: boot_report_unit_body(PLAN_B_PATH),
},
systemctl(&["daemon-reload"]),
systemctl(&["enable", EARLY_UNIT]),
systemctl(&["enable", "skid-bootreport.service"]),
systemctl(&["enable", UNIT]),
PlanStep::PowerOff,
],
));
cases
}
/// **Boot B** — what only a fresh boot can show: did systemd honour the ordering it
/// was given?
///
/// Run by `skid-bootreport.service` in the second boot of the SAME disk, so the unit
/// was brought up by systemd from its `[Install]` wants — not by a `systemctl start`
/// whose timing would mean nothing.
pub fn plan_boot_b() -> Vec<SuiteCase> {
vec![
SuiteCase {
id: "boot-ordering".into(),
steps: vec![PlanStep::Collect {
unit: UNIT.into(),
exec: EXEC.into(),
}],
checks: vec![
Expect::pass(
"ordered-after-network-online",
Check::OrderedAfterNetworkOnline,
),
Expect::pass("active", Check::Active(true)),
Expect::pass("resolved-user", Check::ResolvedUser(SVC_USER.to_string())),
Expect::pass("no-foreign-dropins", Check::NoForeignDropIns(Vec::new())),
],
host_only_blind_spot: "ONLY a fresh boot produces this: systemd, not a test, decided when to activate \
the unit relative to network-online.target. It also proves the install survived \
a reboot as the declared user.",
},
SuiteCase {
id: "boot-ordering-early-control".into(),
steps: vec![PlanStep::Collect {
unit: EARLY_UNIT.into(),
exec: "/bin/true".into(),
}],
checks: vec![Expect::red(
"ordered-after-network-online",
Check::OrderedAfterNetworkOnline,
)],
host_only_blind_spot: "The NEGATIVE control for the ordering guard, and it is only obtainable from a \
boot: a sysinit.target oneshot that genuinely activated before the network.",
},
SuiteCase::setup("boot-b-done", vec![PlanStep::PowerOff]),
]
}
// ──────────────────────────────── the driver ─────────────────────────────────
/// Where the suite finds its inputs and puts its scratch.
#[derive(Debug, Clone)]
pub struct SuiteConfig {
/// The cached cloud qcow2. Never booted directly — a per-run copy is made, so a
/// shared base image cannot be mutated (`draupnir`'s `BootloaderDisk` has no
/// per-drive `snapshot=on`; see `KvmController::to_boot_spec`'s COW gap note).
pub base_image: PathBuf,
pub work_root: PathBuf,
pub scenario_bin: PathBuf,
pub testsvc_bin: PathBuf,
pub probe_bin: PathBuf,
pub boot_timeout: Duration,
/// How long to let boot A keep running after its terminal marker, so cloud-init's
/// `scripts-user` module returns (writing its per-instance semaphore) and the
/// scheduled power-off lands. Kill the guest before that and the second boot
/// re-runs the whole plan from cloud-init instead of being a fresh boot.
pub shutdown_grace: Duration,
pub memory_mb: u32,
pub cpus: u32,
}
/// One graded expectation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Outcome {
pub case: String,
pub check: String,
pub verdict: Verdict,
/// What the case declared it wanted the guard to say.
pub expect_pass: bool,
/// The guard behaved as declared. **This** is the suite's pass/fail, not
/// `verdict.is_pass()` — a deliberate-red case passes by going red.
pub as_declared: bool,
/// Milliseconds from the boot's start to this case's facts line arriving.
pub elapsed_ms: u128,
}
/// The result of a whole suite run.
#[derive(Debug, Default)]
pub struct SuiteRun {
pub outcomes: Vec<Outcome>,
pub facts: Vec<Facts>,
/// `(case, wall-clock ms since that boot started)` in arrival order.
pub case_timings: Vec<(String, u128)>,
pub boot_a_ms: u128,
pub boot_b_ms: u128,
/// The case ids boot B reported. If this contains a boot-A case, boot B was a
/// cloud-init REPLAY rather than a fresh boot of an installed system — and its
/// ordering facts would then be meaningless.
pub boot_b_cases: Vec<String>,
/// Anything the run could not determine, verbatim.
pub notes: Vec<String>,
}
impl SuiteRun {
pub fn failures(&self) -> Vec<&Outcome> {
self.outcomes.iter().filter(|o| !o.as_declared).collect()
}
}
/// Grade every case's facts. Cases with no facts line are graded as a **hard fail**
/// per expectation, never skipped: a case that did not report is a case that asserted
/// nothing, and silence must not read as success.
pub fn grade(cases: &[SuiteCase], facts: &[Facts], arrival_ms: &[(String, u128)]) -> Vec<Outcome> {
let mut out = Vec::new();
for c in cases {
if c.checks.is_empty() {
continue;
}
let elapsed = arrival_ms
.iter()
.find(|(id, _)| *id == c.id)
.map(|(_, ms)| *ms)
.unwrap_or(0);
match facts.iter().find(|f| f.case == c.id) {
Some(f) => {
for e in &c.checks {
let v = judge(&e.check, f);
let as_declared = v.is_pass() == e.expect_pass;
out.push(Outcome {
case: c.id.clone(),
check: e.name.clone(),
verdict: v,
expect_pass: e.expect_pass,
as_declared,
elapsed_ms: elapsed,
});
}
}
None => {
for e in &c.checks {
out.push(Outcome {
case: c.id.clone(),
check: e.name.clone(),
verdict: Verdict::Fail(
"no facts line arrived for this case — it asserted NOTHING".into(),
),
expect_pass: e.expect_pass,
as_declared: false,
elapsed_ms: 0,
});
}
}
}
}
out
}
/// The cloud-init `user-data` for boot A: mount the payload, install the three
/// injected binaries, stage the plan, run the scenario.
///
/// Pure (strings in → string out) so its shape is unit-testable without booting.
pub fn render_user_data(plan_written_to: &str) -> String {
format!(
"#cloud-config\n\
# Skidbladnir VM systemd suite — boot A.\n\
runcmd:\n\
\x20 - [ sh, -c, \"mkdir -p /mnt/skid; for d in /dev/vd* /dev/sd*; do mount -t vfat $d /mnt/skid 2>/dev/null && [ -f /mnt/skid/skidbladnir-scenario ] && break; umount /mnt/skid 2>/dev/null; done\" ]\n\
\x20 - [ sh, -c, \"install -m0755 /mnt/skid/skidbladnir-scenario /usr/local/bin/skidbladnir-scenario; install -m0755 /mnt/skid/skidbladnir-testsvc {EXEC}; install -m0755 /mnt/skid/skidbladnir-probe /usr/local/bin/skidbladnir-probe\" ]\n\
\x20 - [ sh, -c, \"mkdir -p /var/lib/skid; cp /mnt/skid/plan.txt {plan_written_to}\" ]\n\
\x20 - [ sh, -c, \"{SCENARIO} --plan {plan_written_to} --report {REPORT_PORT} --also /var/log/skid-report-a.jsonl\" ]\n"
)
}
/// The FAT payload image (label `skiddata`) — authored by the ONE seed builder
/// (`draupnir::seed::build_fat_image`), never a twinned `fatfs` call (dup-audit L5).
#[cfg(feature = "matrix")]
fn build_payload(out: &Path, cfg: &SuiteConfig, plan: &str) -> Result<()> {
let files: Vec<(String, Vec<u8>)> = vec![
(
"skidbladnir-scenario".into(),
std::fs::read(&cfg.scenario_bin)
.with_context(|| format!("read {}", cfg.scenario_bin.display()))?,
),
(
"skidbladnir-testsvc".into(),
std::fs::read(&cfg.testsvc_bin)
.with_context(|| format!("read {}", cfg.testsvc_bin.display()))?,
),
(
"skidbladnir-probe".into(),
std::fs::read(&cfg.probe_bin)
.with_context(|| format!("read {}", cfg.probe_bin.display()))?,
),
("plan.txt".into(), plan.as_bytes().to_vec()),
];
draupnir::seed::build_fat_image(out, "skiddata", &files)?;
Ok(())
}
/// Parse every complete facts line present in `text`, ignoring the scenario's own
/// start/end markers and any partial trailing line.
pub fn parse_report(text: &str) -> Vec<Facts> {
let mut out = Vec::new();
for line in text.lines() {
let line = line.trim().trim_matches('\0').trim();
if !line.starts_with('{') || !line.ends_with('}') {
continue;
}
if let Ok(f) = serde_json::from_str::<Facts>(line) {
if !f.case.is_empty() {
out.push(f);
}
}
}
out
}
/// The scenario's terminal marker. Waiting for it — not merely for the last
/// *asserting* case — is load-bearing: the arming case that stages boot B runs
/// AFTER the last assertion, and stopping the guest at the last assertion killed it
/// mid-cloud-init (measured on oden 2026-08-03, which is why this exists).
#[cfg(feature = "matrix")]
const END_MARKER: &str = "\"scenario_end\"";
/// Tail the host-side serial file until every id in `want` has arrived AND the
/// scenario reported it was done (or the timeout), stamping each case's **arrival
/// wall-clock** relative to `t0` — the honest per-case cost of the suite, since the
/// guest does not time itself.
#[cfg(feature = "matrix")]
fn tail_report(
path: &Path,
want: &[String],
t0: Instant,
timeout: Duration,
) -> (Vec<Facts>, Vec<(String, u128)>, bool) {
let deadline = t0 + timeout;
let mut seen: Vec<Facts> = Vec::new();
let mut timings: Vec<(String, u128)> = Vec::new();
let mut ended = false;
loop {
if let Ok(text) = std::fs::read_to_string(path) {
for f in parse_report(&text) {
if !seen.iter().any(|s| s.case == f.case) {
timings.push((f.case.clone(), t0.elapsed().as_millis()));
seen.push(f);
}
}
ended = text.contains(END_MARKER);
}
if ended && want.iter().all(|w| seen.iter().any(|f| f.case == *w)) {
return (seen, timings, true);
}
if Instant::now() >= deadline {
return (seen, timings, ended);
}
std::thread::sleep(Duration::from_millis(750));
}
}
#[cfg(feature = "matrix")]
fn boot_once(
cfg: &SuiteConfig,
name: &str,
disk: &Path,
payload: Option<&Path>,
user_data: String,
report_path: &Path,
) -> Result<(KvmController, crate::machine::Handle)> {
let _ = std::fs::write(report_path, b"");
let spec = MachineSpec {
name: name.to_string(),
image: ImageRef::Path(disk.to_path_buf()),
cpus: cfg.cpus,
memory_mb: cfg.memory_mb,
// cloud-init's first boot wants a datasource + package mirror reachability;
// the suite itself installs nothing from the network.
net: NetMode::Nat,
provision: Provision::CloudInit {
user_data,
meta_data: None,
},
mounts: Vec::new(),
readiness: Readiness::Immediate,
iso: None,
data_iso: payload.map(|p| p.to_path_buf()),
serial_out: Some(report_path.to_path_buf()),
qga_sock: None,
// The suite handed over its OWN per-run copy (see `run_suite`), and it boots
// that copy TWICE: once to install, once to watch systemd bring the installed
// system up. Without this the second boot gets a pristine image, re-runs
// cloud-init, and produces confident ordering facts about a machine that was
// never installed. draupnir >= 0.1.9.
persist_disk: true,
};
let ctrl = KvmController::new();
let handle = ctrl
.start(&spec)
.with_context(|| format!("boot `{name}`"))?;
Ok((ctrl, handle))
}
/// Run the whole suite: boot A (lifecycle + drop-ins), then boot B (fresh-boot
/// ordering) off the SAME disk.
#[cfg(feature = "matrix")]
pub fn run_suite(cfg: &SuiteConfig) -> Result<SuiteRun> {
let mut run = SuiteRun::default();
std::fs::create_dir_all(&cfg.work_root)?;
// A per-run copy: the cached base image is never written to.
let disk = cfg.work_root.join("suite-disk.qcow2");
let t_copy = Instant::now();
std::fs::copy(&cfg.base_image, &disk)
.with_context(|| format!("copy base image {}", cfg.base_image.display()))?;
run.notes.push(format!(
"copied base image → per-run disk in {} ms ({} MiB)",
t_copy.elapsed().as_millis(),
std::fs::metadata(&disk)
.map(|m| m.len() / 1_048_576)
.unwrap_or(0),
));
// ── boot A ───────────────────────────────────────────────────────────────
let cases_a = plan_boot_a();
let plan_a = render_plan(&cases_a);
let payload = cfg.work_root.join("skiddata.img");
build_payload(&payload, cfg, &plan_a)?;
let report_a = cfg.work_root.join("report-a.jsonl");
let user_data = render_user_data("/var/lib/skid/plan.txt");
let t0 = Instant::now();
let (ctrl_a, h_a) = boot_once(
cfg,
"skid-vmsuite-a",
&disk,
Some(&payload),
user_data.clone(),
&report_a,
)?;
let want_a: Vec<String> = cases_a
.iter()
.filter(|c| !c.checks.is_empty())
.map(|c| c.id.clone())
.collect();
let (facts_a, timings_a, ended_a) = tail_report(&report_a, &want_a, t0, cfg.boot_timeout);
if !ended_a {
run.notes.push(
"boot A never reported `scenario_end` — the arming case that stages boot B may not \
have run"
.into(),
);
}
// Let cloud-init's `scripts-user` module return and write its per-instance
// semaphore, and let the scheduled power-off land. Without this grace the guest
// is killed mid-cloud-init and the SECOND boot re-runs the whole plan from
// cloud-init instead of being a fresh boot of an installed system.
std::thread::sleep(cfg.shutdown_grace);
run.boot_a_ms = t0.elapsed().as_millis();
let _ = ctrl_a.stop(&h_a);
run.outcomes.extend(grade(&cases_a, &facts_a, &timings_a));
run.case_timings.extend(timings_a);
run.facts.extend(facts_a);
// ── boot B: the SAME disk, booted again. cloud-init sees the same instance-id
// and skips its per-instance modules, so the only thing that runs our code is
// `skid-bootreport.service`, started by systemd from its [Install] wants. ────
let cases_b = plan_boot_b();
let report_b = cfg.work_root.join("report-b.jsonl");
let t1 = Instant::now();
let (ctrl_b, h_b) = boot_once(
cfg,
"skid-vmsuite-b",
&disk,
Some(&payload),
user_data,
&report_b,
)?;
let want_b: Vec<String> = cases_b
.iter()
.filter(|c| !c.checks.is_empty())
.map(|c| c.id.clone())
.collect();
let (facts_b, timings_b, _) = tail_report(&report_b, &want_b, t1, cfg.boot_timeout);
run.boot_b_ms = t1.elapsed().as_millis();
let _ = ctrl_b.stop(&h_b);
run.boot_b_cases = facts_b.iter().map(|f| f.case.clone()).collect();
run.outcomes.extend(grade(&cases_b, &facts_b, &timings_b));
run.case_timings.extend(timings_b);
run.facts.extend(facts_b);
Ok(run)
}
/// Collect [`Facts`] for a unit on **this host's** live systemd — read-only.
///
/// The judges are pure functions over facts, so nothing about them is VM-specific:
/// the same `ResolvedUser` / `NoForeignDropIns` guards that grade a guest can grade
/// the box you are standing on. That is the cheap half of this suite and it needs no
/// boot at all — which matters, because the defect that started all of this is
/// sitting on oden right now (`korp-server`: fragment declares `User=korp`, five
/// drop-ins resolve it to `alice`).
///
/// Strictly read-only: `systemctl show` and `systemctl cat`, plus reads under
/// `/etc/systemd/system` and `/proc`. It never starts, stops, reloads or writes
/// anything — a diagnostic that could perturb the machine it is diagnosing would be
/// useless on the production box you most want to point it at.
///
/// This mirrors `skidbladnir_probe::facts::collect`, and cannot share code with it:
/// the guest collector must cross-compile to static musl depending on nothing but
/// `std`, so it lives in a separate cargo workspace (`probe/`) this crate cannot
/// depend on. What the two share is the *format*, pinned from both sides by
/// [`tests::the_guest_facts_line_deserializes`] and the golden-plan pair.
pub fn collect_host_facts(unit: &str) -> Facts {
use std::process::Command;
/// The properties a judge may read. Mirrors `skidbladnir_probe::facts::SHOW_PROPS`
/// — the guest crate is a separate workspace this one cannot depend on. A property
/// missing here is not a silent "" : every judge treats an uncaptured property as
/// a **Fail**, so a drift between the two lists surfaces as a red, not as a pass.
const SHOW_PROPS: &[&str] = &[
"Id",
"LoadState",
"ActiveState",
"SubState",
"UnitFileState",
"FragmentPath",
"DropInPaths",
"User",
"Group",
"Environment",
"EnvironmentFiles",
"WorkingDirectory",
"After",
"Wants",
"MainPID",
"ExecMainStatus",
"Result",
"ActiveEnterTimestampMonotonic",
];
let run = |args: &[&str]| -> String {
Command::new("systemctl")
.args(args)
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default()
};
let mut show_args: Vec<&str> = vec!["show", "--no-pager"];
for p in SHOW_PROPS {
show_args.push("-p");
show_args.push(p);
}
show_args.push(unit);
let mut show: Vec<String> = run(&show_args)
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| l.trim_end().to_string())
.collect();
if let Some(v) = run(&[
"show",
"--no-pager",
"-p",
"ActiveEnterTimestampMonotonic",
"network-online.target",
])
.lines()
.find_map(|l| l.strip_prefix("ActiveEnterTimestampMonotonic="))
{
show.push(format!(
"NetworkOnlineActiveEnterTimestampMonotonic={}",
v.trim()
));
}
let mut f = Facts {
case: format!("host:{unit}"),
cat: run(&["cat", "--no-pager", unit]),
show,
at: chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(),
..Default::default()
};
if let Some(pid) = f
.show_value("MainPID")
.and_then(|v| v.trim().parse::<u32>().ok())
.filter(|p| *p != 0)
{
if let Ok(raw) = std::fs::read(format!("/proc/{pid}/environ")) {
f.proc_env = raw
.split(|b| *b == 0)
.filter(|s| !s.is_empty())
.map(|s| String::from_utf8_lossy(s).to_string())
.collect();
}
let uid = std::fs::read_to_string(format!("/proc/{pid}/status"))
.ok()
.and_then(|s| {
s.lines()
.find_map(|l| l.strip_prefix("Uid:"))
.and_then(|v| {
v.split_whitespace()
.next()
.and_then(|n| n.parse::<u32>().ok())
})
});
if let Some(uid) = uid {
f.proc_user = std::fs::read_to_string("/etc/passwd")
.ok()
.and_then(|pw| {
pw.lines().find_map(|l| {
let c: Vec<&str> = l.split(':').collect();
(c.len() >= 3 && c[2].parse::<u32>() == Ok(uid)).then(|| c[0].to_string())
})
})
.unwrap_or_else(|| format!("uid:{uid}"));
}
}
let frag = format!("/etc/systemd/system/{unit}");
if std::path::Path::new(&frag).exists() {
f.files.push(frag);
}
let d = format!("/etc/systemd/system/{unit}.d");
if std::path::Path::new(&d).is_dir() {
f.files.push(format!("{d}/"));
if let Ok(rd) = std::fs::read_dir(&d) {
let mut e: Vec<String> = rd
.flatten()
.map(|x| x.path().to_string_lossy().to_string())
.collect();
e.sort();
f.files.extend(e);
}
}
f
}
/// Every case of the suite (both boots), for reporting and for the arms declaration.
pub fn all_cases() -> Vec<SuiteCase> {
let mut v = plan_boot_a();
v.extend(plan_boot_b());
v
}
#[cfg(test)]
mod tests {
use super::*;
fn facts(pairs: &[(&str, &str)]) -> Facts {
Facts {
case: "t".into(),
show: pairs.iter().map(|(k, v)| format!("{k}={v}")).collect(),
..Default::default()
}
}
// ─────────────────── the wire: host renders what the guest parses ───────────
/// The GOLDEN plan text, byte-for-byte the same literal as
/// `skidbladnir_probe::plan::tests::GOLDEN` in the guest crate. The two crates
/// live in different cargo workspaces and share no type, so this pair of tests is
/// the only thing standing between them and a silent wire drift.
#[test]
fn the_rendered_plan_is_exactly_the_golden_the_guest_parser_pins() {
let cases = vec![
SuiteCase::setup(
"clean-install",
vec![
PlanStep::Write {
path: "/etc/systemd/system/skidtest.service".into(),
mode: 0o644,
contents: "[Unit]\n".into(),
},
PlanStep::Run(vec!["systemctl".into(), "daemon-reload".into()]),
PlanStep::WaitFor {
timeout_ms: 60_000,
poll_ms: 500,
argv: vec![
"systemctl".into(),
"is-active".into(),
"skidtest.service".into(),
],
},
PlanStep::Capture {
key: "footer".into(),
argv: vec![
"tail".into(),
"-c".into(),
"4".into(),
"/var/lib/skid/nodes.parquet".into(),
],
},
PlanStep::Sleep(2000),
PlanStep::Collect {
unit: "skidtest.service".into(),
exec: "/usr/local/bin/skidbladnir-testsvc".into(),
},
],
),
SuiteCase::setup("done", vec![PlanStep::PowerOff]),
];
let expected = "\
# skidbladnir vm-suite plan
CASE clean-install
WRITE /etc/systemd/system/skidtest.service 0644 5b556e69745d0a
RUN systemctl daemon-reload
WAITFOR 60000 500 systemctl is-active skidtest.service
CAPTURE footer tail -c 4 /var/lib/skid/nodes.parquet
SLEEP 2000
COLLECT skidtest.service /usr/local/bin/skidbladnir-testsvc
END
CASE done
POWEROFF
END
";
assert_eq!(render_plan(&cases), expected);
}
/// A real emitted guest line (captured from `Facts::to_json_line`) must
/// deserialize into every field this side reads.
#[test]
fn the_guest_facts_line_deserializes() {
// `r##` (not `r#`): the captured line contains `"#` — the `systemctl cat`
// header comment — which would end an `r#"…"#` literal mid-JSON.
let line = r##"{"case":"clean-install","exits":["0 systemctl daemon-reload"],"show":["User=skidsvc","DropInPaths=","ActiveState=active","MainPID=812"],"cat":"# /etc/systemd/system/skidtest.service\n[Service]\n","proc_env":["USER=skidsvc","HOME=/var/lib/skidsvc"],"proc_user":"skidsvc","files":["/etc/systemd/system/skidtest.service"],"at":"2026-08-03T09:00:00Z"}"##;
let f: Facts = serde_json::from_str(line).expect("guest line parses");
assert_eq!(f.case, "clean-install");
assert_eq!(f.show_value("User"), Some("skidsvc"));
assert_eq!(f.env_value("HOME"), Some("/var/lib/skidsvc"));
assert_eq!(f.proc_user, "skidsvc");
assert!(f.cat.contains("[Service]"));
}
/// `capture` must tell "never asked" from "asked, got nothing" — and must not be
/// answered by a longer key that merely starts the same way.
#[test]
fn a_capture_distinguishes_absence_from_emptiness_and_refuses_a_prefix_match() {
let f = Facts {
captures: vec![
"footer=PAR1".into(),
"empty=".into(),
"journal=z12 → 41 tiles\nz9 → 3 tiles\n".into(),
],
..Default::default()
};
assert_eq!(f.capture("footer"), Some("PAR1"));
assert_eq!(
f.capture("empty"),
Some(""),
"ran and printed nothing IS an answer"
);
assert_eq!(
f.capture("nope"),
None,
"never captured is not an empty answer"
);
assert!(
f.capture("journal").unwrap().lines().count() == 2,
"multi-line survives"
);
// The trap: `foot` must not be answered by `footer=PAR1`.
assert_eq!(f.capture("foot"), None, "prefix collision must not match");
}
/// The waited-for latency is parsed back out of the audit line, in BOTH
/// directions — a timeout carries its non-zero code, not a shrug.
#[test]
fn the_waitfor_latency_is_readable_and_a_timeout_is_visible() {
let f = Facts {
exits: vec![
"0 RUN systemctl daemon-reload".into(),
"0 WAITFOR 4210ms/120000ms korp-probe --expect vaduz".into(),
"1 WAITFOR 120000ms/120000ms test -f /var/x :: TIMED OUT ::".into(),
],
..Default::default()
};
assert_eq!(f.waited("korp-probe"), Some((0, 4210)));
assert_eq!(
f.waited("/var/x"),
Some((1, 120_000)),
"a timeout is data, not absence"
);
assert_eq!(f.waited("never-ran"), None);
}
#[test]
fn parse_report_skips_the_scenario_markers_and_partial_lines() {
let text = "{\"scenario_start\":\"0.1.0\",\"cases\":9}\n\
{\"case\":\"a\",\"show\":[\"User=x\"]}\n\
{\"case\":\"b\",\"sho";
let f = parse_report(text);
assert_eq!(
f.len(),
1,
"the marker has no `case`, the last line is truncated"
);
assert_eq!(f[0].case, "a");
}
// ─────────────────────── every judge, seen RED ─────────────────────────────
/// The korp defect, as a unit test: the fragment says `skidsvc`, systemd
/// resolved `alice`.
#[test]
fn resolved_user_goes_red_when_a_dropin_overrode_it() {
let mut f = facts(&[
("User", "alice"),
(
"DropInPaths",
"/etc/systemd/system/skidtest.service.d/99-stale-conflict.conf",
),
]);
f.proc_user = "alice".into();
let v = judge(&Check::ResolvedUser("skidsvc".into()), &f);
assert!(!v.is_pass(), "must be red");
assert!(
v.detail().contains("alice"),
"names what it saw: {}",
v.detail()
);
assert!(
v.detail().contains("99-stale-conflict"),
"names the hijacker: {}",
v.detail()
);
}
/// The subtler half: systemd's property is right but the live process is not.
#[test]
fn resolved_user_goes_red_when_only_the_live_process_disagrees() {
let mut f = facts(&[("User", "skidsvc")]);
f.proc_user = "root".into();
assert!(!judge(&Check::ResolvedUser("skidsvc".into()), &f).is_pass());
let mut f = facts(&[("User", "skidsvc")]);
f.proc_env = vec!["USER=root".into()];
let v = judge(&Check::ResolvedUser("skidsvc".into()), &f);
assert!(
!v.is_pass(),
"environ USER disagreeing is a red: {}",
v.detail()
);
}
/// The anti-hollow rule: no `User` property captured is a FAIL. If this returned
/// Pass, every case where the unit failed to load would score green.
#[test]
fn an_uncaptured_property_is_a_fail_not_a_pass() {
let empty = Facts::default();
assert!(!judge(&Check::ResolvedUser("skidsvc".into()), &empty).is_pass());
assert!(!judge(&Check::Active(true), &empty).is_pass());
assert!(
!judge(&Check::Active(false), &empty).is_pass(),
"even the negative form"
);
assert!(!judge(&Check::ProcEnv("USER".into(), "skidsvc".into()), &empty).is_pass());
assert!(!judge(&Check::OrderedAfterNetworkOnline, &empty).is_pass());
}
#[test]
fn resolved_user_passes_only_when_all_three_agree() {
let mut f = facts(&[("User", "skidsvc"), ("DropInPaths", "")]);
f.proc_user = "skidsvc".into();
f.proc_env = vec!["USER=skidsvc".into(), "HOME=/var/lib/skidsvc".into()];
assert!(judge(&Check::ResolvedUser("skidsvc".into()), &f).is_pass());
}
#[test]
fn foreign_dropins_are_caught_from_the_property_and_from_the_disk() {
// (a) the unit is loaded: DropInPaths names it.
let f = facts(&[(
"DropInPaths",
"/etc/systemd/system/skidtest.service.d/99-stale-conflict.conf",
)]);
assert!(!judge(&Check::NoForeignDropIns(Vec::new()), &f).is_pass());
// (b) the unit is NOT loaded (naive uninstall) → DropInPaths is empty and the
// property alone would say "clean". The disk listing is what catches it.
let mut f = facts(&[("DropInPaths", "")]);
f.files = vec!["/etc/systemd/system/skidtest.service.d/99-stale-conflict.conf".into()];
let v = judge(&Check::NoForeignDropIns(Vec::new()), &f);
assert!(
!v.is_pass(),
"a landmine on disk must be red even when systemd forgot the unit"
);
assert!(v.detail().contains("99-stale-conflict"), "{}", v.detail());
// (c) a DECLARED drop-in is fine — the guard is about foreign ones.
let allowed = "/etc/systemd/system/skidtest.service.d/10-declared.conf".to_string();
let f = facts(&[("DropInPaths", &allowed)]);
assert!(judge(&Check::NoForeignDropIns(vec![allowed]), &f).is_pass());
}
/// **RED — `Check::Active(false)` passed for a unit that was still SHUTTING DOWN.**
///
/// The twin of the `restart_over_dbus` defect (fixed 35ab55c), in the grading
/// harness rather than in the installer. `judge_active` asked
/// `ActiveState == "active"` and compared that to `want`, so `want = false` was
/// satisfied by `deactivating` — a unit whose processes are all still running.
///
/// The suite case it grades is `systemctl stop` → `Sleep(1500)` → collect →
/// `Check::Active(false)`. A service that drains for longer than 1.5 s therefore
/// graded PASS on "it stopped" while it had not. MEASURED on t14s 2026-08-25: a
/// real service sat in `deactivating` for 6.4 s after `StopUnit`.
///
/// A false green in the harness that exists to catch false greens is the worst
/// place for this shape to hide.
///
/// RED direction: restore `is_active == want` and the `deactivating` /
/// `activating` rows below flip to pass.
#[test]
fn a_unit_that_is_still_shutting_down_does_not_grade_as_stopped() {
let draining = facts(&[
("ActiveState", "deactivating"),
("SubState", "stop-sigterm"),
]);
assert!(
!judge(&Check::Active(false), &draining).is_pass(),
"`deactivating` means the processes are STILL RUNNING — grading that as \
stopped is the defect this test exists for"
);
assert!(
!judge(&Check::Active(true), &draining).is_pass(),
"…and it is not active either: a draining unit satisfies NEITHER direction"
);
assert!(
judge(&Check::Active(false), &draining)
.detail()
.contains("deactivating"),
"the red must name the state it actually saw: {}",
judge(&Check::Active(false), &draining).detail()
);
// The same asymmetry on the way up: `activating` is not yet running.
let starting = facts(&[("ActiveState", "activating"), ("SubState", "start")]);
assert!(!judge(&Check::Active(true), &starting).is_pass());
assert!(
!judge(&Check::Active(false), &starting).is_pass(),
"`activating` is not stopped either — it has, or is about to have, processes"
);
// `failed` IS stopped. A unit SIGKILLed after its stop timeout settles there,
// never in `inactive`, so a stop check that demanded `inactive` would go red on
// exactly the services slowest to die.
let failed = facts(&[("ActiveState", "failed"), ("SubState", "failed")]);
assert!(
judge(&Check::Active(false), &failed).is_pass(),
"`failed` is a unit that has finished — nothing of it is running"
);
}
#[test]
fn active_is_falsifiable_in_both_directions() {
let up = facts(&[("ActiveState", "active"), ("SubState", "running")]);
let down = facts(&[("ActiveState", "inactive"), ("SubState", "dead")]);
assert!(judge(&Check::Active(true), &up).is_pass());
assert!(!judge(&Check::Active(true), &down).is_pass());
assert!(judge(&Check::Active(false), &down).is_pass());
assert!(!judge(&Check::Active(false), &up).is_pass());
let failed = facts(&[
("ActiveState", "failed"),
("Result", "exit-code"),
("ExecMainStatus", "203"),
]);
let v = judge(&Check::Active(true), &failed);
assert!(
v.detail().contains("203"),
"a red must carry the exit status: {}",
v.detail()
);
}
#[test]
fn clean_slate_is_red_for_each_kind_of_leftover_and_green_only_when_all_are_gone() {
let gone = facts(&[("LoadState", "not-found"), ("FragmentPath", "")]);
assert!(judge(&Check::CleanSlate, &gone).is_pass());
let loaded = facts(&[("LoadState", "loaded"), ("FragmentPath", "")]);
assert!(
!judge(&Check::CleanSlate, &loaded).is_pass(),
"systemd still knows the unit"
);
let frag = facts(&[
("LoadState", "not-found"),
("FragmentPath", "/etc/systemd/system/skidtest.service"),
]);
assert!(!judge(&Check::CleanSlate, &frag).is_pass());
let mut landmine = facts(&[("LoadState", "not-found"), ("FragmentPath", "")]);
landmine.files =
vec!["/etc/systemd/system/skidtest.service.d/99-stale-conflict.conf".into()];
let v = judge(&Check::CleanSlate, &landmine);
assert!(!v.is_pass());
assert!(v.detail().contains("landmine"), "{}", v.detail());
// The exec binary is not a systemd leftover — it must not make clean-slate red.
let mut with_exec = facts(&[("LoadState", "not-found"), ("FragmentPath", "")]);
with_exec.files = vec![EXEC.into()];
assert!(judge(&Check::CleanSlate, &with_exec).is_pass());
}
#[test]
fn ordering_is_red_for_every_way_it_can_be_wrong() {
let ok = facts(&[
("After", "network-online.target basic.target"),
("ActiveEnterTimestampMonotonic", "9000000"),
("NetworkOnlineActiveEnterTimestampMonotonic", "8000000"),
]);
assert!(judge(&Check::OrderedAfterNetworkOnline, &ok).is_pass());
// (a) the unit never declared the ordering.
let no_after = facts(&[
("After", "sysinit.target"),
("ActiveEnterTimestampMonotonic", "9000000"),
("NetworkOnlineActiveEnterTimestampMonotonic", "8000000"),
]);
assert!(!judge(&Check::OrderedAfterNetworkOnline, &no_after).is_pass());
// (b) it declared it and STILL came up first — the real ordering violation.
let early = facts(&[
("After", "network-online.target"),
("ActiveEnterTimestampMonotonic", "3000000"),
("NetworkOnlineActiveEnterTimestampMonotonic", "8000000"),
]);
let v = judge(&Check::OrderedAfterNetworkOnline, &early);
assert!(!v.is_pass());
assert!(v.detail().contains("BEFORE"), "{}", v.detail());
// (c) network-online.target never activated → nothing to order against. The
// identity-value trap: 0 must not read as "0 µs, therefore in order".
let no_net = facts(&[
("After", "network-online.target"),
("ActiveEnterTimestampMonotonic", "9000000"),
("NetworkOnlineActiveEnterTimestampMonotonic", "0"),
]);
assert!(!judge(&Check::OrderedAfterNetworkOnline, &no_net).is_pass());
// (d) the unit never activated.
let no_unit = facts(&[
("After", "network-online.target"),
("ActiveEnterTimestampMonotonic", "0"),
("NetworkOnlineActiveEnterTimestampMonotonic", "8000000"),
]);
assert!(!judge(&Check::OrderedAfterNetworkOnline, &no_unit).is_pass());
}
// ───────────────────────── the grader itself ───────────────────────────────
/// A case that never reported must be a FAIL for every one of its checks —
/// including the deliberate-red ones. A missing report is not "the red we
/// wanted"; it is a case that ran nothing.
#[test]
fn a_case_with_no_facts_line_fails_every_check_including_the_negative_ones() {
let cases = plan_boot_a();
let out = grade(&cases, &[], &[]);
assert!(!out.is_empty());
assert!(
out.iter().all(|o| !o.as_declared),
"silence is never success"
);
assert!(
out.iter()
.all(|o| o.verdict.detail().contains("asserted NOTHING"))
);
}
/// The grader scores a deliberate-red case as PASSING when the guard goes red,
/// and as FAILING when the guard is hollow. This is the mechanism that makes a
/// stuck-on-pass judge impossible to ship.
#[test]
fn a_hollow_always_passing_guard_would_fail_the_negative_cases() {
let cases = plan_boot_a();
// Facts from a machine where the hostile drop-in IS present and winning.
let hijacked = Facts {
case: "dropin-user-override".into(),
show: vec![
"User=root".into(),
format!("DropInPaths={HOSTILE_DROP_IN}"),
"ActiveState=active".into(),
],
proc_user: "root".into(),
proc_env: vec!["USER=root".into(), "SKID_MARK=hijacked".into()],
..Default::default()
};
let out = grade(&cases, std::slice::from_ref(&hijacked), &[]);
let red_ones: Vec<&Outcome> = out
.iter()
.filter(|o| o.case == "dropin-user-override" && !o.expect_pass)
.collect();
assert_eq!(
red_ones.len(),
4,
"four guards are required to notice this machine"
);
assert!(
red_ones
.iter()
.all(|o| o.as_declared && !o.verdict.is_pass()),
"each declared-red guard actually went red: {red_ones:?}"
);
// …and the ONE positive check on that case (still running) also holds.
let active = out
.iter()
.find(|o| o.case == "dropin-user-override" && o.check == "active")
.unwrap();
assert!(active.as_declared && active.verdict.is_pass());
}
// ───────────────────── the suite's own shape ────────────────────────────────
/// The suite must contain deliberate reds in BOTH boots. Without them a judge
/// could be stuck on Pass and every case would still be green.
#[test]
fn both_boots_carry_deliberate_red_cases() {
let a: usize = plan_boot_a()
.iter()
.flat_map(|c| &c.checks)
.filter(|e| !e.expect_pass)
.count();
let b: usize = plan_boot_b()
.iter()
.flat_map(|c| &c.checks)
.filter(|e| !e.expect_pass)
.count();
assert!(a >= 4, "boot A must engineer real failures, has {a}");
assert!(
b >= 1,
"boot B must engineer a real ordering failure, has {b}"
);
let pass: usize = all_cases()
.iter()
.flat_map(|c| &c.checks)
.filter(|e| e.expect_pass)
.count();
assert!(
pass >= 10,
"…and enough positive checks that a stuck-on-red judge fails too ({pass})"
);
}
/// Every asserting case must say why a host-only test could not have caught it.
/// The claim is auditable data, not a comment nobody re-reads.
#[test]
fn every_asserting_case_declares_its_host_only_blind_spot() {
for c in all_cases() {
if !c.checks.is_empty() {
assert!(
c.host_only_blind_spot.len() > 40,
"case `{}` asserts things but does not say what a host-only test would miss",
c.id
);
}
}
}
/// The spec under test is the REAL one: rendered by the shipped renderer, and it
/// must actually carry the ordering the boot-B guard asks systemd about — else
/// that guard would be red for a reason that has nothing to do with systemd.
#[test]
fn the_rendered_unit_carries_the_user_and_the_ordering_the_suite_asserts() {
let r = suite_rendered();
let unit = r
.file_ending("skidtest.service")
.expect("a unit file was rendered");
assert!(unit.contains(&format!("User={SVC_USER}")), "unit:\n{unit}");
assert!(
unit.contains("After=") && unit.contains("network-online.target"),
"unit:\n{unit}"
);
assert!(
unit.contains("SKID_MARK"),
"the declared env is in the unit:\n{unit}"
);
assert_eq!(r.files[0].path, "/etc/systemd/system/skidtest.service");
}
/// The hostile drop-in must genuinely contradict the spec — a "conflict" that
/// agreed with the unit would make every negative case a fake red.
#[test]
fn the_planted_dropin_really_contradicts_the_spec() {
let body = hostile_drop_in_body();
assert!(body.contains("User=root"));
assert_ne!("root", SVC_USER, "the override must name a DIFFERENT user");
assert!(body.contains("SKID_MARK=hijacked"));
let unit = suite_rendered()
.file_ending("skidtest.service")
.unwrap()
.to_string();
assert!(
unit.contains("SKID_MARK=vm-suite"),
"…and a different env value:\n{unit}"
);
}
/// The negative ordering control must be built so it CANNOT be ordered after the
/// network — otherwise its red would be luck, not construction.
#[test]
fn the_early_control_unit_is_constructed_to_activate_before_the_network() {
let b = early_unit_body();
assert!(b.contains("DefaultDependencies=no"), "{b}");
assert!(b.contains("Before=sysinit.target"), "{b}");
assert!(b.contains("WantedBy=sysinit.target"), "{b}");
assert!(
!b.contains("network-online.target"),
"it must not declare the ordering: {b}"
);
assert!(
b.contains("Type=oneshot") && b.contains("/bin/true"),
"cannot wedge the boot: {b}"
);
}
#[test]
fn the_cloud_init_userdata_installs_the_scenario_and_runs_the_plan() {
let ud = render_user_data("/var/lib/skid/plan.txt");
assert!(ud.starts_with("#cloud-config"));
assert!(
ud.contains("mount -t vfat"),
"finds the payload by scanning vfat devices"
);
assert!(ud.contains("install -m0755 /mnt/skid/skidbladnir-scenario"));
assert!(
ud.contains(REPORT_PORT),
"reports over the virtio-serial port"
);
assert!(ud.contains("--plan /var/lib/skid/plan.txt"));
}
/// The boot-B plan is embedded INSIDE boot A's plan (as a WRITE payload). A hex
/// payload can carry a plan containing newlines and its own verbs without the
/// outer parser ever seeing them — assert that nesting really survives.
#[test]
fn boot_b_plan_survives_being_embedded_in_boot_a() {
let a = render_plan(&plan_boot_a());
assert_eq!(
a.lines().filter(|l| l.starts_with("CASE ")).count(),
10,
"boot A case count"
);
let b = render_plan(&plan_boot_b());
assert!(b.contains("COLLECT skidtest.service"));
assert!(
a.contains(&hex_encode(b.as_bytes())),
"boot A must carry boot B's plan verbatim as hex"
);
// …and the outer plan must not have gained stray verbs from the inner one.
assert!(
!a.lines()
.any(|l| l == "COLLECT skidearly.service /bin/true")
);
}
}