Skip to content

feat(runtime): mock backends and a MockRuntime assembly via the builder#202

Open
mfw78 wants to merge 1 commit into
fix/m0-train-hardeningfrom
feat/m0-mock-backends
Open

feat(runtime): mock backends and a MockRuntime assembly via the builder#202
mfw78 wants to merge 1 commit into
fix/m0-train-hardeningfrom
feat/m0-mock-backends

Conversation

@mfw78

@mfw78 mfw78 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

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

Ships engine-side test fakes behind a test-utils feature so a runtime can be assembled and launched entirely through the public builder, on mocks, with no CLI, disk, or network.

Changes

crates/nexum-runtime/Cargo.toml adds [features] test-utils = [] plus a self dev-dependency (nexum-runtime with features = ["test-utils"]) so the crate's own tests and doctests see the mocks without every invocation passing --all-features.

src/test_utils/chain.rs adds MockChainProvider, a Clone + Send + Sync ChainProvider with programmable on_method/on_request responses keyed by (method, params), recorded_requests, and push_block/push_chain_log feeding futures::channel::mpsc receivers returned as the boxed BlockStream/ChainLogStream. Unprogrammed methods return ProviderError::UnknownChain, matching ProviderPool::empty(). A second subscribe_* after the receiver is taken parks on stream::pending() so a reconnect loop cannot busy-spin.

src/test_utils/store.rs adds an in-memory MockStateStore/MockStateHandle (Arc<Mutex<HashMap>>, no redb or disk) that mirrors the redb-backed store's behaviour: the same InvalidNamespace message on an empty namespace, Ok(None) on an absent key, idempotent delete, and prefix-filtered, sorted list_keys.

src/test_utils/builders.rs adds a generic pass-through Prebuilt<T> ComponentBuilder.

src/test_utils/types.rs adds the domain-free MockTypes lattice (Chain = MockChainProvider, Store = MockStateStore, Ext = ()).

src/test_utils/mod.rs re-exports the above, adds mock_components/mock_components_from helpers, a module doctest, and the M0 acceptance and mock unit tests.

src/supervisor.rs deletes the test-only DefaultSupervisor::empty_for_test constructor and corrects the one doc line that named it.

src/supervisor/tests.rs moves both former empty_for_test call sites onto a zero-module Supervisor<MockTypes> booted through the real Supervisor::boot path via a new boot_mock_supervisor helper; empty_supervisor_returns_no_subscriptions becomes #[tokio::test].

Cargo.lock gains the one line for the self-dep edge, required for CI --locked.

Test plan

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace --all-features --no-fail-fast (0 failed; new test_utils and rewritten supervisor::tests cases ran and passed, not skipped)
  • cargo test -p nexum-runtime --locked (no --all-features, confirms the self dev-dependency turns the feature on for just test)
  • cargo doc --workspace --no-deps --all-features with RUSTDOCFLAGS=-D warnings

All runs were under nix develop with an isolated CARGO_TARGET_DIR. Module wasms (example, M2/M3, fixture bombs) were built in-tree before the workspace test run.

Notes for reviewer

empty_for_test was deleted outright rather than replaced by a new bespoke test constructor. The two supervisor tests now boot through the real, public Supervisor::boot path with mock_components() and a default module-less config, which exercises real assembly instead of a synthetic shortcut and better matches the epic's "launched entirely through the public builder" intent.

The M0 acceptance test drives the full RuntimeBuilder::new(cfg).with_types::<MockTypes>().with_components(...).with_add_ons(&[]).launch() chain, asserts it reaches supervisor boot and bails cleanly on no modules, then does a direct request round-trip against the mock chain to prove it serves and records. A full fake-driven block-dispatch end-to-end test was deliberately left out of this hermetic acceptance test; it needs a wasm guest and a written manifest (disk), and the channel plumbing it would exercise is already covered by subscribe_blocks_yields_pushed_headers.

futures::channel::mpsc was chosen over tokio for the mock streams: it is already a dependency, avoids an unused-crate-dep risk, and keeps the wasm cone clean.

Response keying uses (&'static str method name, params) rather than adding Hash to the public wire-surface ChainMethod enum, to keep that closed enum's derive set untouched.

Follow-ups intentionally left out of this PR: scriptable error and stream-end injection on the mock chain (so reconnect and backoff can be unit-tested on fakes); a full fake-driven block-dispatch end-to-end test gated on the wasm fixture; promoting a MockRuntime preset once presets can accept pre-built instances rather than only building from config; a docs page for the test-utils surface (the module doctest carries the minimal example for now).

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

Ship engine-side fakes under a test-utils feature so an in-process runtime
launches entirely on mocks: MockChainProvider (programmable request responses,
recorded calls, channel-driven block and chain-log streams), an in-memory
MockStateStore with no redb or disk, a pass-through Prebuilt component builder,
and the domain-free MockTypes lattice. A self dev-dependency turns the feature
on for this crate's own tests.

The assembly composes through the public type-state path
(with_types::<MockTypes>().with_components(...)), proven by an M0 acceptance
launch test that needs no CLI, disk, or network. The two Supervisor
empty_for_test call sites move onto the mock components via the real boot path,
and that test-only constructor is deleted.
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