---
title: "Testing your panel"
description: "Test resources, policies, queries, and complete authenticated panel flows."
image: "https://adonia.pages.dev/og.png"
version: "next"
---

> Documentation Index
> Fetch the complete documentation index at: https://adonia.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing your panel

The testing helpers enforce server/client mirroring
([mirroring test design](/reactivity/mirroring-test-design)), the
"index serialization MUST be O(rows) with zero per-row queries" budget, and the
"mass assignment is impossible by construction" security contract. Everything below is
exported from `@adonia/core/testing`
(`packages/core/src/testing.ts`) and imports nothing from `@adonisjs/*` at
runtime, so a unit test needs no app boot and no IoC container.

## Public testing surface

The shipped module is a set of plain functions, usable from Japa, Vitest,
`node:test` or none of them:

| §21 wording | What exists |
| --- | --- |
| Japa plugin | Plain exported functions; no runner registration |
| `assertQueryCount(n)` | `assertQueryCount(fn, expected, source)` — the emitter is injected |
| `assertDescriptorOmits(field)` | Not implemented. Assert on `compileFor(...)` directly, or use `assertValidatorOmits` for the validator half |
| authenticated panel-visit helper | Not implemented. `examples/blog-admin/tests/functional/` drives real HTTP through Japa's API client |

The exact export surface is: `makeFakeContext`, `compileFor`, `validateWith`,
`resourceFromFixture`, `runMirroringCase`, `runMirroringFixture`,
`assertValidatorKeys`, `assertValidatorOmits`, `assertFillOmits`,
`assertRequiredWhen`, `countQueries`, `assertQueryCount`, `stableStringify`,
the two probe constants `GARBAGE_PROBE_VALUE` (`12345`) and
`REQUIRED_PROBE_VALUE` (`'adonia-required-probe'`), and the types
`FakeAuthUser`, `FakeGuard`, `FakeRedirect`, `FakeResponse`,
`FakeInertiaRender`, `FakeSharedState`, `FakeInertia`, `FakeHttpContext`,
`FakeContextOverrides`, `ValidationResult`, `MirroringFixture`,
`MirroringFixtureField`, `MirroringFixtureCase`, `MirroringCaseRun`,
`MirroringFixtureRun`, `QueryEventSource`.

