Skip to content

API Reference

All public exports from the @averspec/core package.

Creates a domain with a named vocabulary.

import { defineDomain, action, query, assertion } from '@averspec/core'
const cart = defineDomain({
name: 'shopping-cart',
actions: {
addItem: action<{ name: string; qty: number }>(),
},
queries: {
cartTotal: query<number>(),
},
assertions: {
hasItems: assertion<{ count: number }>(),
},
})

Returns: Domain — a domain object with vocabulary metadata and an extend() method.

Creates an action marker. Actions perform side effects and return void.

addItem: action<{ name: string }>() // typed payload
checkout: action() // no payload (void)
addItem: action<{ name: string }>({ // with telemetry declaration
telemetry: (p) => ({ span: 'cart.add-item', attributes: { 'item.name': p.name } }),
})

Options: MarkerOptions<P> — optional object with a telemetry: TelemetryDeclaration<P> property.

query<Return>(opts?) / query<Payload, Return>(opts?)

Section titled “query<Return>(opts?) / query<Payload, Return>(opts?)”

Creates a query marker. Queries read data and return a typed result. Two overloads:

cartTotal: query<number>() // no input, returns number
tasksByStatus: query<{ status: string }, Task[]>() // input + return type
cartTotal: query<number>({ // with telemetry
telemetry: { span: 'cart.total' },
})

Creates an assertion marker. Assertions verify expectations and throw on failure.

hasItems: assertion<{ count: number }>() // typed payload
isEmpty: assertion() // no payload

Extends a domain with additional vocabulary. The extended domain inherits all items from the parent. The name is passed as the first argument.

const cartUI = cart.extend('shopping-cart-ui', {
assertions: {
showsSpinner: assertion(),
},
})

Creates an adapter binding a domain to a protocol with handler implementations.

import { adapt, unit } from '@averspec/core'
const adapter = adapt(cart, {
protocol: unit(() => []),
actions: {
addItem: async (ctx, payload) => { /* ... */ },
},
queries: {
cartTotal: async (ctx) => { /* ... */ },
},
assertions: {
hasItems: async (ctx, payload) => { /* ... */ },
},
})

TypeScript enforces that every action, query, and assertion declared in the domain is provided. Missing handlers are compile errors.

Returns: Adapter — an adapter object with domain, protocol, and handler references.


Built-in protocol for testing against your code’s public interfaces directly. Zero dependencies.

import { unit } from '@averspec/core'
protocol: unit(() => new Cart()) // object context
protocol: unit(() => ({ db: new DB() })) // compound context
protocol: unit<Cart[]>(() => []) // typed context

The factory runs on each test setup, creating a fresh context. Teardown is a no-op.

http(options) from @averspec/protocol-http

Section titled “http(options) from @averspec/protocol-http”

HTTP protocol providing a fetch-based client.

import { http } from '@averspec/protocol-http'
protocol: http({ baseUrl: 'http://localhost:3000' })

Context provides get, post, put, patch, delete methods.

playwright(options?) from @averspec/protocol-playwright

Section titled “playwright(options?) from @averspec/protocol-playwright”

Playwright protocol providing a browser page.

import { playwright } from '@averspec/protocol-playwright'
protocol: playwright()

Context is a Playwright Page. Browser is launched once and reused; a fresh page is created per test.

Wraps a protocol with before/after hooks.

import { withFixture } from '@averspec/core'
const wrapped = withFixture(myProtocol, {
before: async () => { /* runs before setup */ },
after: async () => { /* runs after teardown */ },
})

Creates a test suite for a domain.

import { suite } from '@averspec/core'
// Multi-adapter: resolves from registry
const { test } = suite(cart)
// Single adapter: passed directly
const { test } = suite(cart, unitAdapter)

Returns: SuiteReturn with the following:

