Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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