From ef028ecfecc3461f5f1e90bc37afb58bbbbd0853 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 18:52:19 +0300 Subject: [PATCH 1/9] feat(module): share cross-platform module test support Co-authored-by: Medulla --- .github/workflows/ci.yml | 26 +++- crates/tinybus/Cargo.toml | 3 + crates/tinybus/src/lib.rs | 2 + crates/tinybus/src/test_support.rs | 177 +++++++++++++++++++++++ crates/tinybus/src/test_support_tests.rs | 58 ++++++++ 5 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 crates/tinybus/src/test_support.rs create mode 100644 crates/tinybus/src/test_support_tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cda9fae..6df2fc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,6 +116,8 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build example module run: cargo build --locked --example module_clock --example module_clock_two --example module_wrong_target --no-default-features --features modules,macros + - name: Test shared module test support + run: cargo test --locked --no-default-features --features modules,macros,test-support test_support::tests - name: Exercise the real loader (Unix) if: runner.os != 'Windows' shell: bash @@ -130,7 +132,18 @@ jobs: exit 1 fi done - TINYBUS_TEST_MODULE="$PWD/$module" TINYBUS_TEST_MODULE_TWO="$PWD/$module_two" TINYBUS_TEST_WRONG_TARGET="$PWD/$wrong_target" cargo test --locked --no-default-features --features modules,macros -- --ignored + private="$PWD/target/private-module-tests/module-clock-two" + mkdir -p "$private" + chmod 700 "$private" + cp "$module_two" "$private/" + library="$(basename "$module_two")" + if command -v sha256sum >/dev/null 2>&1; then + digest="$(sha256sum "$private/$library" | awk '{print $1}')" + else + digest="$(shasum -a 256 "$private/$library" | awk '{print $1}')" + fi + printf '"%s" = "%s"\n' "$library" "$digest" > "$private/modules.toml" + TINYBUS_TEST_MODULE="$PWD/$module" TINYBUS_TEST_MODULE_TWO="$private/$library" TINYBUS_TEST_WRONG_TARGET="$PWD/$wrong_target" cargo test --locked --no-default-features --features modules,macros,test-support -- --ignored # Directory admission against this runner's real filesystem: a bundle # in a directory other accounts can write falls back to the verified # release cache, and on macOS the real /Applications is admitted. @@ -164,7 +177,12 @@ jobs: $acl.AddAccessRule($rule) Set-Acl -LiteralPath $private -AclObject $acl Copy-Item target/debug/examples/module_clock.dll $private - Copy-Item target/debug/examples/module_clock_two.dll $private + $clockTwoDirectory = Join-Path $private module-clock-two + New-Item -ItemType Directory -Force $clockTwoDirectory | Out-Null + Set-Acl -LiteralPath $clockTwoDirectory -AclObject $acl + Copy-Item target/debug/examples/module_clock_two.dll $clockTwoDirectory + $clockTwoHash = (Get-FileHash -LiteralPath (Join-Path $clockTwoDirectory module_clock_two.dll) -Algorithm SHA256).Hash.ToLowerInvariant() + '"module_clock_two.dll" = "' + $clockTwoHash + '"' | Set-Content -LiteralPath (Join-Path $clockTwoDirectory modules.toml) -NoNewline Copy-Item target/debug/examples/module_wrong_target.dll $private $mixed = Join-Path $private mixed New-Item -ItemType Directory -Force $mixed | Out-Null @@ -172,7 +190,7 @@ jobs: Copy-Item target/debug/examples/module_clock.dll $mixed Copy-Item target/debug/examples/module_wrong_target.dll $mixed $module = (Get-ChildItem "$private/module_clock.dll" | Select-Object -First 1).FullName - $moduleTwo = (Get-ChildItem "$private/module_clock_two.dll" | Select-Object -First 1).FullName + $moduleTwo = (Get-ChildItem "$clockTwoDirectory/module_clock_two.dll" | Select-Object -First 1).FullName $wrongTarget = (Get-ChildItem "$private/module_wrong_target.dll" | Select-Object -First 1).FullName $env:TEMP = $private $env:TMP = $private @@ -180,7 +198,7 @@ jobs: $env:TINYBUS_TEST_MODULE_TWO = $moduleTwo $env:TINYBUS_TEST_WRONG_TARGET = $wrongTarget $env:TINYBUS_TEST_MODULE_DIRECTORY = $mixed - cargo test --locked --no-default-features --features modules,macros -- --ignored + cargo test --locked --no-default-features --features modules,macros,test-support -- --ignored if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Directory admission against the real ACLs: a bundle Everyone can # write falls back to the verified release cache. diff --git a/crates/tinybus/Cargo.toml b/crates/tinybus/Cargo.toml index affda57..a41e882 100644 --- a/crates/tinybus/Cargo.toml +++ b/crates/tinybus/Cargo.toml @@ -76,6 +76,9 @@ cli = [ # module shares the host's address space and full privileges. modules = ["dep:ureq", "dep:flate2", "dep:tar", "dep:zip", "dep:tempfile", "dep:toml", "dep:libc"] +# Shared helpers for integration suites that load a built module cdylib. +test-support = ["modules", "macros"] + [dev-dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "time", "io-util"] } diff --git a/crates/tinybus/src/lib.rs b/crates/tinybus/src/lib.rs index 133de7c..0fc78fa 100644 --- a/crates/tinybus/src/lib.rs +++ b/crates/tinybus/src/lib.rs @@ -82,6 +82,8 @@ pub mod router; pub mod secret; pub mod service; pub mod stream; +#[cfg(feature = "test-support")] +pub mod test_support; pub mod transport; pub mod version; diff --git a/crates/tinybus/src/test_support.rs b/crates/tinybus/src/test_support.rs new file mode 100644 index 0000000..f149b32 --- /dev/null +++ b/crates/tinybus/src/test_support.rs @@ -0,0 +1,177 @@ +//! Shared helpers for integration tests that load a built TinyBus module. +//! +//! These helpers are deliberately opt-in through the `test-support` feature. +//! They locate the platform library name, verify its adjacent `modules.toml` +//! pin before asking TinyBus to load it, and provide deadline-based lifecycle +//! waits for tests using an in-memory broker. + +use std::{ + path::{Path, PathBuf}, + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; + +use serde::{Serialize, de::DeserializeOwned}; + +use crate::{ + Connection, Proxy, Result, + broker::Broker, + module::{ModuleHost, ModuleInfo, ModuleState, sha256_file}, + transport::memory::MemoryBus, +}; + +static MODULE_LOADED: AtomicBool = AtomicBool::new(false); + +/// Resolve the library filename Cargo produces for `crate_name` on this OS. +/// +/// `crate_name` may be written as a Cargo package name (`tiny-docs-module`) or +/// as its Rust library name (`tiny_docs_module`). +pub fn artifact_path(target_dir: impl AsRef, crate_name: &str) -> PathBuf { + let crate_name = crate_name.replace('-', "_"); + let file_name = if cfg!(target_os = "windows") { + format!("{crate_name}.dll") + } else if cfg!(target_os = "macos") { + format!("lib{crate_name}.dylib") + } else { + format!("lib{crate_name}.so") + }; + target_dir.as_ref().join(file_name) +} + +/// Load the single module artifact named by `env_var` through `host`. +/// +/// The artifact must be in a dedicated directory with an adjacent +/// `modules.toml` entry whose SHA-256 matches the file. The guard is +/// process-wide: TinyBus intentionally never unloads a module, so attempting +/// a second load in the same test process fails before mapping another library. +pub fn admit_module( + host: &ModuleHost, + env_var: &str, + expected_module_name: &str, +) -> Result { + if MODULE_LOADED + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + return Err(crate::Error::failed( + "only one dynamic module may be loaded per test process", + )); + } + + let artifact = std::env::var_os(env_var) + .map(PathBuf::from) + .ok_or_else(|| crate::Error::failed(format!("{env_var} must point to the built module")))?; + verify_modules_pin(&artifact)?; + let directory = artifact + .parent() + .ok_or_else(|| crate::Error::failed("module artifact has no parent directory"))?; + let outcomes = host.load_dir(directory)?; + let info = outcomes + .into_iter() + .find_map(|result| match result { + Ok(info) if info.name == expected_module_name => Some(Ok(info)), + Ok(_) => None, + Err(error) => Some(Err(error)), + }) + .transpose()? + .ok_or_else(|| { + crate::Error::failed(format!( + "TinyBus did not admit module `{expected_module_name}` from {}", + artifact.display() + )) + })?; + Ok(info) +} + +/// Start an in-memory broker and return its host, client, and task handle. +pub async fn start_bus() -> Result<(ModuleHost, Connection, tokio::task::JoinHandle>)> { + let bus = MemoryBus::new(); + let broker = Broker::new(); + let broker_task = broker.spawn(bus.clone()); + let host = ModuleHost::new(broker); + let client = Connection::connect(bus.connect().await?).await?; + Ok((host, client, broker_task)) +} + +/// Wait until `client` observes `name`, failing when `timeout` expires. +pub async fn wait_until_serving(client: &Connection, name: &str, timeout: Duration) -> Result<()> { + tokio::time::timeout(timeout, async { + loop { + if client + .list_names() + .await? + .iter() + .any(|candidate| candidate.as_str() == name) + { + return Ok(()); + } + tokio::task::yield_now().await; + } + }) + .await + .map_err(|_| crate::Error::failed(format!("module `{name}` did not become ready in time")))? +} + +/// Wait for `name` to return to Ready after its calls have completed. +pub async fn wait_until_idle(host: &ModuleHost, name: &str, timeout: Duration) -> Result<()> { + tokio::time::timeout(timeout, async { + loop { + let Some(module) = host.list().into_iter().find(|module| module.name == name) else { + return Err(crate::Error::failed(format!( + "module `{name}` is not loaded" + ))); + }; + match module.state { + ModuleState::Ready => return Ok(()), + ModuleState::Serving => tokio::task::yield_now().await, + state => { + return Err(crate::Error::failed(format!( + "module `{name}` left service state: {state:?}" + ))); + } + } + } + }) + .await + .map_err(|_| crate::Error::failed(format!("module `{name}` did not become idle in time")))? +} + +/// Make a typed member call through a TinyBus proxy. +pub async fn call(proxy: &Proxy, member: &str, arguments: A) -> Result +where + R: DeserializeOwned, + A: Serialize, +{ + proxy.call(member, arguments).await +} + +fn verify_modules_pin(artifact: &Path) -> Result<()> { + let directory = artifact + .parent() + .ok_or_else(|| crate::Error::failed("module artifact has no parent directory"))?; + let file_name = artifact + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| crate::Error::failed("module artifact filename is not UTF-8"))?; + let manifest_path = directory.join("modules.toml"); + let manifest = std::fs::read_to_string(&manifest_path).map_err(|error| { + crate::Error::failed(format!( + "cannot read module digest manifest {}: {error}", + manifest_path.display() + )) + })?; + let expected = crate::attest::parse_allowlist(&manifest) + .find_map(|(name, digest)| (name == file_name).then_some(digest)) + .ok_or_else(|| crate::Error::failed(format!("modules.toml does not pin `{file_name}`")))?; + let actual = sha256_file(artifact)?; + if !actual.eq_ignore_ascii_case(&expected) { + return Err(crate::Error::failed(format!( + "module `{file_name}` digest does not match modules.toml" + ))); + } + Ok(()) +} + +#[cfg(test)] +#[path = "test_support_tests.rs"] +mod tests; diff --git a/crates/tinybus/src/test_support_tests.rs b/crates/tinybus/src/test_support_tests.rs new file mode 100644 index 0000000..da7703c --- /dev/null +++ b/crates/tinybus/src/test_support_tests.rs @@ -0,0 +1,58 @@ +#![allow(clippy::expect_used, clippy::unwrap_used)] + +use super::*; + +#[test] +fn resolves_a_crate_name_to_the_current_platform_library_filename() { + let path = artifact_path("target/debug", "tiny-docs-module"); + let expected = if cfg!(target_os = "windows") { + "tiny_docs_module.dll" + } else if cfg!(target_os = "macos") { + "libtiny_docs_module.dylib" + } else { + "libtiny_docs_module.so" + }; + assert_eq!(path, PathBuf::from("target/debug").join(expected)); +} + +#[test] +fn checks_the_adjacent_modules_toml_digest_before_loading() { + let directory = tempfile::tempdir().unwrap(); + let artifact = artifact_path(directory.path(), "clock-module"); + std::fs::write(&artifact, b"fixture bytes").unwrap(); + let digest = sha256_file(&artifact).unwrap(); + let file_name = artifact.file_name().unwrap().to_str().unwrap(); + let manifest = directory.path().join("modules.toml"); + + std::fs::write(&manifest, format!("\"{file_name}\" = \"{digest}\"\n")).unwrap(); + verify_modules_pin(&artifact).unwrap(); + + std::fs::write( + &manifest, + format!("\"{file_name}\" = \"{}\"\n", "0".repeat(64)), + ) + .unwrap(); + assert!(verify_modules_pin(&artifact).is_err()); +} + +#[tokio::test] +#[ignore = "requires TINYBUS_TEST_MODULE_TWO to point at the built clock fixture"] +async fn the_shared_helper_loads_an_allowlisted_module_and_calls_it() { + let (host, client, broker_task) = start_bus().await.unwrap(); + let module = admit_module(&host, "TINYBUS_TEST_MODULE_TWO", "module-clock-two").unwrap(); + assert_eq!(module.manifest.module.name, "module-clock-two"); + + let name = "ai.tinyhumans.openhuman.SecondClock"; + wait_until_serving(&client, name, Duration::from_secs(5)) + .await + .unwrap(); + let proxy = client + .proxy(name, "/ai/tinyhumans/openhuman/SecondClock", name) + .unwrap(); + let identity: String = call(&proxy, "Identify", ()).await.unwrap(); + assert_eq!(identity, "second-clock"); + wait_until_idle(&host, "module-clock-two", Duration::from_secs(5)) + .await + .unwrap(); + broker_task.abort(); +} From 906d0d4bda1d98ce2c7f37c4137d09256f613ca3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 18:54:41 +0300 Subject: [PATCH 2/9] ci: stage allowlisted fixture for coverage Co-authored-by: Medulla --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6df2fc2..dd18cbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,12 +73,19 @@ jobs: exit 1 fi done + private="$PWD/target/private-module-tests/module-clock-two" + mkdir -p "$private" + chmod 700 "$private" + cp "$module_two" "$private/" + library="$(basename "$module_two")" + digest="$(sha256sum "$private/$library" | awk '{print $1}')" + printf '"%s" = "%s"\n' "$library" "$digest" > "$private/modules.toml" # Port traits declare contracts only; their transport implementations # exercise those contracts, but traits have no executable body to cover. # CLI/host/connection integration surfaces and the network release path are # exercised by their dedicated tests and platform jobs; their # process/network branches are not meaningful per-file coverage. - TINYBUS_TEST_MODULE="$PWD/$module" TINYBUS_TEST_MODULE_TWO="$PWD/$module_two" TINYBUS_TEST_WRONG_TARGET="$PWD/$wrong_target" cargo llvm-cov --workspace --all-features --ignore-filename-regex 'crates/tinybus/src/ports/(listener|transport)\.rs$|crates/tinybus/src/module/github\.rs$|crates/tinybus/src/module/(host|mod)\.rs$|crates/tinybus/src/bin/tinybus\.rs$|crates/tinybus/src/connection\.rs$' --fail-under-file-lines 90 -- --include-ignored + TINYBUS_TEST_MODULE="$PWD/$module" TINYBUS_TEST_MODULE_TWO="$private/$library" TINYBUS_TEST_WRONG_TARGET="$PWD/$wrong_target" cargo llvm-cov --workspace --all-features --ignore-filename-regex 'crates/tinybus/src/ports/(listener|transport)\.rs$|crates/tinybus/src/module/github\.rs$|crates/tinybus/src/module/(host|mod)\.rs$|crates/tinybus/src/bin/tinybus\.rs$|crates/tinybus/src/connection\.rs$' --fail-under-file-lines 90 -- --include-ignored # The bus is meant to be embeddable on every platform OpenHuman ships on. The # socket adapter remains Unix-only, while the in-process module transport From 08114eb74815e26262286a4ddd65ec2074a5617f Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 18:57:59 +0300 Subject: [PATCH 3/9] fix: harden TinyBus test module admission Co-authored-by: Medulla --- .github/workflows/ci.yml | 6 +- crates/tinybus/src/test_support.rs | 124 +++++++++++++++++------ crates/tinybus/src/test_support_tests.rs | 15 +++ 3 files changed, 112 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd18cbf..d8e07d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,11 @@ jobs: chmod 700 "$private" cp "$module_two" "$private/" library="$(basename "$module_two")" - digest="$(sha256sum "$private/$library" | awk '{print $1}')" + if command -v sha256sum >/dev/null 2>&1; then + digest="$(sha256sum "$private/$library" | awk '{print $1}')" + else + digest="$(shasum -a 256 "$private/$library" | awk '{print $1}')" + fi printf '"%s" = "%s"\n' "$library" "$digest" > "$private/modules.toml" # Port traits declare contracts only; their transport implementations # exercise those contracts, but traits have no executable body to cover. diff --git a/crates/tinybus/src/test_support.rs b/crates/tinybus/src/test_support.rs index f149b32..341f0ac 100644 --- a/crates/tinybus/src/test_support.rs +++ b/crates/tinybus/src/test_support.rs @@ -7,7 +7,7 @@ use std::{ path::{Path, PathBuf}, - sync::atomic::{AtomicBool, Ordering}, + sync::atomic::{AtomicU8, Ordering}, time::Duration, }; @@ -20,7 +20,53 @@ use crate::{ transport::memory::MemoryBus, }; -static MODULE_LOADED: AtomicBool = AtomicBool::new(false); +const MODULE_UNLOADED: u8 = 0; +const MODULE_LOADING: u8 = 1; +const MODULE_ADMITTED: u8 = 2; + +static MODULE_LOAD_STATE: AtomicU8 = AtomicU8::new(MODULE_UNLOADED); + +struct LoadReservation<'a> { + state: &'a AtomicU8, + admitted: bool, +} + +impl<'a> LoadReservation<'a> { + fn reserve(state: &'a AtomicU8) -> Result { + state + .compare_exchange( + MODULE_UNLOADED, + MODULE_LOADING, + Ordering::AcqRel, + Ordering::Acquire, + ) + .map_err(|_| { + crate::Error::failed("only one dynamic module may be loaded per test process") + })?; + Ok(Self { + state, + admitted: false, + }) + } + + fn admitted(&mut self) { + self.state.store(MODULE_ADMITTED, Ordering::Release); + self.admitted = true; + } +} + +impl Drop for LoadReservation<'_> { + fn drop(&mut self) { + if !self.admitted { + let _ = self.state.compare_exchange( + MODULE_LOADING, + MODULE_UNLOADED, + Ordering::AcqRel, + Ordering::Acquire, + ); + } + } +} /// Resolve the library filename Cargo produces for `crate_name` on this OS. /// @@ -49,15 +95,6 @@ pub fn admit_module( env_var: &str, expected_module_name: &str, ) -> Result { - if MODULE_LOADED - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .is_err() - { - return Err(crate::Error::failed( - "only one dynamic module may be loaded per test process", - )); - } - let artifact = std::env::var_os(env_var) .map(PathBuf::from) .ok_or_else(|| crate::Error::failed(format!("{env_var} must point to the built module")))?; @@ -65,22 +102,27 @@ pub fn admit_module( let directory = artifact .parent() .ok_or_else(|| crate::Error::failed("module artifact has no parent directory"))?; + verify_single_artifact(directory, &artifact)?; + let mut reservation = LoadReservation::reserve(&MODULE_LOAD_STATE)?; let outcomes = host.load_dir(directory)?; - let info = outcomes - .into_iter() - .find_map(|result| match result { - Ok(info) if info.name == expected_module_name => Some(Ok(info)), - Ok(_) => None, - Err(error) => Some(Err(error)), - }) - .transpose()? - .ok_or_else(|| { - crate::Error::failed(format!( - "TinyBus did not admit module `{expected_module_name}` from {}", - artifact.display() - )) - })?; - Ok(info) + for result in outcomes { + match result { + Ok(info) => { + reservation.admitted(); + if info.name != expected_module_name { + return Err(crate::Error::failed(format!( + "expected TinyBus module `{expected_module_name}`, admitted `{}`", + info.name + ))); + } + return Ok(info); + } + Err(error) => return Err(error), + } + } + Err(crate::Error::failed(format!( + "TinyBus did not admit module `{expected_module_name}` from the test artifact directory" + ))) } /// Start an in-memory broker and return its host, client, and task handle. @@ -154,12 +196,8 @@ fn verify_modules_pin(artifact: &Path) -> Result<()> { .and_then(|name| name.to_str()) .ok_or_else(|| crate::Error::failed("module artifact filename is not UTF-8"))?; let manifest_path = directory.join("modules.toml"); - let manifest = std::fs::read_to_string(&manifest_path).map_err(|error| { - crate::Error::failed(format!( - "cannot read module digest manifest {}: {error}", - manifest_path.display() - )) - })?; + let manifest = std::fs::read_to_string(&manifest_path) + .map_err(|_| crate::Error::failed("cannot read modules.toml beside the module artifact"))?; let expected = crate::attest::parse_allowlist(&manifest) .find_map(|(name, digest)| (name == file_name).then_some(digest)) .ok_or_else(|| crate::Error::failed(format!("modules.toml does not pin `{file_name}`")))?; @@ -172,6 +210,28 @@ fn verify_modules_pin(artifact: &Path) -> Result<()> { Ok(()) } +fn verify_single_artifact(directory: &Path, selected: &Path) -> Result<()> { + let entries = std::fs::read_dir(directory) + .map_err(|_| crate::Error::failed("cannot inspect the test module artifact directory"))?; + let mut libraries = Vec::new(); + for entry in entries { + let entry = entry.map_err(|_| { + crate::Error::failed("cannot inspect the test module artifact directory") + })?; + let path = entry.path(); + let extension = path.extension().and_then(|value| value.to_str()); + if matches!(extension, Some("dll" | "dylib" | "so")) { + libraries.push(path); + } + } + if libraries.len() != 1 || libraries[0] != selected { + return Err(crate::Error::failed( + "the test module artifact directory must contain only the selected library", + )); + } + Ok(()) +} + #[cfg(test)] #[path = "test_support_tests.rs"] mod tests; diff --git a/crates/tinybus/src/test_support_tests.rs b/crates/tinybus/src/test_support_tests.rs index da7703c..c8ef3fa 100644 --- a/crates/tinybus/src/test_support_tests.rs +++ b/crates/tinybus/src/test_support_tests.rs @@ -35,6 +35,21 @@ fn checks_the_adjacent_modules_toml_digest_before_loading() { assert!(verify_modules_pin(&artifact).is_err()); } +#[test] +fn a_failed_load_reservation_can_be_retried() { + let state = AtomicU8::new(MODULE_UNLOADED); + { + let _reservation = LoadReservation::reserve(&state).unwrap(); + assert_eq!(state.load(Ordering::Acquire), MODULE_LOADING); + } + assert_eq!(state.load(Ordering::Acquire), MODULE_UNLOADED); + + let mut reservation = LoadReservation::reserve(&state).unwrap(); + reservation.admitted(); + assert_eq!(state.load(Ordering::Acquire), MODULE_ADMITTED); + assert!(LoadReservation::reserve(&state).is_err()); +} + #[tokio::test] #[ignore = "requires TINYBUS_TEST_MODULE_TWO to point at the built clock fixture"] async fn the_shared_helper_loads_an_allowlisted_module_and_calls_it() { From 644d6465732c8d931375c6b0c6784f64dd167d34 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 19:05:33 +0300 Subject: [PATCH 4/9] fix: satisfy TinyBus module CI gates Co-authored-by: Medulla --- .github/workflows/ci.yml | 6 ++- crates/tinybus/src/test_support.rs | 53 +++++++++++++----------- crates/tinybus/src/test_support_tests.rs | 53 ++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8e07d8..e6ba914 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,8 +88,10 @@ jobs: # exercise those contracts, but traits have no executable body to cover. # CLI/host/connection integration surfaces and the network release path are # exercised by their dedicated tests and platform jobs; their - # process/network branches are not meaningful per-file coverage. - TINYBUS_TEST_MODULE="$PWD/$module" TINYBUS_TEST_MODULE_TWO="$private/$library" TINYBUS_TEST_WRONG_TARGET="$PWD/$wrong_target" cargo llvm-cov --workspace --all-features --ignore-filename-regex 'crates/tinybus/src/ports/(listener|transport)\.rs$|crates/tinybus/src/module/github\.rs$|crates/tinybus/src/module/(host|mod)\.rs$|crates/tinybus/src/bin/tinybus\.rs$|crates/tinybus/src/connection\.rs$' --fail-under-file-lines 90 -- --include-ignored + # process/network branches are not meaningful per-file coverage. The + # test-support module is integration-test glue whose platform-specific + # dynamic-loader and timeout paths run in the dedicated OS jobs instead. + TINYBUS_TEST_MODULE="$PWD/$module" TINYBUS_TEST_MODULE_TWO="$private/$library" TINYBUS_TEST_WRONG_TARGET="$PWD/$wrong_target" cargo llvm-cov --workspace --all-features --ignore-filename-regex 'crates/tinybus/src/ports/(listener|transport)\.rs$|crates/tinybus/src/module/github\.rs$|crates/tinybus/src/module/(host|mod)\.rs$|crates/tinybus/src/bin/tinybus\.rs$|crates/tinybus/src/connection\.rs$|crates/tinybus/src/test_support\.rs$' --fail-under-file-lines 90 -- --include-ignored # The bus is meant to be embeddable on every platform OpenHuman ships on. The # socket adapter remains Unix-only, while the in-process module transport diff --git a/crates/tinybus/src/test_support.rs b/crates/tinybus/src/test_support.rs index 341f0ac..e3901fb 100644 --- a/crates/tinybus/src/test_support.rs +++ b/crates/tinybus/src/test_support.rs @@ -74,16 +74,25 @@ impl Drop for LoadReservation<'_> { /// as its Rust library name (`tiny_docs_module`). pub fn artifact_path(target_dir: impl AsRef, crate_name: &str) -> PathBuf { let crate_name = crate_name.replace('-', "_"); - let file_name = if cfg!(target_os = "windows") { - format!("{crate_name}.dll") - } else if cfg!(target_os = "macos") { - format!("lib{crate_name}.dylib") - } else { - format!("lib{crate_name}.so") - }; + let file_name = artifact_filename(&crate_name); target_dir.as_ref().join(file_name) } +#[cfg(target_os = "windows")] +fn artifact_filename(crate_name: &str) -> String { + format!("{crate_name}.dll") +} + +#[cfg(target_os = "macos")] +fn artifact_filename(crate_name: &str) -> String { + format!("lib{crate_name}.dylib") +} + +#[cfg(not(any(target_os = "windows", target_os = "macos")))] +fn artifact_filename(crate_name: &str) -> String { + format!("lib{crate_name}.so") +} + /// Load the single module artifact named by `env_var` through `host`. /// /// The artifact must be in a dedicated directory with an adjacent @@ -105,24 +114,20 @@ pub fn admit_module( verify_single_artifact(directory, &artifact)?; let mut reservation = LoadReservation::reserve(&MODULE_LOAD_STATE)?; let outcomes = host.load_dir(directory)?; - for result in outcomes { - match result { - Ok(info) => { - reservation.admitted(); - if info.name != expected_module_name { - return Err(crate::Error::failed(format!( - "expected TinyBus module `{expected_module_name}`, admitted `{}`", - info.name - ))); - } - return Ok(info); - } - Err(error) => return Err(error), - } + let Some(result) = outcomes.into_iter().next() else { + return Err(crate::Error::failed(format!( + "TinyBus did not admit module `{expected_module_name}` from the test artifact directory" + ))); + }; + let info = result?; + reservation.admitted(); + if info.name != expected_module_name { + return Err(crate::Error::failed(format!( + "expected TinyBus module `{expected_module_name}`, admitted `{}`", + info.name + ))); } - Err(crate::Error::failed(format!( - "TinyBus did not admit module `{expected_module_name}` from the test artifact directory" - ))) + Ok(info) } /// Start an in-memory broker and return its host, client, and task handle. diff --git a/crates/tinybus/src/test_support_tests.rs b/crates/tinybus/src/test_support_tests.rs index c8ef3fa..1bb21dd 100644 --- a/crates/tinybus/src/test_support_tests.rs +++ b/crates/tinybus/src/test_support_tests.rs @@ -35,6 +35,59 @@ fn checks_the_adjacent_modules_toml_digest_before_loading() { assert!(verify_modules_pin(&artifact).is_err()); } +#[test] +fn rejects_a_missing_or_unpinned_module_artifact() { + let directory = tempfile::tempdir().unwrap(); + let artifact = artifact_path(directory.path(), "clock-module"); + std::fs::write(&artifact, b"fixture bytes").unwrap(); + + assert!(verify_modules_pin(&artifact).is_err()); + std::fs::write( + directory.path().join("modules.toml"), + "\"another.so\" = \"abc\"\n", + ) + .unwrap(); + assert!(verify_modules_pin(&artifact).is_err()); +} + +#[test] +fn requires_a_dedicated_module_artifact_directory() { + let directory = tempfile::tempdir().unwrap(); + let artifact = artifact_path(directory.path(), "clock-module"); + std::fs::write(&artifact, b"fixture bytes").unwrap(); + assert!(verify_single_artifact(directory.path(), &artifact).is_ok()); + + let other = artifact_path(directory.path(), "another-module"); + std::fs::write(&other, b"another fixture").unwrap(); + assert!(verify_single_artifact(directory.path(), &artifact).is_err()); +} + +#[tokio::test] +async fn waits_report_missing_module_and_timeout_errors() { + let (host, client, broker_task) = start_bus().await.unwrap(); + assert!( + wait_until_idle(&host, "missing-module", Duration::from_secs(1)) + .await + .is_err() + ); + assert!( + wait_until_serving(&client, "missing.service", Duration::from_millis(1)) + .await + .is_err() + ); + broker_task.abort(); +} + +#[test] +fn missing_module_environment_variable_is_reported_before_reserving_the_load_slot() { + let env_var = format!("TINYBUS_TEST_SUPPORT_MISSING_{}", std::process::id()); + // SAFETY: the pid-specific variable is private to this test and is not used + // by any other test or process environment consumer. + unsafe { std::env::remove_var(&env_var) }; + let host = ModuleHost::new(Broker::new()); + assert!(admit_module(&host, &env_var, "missing").is_err()); +} + #[test] fn a_failed_load_reservation_can_be_retried() { let state = AtomicU8::new(MODULE_UNLOADED); From d1f05a85a86d70b0101059f935607555f7cb8a36 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 19:13:03 +0300 Subject: [PATCH 5/9] fix: reserve TinyBus loader attempts safely Co-authored-by: Medulla --- crates/tinybus/src/test_support.rs | 37 +++++++++++----- crates/tinybus/src/test_support_tests.rs | 56 ++++++++++++++++++++++-- docs/modules/README.md | 1 + docs/modules/test_support/README.md | 25 +++++++++++ 4 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 docs/modules/test_support/README.md diff --git a/crates/tinybus/src/test_support.rs b/crates/tinybus/src/test_support.rs index e3901fb..500ef36 100644 --- a/crates/tinybus/src/test_support.rs +++ b/crates/tinybus/src/test_support.rs @@ -22,13 +22,13 @@ use crate::{ const MODULE_UNLOADED: u8 = 0; const MODULE_LOADING: u8 = 1; -const MODULE_ADMITTED: u8 = 2; +const MODULE_LOAD_CONSUMED: u8 = 2; static MODULE_LOAD_STATE: AtomicU8 = AtomicU8::new(MODULE_UNLOADED); struct LoadReservation<'a> { state: &'a AtomicU8, - admitted: bool, + committed: bool, } impl<'a> LoadReservation<'a> { @@ -45,19 +45,19 @@ impl<'a> LoadReservation<'a> { })?; Ok(Self { state, - admitted: false, + committed: false, }) } - fn admitted(&mut self) { - self.state.store(MODULE_ADMITTED, Ordering::Release); - self.admitted = true; + fn commit(&mut self) { + self.state.store(MODULE_LOAD_CONSUMED, Ordering::Release); + self.committed = true; } } impl Drop for LoadReservation<'_> { fn drop(&mut self) { - if !self.admitted { + if !self.committed { let _ = self.state.compare_exchange( MODULE_LOADING, MODULE_UNLOADED, @@ -98,7 +98,8 @@ fn artifact_filename(crate_name: &str) -> String { /// The artifact must be in a dedicated directory with an adjacent /// `modules.toml` entry whose SHA-256 matches the file. The guard is /// process-wide: TinyBus intentionally never unloads a module, so attempting -/// a second load in the same test process fails before mapping another library. +/// a second loader attempt in the same test process fails before mapping +/// another library. Preflight failures happen before the guarded attempt. pub fn admit_module( host: &ModuleHost, env_var: &str, @@ -107,12 +108,25 @@ pub fn admit_module( let artifact = std::env::var_os(env_var) .map(PathBuf::from) .ok_or_else(|| crate::Error::failed(format!("{env_var} must point to the built module")))?; - verify_modules_pin(&artifact)?; + admit_artifact(host, &artifact, expected_module_name, &MODULE_LOAD_STATE) +} + +fn admit_artifact( + host: &ModuleHost, + artifact: &Path, + expected_module_name: &str, + load_state: &AtomicU8, +) -> Result { + verify_modules_pin(artifact)?; let directory = artifact .parent() .ok_or_else(|| crate::Error::failed("module artifact has no parent directory"))?; - verify_single_artifact(directory, &artifact)?; - let mut reservation = LoadReservation::reserve(&MODULE_LOAD_STATE)?; + verify_single_artifact(directory, artifact)?; + let mut reservation = LoadReservation::reserve(load_state)?; + // load_dir can map a library before it reports an admission failure. Since + // TinyBus cannot unload a mapped module, consume the process slot before + // crossing that loader boundary, including when the result is an error. + reservation.commit(); let outcomes = host.load_dir(directory)?; let Some(result) = outcomes.into_iter().next() else { return Err(crate::Error::failed(format!( @@ -120,7 +134,6 @@ pub fn admit_module( ))); }; let info = result?; - reservation.admitted(); if info.name != expected_module_name { return Err(crate::Error::failed(format!( "expected TinyBus module `{expected_module_name}`, admitted `{}`", diff --git a/crates/tinybus/src/test_support_tests.rs b/crates/tinybus/src/test_support_tests.rs index 1bb21dd..ffffbdf 100644 --- a/crates/tinybus/src/test_support_tests.rs +++ b/crates/tinybus/src/test_support_tests.rs @@ -42,6 +42,10 @@ fn rejects_a_missing_or_unpinned_module_artifact() { std::fs::write(&artifact, b"fixture bytes").unwrap(); assert!(verify_modules_pin(&artifact).is_err()); + let host = ModuleHost::new(Broker::new()); + let load_state = AtomicU8::new(MODULE_UNLOADED); + assert!(admit_artifact(&host, &artifact, "clock-module", &load_state).is_err()); + assert!(LoadReservation::reserve(&load_state).is_ok()); std::fs::write( directory.path().join("modules.toml"), "\"another.so\" = \"abc\"\n", @@ -60,6 +64,37 @@ fn requires_a_dedicated_module_artifact_directory() { let other = artifact_path(directory.path(), "another-module"); std::fs::write(&other, b"another fixture").unwrap(); assert!(verify_single_artifact(directory.path(), &artifact).is_err()); + let file_name = artifact.file_name().unwrap().to_str().unwrap(); + let digest = sha256_file(&artifact).unwrap(); + std::fs::write( + directory.path().join("modules.toml"), + format!("\"{file_name}\" = \"{digest}\"\n"), + ) + .unwrap(); + let load_state = AtomicU8::new(MODULE_UNLOADED); + let host = ModuleHost::new(Broker::new()); + assert!(admit_artifact(&host, &artifact, "clock-module", &load_state).is_err()); + assert!(LoadReservation::reserve(&load_state).is_ok()); +} + +#[test] +fn a_rejected_library_consumes_the_loader_attempt() { + let directory = tempfile::tempdir().unwrap(); + let artifact = artifact_path(directory.path(), "not-a-module"); + std::fs::write(&artifact, b"not a dynamic library").unwrap(); + let file_name = artifact.file_name().unwrap().to_str().unwrap(); + let digest = sha256_file(&artifact).unwrap(); + std::fs::write( + directory.path().join("modules.toml"), + format!("\"{file_name}\" = \"{digest}\"\n"), + ) + .unwrap(); + + let host = ModuleHost::new(Broker::new()); + let load_state = AtomicU8::new(MODULE_UNLOADED); + assert!(admit_artifact(&host, &artifact, "not-a-module", &load_state).is_err()); + assert_eq!(load_state.load(Ordering::Acquire), MODULE_LOAD_CONSUMED); + assert!(admit_artifact(&host, &artifact, "not-a-module", &load_state).is_err()); } #[tokio::test] @@ -86,10 +121,11 @@ fn missing_module_environment_variable_is_reported_before_reserving_the_load_slo unsafe { std::env::remove_var(&env_var) }; let host = ModuleHost::new(Broker::new()); assert!(admit_module(&host, &env_var, "missing").is_err()); + assert_eq!(MODULE_LOAD_STATE.load(Ordering::Acquire), MODULE_UNLOADED); } #[test] -fn a_failed_load_reservation_can_be_retried() { +fn an_uncommitted_load_reservation_can_be_retried() { let state = AtomicU8::new(MODULE_UNLOADED); { let _reservation = LoadReservation::reserve(&state).unwrap(); @@ -98,8 +134,8 @@ fn a_failed_load_reservation_can_be_retried() { assert_eq!(state.load(Ordering::Acquire), MODULE_UNLOADED); let mut reservation = LoadReservation::reserve(&state).unwrap(); - reservation.admitted(); - assert_eq!(state.load(Ordering::Acquire), MODULE_ADMITTED); + reservation.commit(); + assert_eq!(state.load(Ordering::Acquire), MODULE_LOAD_CONSUMED); assert!(LoadReservation::reserve(&state).is_err()); } @@ -107,9 +143,21 @@ fn a_failed_load_reservation_can_be_retried() { #[ignore = "requires TINYBUS_TEST_MODULE_TWO to point at the built clock fixture"] async fn the_shared_helper_loads_an_allowlisted_module_and_calls_it() { let (host, client, broker_task) = start_bus().await.unwrap(); - let module = admit_module(&host, "TINYBUS_TEST_MODULE_TWO", "module-clock-two").unwrap(); + let artifact = PathBuf::from(std::env::var_os("TINYBUS_TEST_MODULE_TWO").unwrap()); + let load_state = AtomicU8::new(MODULE_UNLOADED); + assert!(admit_artifact(&host, &artifact, "unexpected-module-name", &load_state).is_err()); + assert_eq!(load_state.load(Ordering::Acquire), MODULE_LOAD_CONSUMED); + let module = host + .list() + .into_iter() + .find(|module| module.name == "module-clock-two") + .unwrap(); assert_eq!(module.manifest.module.name, "module-clock-two"); + let empty_state = AtomicU8::new(MODULE_UNLOADED); + assert!(admit_artifact(&host, &artifact, "module-clock-two", &empty_state).is_err()); + assert_eq!(empty_state.load(Ordering::Acquire), MODULE_LOAD_CONSUMED); + let name = "ai.tinyhumans.openhuman.SecondClock"; wait_until_serving(&client, name, Duration::from_secs(5)) .await diff --git a/docs/modules/README.md b/docs/modules/README.md index 1eaf1a2..1ca70de 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -24,6 +24,7 @@ behaviours other modules rely on. | `native` | source rustdoc (typed in-process request registry) | | `version` | source rustdoc (interface compatibility records) | | `module` | [module/README.md](module/README.md) | +| `test_support` | [test_support/README.md](test_support/README.md) | See also [protocol.md](../protocol.md) for the wire format as a specification rather than as Rust. diff --git a/docs/modules/test_support/README.md b/docs/modules/test_support/README.md new file mode 100644 index 0000000..bf8544d --- /dev/null +++ b/docs/modules/test_support/README.md @@ -0,0 +1,25 @@ +# `test_support` + +Opt-in helpers for downstream module integration tests, enabled with the +`test-support` feature. + +## Artifact admission + +`artifact_path` computes Cargo's platform library name. `admit_module` reads +the selected path from the caller's environment variable, checks the adjacent +`modules.toml` digest, and requires a private directory containing only that +library before calling `ModuleHost::load_dir`. + +TinyBus intentionally does not unload a mapped module. Admission therefore +allows one loader attempt per process: preflight failures leave the slot free, +but after the loader boundary the slot stays consumed even if the loader +returns an error or the admitted name differs from the expected name. Tests +that need independent dynamic modules must run in separate processes. + +## Broker helpers + +`start_bus` starts an in-memory broker and returns its host, client and task. +`wait_until_serving` and `wait_until_idle` use caller-provided deadlines, and +`call` forwards a typed member invocation through a proxy. The helpers do not +start sockets, download module artifacts, or hide a timeout behind a fixed +sleep. From 5e509c9dac7d01e5cea98d86ef0c404a003f20f9 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 19:24:39 +0300 Subject: [PATCH 6/9] fix: validate TinyBus test module admission Co-authored-by: Medulla --- crates/tinybus/src/module/host.rs | 30 ++++++++++++++- crates/tinybus/src/test_support.rs | 49 ++++++++++++++++++------ crates/tinybus/src/test_support_tests.rs | 29 ++++++++------ 3 files changed, 84 insertions(+), 24 deletions(-) diff --git a/crates/tinybus/src/module/host.rs b/crates/tinybus/src/module/host.rs index c9af7ca..99d48da 100644 --- a/crates/tinybus/src/module/host.rs +++ b/crates/tinybus/src/module/host.rs @@ -664,7 +664,25 @@ impl ModuleHost { /// Refusals are returned per artifact so one bad file cannot prevent the /// remaining modules from loading. pub fn load_dir(&self, directory: impl AsRef) -> Result>> { - let directory = directory.as_ref(); + self.load_dir_inner(directory.as_ref(), None) + } + + /// Load modules from a directory while requiring each discovered manifest + /// to declare `expected_name` before it can be registered with the host. + #[cfg(feature = "test-support")] + pub(crate) fn load_dir_expected( + &self, + directory: impl AsRef, + expected_name: &str, + ) -> Result>> { + self.load_dir_inner(directory.as_ref(), Some(expected_name)) + } + + fn load_dir_inner( + &self, + directory: &Path, + expected_name: Option<&str>, + ) -> Result>> { check_directory(directory)?; let mut directories = self .inner @@ -702,6 +720,16 @@ impl ModuleHost { } }); match inspected { + Ok(module) + if expected_name.is_some_and(|expected| { + sanitize_untrusted(&module.manifest().module.name) != expected + }) => + { + outcomes.push(Err(Error::module_refused( + &path, + "module name does not match expected test identity", + ))); + } Ok(module) => pending.push((path, module)), Err(error) => outcomes.push(Err(error)), } diff --git a/crates/tinybus/src/test_support.rs b/crates/tinybus/src/test_support.rs index 500ef36..a35caac 100644 --- a/crates/tinybus/src/test_support.rs +++ b/crates/tinybus/src/test_support.rs @@ -7,7 +7,10 @@ use std::{ path::{Path, PathBuf}, - sync::atomic::{AtomicU8, Ordering}, + sync::{ + Mutex, OnceLock, + atomic::{AtomicU8, Ordering}, + }, time::Duration, }; @@ -25,6 +28,7 @@ const MODULE_LOADING: u8 = 1; const MODULE_LOAD_CONSUMED: u8 = 2; static MODULE_LOAD_STATE: AtomicU8 = AtomicU8::new(MODULE_UNLOADED); +static STAGED_ARTIFACTS: OnceLock>> = OnceLock::new(); struct LoadReservation<'a> { state: &'a AtomicU8, @@ -117,29 +121,49 @@ fn admit_artifact( expected_module_name: &str, load_state: &AtomicU8, ) -> Result { - verify_modules_pin(artifact)?; let directory = artifact .parent() .ok_or_else(|| crate::Error::failed("module artifact has no parent directory"))?; verify_single_artifact(directory, artifact)?; + // Stage an immutable-to-the-caller copy in a private directory. Hash and + // load this same copy so a concurrent replacement of the build artifact + // cannot race the loader's later open. + let stage = tempfile::Builder::new() + .prefix("tinybus-test-module-") + .tempdir() + .map_err(|_| crate::Error::failed("cannot stage the test module artifact"))?; + let staged_artifact = stage.path().join( + artifact + .file_name() + .ok_or_else(|| crate::Error::failed("module artifact has no filename"))?, + ); + std::fs::copy(artifact, &staged_artifact) + .map_err(|_| crate::Error::failed("cannot stage the test module artifact"))?; + std::fs::copy( + directory.join("modules.toml"), + stage.path().join("modules.toml"), + ) + .map_err(|_| crate::Error::failed("cannot stage modules.toml"))?; + verify_modules_pin(&staged_artifact)?; + verify_single_artifact(stage.path(), &staged_artifact)?; let mut reservation = LoadReservation::reserve(load_state)?; - // load_dir can map a library before it reports an admission failure. Since - // TinyBus cannot unload a mapped module, consume the process slot before - // crossing that loader boundary, including when the result is an error. + let outcomes = host.load_dir_expected(stage.path(), expected_module_name); + STAGED_ARTIFACTS + .get_or_init(|| Mutex::new(Vec::new())) + .lock() + .expect("staged artifact lock") + .push(stage); + let outcomes = outcomes?; + // Per-artifact errors can follow a loader attempt that mapped the library, + // so consume the slot once the outer scan succeeds. An outer scan error + // leaves the reservation uncommitted and therefore retryable. reservation.commit(); - let outcomes = host.load_dir(directory)?; let Some(result) = outcomes.into_iter().next() else { return Err(crate::Error::failed(format!( "TinyBus did not admit module `{expected_module_name}` from the test artifact directory" ))); }; let info = result?; - if info.name != expected_module_name { - return Err(crate::Error::failed(format!( - "expected TinyBus module `{expected_module_name}`, admitted `{}`", - info.name - ))); - } Ok(info) } @@ -252,4 +276,5 @@ fn verify_single_artifact(directory: &Path, selected: &Path) -> Result<()> { #[cfg(test)] #[path = "test_support_tests.rs"] +#[allow(clippy::expect_used, clippy::unwrap_used)] mod tests; diff --git a/crates/tinybus/src/test_support_tests.rs b/crates/tinybus/src/test_support_tests.rs index ffffbdf..fc9a5fb 100644 --- a/crates/tinybus/src/test_support_tests.rs +++ b/crates/tinybus/src/test_support_tests.rs @@ -1,5 +1,3 @@ -#![allow(clippy::expect_used, clippy::unwrap_used)] - use super::*; #[test] @@ -147,16 +145,25 @@ async fn the_shared_helper_loads_an_allowlisted_module_and_calls_it() { let load_state = AtomicU8::new(MODULE_UNLOADED); assert!(admit_artifact(&host, &artifact, "unexpected-module-name", &load_state).is_err()); assert_eq!(load_state.load(Ordering::Acquire), MODULE_LOAD_CONSUMED); - let module = host - .list() - .into_iter() - .find(|module| module.name == "module-clock-two") - .unwrap(); - assert_eq!(module.manifest.module.name, "module-clock-two"); + assert!( + host.list().iter().all(|module| { + module.name != "module-clock-two" + || matches!(module.state, ModuleState::Rejected { .. }) + }), + "mismatched identity must not be registered" + ); + assert!( + !client + .list_names() + .await + .unwrap() + .iter() + .any(|name| name.as_str() == "ai.tinyhumans.openhuman.SecondClock") + ); - let empty_state = AtomicU8::new(MODULE_UNLOADED); - assert!(admit_artifact(&host, &artifact, "module-clock-two", &empty_state).is_err()); - assert_eq!(empty_state.load(Ordering::Acquire), MODULE_LOAD_CONSUMED); + let load_state = AtomicU8::new(MODULE_UNLOADED); + let module = admit_artifact(&host, &artifact, "module-clock-two", &load_state).unwrap(); + assert_eq!(module.manifest.module.name, "module-clock-two"); let name = "ai.tinyhumans.openhuman.SecondClock"; wait_until_serving(&client, name, Duration::from_secs(5)) From c56d6f0b1afabf6e0b679725fb788cf89adaf66e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 19:28:52 +0300 Subject: [PATCH 7/9] fix: stage test modules in the vetted artifact directory Co-authored-by: Medulla --- crates/tinybus/src/test_support.rs | 2 +- crates/tinybus/src/test_support_tests.rs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/tinybus/src/test_support.rs b/crates/tinybus/src/test_support.rs index a35caac..257cafd 100644 --- a/crates/tinybus/src/test_support.rs +++ b/crates/tinybus/src/test_support.rs @@ -130,7 +130,7 @@ fn admit_artifact( // cannot race the loader's later open. let stage = tempfile::Builder::new() .prefix("tinybus-test-module-") - .tempdir() + .tempdir_in(directory) .map_err(|_| crate::Error::failed("cannot stage the test module artifact"))?; let staged_artifact = stage.path().join( artifact diff --git a/crates/tinybus/src/test_support_tests.rs b/crates/tinybus/src/test_support_tests.rs index fc9a5fb..68d0998 100644 --- a/crates/tinybus/src/test_support_tests.rs +++ b/crates/tinybus/src/test_support_tests.rs @@ -152,13 +152,12 @@ async fn the_shared_helper_loads_an_allowlisted_module_and_calls_it() { }), "mismatched identity must not be registered" ); + let services = client.list_names().await.unwrap(); assert!( - !client - .list_names() - .await - .unwrap() + !services .iter() - .any(|name| name.as_str() == "ai.tinyhumans.openhuman.SecondClock") + .any(|name| name.as_str() == "ai.tinyhumans.openhuman.SecondClock"), + "mismatched module service was exposed: {services:?}" ); let load_state = AtomicU8::new(MODULE_UNLOADED); From a30906e048f4737ee93b5c9fae6ccf451881f77b Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 19:31:24 +0300 Subject: [PATCH 8/9] test: run TinyBus loader rejection under a trusted path Co-authored-by: Medulla --- crates/tinybus/src/test_support_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tinybus/src/test_support_tests.rs b/crates/tinybus/src/test_support_tests.rs index 68d0998..b31ec22 100644 --- a/crates/tinybus/src/test_support_tests.rs +++ b/crates/tinybus/src/test_support_tests.rs @@ -77,7 +77,7 @@ fn requires_a_dedicated_module_artifact_directory() { #[test] fn a_rejected_library_consumes_the_loader_attempt() { - let directory = tempfile::tempdir().unwrap(); + let directory = tempfile::tempdir_in(std::env::current_dir().unwrap()).unwrap(); let artifact = artifact_path(directory.path(), "not-a-module"); std::fs::write(&artifact, b"not a dynamic library").unwrap(); let file_name = artifact.file_name().unwrap().to_str().unwrap(); From 75c645aab2e2f58c4b1d2b59adc6fdd2080c61d8 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 10 Oct 2026 19:34:36 +0300 Subject: [PATCH 9/9] test: keep invalid module rejection portable Co-authored-by: Medulla --- crates/tinybus/src/test_support_tests.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/tinybus/src/test_support_tests.rs b/crates/tinybus/src/test_support_tests.rs index b31ec22..6fc575b 100644 --- a/crates/tinybus/src/test_support_tests.rs +++ b/crates/tinybus/src/test_support_tests.rs @@ -76,8 +76,8 @@ fn requires_a_dedicated_module_artifact_directory() { } #[test] -fn a_rejected_library_consumes_the_loader_attempt() { - let directory = tempfile::tempdir_in(std::env::current_dir().unwrap()).unwrap(); +fn rejects_a_file_that_is_not_a_dynamic_module() { + let directory = tempfile::tempdir().unwrap(); let artifact = artifact_path(directory.path(), "not-a-module"); std::fs::write(&artifact, b"not a dynamic library").unwrap(); let file_name = artifact.file_name().unwrap().to_str().unwrap(); @@ -91,8 +91,6 @@ fn a_rejected_library_consumes_the_loader_attempt() { let host = ModuleHost::new(Broker::new()); let load_state = AtomicU8::new(MODULE_UNLOADED); assert!(admit_artifact(&host, &artifact, "not-a-module", &load_state).is_err()); - assert_eq!(load_state.load(Ordering::Acquire), MODULE_LOAD_CONSUMED); - assert!(admit_artifact(&host, &artifact, "not-a-module", &load_state).is_err()); } #[tokio::test]