PropertyTypeDescription
test(name, fn) => voidWraps Vitest’s test() with domain proxies
it(name, fn) => voidAlias for test
describe(name, fn) => voidWraps Vitest’s describe() for grouping
context(name, fn) => voidAlias for describe
actActProxyProgrammatic access to actions
queryQueryProxyProgrammatic access to queries
assertAssertProxyProgrammatic access to assertions
setup() => Promise<void>Manual setup (for programmatic use)
teardown() => Promise<void>Manual teardown (for programmatic use)
getTrace() => TraceEntry[]Get the current test steps
getCoverage() => VocabularyCoverageGet vocabulary coverage stats
getPlannedTests(name) => PlannedTest[]Preview what test names would be registered

Wraps Vitest’s test(), passing typed domain proxies via callback:

test('add item', async ({ given, when, query, assert, trace }) => {
await given.addItem({ name: 'Widget' })
await when.checkout()
await assert.hasItems({ count: 1 })
const total = await query.cartTotal()
const entries = trace() // trace is a function
})

The callback receives a TestContext:

PropertyTypeDescription
actActProxy<D>Typed proxy for actions
givenActProxy<D>Alias for act — setup steps (Given-When-Then)
whenActProxy<D>Alias for act — trigger steps (Given-When-Then)
queryQueryProxy<D>Typed proxy for queries
assertAssertProxy<D>Typed proxy for assertions
thenAssertProxy<D>Alias for assert — verification steps (Given-When-Then)
trace() => TraceEntry[]Returns the current test steps (callable)

Creates an Aver configuration and auto-registers adapters.

import { defineConfig } from '@averspec/core'
import { unitAdapter } from './adapters/cart.unit'
import { httpAdapter } from './adapters/cart.http'
export default defineConfig({
adapters: [unitAdapter, httpAdapter],
})

AverConfigInput:

PropertyTypeDefaultDescription
adaptersAdapter[]requiredAdapters to register
coverage{ minPercentage?: number }{ minPercentage: 0 }Vocabulary coverage threshold
teardownFailureMode'fail' | 'warn''fail'Whether teardown errors fail the test

Manually registers an adapter in the global registry.

Returns the first registered adapter matching a domain, or undefined.

Returns all registered adapters matching a domain.

Returns all registered adapters.

Clears all registered adapters. Useful in test setup.

getRegistrySnapshot() / restoreRegistrySnapshot(snapshot)

Section titled “getRegistrySnapshot() / restoreRegistrySnapshot(snapshot)”

Capture and restore registry state. Useful for test isolation.

Runs a function with an isolated registry that resets afterward.


Interface for providing spans to the framework. Set on Protocol.telemetry.

interface TelemetryCollector {
getSpans(): CollectedSpan[]
reset(): void
}

Span data for telemetry verification.

interface CollectedSpan {
readonly traceId: string
readonly spanId: string
readonly parentSpanId?: string
readonly name: string
readonly attributes: Readonly<Record<string, unknown>>
readonly links?: ReadonlyArray<SpanLink>
}
interface SpanLink {
readonly traceId: string
readonly spanId: string
}

Creates an OTLP HTTP receiver for cross-process telemetry testing.

import { createOtlpReceiver } from '@averspec/telemetry'
const receiver = createOtlpReceiver()
await receiver.start()
// receiver.port — port the OTLP HTTP endpoint listens on
// receiver.getSpans() — returns CollectedSpan[]
// receiver.reset() — clears collected spans
// receiver.stop() — shuts down the server

The receiver implements TelemetryCollector so it can be set directly on a protocol’s telemetry property.

Verifies that correlated trace entries have causally connected spans.

import { verifyCorrelation } from '@averspec/core/internals'

Declared on domain markers via the telemetry option:

// Static — fixed span name and attributes
action({ telemetry: { span: 'order.checkout' } })
// Parameterized — attributes derived from payload
action<{ orderId: string }>({
telemetry: (p) => ({
span: 'order.checkout',
attributes: { 'order.id': p.orderId },
}),
})