::: info Samples on this page
The docs typecheck program resolves `@adonia/*` and `#models/*` but neither
`@japa/runner` nor `@types/node`. Test-shaped samples are therefore written as
plain function bodies and top-level statements; in a real suite each block is
the body of a `test(...)` callback. Signature blocks are written as
`declare function`, so the typechecker proves every type they name still
exists. The repo's own style is shown in
[Test style in this repo](#test-style-in-this-repo).
:::

## Where tests live and how to run them

**In a host app.** Adonia adds no test conventions of its own. An AdonisJS app
already has `tests/unit/` and `tests/functional/` wired through
`node ace test`; resource tests are unit tests (nothing is booted), and HTTP
tests are functional tests. Adonia's helpers are ordinary imports:

```ts
import { compileFor, makeFakeContext } from '@adonia/core/testing'
```

**In this repo.** Per-package, never project-wide, because the runners differ:

| Package | Runner | Command |
| --- | --- | --- |
| `@adonia/core` | Japa (`bin/test.ts`, `tests/**/*.spec.ts`, run through `tsx`) | `pnpm --filter @adonia/core test` |
| `@adonia/devtools` | Japa, same bootstrap | `pnpm --filter @adonia/devtools test` |
| `@adonia/ui` | Vitest + Testing Library (`tests/**/*.test.tsx`) | `pnpm --filter @adonia/ui test` |
| `examples/blog-admin` | Japa via the app kernel, against a real database | `pnpm --filter blog-admin test` |

CI runs the first three as one `pnpm -r --filter '@adonia/*' test` job and
blog-admin as a separate matrix over `DB_CONNECTION=pg|mysql|sqlite` — see
[CI](/ci). Descriptor snapshots are regenerated deliberately with
`UPDATE_SNAPSHOTS=1 pnpm --filter @adonia/core test`.

## The three things worth asserting about a resource

A resource is a declaration, and three artifacts are compiled from it. Each has
its own failure mode, and asserting one does not cover the others.

```mermaid
graph LR
  R[Resource declaration] --> D[Descriptor: what renders]
  R --> V[Validator: what is accepted]
  R --> F[Fill set: what is written]
```

1. **The compiled descriptor** — what the client renders. Snapshot it with
   `compileFor` + `stableStringify`. Failure mode: silent protocol drift.
2. **The compiled validator** — what the server accepts. Drive it with
   `validateWith` / `assertValidatorKeys`. Failure mode: a rule the UI enforces
   and the server does not.
3. **The fill set** — what actually reaches the model. Assert it with
   `assertFillOmits`.

The third is the one teams skip, and it is the only one that is a **security
property**. §19 states that mass assignment is impossible by construction
because "fill sets derive from the compiled schema for the exact
(mode, visibility-state) of the submission". Validation and fill are two
separate derivations from that schema, and passing validation does not imply
being persisted: a `disabledWhen`-true field is shape-validated but dropped
from the fill set (rule R5), and a hidden field is absent from both. A client
that ignores the UI entirely and POSTs `{ authorId: 1, isAdmin: true }` is
stopped by the fill set, not by Vine. So assert the fill set explicitly —
otherwise a refactor that widens it fails no test.

## The resource under test

Every sample below drives this resource. `subtitle` is reactive: hidden unless
`title` is exactly `'special'`, and required when it is.

```ts
import { BaseResource, F } from '@adonia/core'
import type { SchemaComponent } from '@adonia/core'
import Post from '#models/post'

export default class PostResource extends BaseResource<typeof Post> {
  static override model = Post
  static override slug = 'posts'

  override schema(): SchemaComponent[] {
return [
  F.text('title').required(),
  F.text('subtitle').visibleWhen('title', 'special').requiredWhen('title', 'special'),
  F.text('editorNote').virtual(),
]
  }
}
```

<!-- @sample-preamble
import type {
  BaseResource,
  CompileMode,
  HttpContextLike,
  ResourceDescriptor,
  ValidatorMode,
} from '@adonia/core'
import type {
  FakeAuthUser,
  FakeContextOverrides,
  FakeHttpContext,
  MirroringCaseRun,
  MirroringFixtureRun,
  ValidationResult,
} from '@adonia/core/testing'

declare const resource: BaseResource
-->

From here on, `resource` is `new PostResource()`. Resources are instantiated
per request and hold no state, so constructing one per test is free.

## `makeFakeContext`

```ts
declare function makeFakeContext<TUser extends FakeAuthUser = FakeAuthUser>(
  overrides?: FakeContextOverrides<TUser>
): FakeHttpContext<TUser>
```

Builds the slice of `HttpContext` that Adonia's pipeline touches — auth,
request, session flash, response, Inertia, and a container-ish resolver — with
no AdonisJS dependency. It satisfies `HttpContextLike`, which is the parameter
type of every compile-time callback (`canSee`) and of the descriptor compiler,
so a fake and a real context are interchangeable at those call sites.

Defaults: an authenticated admin (`{ id: 1, email: 'admin@adonia.dev' }`),
`GET /`, `accepts: 'html'`, empty params/query/body/headers/flash/bindings, and
**no** Bouncer. Every override is shallow; nested facades are built from them,
so the fake is always internally consistent.

```ts
import { makeFakeContext } from '@adonia/core/testing'

const ctx = makeFakeContext({
  user: { id: 7, email: 'editor@example.com' },
  method: 'post',
  url: '/admin/posts',
  params: { id: '3' },
  query: { page: '2', sort: '-publishedAt' },
  body: { title: 'Hello' },
  flash: { success: 'Saved' },
  bindings: { 'app.name': 'blog-admin' },
})

ctx.request.method() // 'POST' — upper-cased for you
ctx.request.param('id') // '3'
ctx.request.param('missing', 'fallback') // 'fallback'
ctx.auth.authenticate().email // 'editor@example.com'
ctx.container.resolve<string>('app.name') // 'blog-admin'
```

Pass `user: null` for a guest request: `auth.check()` returns `false` and
`auth.authenticate()` throws. Note the distinction — omitting `user` gives you
the default admin, passing `null` gives you a guest.

The auth, session, response and Inertia facades **record** rather than act, and
the recordings are the assertion surface:

```ts
import { makeFakeContext } from '@adonia/core/testing'

const ctx = makeFakeContext({ user: null })

ctx.response.status(302).redirect().toPath('/admin/login')
ctx.session.flash('error', 'Sign in first')
ctx.inertia.render('adonia/login', { panel: 'admin' })

ctx.response.statusCode // 302
ctx.response.redirectedTo // '/admin/login'
ctx.response.headers.get('cache-control') // header names are lower-cased
ctx.session.flashMessages.get('error') // 'Sign in first'
ctx.inertia.rendered?.component // 'adonia/login'
await ctx.inertia.sharedProps() // every share() provider, resolved and merged
```

`makeFakeContext({ bouncer })` accepts a structural Bouncer double, which is
how step 2 of the [authorization chain](/authorization) is tested without
installing the optional `@adonisjs/bouncer` peer.

## `compileFor`

```ts
declare function compileFor(
  resource: BaseResource,
  mode: CompileMode,          // 'create' | 'edit' | 'detail'
  ctx?: HttpContextLike       // defaults to makeFakeContext()
): ResourceDescriptor
```

A thin wrapper over `DescriptorCompiler.compile`. The context is what makes
`canSee` and ability-based omission observable, so pass a real-shaped fake when
the assertion is about visibility; omit it when it is about structure.

```ts
import { compileFor, makeFakeContext } from '@adonia/core/testing'

const create = compileFor(resource, 'create', makeFakeContext({ user: { id: 7 } }))
const detail = compileFor(resource, 'detail')

create.mode // 'create'
create.resource.slug // 'posts'
create.schema?.type // 'grid' — the root is ALWAYS a grid (protocol §2 rule 4)
detail.resource.abilities.view // resolved through the §12 chain
```

`schema` is optional on `ResourceDescriptor` because an `index` descriptor
carries a `table` instead — see the [protocol](/protocol/v1).

## `stableStringify`

```ts
declare function stableStringify(value: unknown): string
```

`JSON.stringify` with recursively sorted object keys and two-space indent.
Array order is preserved, because array order is semantic on the wire. Two
compilations of the same descriptor produce byte-identical output, so a
snapshot diff is always a semantic diff and never key-order noise.

```ts
import { compileFor, stableStringify } from '@adonia/core/testing'

const a = stableStringify({ b: 1, a: { d: 2, c: 3 } })
const b = stableStringify({ a: { c: 3, d: 2 }, b: 1 })
a === b // true

// What a golden file holds — see packages/core/tests/helpers/snapshot.ts.
const golden = `${stableStringify(compileFor(resource, 'create'))}\n`
golden.startsWith('{\n  "mode": "create"') // keys sorted: mode before resource
```

## `validateWith`

```ts
declare function validateWith(
  resource: BaseResource,
  mode: ValidatorMode,             // 'create' | 'edit'
  input: Record<string, unknown>,
  ctx?: HttpContextLike,
  record?: unknown
): Promise<ValidationResult>       // { outcome, errors, output }
```

Runs the whole §11.2 submit pipeline — prune keys hidden for the R1 evaluation
state, compile and run the Vine validator, re-derive `sets` — and **returns**
instead of throwing. `outcome` is `'success'` or `'failure'`, `errors` is the
Inertia `inputErrorsBag` map (`field → first message`), and `output` is the
validated payload or `null`.

`record` is forwarded to record-dependent `canSee` predicates. That is how
per-mode projection is expressed: the compilers pass a record in `edit` and
nothing in `create`, so record presence *is* the mode distinction at compile
time.

```ts
import { validateWith } from '@adonia/core/testing'

// `subtitle` is hidden for this state, so it is pruned before Vine runs —
// a bypassing client cannot even provoke a shape error for it.
const pruned = await validateWith(resource, 'create', {
  title: 'hello',
  subtitle: 'sneaked',
})
pruned.outcome // 'success'
pruned.output // { title: 'hello' }

// Flip `title` and the same key becomes required.
const missing = await validateWith(resource, 'create', { title: 'special' })
missing.outcome // 'failure'
missing.errors['subtitle'] // 'The subtitle field must be defined'
```

Anything that is not a Vine `E_VALIDATION_ERROR` is re-thrown, so a broken
compiler surfaces as an error rather than as a false `'failure'`.

## `assertValidatorKeys`

```ts
declare function assertValidatorKeys(
  resource: BaseResource,
  mode: ValidatorMode,
  submission: Record<string, unknown>,
  keys: readonly string[]
): void
```

Asserts the **exact** validator key set for the submission's R1 state
(defaults merged under the submitted values). Exactness is the point: a subset
assertion lets a leaking field pass unnoticed. Order does not matter — both
sides are sorted before comparison, and the failure message prints both sets.

```ts
import { assertValidatorKeys } from '@adonia/core/testing'

// `editorNote` is virtual and `subtitle` is hidden for this state.
assertValidatorKeys(resource, 'create', { title: 'hello' }, ['title'])
assertValidatorKeys(resource, 'create', { title: 'special' }, ['title', 'subtitle'])
```

## `assertValidatorOmits`

```ts
declare function assertValidatorOmits(
  resource: BaseResource,
  mode: ValidatorMode,
  submission: Record<string, unknown>,
  keys: readonly string[]
): Promise<void>
```

Invariant (a) of the mirroring rule, as a probe. It rewrites each named key to
`GARBAGE_PROBE_VALUE` (`12345`, a number where every Phase-1 field expects a
string) and asserts the result has **neither** an error entry **nor** an output
entry for it. An error entry would prove the key reached the validator; an
output entry would prove it survived.

```ts
import { assertValidatorOmits } from '@adonia/core/testing'

await assertValidatorOmits(resource, 'create', { title: 'hello' }, ['subtitle'])
await assertValidatorOmits(resource, 'edit', { title: 'hello' }, ['subtitle'])
```

## `assertFillOmits`

```ts
declare function assertFillOmits(
  resource: BaseResource,
  mode: ValidatorMode,
  payload: Record<string, unknown>,
  keys: readonly string[]
): void
```

Invariant (b): values submitted for `keys` never reach the model. The fill set
is derived for the payload's R1 state and applied to a plain-object model
stand-in, so the assertion is about the fill *set* — a real Lucid `save()`
belongs to an integration suite. The failure message prints the fill set that
did let the key through.

This is the mass-assignment assertion. Write it for every field a bypassing
client must not be able to write: hidden fields, disabled fields, virtual
fields, and any foreign key the form does not expose.

```ts
import { assertFillOmits } from '@adonia/core/testing'

// A tampered payload: the client sends keys the rendered form never had.
const tampered = { title: 'hello', subtitle: 'sneaked', editorNote: 'internal' }

assertFillOmits(resource, 'create', tampered, ['subtitle', 'editorNote'])
assertFillOmits(resource, 'edit', tampered, ['subtitle', 'editorNote'])
```

## `assertRequiredWhen`

```ts
declare function assertRequiredWhen(
  resource: BaseResource,
  mode: ValidatorMode,
  submission: Record<string, unknown>,
  key: string
): Promise<void>
```

Invariant (c), asserted in both directions for one key: omitting it fails with
an error keyed to that field, and including `REQUIRED_PROBE_VALUE` satisfies
the requirement. The second half matters — a validator that rejects everything
would pass the first half alone.

```ts
import { assertRequiredWhen } from '@adonia/core/testing'

// `title: 'special'` makes `subtitle` required, even though a bypassing
// client never rendered it.
await assertRequiredWhen(resource, 'create', { title: 'special' }, 'subtitle')
await assertRequiredWhen(resource, 'edit', { title: 'special' }, 'subtitle')
```

## `countQueries` and `assertQueryCount`

```ts
interface QueryEventSource {
  on(event: 'query', listener: (query: unknown) => void): unknown
  off(event: 'query', listener: (query: unknown) => void): unknown
}

declare function countQueries(source: QueryEventSource, fn: () => unknown | Promise<unknown>): Promise<unknown[]>
declare function assertQueryCount(fn: () => unknown | Promise<unknown>, expected: number, source: QueryEventSource): Promise<void>
```

Note the argument orders differ: `countQueries` takes the source first,
`assertQueryCount` takes it last. `countQueries` returns the raw query events
for callers that assert on the SQL too; `assertQueryCount` is the count-only
shorthand. Both subscribe for the duration of `fn` and unsubscribe in a
`finally`, so a throwing `fn` leaves no listener behind.

The emitter is **injected** rather than discovered, because counting a real
database needs a booted app's connection, which core's hermetic unit suite does
not have. Any Node-style `'query'` emitter works — a knex client, a Lucid
connection, or a fake:

```ts
import { assertQueryCount, countQueries, type QueryEventSource } from '@adonia/core/testing'

/** The shape a knex client already has. */
class FakeExecutor implements QueryEventSource {
  readonly #listeners = new Set<(query: unknown) => void>()

  on(_event: 'query', listener: (query: unknown) => void): this {
this.#listeners.add(listener)
return this
  }

  off(_event: 'query', listener: (query: unknown) => void): this {
this.#listeners.delete(listener)
return this
  }

  run(statements: readonly string[]): void {
for (const sql of statements) for (const listener of this.#listeners) listener({ sql })
  }
}

const executor = new FakeExecutor()

await assertQueryCount(() => executor.run(['select count(*) from posts', 'select * from posts']), 2, executor)

const queries = await countQueries(executor, () => executor.run(['select 1']))
queries // [{ sql: 'select 1' }]
```

### The §18 zero-per-row-query budget

§18 requires index serialization to be O(rows) with **zero per-row queries**:
every relation or aggregate a column needs comes from the eager-load stage.
Column costs, as the pipeline actually issues them:

| Call | Statements |
| --- | --- |
| `paginate()` | 2 — one `COUNT(*)`, one page select |
| `withCount` / `withAggregate` (`C.count`, `C.sum`) | 0 — correlated sub-selects fold into the page select |
| `preload(relation)` (`C.relation`) | 1 — one batched `whereIn` per relation, whatever the row count |
| reading an **unpreloaded** relation | 1 **per row** — the N+1 this budget forbids |

So a page with one relation column and any number of aggregate columns costs a
fixed **3** statements, and the assertion that proves it is a comparison across
two row counts, not a single magic number:

```ts
import { countQueries, type QueryEventSource } from '@adonia/core/testing'

declare const client: QueryEventSource // db.connection().getReadClient() in a booted app
declare function indexRequest(perPage: number): Promise<unknown>

await indexRequest(5) // warm-up: the first statement may carry connection chatter

const small = await countQueries(client, () => indexRequest(5))
const large = await countQueries(client, () => indexRequest(50))

small.length === large.length // the budget: count must not grow with rows
large.length === 3 // count + page select + one batched preload
```

A column that forgets `contributeHints` does not merely render wrong — it turns
a 50-row page into 50 extra statements. `packages/core/tests/query_count.spec.ts`
asserts both halves against a fake connection whose unpreloaded relations are
lazy getters that emit a query when touched, including a deliberate N+1 that
proves the harness detects one; `examples/blog-admin/tests/functional/adonia_pipeline.spec.ts`
repeats it against a real seeded database. See
[the index table](/index-table) and
[the table pipeline](/table-pipeline) for what produces the hints.

## Mirroring fixtures

The §11.2 rule — every declarative reactive rule affecting visibility or
requiredness MUST be enforced in the compiled validator, so client evaluation
is purely cosmetic — is tested from a shared corpus of JSON fixtures in
`docs/reactivity/fixtures/*.fixture.json`. The corpus is the oracle for **both**
sides: `packages/core/tests/mirroring.spec.ts` drives the server compilers and
`packages/ui/tests/reactivity_fixtures.ts` drives the client evaluator, both
reading the same files by relative path so neither can drift from the other.
The format, the R1–R6 rules and the test matrix are normative in
[the mirroring test design](/reactivity/mirroring-test-design); the server
half of the evaluator is described in
[server-side reactivity](/reactivity/server).

Add a fixture whenever you add a reactive pattern to a resource. It costs one
JSON file and buys the whole invariant.

### `resourceFromFixture`

```ts
import type { MirroringFixture } from '@adonia/core/testing'

declare function resourceFromFixture(fixture: MirroringFixture): BaseResource
```

Builds a throwaway `BaseResource` from a fixture by replaying it through the
authoring DSL. The fixture's `type` selects a field factory;
`default`/`required`/`virtual` and reactive rules become their matching DSL calls.
Fixture `hiddenOn`, however, names the fixture modes `create` and `edit`, not the
public field projection contexts (`form`, `detail`, `index`). The adapter therefore
uses a record-sensitive `canSee`: descriptor/validator compilers pass no record for
create and a record for edit. Choice fields declare no options in the format, so the
harness derives enum members from values used by the fixture. An unknown `type`
throws and lists the known ones.

```ts
import {
  assertFillOmits,
  assertValidatorKeys,
  resourceFromFixture,
  type MirroringFixture,
} from '@adonia/core/testing'

const fixture: MirroringFixture = {
  version: 1,
  name: 's3-w1-10-smoke',
  pattern: 'w1-10-smoke',
  classification: 'jsonlogic',
  fields: [
{ key: 'title', type: 'text', required: true },
{
  key: 'subtitle',
  type: 'text',
  reactive: {
    visibleWhen: { '===': [{ var: 'title' }, 'special'] },
    requiredWhen: { '===': [{ var: 'title' }, 'special'] },
  },
},
  ],
  cases: [
{
  state: { title: 'hello', subtitle: 'sneaked' },
  expect: {
    validator: { keys: ['title'], requires: ['title'] },
    fill: { keys: ['title'] },
  },
},
  ],
}

const built = resourceFromFixture(fixture)
const state = fixture.cases[0]!.state

assertValidatorKeys(built, 'create', state, ['title'])
assertFillOmits(built, 'create', state, ['subtitle'])
```

### `runMirroringFixture`

```ts
import type { MirroringFixture } from '@adonia/core/testing'

declare function runMirroringCase(
  fixture: MirroringFixture,
  caseIndex: number,
  mode: ValidatorMode
): Promise<MirroringCaseRun>

declare function runMirroringFixture(fixture: MirroringFixture): Promise<MirroringFixtureRun>
```

`runMirroringCase` drives one case × mode row: exact `keysFor` / `requiredFor` /
`fillFor` set equality against `case.expect`, the garbage probe for every hidden
key, the fill simulation with the R6 derivations overlaid, the requiredness
check for every expected-required key, and — when the fixture declares them —
the exact `sets` re-derivation map and the capability-URL block on the compiled
node. It throws on the first violation with the fixture, case number and mode in
the message, and otherwise returns the row report.

`runMirroringFixture` is the loop over that: every case, in `case.mode` when the
case pins one and in **both** `create` and `edit` otherwise. One call per fixture
is the whole conformance check.

```ts
import { runMirroringFixture, type MirroringFixture } from '@adonia/core/testing'

/** One test per fixture; the driver throws on the first violated invariant. */
export async function checkFixture(fixture: MirroringFixture): Promise<number> {
  const report = await runMirroringFixture(fixture)

  for (const run of report.runs) {
// e.g. "s3-w1-10-smoke case #1 × create — fill: title"
console.log(`${report.fixture} case #${run.caseIndex + 1} × ${run.mode} — fill: ${run.fillKeys.join(', ')}`)
  }

  return report.runs.length
}
```

Each `MirroringCaseRun` reports `fixture`, `caseIndex`, `mode`, `hiddenKeys`,
`requiredKeys` and `fillKeys`, which is enough to assert the *shape* of the run
on top of the invariants the driver already enforced — the repo's suite does
exactly that, so a fixture that accidentally expects nothing still fails.

`packages/core/tests/mirroring.spec.ts` discovers the corpus by listing
`docs/reactivity/fixtures/` at load time and enrols one Japa test per case ×
mode row, which is why a new fixture file needs no test-file edit. A fixture
classified `server-closure` may carry `cases: []` on purpose: cross-field
validation must contribute *nothing* to the wire grammar, and absence is the
assertion.

## Test style in this repo

Japa in `@adonia/core` and `@adonia/devtools`. Groups are named after the thing
under test with the spec section in parentheses, `assert` arrives destructured
from the test context, and a file-level TSDoc block states which plan item and
spec section the suite discharges.

Neither runner is installed for the docs typecheck program, so the two samples
below **declare** the runner functions they call instead of importing them. Read
`declare const test` as `import { test } from '@japa/runner'` and the Vitest
declarations as `import { describe, it, expect, beforeEach, vi } from 'vitest'`
/ `import { renderHook } from '@testing-library/react'`. Everything else —
including the shape of `test.group`, `assert` and `result.current` — is the real
thing.

<!-- @sample-preamble
interface JapaAssert {
  equal(actual: unknown, expected: unknown, message?: string): void
  deepEqual(actual: unknown, expected: unknown, message?: string): void
}
interface JapaTest {
  (title: string, callback: (ctx: { assert: JapaAssert }) => void | Promise<void>): void
  group(title: string, callback: () => void): void
}
declare const test: JapaTest
declare function goldenFor(mode: string): string
-->

```ts
import { compileFor, stableStringify } from '@adonia/core/testing'

