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(())
}
}