Overview

Voltro's test story — the @voltro/testing package, the test pyramid (unit handlers/tools → workflow runner → e2e), and the voltro test / voltro e2e CLI commands.

The package

Test utilities live in @voltro/testing — a separate, public package. It is not bundled into a scaffolded project, so the moment you write your first test, add it (and vitest) as a devDependency of the app you're testing:

pnpm --filter @my-app/api add -D @voltro/testing vitest

Everything in it is in-memory and synchronous-friendly — no docker, no running server, no real model calls. A handler test boots in milliseconds.

Export Use
makeTestContext(options?) The request ctx a handler/tool sees at runtime — subject-scoped ctx.store, clock, email, optional ai. → Unit testing
mockStore(seed) Seed rows: makeTestContext({ store: mockStore({ todos: [...] }) }).
MockClock / MockEmail / MockLLM Deterministic clock, captured email, queued LLM responses.
makeVoltroTestClient(config) The frontend harness — render a component against mocked useSubscription / useMutation, from the @voltro/testing/client subpath. → Component testing
makeWorkflowRunner({ ctx, workflows }) Drive a workflow in-process and assert on its steps. → Workflows
runDialectParity(fixture) Run one fixture across a SQL dialect for portability — from the @voltro/testing/dialect subpath. → Dialect parity

The test pyramid

Voltro's primitives are designed so the cheapest test covers the most surface. Reach for the lowest layer that proves what you need:

  1. Unit — handlers, tools, mutations, queries. Call the executor directly with a makeTestContext() ctx. The store is the real mixin-wrapped store, so tenant scoping, soft-delete, and audit auto-fill all behave as in production — without a database. This is 90% of your tests. See Unit testing.
  2. Component — the reactive frontend. makeVoltroTestClient renders a component against mocked useSubscription / useMutation, so loading, empty, stream-error and failing-write branches are all assertable without a running server. See Component testing.
  3. Workflow — durable, multi-step logic. makeWorkflowRunner drives a workflow end-to-end in-process and hands back the same step log the dashboard shows, including real retry counts. See Workflows.
  4. Parity — hand-written SQL that must stay portable. runDialectParity runs a fixed scenario suite against a dialect you stand up. Mostly for dialect-package authors and apps with unsafe() SQL. See Dialect parity.
  5. End-to-end — the running stack. voltro e2e boots the api + web siblings, runs your tests against the live processes, and tears them down.

Agents are integration tests; tools are unit tests. Don't reach for an e2e harness to assert logic a makeTestContext test can prove.

The CLI

voltro test    # vitest against the current app
voltro e2e     # boot api + web siblings, run tests, tear down

voltro test is a thin wrapper over vitest — it runs the app's test files with the framework's config. Everything @voltro/testing exports is plain TypeScript you import inside those files; there's no special test runner.

What NOT to do

  • Don't spin up Postgres for a handler test. makeTestContext's in-memory store enforces the same mixin behaviour — tenant scoping, soft-delete, audit — so a unit test catches the same class of bug far faster. Save a real database for dialect parity and e2e.
  • Don't call a real model in agent tests. Queue deterministic responses with mockAi / useMockAi (see Unit testing → agents). A flaky test that hits the network on every run is worse than no test.
  • Don't assert on wall-clock time. Inject MockClock and advance(...) deterministically — never setTimeout + real sleeps.