Skip to content

feat(runtime): in-process module test harness over the mock assembly#203

Open
mfw78 wants to merge 1 commit into
feat/m0-poison-policy-configfrom
feat/m0-test-harness
Open

feat(runtime): in-process module test harness over the mock assembly#203
mfw78 wants to merge 1 commit into
feat/m0-poison-policy-configfrom
feat/m0-test-harness

Conversation

@mfw78

@mfw78 mfw78 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #199; part of the epic #80 train, merges in stack order.

Adds a TestRuntime / TestRuntimeBuilder harness in test_utils that launches one module over the mock assembly through the public RuntimeBuilder path, replacing the hand-built EngineConfig plus boot_single plus manual event_loop::run ceremony that today's supervisor e2e tests repeat.

Changes

  • New crates/nexum-runtime/src/test_utils/harness.rs: TestRuntime and TestRuntimeBuilder over the mock assembly.
    The builder takes a wasm path plus a manifest (manifest_path or manifest_inline, temp-filed at launch), optional extensions, and an ext payload.
    Pre-launch accessors (chain/store/clock) let a caller program the mocks before the module starts.
    The launched handle bundles the RuntimeHandle with the retained MockChainProvider, MockStateStore, ManualClock, and ext payload, and exposes push_block/push_chain_log, logs(), ext(), a wait_for_log poll helper that doubles as the dispatch barrier, and shutdown/wait.
  • Two new tests: an end-to-end launch, dispatch, and log read, and a trivial-extension plus ext-payload threading proof.
  • crates/nexum-runtime/src/test_utils/types.rs: MockTypes generalised to MockTypes<E = ()> so the ext slot is the lattice type parameter (the () default preserves every existing call site).
  • crates/nexum-runtime/src/test_utils/mod.rs: registers pub mod harness and re-exports TestRuntime/TestRuntimeBuilder.
  • crates/nexum-runtime/src/supervisor/tests.rs: host_interface_records_are_retrievable_after_a_run ported onto the harness (inline-manifest launch plus injected header plus polled log read, replacing the boot_single plus manual dispatch_block ceremony), with the same assertions.
  • crates/nexum-runtime/Cargo.toml: test-utils = ["dep:tempfile"], with tempfile promoted from a dev-only dependency to an optional one, since the harness ships in the library gated behind the feature.

Test plan

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace --all-features (all pass, including doctests; new tests harness_launches_dispatches_and_reads_logs and harness_threads_an_extension_and_ext_payload pass, and the ported host_interface_records_are_retrievable_after_a_run passes with the same assertions as before)
  • RUSTDOCFLAGS="-D warnings" cargo doc -p nexum-runtime --all-features --no-deps
  • Verified --no-default-features clippy stays clean, so the harness and tempfile do not compile into the bare default build

Notes for reviewer

The harness composes over the public RuntimeBuilder builder methods (with_types, with_extensions, with_module_source, with_wasi_clocks, with_components, with_add_ons) rather than reaching into the supervisor internals, so events dispatch through the real spawned event loop.
One consequence: a test injects a header and awaits an observable effect rather than reading a dispatch count, so wait_for_log polls the log pipeline (a 2ms-cadence sleep that yields to the event-loop task on both runtime flavours) and acts as the barrier proving the block was dispatched.

MockTypes becoming generic over E lets the same harness carry a real extension payload, since Extension<MockTypes<E>> and the components' ext: E slot both key off it.
Two entry points keep this ergonomic without type-state: TestRuntime::builder(wasm) for the () case, and TestRuntime::builder_with_ext(wasm, ext) for the extension case, since extensions depend on E and so E must be fixed up front.

The trivial-extension proof uses a no-op linker hook (asserted to have run at boot via an AtomicUsize) and an empty NamespaceCaps { prefix: "test:ext/", ifaces: &[] }, plus an Arc<AtomicUsize> ext payload the harness hands back via ext().
No custom WIT guest is needed; it proves the extension and ext payload thread through and the module still boots and dispatches under the extension-bearing lattice.

Follow-ups noted but out of scope here: a harness helper for chain-log dispatch assertions (only the block leg is locked today), swapping wait_for_log's wall-clock poll for a deterministic barrier, and porting the remaining boot_single-shaped e2e tests (multi-chain isolation, poison-pill, restart-backoff) once the harness grows a multi-module builder variant.

AI Assistance: Claude Code (Opus 4.8 implementation and adversarial review, Sonnet 5 PR authoring) used for the full change.

TestRuntime wraps the public builder path over the mock lattice: it
opens one module from a wasm path plus a manifest (a path, or inline
TOML temp-filed at launch), installs a manually-driven clock, and
returns a handle bundling the running RuntimeHandle with the retained
mock backends. A test programs chain responses, injects block headers
and chain logs, advances the clock, reads what a module wrote, and
reads runs and log pages, then shuts down and waits.

The extension slot is the lattice type parameter (MockTypes now carries
an Ext payload, defaulting to the empty slot), so an extension crate
drives its backend through the same harness by passing an ext payload
and its extensions. A trivial in-tree extension proves the threading.

Port the host-interface-records supervisor e2e test onto the harness to
lock the ergonomics, holding the original coverage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant