plugin/host/src/agreement.rs
//! **The native-vs-wasm agreement harness, written once.**
//!
//! Every handler ships a pair — a native crate that owns the logic and a wasm
//! crate that is ABI glue — and the claim "they are the same code" is only worth
//! anything if something checks it over real bytes. Each `-wasm` crate's
//! `tests/native_wasm_agree.rs` calls in here, so the *assertions themselves*
//! are one implementation: two hand-written guards would be two things that can
//! drift, and one of them would rot silently.
//!
//! # What it refuses to do
//!
//! * **It never skips.** A missing guest is built on demand; a failed build,
//! or a missing or stale forge-built module, panics with the fix. A test
//! that skipped when the module was absent would be a permanent green — the exact shape that let znippy's wasm example sit
//! "documented but never built" from the day its loader landed.
//! * **It never asserts a bare equality.** `assert_eq!(native, wasm)` passes
//! when both are empty, so every comparison first asserts the *content* of the
//! native side, then asserts the two match, and names the field that differs.
//!
//! # Both surfaces share one store
//!
//! The native backend and the loaded module are handed the same [`DirStore`], so
//! a disagreement is a disagreement about *logic*, never about which bytes each
//! one could see.
use std::path::PathBuf;
use std::sync::Arc;
use std::time::SystemTime;
use holger_plugin_abi::PackageHandler;
use traits::{ArtifactId, RepositoryBackendTrait};
use crate::backend::{HandlerBackend, SharedStore};
use crate::store::DirStore;
use crate::wasm::WasmHandler;
/// How a red test names its fix when a forge-built module is missing or stale.
pub const BUILD_CMD: &str = "dwarves buildthing ./holger --suite MVP --variant <row>";
/// Locate the module `wasm_file_name`, building it when needed.
///
/// * `HOLGER_PLUGIN_WASM_DIR` set: the FORGE-built module in that directory is
/// used as is. It must exist and must **not be older** than `source_dirs` — a
/// stale module would let a changed handler pass a guard that compared it
/// against the new native code.
/// * Otherwise the guest is built now by `traits::test_support::wasm_guest`
/// (`cargo build --release --target wasm32-unknown-unknown`, no container),
/// whose cargo freshness check means the module is never stale.
///
/// It never skips: a missing module panics with the fix.
pub fn locate_module(wasm_file_name: &str, source_dirs: &[PathBuf]) -> PathBuf {
let Some(dir) = std::env::var_os("HOLGER_PLUGIN_WASM_DIR") else {
let package = wasm_file_name.trim_end_matches(".wasm").replace('_', "-");
return traits::test_support::wasm_guest(&package);
};
let found = PathBuf::from(dir).join(wasm_file_name);
assert!(
found.is_file(),
"no {} (HOLGER_PLUGIN_WASM_DIR). This test loads a REAL wasm module; it does not skip.\n\
Build it with:\n {BUILD_CMD}\nor unset HOLGER_PLUGIN_WASM_DIR to build it here.",
found.display()
);
let built = found
.metadata()
.and_then(|m| m.modified())
.unwrap_or_else(|e| panic!("cannot stat {}: {e}", found.display()));
let newest = newest_source_mtime(source_dirs);
assert!(
built >= newest,
"{} is STALE — it predates the handler sources it is built from.\n\
Rebuild with:\n {BUILD_CMD}",
found.display()
);
found
}
/// Newest mtime across every `.rs` that goes into a module.
///
/// A source root that yields no `.rs` is a hard failure: it would make the
/// staleness check vacuously true, which is a guard that can never fire.
fn newest_source_mtime(dirs: &[PathBuf]) -> SystemTime {
assert!(
!dirs.is_empty(),
"no source dirs given — the staleness check would be blind"
);
let mut newest = SystemTime::UNIX_EPOCH;
for root in dirs {
let entries = std::fs::read_dir(root)
.unwrap_or_else(|e| panic!("cannot read handler source dir {}: {e}", root.display()));
let mut seen = 0usize;
for entry in entries.flatten() {
if entry.path().extension().is_some_and(|e| e == "rs") {
seen += 1;
if let Ok(m) = entry.metadata().and_then(|m| m.modified()) {
newest = newest.max(m);
}
}
}
assert!(
seen > 0,
"no .rs sources under {} — the staleness check would be blind",
root.display()
);
}
newest
}
/// The two surfaces of one handler, over one shared store.
pub struct Surfaces {
pub native: HandlerBackend,
pub wasm: HandlerBackend,
/// The directory both surfaces read and write.
pub dir: PathBuf,
store: SharedStore,
}
impl Surfaces {
/// Build both surfaces of `handler` over a fresh temp directory.
///
/// `source_dirs` are the `src/` directories whose mtimes decide staleness —
/// the wasm crate's own and every crate whose logic ends up inside the
/// module, because the whole point of the design is that the logic is shared.
pub fn build<H: PackageHandler + Send + Sync + 'static>(
repo_name: &str,
handler: H,
wasm_file_name: &str,
source_dirs: &[PathBuf],
) -> Self {
let dir = std::env::temp_dir().join(format!(
"holger-agree-{repo_name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temp store dir");
let store: SharedStore = Arc::new(DirStore::new(&dir));
let native = crate::mount_linked_with(repo_name, handler, Arc::clone(&store))
.expect("linked handler did not mount");
let path = locate_module(wasm_file_name, source_dirs);
let loaded = WasmHandler::load(&path, Arc::clone(&store))
.unwrap_or_else(|e| panic!("loading {}: {e}", path.display()));
let wasm = HandlerBackend::new(repo_name, loaded.into_runtime())
.unwrap_or_else(|e| panic!("wrapping {}: {e}", path.display()));
Self {
native,
wasm,
dir,
store,
}
}
/// Write a blob both surfaces will see.
pub fn seed(&self, key: &str, data: &[u8]) {
self.store.put(key, data).expect("seed the shared store");
}
/// Identity must match, and must be the value asked for — not merely equal
/// on both sides.
pub fn assert_identity(&self, handler: &str, format: traits::ArtifactFormat, writable: bool) {
assert_eq!(self.native.handler_name(), handler, "native handler name");
assert_eq!(self.wasm.handler_name(), handler, "wasm handler name");
assert_eq!(self.native.format(), format, "native format");
assert_eq!(self.wasm.format(), format, "wasm format");
assert_eq!(self.native.is_writable(), writable, "native writable");
assert_eq!(self.wasm.is_writable(), writable, "wasm writable");
assert_eq!(self.native.name(), self.wasm.name(), "repository name");
}
/// `fetch` must return the same bytes. The expected body is asserted first,
/// so two identically-broken surfaces returning `None` cannot pass.
pub fn assert_fetch(&self, id: &ArtifactId, expect: Option<&[u8]>) {
let n = self.native.fetch(id).expect("native fetch errored");
let w = self.wasm.fetch(id).expect("wasm fetch errored");
assert_eq!(
n.as_deref(),
expect,
"native fetch({}/{}) returned the wrong body",
id.name,
id.version
);
assert_eq!(
w.as_deref(),
expect,
"wasm fetch({}/{}) returned the wrong body",
id.name,
id.version
);
assert_eq!(
n, w,
"native and wasm disagree on fetch({}/{})",
id.name, id.version
);
}
/// `coordinate_for_path` must resolve identically — the quarantine gate's
/// lookup, and pure path logic, so any difference is a real ABI defect.
pub fn assert_coordinate(&self, suburl: &str, expect: Option<(&str, &str)>) {
let n = self
.native
.coordinate_for_path(suburl)
.expect("handlers map paths");
let w = self
.wasm
.coordinate_for_path(suburl)
.expect("handlers map paths");
let want = expect.map(|(name, version)| ArtifactId {
namespace: None,
name: name.to_string(),
version: version.to_string(),
});
assert_eq!(n, want, "native coordinate_for_path({suburl})");
assert_eq!(w, want, "wasm coordinate_for_path({suburl})");
}
/// The HTTP door must answer with the same status, headers and body.
///
/// Every part is compared separately so a failure names *which* part
/// differs — a single `assert_eq!` on the tuple would only say "they
/// differ".
pub fn assert_http(
&self,
method: &str,
suburl: &str,
body: &[u8],
expect_status: u16,
expect_body: Option<&[u8]>,
) -> (Vec<(String, String)>, Vec<u8>) {
let (ns, nh, nb) = self
.native
.handle_http2_request(method, suburl, body)
.expect("native http errored");
let (ws, wh, wb) = self
.wasm
.handle_http2_request(method, suburl, body)
.expect("wasm http errored");
assert_eq!(ns, expect_status, "native status for {method} {suburl}");
assert_eq!(ws, expect_status, "wasm status for {method} {suburl}");
if let Some(want) = expect_body {
assert_eq!(nb.as_slice(), want, "native body for {method} {suburl}");
assert_eq!(wb.as_slice(), want, "wasm body for {method} {suburl}");
}
assert_eq!(ns, ws, "status differs for {method} {suburl}");
assert_eq!(nh, wh, "headers differ for {method} {suburl}");
assert_eq!(nb, wb, "body differs for {method} {suburl}");
(nh, nb)
}
/// `list` must return the same rows in the same order.
pub fn assert_list(&self, filter: Option<&str>, limit: usize, expect: &[(&str, &str, i64)]) {
let n = self
.native
.list(filter, limit)
.expect("native list errored");
let w = self.wasm.list(filter, limit).expect("wasm list errored");
let got: Vec<(String, String, i64)> = n
.iter()
.map(|e| (e.id.name.clone(), e.id.version.clone(), e.size_bytes))
.collect();
let want: Vec<(String, String, i64)> = expect
.iter()
.map(|(a, b, c)| (a.to_string(), b.to_string(), *c))
.collect();
assert_eq!(
got, want,
"native list(filter={filter:?}, limit={limit}) is wrong"
);
assert_eq!(
n.len(),
w.len(),
"native listed {} rows, wasm {}",
n.len(),
w.len()
);
for (a, b) in n.iter().zip(w.iter()) {
assert_eq!(a.id, b.id, "list coordinate differs");
assert_eq!(
a.size_bytes, b.size_bytes,
"list size differs for {}",
a.id.name
);
assert_eq!(
a.content_type, b.content_type,
"list content_type differs for {}",
a.id.name
);
}
}
/// A write through one surface must be readable through the other — the
/// strongest form of "same store, same key scheme".
pub fn assert_put_crosses(&self, id: &ArtifactId, data: &[u8]) {
self.native.put(id, data).expect("native put errored");
let via_wasm = self.wasm.fetch(id).expect("wasm fetch after native put");
assert_eq!(
via_wasm.as_deref(),
Some(data),
"a bundle written by the native handler was not readable through the module — \
the two disagree about the store key"
);
let other = ArtifactId {
namespace: None,
name: format!("{}-w", id.name),
version: id.version.clone(),
};
self.wasm.put(&other, data).expect("wasm put errored");
let via_native = self
.native
.fetch(&other)
.expect("native fetch after wasm put");
assert_eq!(
via_native.as_deref(),
Some(data),
"a bundle written through the module was not readable natively"
);
}
/// A coordinate both surfaces must REFUSE to store. Asserted on both,
/// because a `put` that silently succeeded would write a blob whose key
/// `coordinate_from_key` can never read back.
pub fn assert_put_refused(&self, id: &ArtifactId, why: &str) {
assert!(
self.native.put(id, b"x").is_err(),
"native accepted a put it should have refused ({why})"
);
assert!(
self.wasm.put(id, b"x").is_err(),
"wasm accepted a put it should have refused ({why})"
);
}
}
impl Drop for Surfaces {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}