holger-build-thing/tests/loaded_as_a_plugin.rs
//! **The acceptance proof: a forge that has never heard of holger.**
//!
//! This test loads `holger_build_thing.wasm` through the generic
//! [`nornir_build_thing::plugin_host`] loader and asks it everything the forge asks —
//! menu, validate, plan, provenance, artifact judgement — with:
//!
//! * **no holger source tree** consulted (the module is bytes; the test never reads
//! holger's repo);
//! * **no capability granted** — the sandbox answers three questions and offers
//! nothing else, and a module reaching for anything more is refused at load;
//! * **nothing executed** but the plugin. No cargo, no artifact, no subprocess.
//!
//! It deliberately calls the loader by its generic name and never mentions the holger
//! *crate*: everything below would be identical for a build thing this test had never
//! seen. That is the property being proved — the forge does not link the app.
//!
//! The module is built by `traits::test_support::wasm_guest` rather than being
//! checked in, so the wasm under test is always this source, never a stale copy.
//! A missing wasm32 target or a failed build is red, never a skip.
use holger_build_thing::HolgerBuildThing;
use nornir_build_thing::plugin::{ArtifactFacts, BuildRequest, BuildThing, Role};
use nornir_build_thing::plugin_host::{HostFacts, Limits, PluginHandle};
/// The facts the forge is willing to answer. This is the ENTIRE world the plugin can
/// see: one revision, and whatever manifests the host chose to offer. No filesystem,
/// no network, no clock.
fn facts(rev: &str) -> HostFacts {
HostFacts::at_rev(rev, false)
}
#[test]
fn a_forge_that_never_heard_of_holger_lists_validates_and_plans_it() {
let wasm = traits::test_support::wasm_guest("holger-build-thing");
// Copy the module somewhere with NO source tree beside it, to make the "no
// checkout needed" claim physical rather than rhetorical.
let elsewhere = tempfile::tempdir().unwrap();
let isolated = elsewhere.path().join("unknown-app.wasm");
std::fs::copy(&wasm, &isolated).unwrap();
assert!(
!elsewhere.path().join("Cargo.toml").exists() && !elsewhere.path().join(".nornir").exists(),
"the module sits alone — no manifest, no .nornir, no repo"
);
let rev = "c0ffee".repeat(6) + "abcd";
let mut p = PluginHandle::load_file_with(&isolated, Limits::default(), facts(&rev))
.expect("a build thing loads from bytes alone");
// ── who is this? ─────────────────────────────────────────────────────────
let m = p.manifest().clone();
assert_eq!(
m.repo, "holger",
"the module says what it is; the host did not know"
);
assert_eq!(m.source_crate, "holger-build-thing");
// ── the MENU: the SAME declaration the source declares ───────────────────
//
// ⚠ THE ROSTER IS THE POINT, and a COUNT IS NOT THE ROSTER. holger's on-disk
// `.nornir/build-variants.toml` twice grew a variant the plugin did not declare
// (`henriks-mini-holger` 2026-07-23; five more by 2026-08-01), and once RENAMED
// two it did declare (`henriks-{mini,mikro}-holger` → `ui-{mini,mikro}`) plus
// every `holger-ui*` bin to `holger-server-ui*`. A rename keeps the count and
// breaks the build, so what is asserted here — through the LOADED MODULE, which
// is what the forge reads — is every name, per role, and the artifact each
// cargo row plans.
//
// The expectation is taken from the declaration ITSELF (`holger_recipe::declared`
// and the native `HolgerBuildThing`), not from a literal list: the literal list
// that stood here went stale when the egui rungs left (2026-09-16) and nothing but
// this test noticed. What is proved is unchanged — the WASM answers the same
// declaration the source declares, not a stale compiled-in copy. The literal
// roster, reviewable as a diff, lives in `holger-recipe/src/rules.rs`
// (`the_declared_builds_are_holgers_real_ones`).
let declared = holger_recipe::declared();
let native = HolgerBuildThing.menu();
let menu = p.menu().expect("menu");
assert_eq!(menu.repo, "holger");
for role in [Role::Server, Role::Client] {
assert_eq!(
menu.in_role(role),
native.in_role(role),
"{role:?}: the module and the source disagree"
);
}
// Coverage both ways: every declared variant is on the loaded menu, and nothing
// undeclared is.
let mut on_menu: Vec<&str> = menu.entries.iter().map(|e| e.id.as_str()).collect();
on_menu.sort_unstable();
let mut names: Vec<&str> = declared.variants.iter().map(|v| v.name.as_str()).collect();
names.sort_unstable();
assert_eq!(
on_menu, names,
"the forge sees exactly the declared variants"
);
// The media the owner asked for are on the menu the forge reads.
for medium in [
"holger-installer",
"holger-installer-metered",
"holger-appliance-iso",
] {
assert_eq!(menu.role_of(medium), Some(Role::Server), "{medium}");
}
// Linkage, per entry, as the source declares it — asserted through the LOADED
// MODULE, because that is what the forge reads.
for e in &menu.entries {
let v = declared.get(&e.id).expect("a declared variant");
assert_eq!(
e.linkage,
nornir_build_thing::plugin::Linkage::from(v.shape),
"{}",
e.id
);
}
assert!(
menu.entries
.iter()
.any(|e| e.linkage == nornir_build_thing::plugin::Linkage::Thin),
"the clients are thin (the command-line tool and the console)"
);
assert!(
menu.entries.iter().all(|e| e.ecosystem.as_str() == "cargo"),
"every entry names the build system that builds it"
);
// ── the rev PIN comes from the host, through the import ──────────────────
let prov = p.provenance().expect("provenance");
assert_eq!(
prov.rev, rev,
"the plugin learned the revision by ASKING the host — it cannot run git"
);
assert!(!prov.check(&rev).refuse_rebuild());
let mismatch = prov.check(&"d".repeat(40));
assert!(mismatch.refuse_rebuild(), "a different tree is refused");
assert!(
mismatch.line().contains("REFUSING to rebuild"),
"{}",
mismatch.line()
);
// ── VALIDATION is the app's logic, executed in the sandbox ───────────────
assert!(p.validate(&BuildRequest::variant("holger")).unwrap().ok);
let v = p.validate(&BuildRequest::variant("nope")).unwrap();
assert!(!v.ok && v.reasons[0].contains("no variant"), "{v:?}");
// The rule the declaration alone would permit — holger's own.
let v = p
.validate(&BuildRequest::features(["downstream", "served"]))
.unwrap();
assert!(
!v.ok,
"holger refuses an airgap build that asks for an egress feature"
);
assert!(
v.line().contains("provably unable to reach the network"),
"{}",
v.line()
);
// ── the PLAN is data; this test executes none of it ──────────────────────
let plan = p
.plan("holger-downstream")
.unwrap()
.expect("a plan for a declared variant");
assert_eq!(plan.variant, "holger-downstream");
assert!(
plan.steps.iter().all(|s| s.program == "cargo"),
"no shell in a plan"
);
assert_eq!(
plan.expected_artifacts,
vec!["target/release/holger-downstream".to_string()]
);
// The bin names, asserted where they bite: the artifact path the forge waits
// for, for EVERY declared variant, equal to what the source plans. (The plugin
// once planned `target/release/holger-ui-mini`, which cargo had stopped
// producing; the egui rungs have since left for holger-egui.)
for v in &holger_recipe::declared().variants {
let wasm_plan = p.plan(&v.name).unwrap().map(|x| x.expected_artifacts);
let native_plan = HolgerBuildThing.plan(&v.name).map(|x| x.expected_artifacts);
assert_eq!(
wasm_plan, native_plan,
"{}: the module plans another artifact than the source",
v.name
);
}
let paid = p
.plan("mvp-server-musl-metered")
.unwrap()
.expect("the metered payload plans");
assert!(
paid.expected_artifacts.len() == 1
&& paid.expected_artifacts[0].ends_with("/holger-mvp-metered"),
"the paid payload is the holger-mvp-metered bin: {:?}",
paid.expected_artifacts
);
assert!(plan.preview().contains("cargo build"), "{}", plan.preview());
// The plugin logged through the one import that lets it speak.
assert!(
p.logged().iter().any(|l| l.contains("planned")),
"the plugin's log arrived over `nordisk::log`: {:?}",
p.logged()
);
// ── the ARTIFACT judgement, both directions ──────────────────────────────
let judge = |variant: &str, linked: &[&str]| ArtifactFacts {
variant: Some(variant.to_string()),
file_name: format!("holger-{variant}"),
bytes: 26_700_000,
sha256: "0".repeat(64),
linked_crates: linked.iter().map(|s| s.to_string()).collect(),
};
let v = p
.judge_artifact(&judge("holger", &["serde", "hyper", "tonic"]))
.unwrap();
assert!(
v.ok && v.line().contains("cannot phone home"),
"{}",
v.line()
);
let v = p.judge_artifact(&judge("holger", &["reqwest"])).unwrap();
assert!(
!v.ok && v.line().contains("must not be promoted"),
"{}",
v.line()
);
// The sold box fetches its own advisories: an HTTP client there is not refused.
let v = p
.judge_artifact(&judge("mvp-server-metered", &["reqwest", "ureq"]))
.unwrap();
assert!(v.ok && v.line().contains("not constrained"), "{}", v.line());
}
/// The permission list, proved from the module side: holger's real plugin imports
/// **only** the three the host grants. If a build thing ever grew a WASI import, this
/// is where it would be caught — and the loader would refuse it by name.
#[test]
fn the_plugin_imports_only_what_the_host_grants() {
let wasm = traits::test_support::wasm_guest("holger-build-thing");
let bytes = std::fs::read(&wasm).unwrap();
// Parse the module's import section without running anything.
let mut imports: Vec<String> = Vec::new();
for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
if let wasmparser::Payload::ImportSection(section) = payload.unwrap() {
// wasmparser 0.261 (2026-10-02) groups imports (compact encoding);
// `into_imports` flattens the groups back to one entry per import.
for imp in section.into_imports() {
let imp = imp.unwrap();
imports.push(format!("{}::{}", imp.module, imp.name));
}
}
}
let granted = nornir_build_thing::plugin::host::IMPORTS;
for i in &imports {
let (m, f) = i.split_once("::").unwrap();
assert_eq!(
m, "nordisk",
"the only namespace a build thing may import from: {i}"
);
assert!(
granted.contains(&f),
"'{i}' is not on the permission list {granted:?}"
);
}
assert!(
imports.iter().any(|i| i.ends_with("::git_rev")),
"the rev pin really does come over an import, not out of the plugin's own head: {imports:?}"
);
}
/// The section path: a build thing embedded in an artifact is read with an object
/// parser and **never** by executing the artifact — which is what makes it work for a
/// binary this host cannot run at all.
#[test]
fn a_build_thing_is_read_out_of_a_binary_without_executing_it() {
let wasm = traits::test_support::wasm_guest("holger-build-thing");
let module = std::fs::read(&wasm).unwrap();
// Forge an ELF for a machine this host cannot execute: aarch64. If interrogation
// required RUNNING the artifact, this file would be unreadable here — which is
// exactly the case that disqualifies `--describe-build` as the general mechanism.
let dir = tempfile::tempdir().unwrap();
let artifact = dir.path().join("holger-downstream.aarch64");
std::fs::write(&artifact, fake_aarch64_elf_with_section(&module)).unwrap();
let got = nornir_build_thing::plugin_host::read_section(&artifact)
.expect("parsing an object file is not running it")
.expect("the artifact carries its build thing");
assert_eq!(
got, module,
"the section round-trips the module byte for byte"
);
// And the extracted module still loads and answers — the pulled-artifact flow.
let mut p = PluginHandle::load(&got, Limits::default()).expect("load from the section");
assert_eq!(p.manifest().repo, "holger");
assert_eq!(
p.menu().unwrap().in_role(Role::Server),
HolgerBuildThing.menu().in_role(Role::Server)
);
// An artifact with no build thing is a REPORTED state, not an error and not a
// silent empty: `Ok(None)` is a different answer from `Err`.
let bare = dir.path().join("bare.aarch64");
std::fs::write(&bare, fake_aarch64_elf_with_section(&[])).unwrap();
assert!(
nornir_build_thing::plugin_host::read_section(&bare)
.unwrap()
.is_none(),
"no section ⇒ NoBuildThing, which the caller renders — never an absent row"
);
}
/// Assemble a minimal but genuine little-endian aarch64 ELF64 carrying `payload` in a
/// `.nordisk.buildthing` section. Hand-built rather than cross-compiled so the test
/// needs no aarch64 toolchain — the point is only that the *object format* is one this
/// host cannot execute.
fn fake_aarch64_elf_with_section(payload: &[u8]) -> Vec<u8> {
const EHSIZE: u64 = 64;
const SHENTSIZE: u64 = 64;
let shnum: u64 = if payload.is_empty() { 2 } else { 3 };
// .shstrtab contents.
let mut names = vec![0u8];
let name_shstrtab = names.len() as u32;
names.extend_from_slice(b".shstrtab\0");
let name_section = names.len() as u32;
names.extend_from_slice(b".nordisk.buildthing\0");
let payload_off = EHSIZE;
let names_off = payload_off + payload.len() as u64;
let sh_off = names_off + names.len() as u64;
let mut out = Vec::new();
// ── ELF header ──
out.extend_from_slice(&[0x7f, b'E', b'L', b'F', 2, 1, 1, 0]); // magic, 64-bit, LE, v1
out.extend_from_slice(&[0u8; 8]);
out.extend_from_slice(&2u16.to_le_bytes()); // ET_EXEC
out.extend_from_slice(&183u16.to_le_bytes()); // EM_AARCH64 — not this host
out.extend_from_slice(&1u32.to_le_bytes()); // version
out.extend_from_slice(&0u64.to_le_bytes()); // entry
out.extend_from_slice(&0u64.to_le_bytes()); // phoff
out.extend_from_slice(&sh_off.to_le_bytes()); // shoff
out.extend_from_slice(&0u32.to_le_bytes()); // flags
out.extend_from_slice(&(EHSIZE as u16).to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes()); // phentsize
out.extend_from_slice(&0u16.to_le_bytes()); // phnum
out.extend_from_slice(&(SHENTSIZE as u16).to_le_bytes());
out.extend_from_slice(&(shnum as u16).to_le_bytes());
out.extend_from_slice(&1u16.to_le_bytes()); // shstrndx
debug_assert_eq!(out.len() as u64, EHSIZE);
out.extend_from_slice(payload);
out.extend_from_slice(&names);
let mut section = |name: u32, kind: u32, off: u64, size: u64| {
out.extend_from_slice(&name.to_le_bytes());
out.extend_from_slice(&kind.to_le_bytes());
out.extend_from_slice(&0u64.to_le_bytes()); // flags
out.extend_from_slice(&0u64.to_le_bytes()); // addr
out.extend_from_slice(&off.to_le_bytes());
out.extend_from_slice(&size.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes()); // link
out.extend_from_slice(&0u32.to_le_bytes()); // info
out.extend_from_slice(&1u64.to_le_bytes()); // addralign
out.extend_from_slice(&0u64.to_le_bytes()); // entsize
};
section(0, 0, 0, 0); // SHT_NULL
section(name_shstrtab, 3, names_off, names.len() as u64); // .shstrtab
if !payload.is_empty() {
section(name_section, 1, payload_off, payload.len() as u64); // PROGBITS
}
out
}
/// A path that is not a build thing must be refused with a reason — a forge pulling
/// from a registry meets garbage sooner or later.
#[test]
fn a_file_that_is_not_a_build_thing_is_refused_with_a_reason() {
let dir = tempfile::tempdir().unwrap();
let junk = dir.path().join("not-wasm");
std::fs::write(&junk, b"this is not a wasm module").unwrap();
let err = PluginHandle::load_file(&junk, Limits::default()).unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("core wasm32 module") || msg.contains("parse"),
"{msg}"
);
}