plugin/host/tests/router_cannot_tell_them_apart.rs
//! **The bar: a real `.wasm`, loaded from a plugin directory, answering a real
//! request through holger's normal router.**
//!
//! Not a unit test of the loader — the module is registered by scanning a
//! directory, put into the same `FastRoutes` table `server/lib` builds after
//! wiring, looked up by repository name, and asked for bytes and for an HTTP
//! response through `RepositoryBackendTrait`. Nothing here reaches around the
//! router.
//!
//! The claim under test is the design's whole point: **`fast_routes.rs` cannot
//! tell a linked handler from a loaded one.** So the same route table holds one
//! of each, and every assertion is made against both without the caller ever
//! naming which is which.
// Loads real modules through the wasm host: without `wasm-plugins` this file
// did not compile under `cargo test -p holger-plugin-host` and only ran in a
// workspace build through feature unification. Its declared arm turns it on.
use std::sync::Arc;
use holger_plugin_host::registry::{scan, FileStemMounts, ScanError};
use holger_plugin_host::{mount_linked_with, DirStore};
use holger_server_lib::exposed::fast_routes::FastRoutes;
use traits::{ArtifactFormat, ArtifactId, RepositoryBackendTrait};
mod common;
use common::{plugin_dir_with_modules, scratch, BUILD_CMD};
/// Registration reads a DIRECTORY. Nothing in `server/lib/Cargo.toml` mentions
/// these handlers, and the route they answer on comes from the module file's
/// name — so a new handler needs no rebuild of holger.
#[test]
fn a_module_dropped_in_a_directory_serves_a_real_request_through_the_router() {
let plugins = plugin_dir_with_modules(&["holger_handler_skidbladnir_wasm.wasm"]);
let data_root = scratch("data");
let reg = scan(
&plugins,
&FileStemMounts {
data_root: data_root.clone(),
},
)
.unwrap_or_else(|e| panic!("scanning {}: {e}", plugins.display()));
assert!(
reg.failures.is_empty(),
"modules failed to load: {:?}",
reg.failures
);
assert_eq!(
reg.backends.len(),
1,
"expected exactly one handler from {}",
plugins.display()
);
let repo = reg.backends[0].name().to_string();
assert_eq!(
repo, "holger_handler_skidbladnir_wasm",
"the route name did not come from the module file stem"
);
assert_eq!(reg.backends[0].handler_name(), "skidbladnir");
// Seed a bundle where that backend's store points, then build the SAME route
// table `server/lib` builds after wiring.
let store_dir = data_root.join(&repo);
std::fs::create_dir_all(&store_dir).unwrap();
let body = b"\x00sealed,bundle:bytes\xff".to_vec();
std::fs::write(store_dir.join("tillsynia-20260706.znippy"), &body).unwrap();
let routes = FastRoutes::new(
reg.backends
.into_iter()
.map(|b| (b.name().to_string(), b.into_backend()))
.collect(),
);
// Everything below goes through the router, exactly as a request does.
let backend = routes
.lookup(&repo)
.expect("the router did not resolve the plugin's repo");
assert_eq!(backend.format(), ArtifactFormat::Znippy);
let id = ArtifactId {
namespace: None,
name: "tillsynia".into(),
version: "20260706".into(),
};
assert_eq!(
backend
.fetch(&id)
.expect("fetch through the router errored"),
Some(traits::Bytes::from(body.clone())),
"the router resolved the backend but it served the wrong bytes"
);
let (status, headers, served) = backend
.handle_http2_request("GET", &format!("/{repo}/tillsynia/20260706"), b"")
.expect("http through the router errored");
assert_eq!(status, 200);
assert_eq!(
served, body,
"the HTTP door served different bytes than fetch"
);
assert!(
headers
.iter()
.any(|(k, v)| k == "content-type" && v == "application/vnd.znippy.bundle"),
"content-type missing from a wasm-served response: {headers:?}"
);
// And the shared classifier, reached over the ABI, through the router.
let (status, _, body) = backend
.handle_http2_request(
"GET",
&format!("/{repo}/tillsynia/20260706/classify/secrets/secrets.age"),
b"",
)
.expect("classify through the router errored");
assert_eq!(status, 200);
assert_eq!(
String::from_utf8_lossy(&body),
"kind\tsecret\ncomponent\tsecrets\nencrypted\ttrue"
);
// A name the table does not hold must miss, or `lookup` proves nothing.
assert!(routes.lookup("no-such-repo").is_none());
}
/// One route table, one linked handler and one loaded module, and the caller
/// cannot tell which is which from the outside.
#[test]
fn a_linked_handler_and_a_loaded_module_are_indistinguishable_in_one_route_table() {
let plugins = plugin_dir_with_modules(&["holger_handler_skidbladnir_wasm.wasm"]);
let data_root = scratch("mixed");
let reg = scan(
&plugins,
&FileStemMounts {
data_root: data_root.clone(),
},
)
.expect("scan");
assert_eq!(reg.backends.len(), 1);
let loaded_repo = reg.backends[0].name().to_string();
// The linked half: the SAME handler the module is built from, compiled in.
let linked_dir = data_root.join("linked");
std::fs::create_dir_all(&linked_dir).unwrap();
let linked = mount_linked_with(
"linked-bundles",
holger_handler_skidbladnir::SkidbladnirHandler::new(),
Arc::new(DirStore::new(&linked_dir)),
)
.expect("linked mount");
let body = b"\x00same,bytes:\xff".to_vec();
std::fs::write(linked_dir.join("tillsynia-1.znippy"), &body).unwrap();
let loaded_dir = data_root.join(&loaded_repo);
std::fs::create_dir_all(&loaded_dir).unwrap();
std::fs::write(loaded_dir.join("tillsynia-1.znippy"), &body).unwrap();
let mut entries: Vec<(String, Arc<dyn RepositoryBackendTrait>)> =
vec![("linked-bundles".to_string(), linked.into_backend())];
for b in reg.backends {
entries.push((b.name().to_string(), b.into_backend()));
}
let routes = FastRoutes::new(entries);
let id = ArtifactId {
namespace: None,
name: "tillsynia".into(),
version: "1".into(),
};
// The same assertions against both, with no branch on which kind it is.
for repo in ["linked-bundles", loaded_repo.as_str()] {
let backend = routes
.lookup(repo)
.unwrap_or_else(|| panic!("{repo} not routed"));
assert_eq!(backend.name(), repo);
assert_eq!(backend.format(), ArtifactFormat::Znippy, "{repo}");
assert!(backend.is_writable(), "{repo}");
assert_eq!(
backend.fetch(&id).expect("fetch").as_deref(),
Some(body.as_slice()),
"{repo}"
);
assert_eq!(
backend
.coordinate_for_path(&format!("/{repo}/tillsynia/1"))
.expect("maps"),
Some(id.clone()),
"{repo}"
);
// What the ABI deliberately does not carry, identical on both: no
// archive, and `delete_artifact` fails CLOSED as Unsupported.
assert_eq!(
backend.caps(),
traits::Caps {
listing: true,
path_mapping: true,
..traits::Caps::NONE
},
"{repo}"
);
assert!(
traits::is_unsupported(&backend.archive().unwrap_err()),
"{repo} claims a znippy archive it does not have"
);
assert!(
traits::is_unsupported(&backend.delete_artifact(&id, "deadbeef").unwrap_err()),
"{repo}: delete_artifact did not fail closed"
);
traits::backend::conformance::check(backend.as_ref()).unwrap();
}
// Both really are in the one table.
assert_eq!(routes.all_repos().len(), 2);
}
/// Both modules at once — two handlers, one directory, one scan, one table.
#[test]
fn two_modules_register_side_by_side_under_their_own_routes() {
let plugins = plugin_dir_with_modules(&[
"holger_handler_skidbladnir_wasm.wasm",
"holger_handler_rust_toolchain_wasm.wasm",
]);
let data_root = scratch("two");
let reg = scan(&plugins, &FileStemMounts { data_root }).expect("scan");
assert!(reg.failures.is_empty(), "{:?}", reg.failures);
assert_eq!(reg.backends.len(), 2, "both modules should have registered");
let mut handlers: Vec<&str> = reg.backends.iter().map(|b| b.handler_name()).collect();
handlers.sort();
assert_eq!(handlers, vec!["rust-toolchain", "skidbladnir"]);
let routes = FastRoutes::new(
reg.backends
.into_iter()
.map(|b| (b.name().to_string(), b.into_backend()))
.collect(),
);
assert!(routes.lookup("holger_handler_skidbladnir_wasm").is_some());
assert!(routes
.lookup("holger_handler_rust_toolchain_wasm")
.is_some());
}
/// The loud-absence condition, asserted where a deployment would hit it.
#[test]
fn a_plugin_directory_that_does_not_exist_is_an_error_at_startup() {
let missing = scratch("gone");
std::fs::remove_dir_all(&missing).unwrap();
let err = scan(
&missing,
&FileStemMounts {
data_root: missing.clone(),
},
)
.expect_err("a missing plugin directory returned an empty registry");
assert!(matches!(err, ScanError::Missing { .. }), "{err:?}");
let msg = err.to_string();
assert!(msg.contains(&missing.display().to_string()), "{msg}");
assert!(
msg.contains(BUILD_CMD),
"the error does not carry the build command: {msg}"
);
}