test.group('descriptor snapshots — canonical text resource (W1-10)', () => {
  for (const mode of ['create', 'edit', 'detail'] as const) {
test(`mode=${mode} matches the committed protocol snapshot`, ({ assert }) => {
  const descriptor = compileFor(resource, mode)
  assert.equal(`${stableStringify(descriptor)}\n`, goldenFor(mode))
})
  }
})
```

The real file is `packages/core/tests/descriptor_snapshot.spec.ts`; `goldenFor`
stands in for `packages/core/tests/helpers/snapshot.ts`, which reads the
committed `<name>.snap.json` or rewrites it under `UPDATE_SNAPSHOTS=1`.

Vitest plus Testing Library in `@adonia/ui`, with nested `describe` blocks
naming the protocol section and `it` naming the behaviour:

<!-- @sample-preamble-reset -->

<!-- @sample-preamble
declare function describe(name: string, fn: () => void): void
declare function it(name: string, fn: () => void | Promise<void>): void
declare function beforeEach(fn: () => void): void
declare function expect(value: unknown): { toBe(expected: unknown): void }
declare const vi: { clearAllMocks(): void }
declare function renderHook<T>(callback: () => T): { result: { current: T } }
-->

```ts
import { useFormEngine, type ResourceDescriptor } from '@adonia/ui'

declare const descriptor: ResourceDescriptor

describe('useFormEngine', () => {
  beforeEach(() => vi.clearAllMocks())

  describe('state tree (protocol v1 §7)', () => {
it('builds the flat state from node keys, applying defaults under incoming state', () => {
  const { result } = renderHook(() => useFormEngine(descriptor, { title: 'Hello' }))
  expect(result.current.state['title']).toBe('Hello')
})
  })
})
```

The real file is `packages/ui/tests/use_form_engine.test.tsx`.

Three conventions hold across all of them:

- **Exact sets, never subsets.** `assert.deepEqual(sorted(actual), sorted(expected))`
  over a validator key set, a fill set or a hint set. A subset assertion is how
  a leak ships.
- **Assert the failure too.** Every helper in this module has a test proving it
  *fails* when the invariant is violated — `assertFillOmits` is checked against
  a key that is genuinely fillable, and the query-count suite contains a
  deliberate N+1. A guard that cannot fail is not a guard.
- **Hermetic by default.** Core's suite imports nothing from `@adonisjs/*` at
  runtime. Anything needing a real connection goes to
  `examples/blog-admin/tests/functional/`.

Source: https://adonia.pages.dev/guide/testing/index.mdx
