Skip to main content
ouroborai maintains 419 TypeScript tests (38 files) and 115 Rust tests across the four Stylus contracts. This page documents the testing conventions and patterns used throughout the codebase.

TypeScript (Vitest)

The project uses Vitest v3 with globals enabled. All tests run through the workspace Vitest configuration.

Running Tests

# Run all TypeScript tests
./node_modules/.bin/vitest run

# Run a specific test file
./node_modules/.bin/vitest run apps/api/src/routes/agent.test.ts

# Watch mode
./node_modules/.bin/vitest watch
Do NOT use bun test or npx vitest — version mismatches between Bun’s bundled test runner and the project’s Vitest installation cause failures. Always use ./node_modules/.bin/vitest run.

vi.hoisted for Mock Variables

When mock variables need to be referenced inside vi.mock() factory functions, use vi.hoisted() to ensure they are initialized before the mock factory runs:
const mocks = vi.hoisted(() => ({
  mockFetch: vi.fn(),
  mockSend: vi.fn(),
}));

vi.mock("../utils/fetch.js", () => ({
  safeFetch: mocks.mockFetch,
}));

describe("MyModule", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("calls fetch", async () => {
    mocks.mockFetch.mockResolvedValue({ ok: true });
    // ... test logic
    expect(mocks.mockFetch).toHaveBeenCalledOnce();
  });
});
vi.hoisted() lifts the callback above all vi.mock() calls in the module scope. Without it, mock variables would be undefined when the factory runs.

vi.clearAllMocks vs vi.restoreAllMocks

Use vi.clearAllMocks() in beforeEach — not vi.restoreAllMocks():
beforeEach(() => {
  vi.clearAllMocks();  // correct
  // vi.restoreAllMocks();  // wrong -- strips mockImplementation
});
restoreAllMocks() removes mockImplementation callbacks, which breaks tests that rely on them. clearAllMocks() resets call counts and return values while preserving the mock structure.

Mocking with vi.mock

Factory-based mocking for module dependencies:
vi.mock("@arb-agent/adapters", () => ({
  UniswapV3Adapter: vi.fn().mockImplementation(() => ({
    quote: vi.fn().mockResolvedValue({
      amountOut: 1000000n,
      priceImpactBps: 5,
      route: [],
      fee: 100n,
    }),
    swap: vi.fn().mockResolvedValue({ hash: "0xabc", success: true }),
  })),
}));

Globals Enabled

The Vitest config has globals: true, so describe, it, expect, beforeEach, vi, etc. are available without imports:
// No import needed -- globals are enabled
describe("swap router", () => {
  it("returns a quote", async () => {
    const res = await app.request("/swap/quote?tokenIn=ETH&tokenOut=USDC&amountIn=1000");
    expect(res.status).toBe(200);
  });
});

Testing Hono Routes

Use Hono’s built-in app.request() for route testing:
import { Hono } from "hono";
import { swapRouter } from "./swap.js";

const app = new Hono();
app.route("/swap", swapRouter);

describe("swap routes", () => {
  it("GET /swap/quote returns 400 without params", async () => {
    const res = await app.request("/swap/quote");
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toContain("Missing required params");
  });
});

Rust (Stylus Contracts)

Stylus contracts use cargo test with the stylus-test feature flag for testing utilities.

Running Tests

# Run all Rust tests
cargo test --features stylus-test \
  --manifest-path contracts/Cargo.toml

# Run tests for a specific contract
cargo test --features stylus-test \
  --manifest-path contracts/agent-registry/Cargo.toml

# Run with output
cargo test --features stylus-test \
  --manifest-path contracts/Cargo.toml -- --nocapture

TestVM Setup Pattern

Every test creates a TestVM and instantiates the contract from it:
use stylus_test::TestVM;
use alloy_primitives::{address, Address, U256};

const ALICE: Address = address!("aaa0000000000000000000000000000000000001");
const BOB: Address = address!("bbb0000000000000000000000000000000000002");

fn setup() -> (TestVM, MyContract) {
    let vm = TestVM::new();
    let contract = MyContract::from(&vm);
    (vm, contract)
}

#[test]
fn test_something() {
    let (vm, mut contract) = setup();
    vm.set_sender(ALICE);
    // ... test logic
}

Setting Sender

Use vm.set_sender() to simulate calls from different addresses:
vm.set_sender(ALICE);
let id = contract.register(capabilities, 500).unwrap();

vm.set_sender(BOB);
let ok = contract.update(id, new_caps, 0).unwrap();
assert!(!ok); // BOB is not the owner

Mocking External Calls

Use mock_call for state-changing calls and mock_static_call for view calls:
// Mock an ERC20 transferFrom (state-changing)
let calldata = transferFromCall { from, to, amount }.abi_encode();
vm.mock_call(USDC_ADDR, calldata, U256::ZERO, Ok(encode_bool_true()));

// Mock a balanceOf (view)
let calldata = balanceOfCall { account }.abi_encode();
vm.mock_static_call(USDC_ADDR, calldata, Ok(balance.to_be_bytes_vec()));
mock_static_call in stylus-test 0.10.0 always returns the LAST registered mock’s data when multiple mocks target the same contract. Register losing mocks first, winning mock last.

Required Imports

Stylus tests require specific imports that differ from the main code:
// Test imports -- use from alloy_primitives, not stylus_sdk
use alloy_primitives::{address, Address, B256, U256};
use alloy_sol_types::SolCall;
use stylus_test::TestVM;

// Main code imports
use stylus_sdk::alloy_primitives::{Address, U256};

Verifying Events

Check emitted events via vm.get_emitted_logs():
let logs = vm.get_emitted_logs();
assert!(!logs.is_empty());

let (topics, data) = &logs[logs.len() - 1];
// topics[0] = event signature hash
// topics[1..] = indexed parameters
// data = non-indexed parameters ABI-encoded

let indexed_param = B256::left_padding_from(ALICE.as_slice());
assert_eq!(topics[1], indexed_param);

let amount = U256::from_be_slice(&data[0..32]);
assert_eq!(amount, expected_amount);

Private Helpers

Private helper functions that take &mut parameters must live in a separate impl block (not #[public]). The Stylus SDK requires ABI-compatible types in all #[public] methods:
// Public methods
#[public]
impl MyContract {
    pub fn do_thing(&mut self) -> Result<(), Vec<u8>> {
        self.helper_method()?;
        Ok(())
    }
}

// Private helpers in a separate impl block
impl MyContract {
    fn helper_method(&mut self) -> Result<(), Vec<u8>> {
        // ... internal logic with &mut params
        Ok(())
    }
}