TelemetryExpectation:

PropertyTypeDescription
spanstringOTel span name to match
attributesRecord<string, TelemetryAttributeValue>Required span attributes. Primitives for exact match, or asymmetric matchers (e.g. expect.any(String))
causesstring[]Span names this operation causally triggers. Opts in to causal correlation — verifies trace connection (same trace or span link) to the named spans.

Environment variable controlling telemetry verification:

ValueBehaviorDefault when
failMissing/mismatched spans fail the testCI is set
warnMismatches recorded but tests passCI is not set
offNo telemetry verification

Returns the current test context from async-local storage, or undefined if not in a test.

Runs a function within a test context (for framework-level use).


defineConfig({ adapters }) calls registerAdapter() for each adapter when the config module is evaluated. This is the standard path — your aver.config.ts runs once and registers all adapters for the process.

You can also call registerAdapter() directly in test files or setup files.

suite(domain) resolves adapters lazily — at test execution time, not when suite() is called. On first invocation, suite() calls maybeAutoloadConfig() to import aver.config.ts if it hasn’t been loaded yet. Set AVER_AUTOLOAD_CONFIG=false to skip this.

Passing an adapter directly — suite(domain, adapter) — bypasses the registry entirely.

Two environment variables control which tests run:

  • AVER_ADAPTER=unit — only run tests for adapters whose protocol name matches
  • AVER_DOMAIN=ShoppingCart — only register tests for the named domain

These map to the CLI flags aver run --adapter unit and aver run --domain ShoppingCart.

When multiple adapters are registered for one domain, suite() creates a parameterized test for each:

add item [unit] ← runs against unit adapter
add item [http] ← runs against http adapter

Each adapter gets its own protocol context (fresh setup() / teardown() per test per adapter).

If no adapter is registered for a domain, findAdapter() walks the domain.parent chain. This means an adapter registered for a parent domain works for extended domains that haven’t overridden it.

The registry is process-global state. If your tests register their own adapters (common in framework-level testing), call resetRegistry() in beforeEach to prevent cross-test leakage:

import { resetRegistry, registerAdapter } from '@averspec/core/internals'
beforeEach(() => {
resetRegistry()
registerAdapter(myTestAdapter)
})

import type {
// Domain & markers
Domain,
MarkerOptions,
// Adapters & protocols
Adapter,
Protocol,
TestMetadata,
TestCompletion,
ProtocolExtensions,
Screenshotter,
// Telemetry
TelemetryCollector,
CollectedSpan,
SpanLink,
TelemetryMatchResult,
// Suite & testing
TestContext,
SuiteReturn,
// Config
AverConfig,
AverConfigInput,
// Trace
TraceEntry,
TraceAttachment,
} from '@averspec/core'

These types are not re-exported from @averspec/core. Import them from the @averspec/core/internals subpath.

import type {
// Domain & markers
ActionMarker,
QueryMarker,
AssertionMarker,
TelemetryExpectation,
TelemetryDeclaration,
TelemetryAttributeValue,
AsymmetricMatcher,
// Suite & testing
ActProxy,
QueryProxy,
AssertProxy,
PlannedTest,
RunningTestContext,
// Config
CoverageConfig,
TeardownFailureMode,
// Trace & coverage
VocabularyCoverage,
// Correlation
CorrelationResult,
CorrelationGroup,
CorrelationViolation,
// Registry
RegistrySnapshot,
} from '@averspec/core/internals'
interface TraceEntry {
kind: 'action' | 'query' | 'assertion' | 'test'
category?: 'given' | 'when' | 'act' | 'query' | 'then' | 'assert'
name: string
domainName?: string
payload: unknown
status: 'pass' | 'fail'
result?: unknown
error?: unknown
startAt?: number
endAt?: number
durationMs?: number
attachments?: TraceAttachment[]
metadata?: Record<string, unknown>
correlationId?: string
telemetry?: TelemetryMatchResult
}
interface Protocol<Context> {
readonly name: string
setup(): Promise<Context>
teardown(ctx: Context): Promise<void>
onTestStart?(ctx: Context, meta: TestMetadata): Promise<void> | void
onTestFail?(ctx: Context, meta: TestCompletion): Promise<TestFailureResult> | TestFailureResult
onTestEnd?(ctx: Context, meta: TestCompletion): Promise<void> | void
extensions?: ProtocolExtensions
telemetry?: TelemetryCollector
}

