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, webhooks, llm, optional ai. → Unit testing
mockStore(seed) Seed rows: makeTestContext({ store: mockStore({ todos: [...] }) }).
user(id, opts?) / apiKey / serviceAccount / anonymous / system Subject factories — the acting identity, instead of a hand-written { type: 'user', … } literal.
MockClock / MockLLM Deterministic clock, queued LLM responses.
invoke(descriptor, handler, input, ctx) Run a procedure the way the dispatcher does — guards, decode, transaction, plugin interceptors. → Unit testing
makeTestApp({ ctx, restRoutes, publicApi, strategies }) The request-level harness — a real HTTP request through the real REST pipeline. → Unit testing
fixtureRow(table, over?) / defineFactory(table, opts?) Complete a required-column row; a named factory with defaults, traits and associations.
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. Request — the transport hop. makeTestApp sends a real request through the framework's own REST pipeline: the x-tenant header, the auth strategy chain, path-param decode, the 405/404/410 the transport produces, HTTP idempotency, and a publicApi annotation's scopes. Everything a handler test structurally cannot reach, and still no server, no port, no docker. See Unit testing → request-level testing.
  3. 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.
  4. 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.
  5. 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.
  6. End-to-end — the running stack. voltro e2e boots the api + web siblings, runs each e2e/**/*.spec.ts as a plain tsx script against the live processes (with WEB_URL + API_URL in the environment), and tears them down. No test runner and no browser ship with it — bring your own driver. See voltro e2e.

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.

e2e/ is left alone: those specs belong to voltro e2e, which drives them through tsx against a booted api + web. They define no vitest suite, so collecting them would report "No test suite found" — a red run for an app laid out exactly as the framework asks. Pass your own --exclude and it wins outright.

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.