withExternalRuntime({ runtime, runtimeMode?, clients? })
Test runners: Examples use Vitest test and expect. scenario(...) returns an async function you can pass to any runner with a similar test callback (for example Jest or Node node:test).
Why it is useful
Use withExternalRuntime() when your test suite (not the scenario) owns the Anvil lifecycle, so you can reuse one running runtime across many scenarios.
This enables two important patterns:
- Isolated scenarios: add
withSnapshot()inside eachscenario(...)to roll back per test. - Shared state scenarios: skip
withSnapshot()when tests in one file should build on prior chain state.
Example
index.ts
import { beforeAll, afterAll, describe, test, expect } from "vitest";
import {
scenario,
startRuntime,
stopRuntime,
withExternalRuntime,
withSnapshot,
type RuntimeHandle,
} from "@st8craft/core";
describe.sequential("external runtime lifecycle owned by the test file", () => {
let handle!: RuntimeHandle;
let sharedBlock!: bigint;
beforeAll(async () => {
handle = await startRuntime({ mode: "chain" });
});
afterAll(async () => {
await stopRuntime(handle);
});
test(
"builds shared state for later tests",
scenario(
withExternalRuntime({ runtime: handle }),
async ({ publicClient, testClient }) => {
const start = await publicClient.getBlockNumber();
await testClient.mine({ blocks: 2 });
sharedBlock = await publicClient.getBlockNumber();
expect(sharedBlock).toBe(start + 2n);
},
),
);
test(
"depends on the previous test's state",
scenario(
withExternalRuntime({ runtime: handle }),
async ({ publicClient }) => {
expect(await publicClient.getBlockNumber()).toBe(sharedBlock);
},
),
);
test(
"runs isolated work with snapshot",
scenario(
withExternalRuntime({ runtime: handle }),
withSnapshot(),
async ({ publicClient, testClient }) => {
const before = await publicClient.getBlockNumber();
await testClient.mine({ blocks: 5 });
expect(await publicClient.getBlockNumber()).toBe(before + 5n);
},
),
);
test(
"proves isolated test did not leak shared state",
scenario(
withExternalRuntime({ runtime: handle }),
async ({ publicClient }) => {
expect(await publicClient.getBlockNumber()).toBe(sharedBlock);
},
),
);
});Options
| Option | Type | Required? | Meaning (units/semantics) | Used for / affects |
|---|---|---|---|---|
runtime | RuntimeHandle | Yes | Live runtime handle created by startRuntime(...). | Provides the RPC endpoint and runtime identity used by the scenario clients. |
runtimeMode | "chain" | "fork" | No | Declares whether the attached runtime handle is chain or fork mode. Defaults to "chain". | Enables mode-sensitive fixtures (for example withBundler, which requires fork mode). |
clients | CreateClientsOptions | No | Optional client wiring overrides for chain identity and signer key. | Selects viem chain id (chainId) and signer private key (privateKey). |
Adds to context
runtimeruntimeMode(defaults to"chain"when omitted)chainpublicClientwalletClienttestClient
Context requirements
None at the scenario level, but you must pass a live runtime handle.
Lifecycle
External lifecycle, the fixture never starts or stops Anvil.
Notes and caveats
- Add
withSnapshot()when you want per-test rollback while reusing one external runtime handle. - Skip
withSnapshot()when you intentionally want cumulative state across sequential tests in one file ordescribe. - Mixing both patterns in one
describelets you model realistic flows, cumulative checkpoints, and one-off isolated assertions without paying runtime startup cost for every test. - A common performance pattern is one runtime handle per test file (
beforeAllandafterAll). This keeps state local to that file and still lets the test runner run different files in parallel, which can significantly reduce total suite time.