The lifecycle hooks are optional. onTestStart runs before each test body. onTestFail runs when a test fails and can return TraceAttachment[] (e.g., screenshots). onTestEnd runs after each test regardless of outcome.


Approves a value against a stored baseline. Auto-detects serializer: objects use JSON, strings use text. Also exported as characterize for characterization test contexts.

import { approve } from '@averspec/approvals'
// or: import { characterize } from '@averspec/approvals'
await approve({ count: 42 }) // default name "approval"
await approve(reportText, { name: 'report' }) // named approval

First run fails with “Baseline missing”. Run npx aver approve to create baselines.

Baselines are stored in __approvals__/<test-name>/ next to the test file. Commit .approved files; gitignore .received and .diff files.

Options:

PropertyTypeDefaultDescription
namestring'approval'Name for the approval file
fileExtensionstringautoOverride file extension
filePathstringautoOverride test file path (for programmatic use)
testNamestringautoOverride test name (for programmatic use)
serializerSerializerNameautoSerializer to use ('json', 'text', or custom name)
comparatorComparatordefaultCustom comparison function (approved, received) => { equal: boolean }

Approves a screenshot against a stored baseline image. Requires a protocol with screenshotter extension (e.g., Playwright). Throws an error on protocols without one.

await approve.visual('board-state') // full page
await approve.visual({ name: 'backlog', region: 'backlog' }) // scoped region

Options (when passing object):

PropertyTypeRequiredDescription
namestringyesName for the approval image file
regionstringnoNamed region (maps to CSS selector in adapter)
thresholdnumbernoPixel difference threshold (0-1) for visual comparison

Extension interface for visual approval support. Protocols implement this.

interface Screenshotter {
capture(outputPath: string, options?: { region?: string }): Promise<void>
regions?: Record<string, string>
}

Playwright configures regions at adapter creation:

const proto = playwright({
regions: {
'board': '.board',
'backlog': '[data-testid="column-backlog"]',
},
})

approve() integrates with test runners by throwing standard Error-based assertion errors when a baseline mismatch is detected. The test runner catches these errors and reports them as test failures.

  • Vitest and Jest work out of the box — both catch thrown errors as assertion failures
  • Other test runners need to support standard Error-based assertions (most do)
  • Run npx aver approve to update baselines. Under the hood this sets AVER_APPROVE=1, which tells approve() to write received values as the new baselines instead of comparing.

Runs tests via Vitest.

Terminal window
npx aver run # all tests
npx aver run --adapter unit # filter by adapter
npx aver run --domain ShoppingCart # filter by domain
npx aver run --watch # watch mode

Interactive scaffolding wizard. Prompts for domain name and protocol, then generates:

  • domains/<kebab>.ts
  • adapters/<kebab>.<protocol>.ts
  • tests/<kebab>.spec.ts
  • aver.config.ts (if it doesn’t exist)
Terminal window
npx aver init

Updates approval baselines by running tests with AVER_APPROVE=1.

Terminal window
npx aver approve # approve all
npx aver approve tests/my-test.spec.ts # approve specific file
npx aver approve --adapter playwright # approve for specific adapter

Polls a function until it succeeds or a timeout expires. Useful for async assertions in integration tests.

import { eventually } from '@averspec/core'
await eventually(async () => {
const count = await query.taskCount()
expect(count).toBe(3)
}, { timeout: 5000, interval: 100 })