Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7f7dd72
feat(test-harness): scaffold wavs-test-harness crate (#1147)
JakeHartnell May 16, 2026
0721885
feat(test-harness): chain control layer — anvil, fork, snapshot, impe…
JakeHartnell May 16, 2026
71425d2
feat(test-harness): TOML chain profiles + typed Addresses (#1147)
JakeHartnell May 16, 2026
2b4051e
feat(test-harness): service spec + operator middleware re-exports (#1…
JakeHartnell May 16, 2026
6bad36a
feat(test-harness): in-process runner + lifecycle helpers (#1147)
JakeHartnell May 16, 2026
21bbd1c
feat(test-harness): canonical envelope + ECDSA signing helpers (#1147)
JakeHartnell May 16, 2026
978f201
feat(test-harness): subprocess runner preview shape (#1147)
JakeHartnell May 16, 2026
87207c0
feat(test-harness): TestHarness builder + working examples + justfile…
JakeHartnell May 16, 2026
d416e67
docs(test-harness): comprehensive README with tier matrix + CI guidan…
JakeHartnell May 16, 2026
cd41fd3
chore(test-harness): refresh Cargo.lock with wavs-engine + wasmtime e…
JakeHartnell May 16, 2026
e5e2f91
style(test-harness): apply cargo fmt to satisfy Lint CI (#1147)
JakeHartnell May 16, 2026
7c700f6
feat(test-harness): wire FORK_BLOCK_NUMBER env + ForkOptions::from_pr…
JakeHartnell May 16, 2026
3ce1e1d
feat(test-harness): end-to-end on-chain submission lifecycle (#1147)
JakeHartnell May 16, 2026
1c3f834
feat(test-harness): Chainlink oracle mocking via anvil_setCode (#1147)
JakeHartnell May 16, 2026
ccb318c
feat(test-harness): functional subprocess runner (#1147)
JakeHartnell May 16, 2026
e526954
docs(test-harness): README reflects E2E lifecycle, oracle, subprocess…
JakeHartnell May 16, 2026
e3528de
fix(test-harness): satisfy clippy -D warnings (#1147)
JakeHartnell May 16, 2026
d247c8e
feat(test-harness): wire aggregator stage into end-to-end lifecycle (…
JakeHartnell May 16, 2026
5a1231b
chore(deps): loosen exact alloy pins to caret to allow downstream uni…
JakeHartnell May 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"packages/engine",
"packages/gui/shared",
"packages/layer-tests",
"packages/test-harness",
"packages/types",
"packages/utils",
"packages/version-pins",
Expand Down Expand Up @@ -282,6 +283,7 @@ awsm_web = {version = "0.45.0", default-features = false, features = ["dom", "fi
# local
utils = { path = "packages/utils" }
wavs = { path = "packages/wavs" }
wavs-test-harness = { path = "packages/test-harness" }
wavs-engine = { path = "packages/engine" }
wavs-types = { path = "packages/types" }
wavs-cli = { path = "packages/cli" }
Expand Down
8 changes: 8 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ cosmwasm-build-inner CONTRACT_PATH:
test-wavs-e2e:
ulimit -n 65536 && RUST_LOG=debug,alloy_rpc=off,alloy_provider=off,wasmtime=off,cranelift=off,hyper_util=off cargo test -p layer-tests

# reusable integration test harness — local Anvil + in-process runner (deterministic, no fork required)
test-harness:
cargo test -p wavs-test-harness

# reusable integration test harness — pinned-fork tier (requires FORK_RPC_URL)
test-harness-fork:
cargo test -p wavs-test-harness --features fork

update-submodules:
git submodule update --init --recursive

Expand Down
56 changes: 56 additions & 0 deletions packages/test-harness/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
[package]
name = "wavs-test-harness"
description = "Reusable integration test harness for WAVS apps — chain control, service lifecycle, and contract assertions"
version.workspace = true
edition.workspace = true
authors.workspace = true
rust-version.workspace = true
repository.workspace = true
license.workspace = true
publish = false

# Features are additive. `fork` and `inproc` are on by default because the harness
# is only useful when at least one runner tier is enabled. `subprocess` is preview
# and stays off by default to keep the dep graph small.
#
# Consumers should add `wavs-test-harness` as a [dev-dependencies] entry only —
# never as a regular dependency — to keep the `utils/test-utils` feature out of
# their production builds.
[features]
default = ["fork", "inproc"]
fork = []
inproc = []
subprocess = []

[dependencies]
utils = { workspace = true, features = ["test-utils"] }
wavs-types = { workspace = true, features = ["full"] }
wavs-engine = { workspace = true }
wasmtime = { workspace = true }
tempfile = { workspace = true }

anyhow = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }

alloy-contract = { workspace = true }
alloy-network = { workspace = true }
alloy-node-bindings = { workspace = true }
alloy-primitives = { workspace = true }
alloy-provider = { workspace = true, features = ["anvil-api", "anvil-node"] }
alloy-signer = { workspace = true }
alloy-signer-local = { workspace = true }
alloy-sol-types = { workspace = true }
alloy-rpc-types-eth = { workspace = true }

[dev-dependencies]
tokio = { workspace = true }
tempfile = { workspace = true }
temp-env = { workspace = true }
184 changes: 184 additions & 0 deletions packages/test-harness/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# wavs-test-harness

Reusable integration test harness for WAVS apps.

Tracks issue [Lay3rLabs/WAVS#1147](https://github.com/Lay3rLabs/WAVS/issues/1147).

## What this crate is for

Downstream app repos — `wavs-defi`, `wavs-aave-guardian`, `wavs-prediction-market`,
and the rest — write integration tests that drive the full WAVS path:

```
trigger → operator component → aggregator (sig + quorum) → service handler → app contract
```

`wavs-test-harness` provides chain control, service-lifecycle, envelope
helpers, and waiters so each downstream test only has to specify its
app-specific contract deployment and assertions.

## Quickstart — local Anvil

```rust
use wavs_test_harness::{chain, service::{InProcRunner, ServiceSpec}};

let (provider, anvil) = chain::spawn_local().await?;

let spec = ServiceSpec::new()
.component_wasm("path/to/strategy.wasm")
.aggregator_wasm("path/to/aggregator.wasm")
.config_var("CHAINLINK_ETH_USD", chainlink.to_string());

let runner = InProcRunner::from_spec(&spec)?;
let outputs = runner.run_component(b"hello".to_vec()).await?;
```

Two runnable examples ship with the crate:

```bash
cargo run -p wavs-test-harness --example minimal_local
FORK_RPC_URL=<base-rpc> cargo run -p wavs-test-harness --example fork_with_addresses
```

## Quickstart — pinned fork

```rust
use wavs_test_harness::{chain, fixtures::ChainProfile};

let profile = ChainProfile::load("base")?; // chain_id + addresses + rpc_env
let opts = chain::ForkOptions {
rpc_url: profile.resolve_rpc_url()?, // reads FORK_RPC_URL
block_number: profile.chain.fork_block,
timeout_ms: None,
};
let (provider, _anvil) = chain::spawn_fork(opts).await?;

let usdc = profile.address("usdc")?;
let weth = profile.address("weth")?;
let whale = profile.accounts.address("usdc_whale")?;

chain::impersonate_funded(&provider, whale).await?;
// … your test …
chain::stop_impersonating(&provider, whale).await?;
```

The RPC URL is never logged verbatim — only via [`chain::redact_url`].

## Tier matrix

| Tier | Stages run | Stages mocked | Use case |
|---|---|---|---|
| **InProc** (default) | compute (real WASM via `wavs-engine`) + aggregate (real WASM) + envelope signing | dispatcher subsystems | deterministic PR tier on local Anvil or fork |
| **Subprocess** (preview) | everything in the real `wavs` binary | nothing | nightly fork + pre-release |

InProc executes the actual operator and aggregator WASM directly through
`wavs-engine`, bypassing the full dispatcher (trigger manager, submission
manager, signing). It's fast, deterministic, and exercises the same WASM
code path that runs in production.

Subprocess ships in this release with API shape locked but lifecycle methods
stubbed — calling `start()` returns a descriptive `unimplemented` error.
See `src/service/runner_subprocess.rs` for the planned wiring.

## CI tiers

| Tier | Runner | When | Command |
|---|---|---|---|
| **PR deterministic** | local Anvil only | every PR | `just test-harness` |
| **PR labeled fork** | pinned Base / mainnet fork | PRs labeled `fork-tests` | `just test-harness-fork` |
| **Nightly fork matrix** | broader protocol matrix | scheduled | TBD — wired alongside Subprocess landing |
| **Pre-release subprocess** | full `wavs` binary | release branches | TBD — depends on Subprocess landing |

Fork tier requires `FORK_RPC_URL` set in the CI secret store. The harness
never logs the URL; the secret should be scoped to fork-tier jobs only.

## Bundled chain profiles

Three profiles ship via `include_str!`:

- `local.toml` — `chain_id=31337`, no fork, no env vars.
- `base.toml` — `chain_id=8453`, `FORK_RPC_URL`, complete protocol address set
(Aerodrome pool / NFT manager / router, Avantis trading / storage / gov,
Chainlink, Pyth, USDC, WETH, USDC whale, Avantis operator).
- `mainnet.toml` — `chain_id=1`, `FORK_RPC_URL`, tokens + Aave v3 pool.

Consumers can ship their own profiles via `ChainProfile::from_path(...)`
or `ChainProfile::from_str(...)`.

## Determinism boundary

- **Block-time control.** `chain::set_automine(false)` + `chain::mine_blocks(n)`
gives deterministic block production. Pair with `chain::set_next_block_timestamp`
for tests that depend on block timestamps.
- **Snapshot / revert.** `chain::SnapshotGuard::take(&provider)` captures
state; explicit `.revert(&provider)` rolls back. The guard logs a warning
if dropped without explicit revert (async-drop is unstable).
- **Operator signing.** All envelope signatures are deterministic for a given
signer key. Use the same operator set across re-runs to keep signature
bytes stable.

## Tier selection in downstream repos

Add `wavs-test-harness` as a `[dev-dependencies]` entry only, never as a
regular dependency. This keeps `utils/test-utils` out of production builds.

```toml
[dev-dependencies]
wavs-test-harness = { git = "https://github.com/Lay3rLabs/WAVS", rev = "<commit-sha>" }
```

**Branch references are not supported in CI — pin to a commit SHA.**

### Cross-repo alloy version barrier

When the downstream repo uses a different alloy major version than the WAVS
workspace pin (`1.0.42` at the time of writing), cargo workspace feature
unification can produce compile errors in `alloy-rpc-types-eth` (e.g.
`BlobTransactionSidecarVariant` vs `BlobTransactionSidecar` mismatch).
Resolution paths:

1. Bump alloy in WAVS to match the downstream consumer.
2. Pin the downstream test crate to the WAVS alloy version.
3. Publish `wavs-test-harness` as a versioned crate so cargo handles
version-mixing instead of unification.

Tracked as the highest-priority follow-up under #1147.

## Layer-tests relationship

`packages/layer-tests` continues to run as-is. `wavs-test-harness` is
**canonical** for new integration tests; `layer-tests` is treated as legacy
and is expected to migrate onto the harness in a follow-up. Two parallel
test crates drift over time, so migration shouldn't sit forever — but it
also doesn't block this v1.

## What ships in v1

- `chain`: local Anvil + pinned fork (feature `fork`, on by default).
`snapshot` / `revert` with RAII guard. `impersonate_funded`,
`enable_auto_impersonate`, `whale_fund`. `mine_blocks`, `set_automine`,
`increase_time`, `set_next_block_timestamp`. `redact_url` / `redact_key`
for sanitized logs.
- `fixtures`: `ChainProfile` (TOML), `Addresses` typed lookup. Three
bundled profiles.
- `service`: `ServiceSpec` builder, middleware mock re-exports
(`EvmMiddleware`, `MockEvmServiceManager`, `AvsOperator`),
`InProcRunner` (real WASM via `wavs-engine`), `SubprocessRunner`
(preview, off-by-default feature).
- `lifecycle`: `manual_input_json` / `manual_input_raw`, `wait_for` /
`wait_until` polling helpers, `assert_within` tolerance check.
- `envelope`: canonical `Envelope` / `SignatureData` shape mirroring
`@wavs/solidity@0.6.x`, `sign_envelope`, `event_id_from_nonce` /
`event_id_from_seed`, `Envelope::message_hash` / `signing_hash`.
- `harness::TestHarness<P>` convenience wrapper.

## What's not in v1

- Full subprocess wiring (API shape only).
- Cosmos fork support.
- A `with_deploy(closure)` builder hook on `TestHarness` (deferred until
async-closure ergonomics stabilize; compose primitives directly today).
- Operator-side oracle mocking — needed before downstream apps can run
their real strategy WASM end-to-end via the InProc runner.
- A version of the harness compatible with newer alloy lines used by
downstream apps — see "Cross-repo alloy version barrier" above.
46 changes: 46 additions & 0 deletions packages/test-harness/examples/fork_with_addresses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Fork-tier example — load the Base profile, log addresses, never leak the RPC URL.
//!
//! Run with:
//! `FORK_RPC_URL=<your-base-rpc> cargo run -p wavs-test-harness \
//! --example fork_with_addresses --features fork`
//!
//! This example does NOT spawn the fork — it only demonstrates loading the
//! profile, redacted logging, and address lookup. Spawning is a one-liner via
//! `chain::spawn_fork(ForkOptions::from_env(...))`.

use wavs_test_harness::{chain, fixtures::ChainProfile};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();

let profile = ChainProfile::load("base")?;
println!("[example] profile: name={} chain_id={}",
profile.chain.name, profile.chain.chain_id);

// The RPC URL is read from FORK_RPC_URL and never printed verbatim.
match profile.resolve_rpc_url() {
Ok(Some(url)) => println!(
"[example] fork RPC ready (redacted): {}",
chain::redact_url(&url)
),
Ok(None) => println!("[example] profile does not declare an rpc_env"),
Err(e) => {
eprintln!("[example] FORK_RPC_URL unset: {e}");
eprintln!("[example] set it and re-run to actually spawn the fork");
return Ok(());
}
}

// Address lookup demo.
let usdc = profile.address("usdc")?;
let weth = profile.address("weth")?;
let avantis = profile.address("avantis_trading")?;
let whale = profile.accounts.address("usdc_whale")?;
println!("[example] USDC = {usdc}");
println!("[example] WETH = {weth}");
println!("[example] Avantis Trade = {avantis}");
println!("[example] USDC whale = {whale}");

Ok(())
}
Loading
Loading