Skip to content

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 each scenario(...) 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

OptionTypeRequired?Meaning (units/semantics)Used for / affects
runtimeRuntimeHandleYesLive runtime handle created by startRuntime(...).Provides the RPC endpoint and runtime identity used by the scenario clients.
runtimeMode"chain" | "fork"NoDeclares whether the attached runtime handle is chain or fork mode. Defaults to "chain".Enables mode-sensitive fixtures (for example withBundler, which requires fork mode).
clientsCreateClientsOptionsNoOptional client wiring overrides for chain identity and signer key.Selects viem chain id (chainId) and signer private key (privateKey).

Adds to context

  • runtime
  • runtimeMode (defaults to "chain" when omitted)
  • chain
  • publicClient
  • walletClient
  • testClient

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 or describe.
  • Mixing both patterns in one describe lets 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 (beforeAll and afterAll). 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.