AvelonDocs

@avelon

assay

@avelon/assay

@avelon/assay is the test harness. It sits on bun test and gives you HTTP helpers that dispatch through the core kernel, model factories, and seeds. Reach for it when a feature test should speak in requests, factories, and assertions rather than constructing HttpRequest by hand.

Assay tests a material for what it really is: the same kernel, routes, and models the adapter will serve.

Installation

bun add -d @avelon/assay

Assay expects @avelon/core and @avelon/orm. You pass a kernel and route table in; vendor drivers stay in test setup, never in app/.

Basic Usage

import { FakeDatabase } from '@avelon/conformance'
import { createKernel, defineConfig, view } from '@avelon/core'
import { assay } from '@avelon/assay'

defineConfig({ name: 'tests', drivers: { database: new FakeDatabase() } })

const kernel = createKernel()
const client = assay({ kernel, routes })

const index = await client.get('/users')
index.assertOk().assertView('users.index')
const stored = await client.actingAs({ id: 'u1' }).post('/users', { name: 'Ada' })
stored.assertRedirect('/users/u1')

HTTP Helpers

assay() matches method plus path against a RouteDefinition table, preferring static segments so /users/create does not lose to /users/{user}. Each helper builds an HttpRequest, dispatches through the kernel, and flushes after listeners.

const shown = await client.get('/users/u1')
shown.assertStatus(200)
shown.view().props

call(name, httpRequest()) dispatches a named route when you already have params. Unauthenticated S4-style kernels still read cookies.user; actingAs('ada') writes that cookie.

Factories

Factories fill Scrivener models. Sequence numbers start at 1. There is no bundled faker; you return plain attributes so tests stay deterministic.

import { defineFactory } from '@avelon/assay'
import { User } from '@/app/Models/User'

const UserFactory = defineFactory(User, (sequence) => ({
  id: `user-${sequence}`,
  email: `user-${sequence}@example.test`,
  name: `User ${sequence}`,
}))

UserFactory.state('ada', () => ({ name: 'Ada Lovelace', email: 'ada@example.test' }))

await UserFactory.make()
await UserFactory.create({ name: 'Ada' })
await UserFactory.as('ada').create()
await UserFactory.createMany(3)

Seeds

Seeds are ordinary modules with a default export. reeve db:seed and runSeeds(dir) load database/seeds in filename order.

import { defineSeed } from '@avelon/assay'

export default defineSeed(async () => {
  await UserFactory.createMany(5)
}, 'users')
import { runSeed, runSeeds } from '@avelon/assay'

await runSeed(seed)
await runSeeds('database/seeds')

Method Reference

Method / exportSignatureDescription
assay(options: AssayOptions) => AssayClientCreates an HTTP client bound to a kernel and route table.
AssayClient.get(path, headers?) => Promise<AssayResponse>Dispatches GET.
AssayClient.post(path, body?, headers?) => Promise<AssayResponse>Dispatches POST.
AssayClient.put(path, body?, headers?) => Promise<AssayResponse>Dispatches PUT.
AssayClient.patch(path, body?, headers?) => Promise<AssayResponse>Dispatches PATCH.
AssayClient.delete(path, headers?) => Promise<AssayResponse>Dispatches DELETE.
AssayClient.call(name, request) => Promise<AssayResponse>Dispatches a named route with an existing HttpRequest.
AssayClient.actingAs`(actor: { id: string } \string) => this`Sets the actor cookie for subsequent requests.
AssayClient.asGuest() => thisClears actor cookies.
AssayResponse.assertOk() => thisFails unless the outcome is 2xx and not a failed action.
AssayResponse.assertStatus(expected: number) => thisFails unless the status hint equals expected.
AssayResponse.assertRedirect(location: string) => thisFails unless the kernel redirected to location.
AssayResponse.assertView(view: unknown) => thisFails unless the kernel rendered view.
AssayResponse.status() => numberReturns the transport-neutral status hint.
AssayResponse.view() => ViewResultReturns the view result or throws.
AssayResponse.data() => unknownReturns action data or view props.
AssayResponse.resultKernelResultUnderlying kernel result.
AssayAssertionclass AssayAssertion extends ErrorThrown when an assertion does not hold.
matchRoute(routes, method, path) => { route, params }Resolves a path, preferring static segments.
httpRequest(overrides?) => HttpRequestBuilds a kernel request with test defaults.
parsePath(path: string) => { pathname, query }Splits path and query, preserving repeated keys.
requestFromCall(options) => HttpRequestBuilds a request for an HTTP helper call.
encodeBody(body: unknown) => Uint8ArrayEncodes a body for rawBody().
defineFactory(model, definition) => FactoryCreates a model factory.
Factory.make(overrides?) => Promise<TModel>Builds a model without persisting.
Factory.create(overrides?) => Promise<TModel>Persists one model.
Factory.createMany(count, overrides?) => Promise<readonly TModel[]>Persists count models.
Factory.state(name, attributes) => thisRegisters a named attribute overlay.
Factory.as(name: string) => thisApplies a named state to the next make/create.
Factory.reset() => voidClears sequence and pending states.
Factory.sequencenumberCurrent sequence number.
defineSeed(run, name?) => SeedWraps a seed callback.
runSeed(seed, context?) => Promise<void>Runs one seed.
runSeeds(dir: string) => Promise<readonly string[]>Imports and runs default exports in filename order.
AssayOptionsinterfaceKernel, routes, and optional actor cookie name.
SeedinterfaceNamed seed with a run callback.
FactoryDefinitiontype(sequence) => attributes factory callback.
FactoryStatetype() => attributes overlay registered with Factory.state.
SeedCallbacktype`(context: SeedContext) => Promise<void> \void` seed body.
SeedContextinterfaceOptional path of a seed file loaded from disk.

Testing

Assay is itself tested with FakeDatabase and createKernel. Point assay() at the same kernel your adapter mounts.

import { assay } from '@avelon/assay'
import { FakeDatabase } from '@avelon/conformance'

const client = assay({ kernel, routes })
await client.get('/users').assertOk()
bun test
bun run typecheck