Testing
Scenario tests are colocated with routes as run.test.ts files. Each scenario declares an input state and the expected output, and dawn test runs them against the real route runtime.
A minimal test
A scenario file is a default-exported array of plain scenario records — there is no describe() or test() wrapper, and the route is inferred from the file's directory:
export default [
{
name: "greets a tenant",
input: { tenant: "acme" },
expect: {
status: "passed",
output: { tenant: "acme", greeting: "Hello, acme!" },
},
},
]Run all scenarios:
dawn testDawn discovers every run.test.ts under src/app/, invokes the route for each scenario, and evaluates the declarative expectation.
Scenario shape
Each entry in the default-exported array is { name, input, expect, run?, assert? }:
name— human-readable scenario name.input— the JSON state passed to the route.expect— declarative expectation (see below).run?— optional run config:{ url?: string }to target a live dev server.assert?— optional(result) => voidcallback for custom assertions, evaluated alongsideexpect.
The expect object accepts:
status(required) —"passed" | "failed".output?— deep-equal match against the route's returned state.meta?— match against{ mode, routeId, routePath, executionSource }.error?— forstatus: "failed", match{ kind, message: string | { includes: string } }.
For programmatic assertions in assert(result), import the helpers from @dawn-ai/sdk/testing:
import { expectError, expectMeta, expectOutput } from "@dawn-ai/sdk/testing"
export default [
{
name: "custom assert",
input: { tenant: "acme" },
expect: { status: "passed" },
assert: (result) => {
expectOutput(result, { greeting: "Hello, acme!" })
expectMeta(result, { mode: "agent", routeId: "/hello/[tenant]" })
},
},
]Against a live dev server
To exercise protocol parity against a running dawn dev, set run.url per-scenario. There is no command-level --url flag on dawn test.
export default [
{
name: "greets a tenant via dev server",
input: { tenant: "acme" },
run: { url: "http://127.0.0.1:3001" },
expect: {
status: "passed",
output: { tenant: "acme", greeting: "Hello, acme!" },
},
},
]Start the dev server first, then run dawn test:
dawn dev --port 3001 &
dawn testAgents, retries, and middleware
Two cross-cutting features shape what scenarios assert:
- Agent retries —
agent({ retry: { maxAttempts, baseDelay } })retries on transient errors. To assert the exhausted-retry path, setexpect.status: "failed"and useexpect.error(or anassertcallback withexpectError). See Retry. - Middleware —
src/middleware.tsruns before every/runs/waitand/runs/streamrequest. Live-server scenarios (run: { url }) exercise middleware; in-process scenarios bypass it. A scenario whose middleware callsreject(...)should setexpect.status: "failed"with a matchingexpect.error. See Middleware.
Mocking tools
Rules
- 1
File location
run.test.tsmust live in the route's directory, not a sibling or nested folder. Dawn matches tests to routes by directory. - 2
Default-exported array
The file's default export is the array of scenario records. There is no
describe()ortest()wrapper. - 3
Use the helpers from @dawn-ai/sdk/testing for custom assertions
expectOutput,expectMeta, andexpectErrorcover deep-equal match (with helpful diffs). For partial or fuzzy matching today, use the declarativeexpectshape (output/meta/error) or write a custom predicate inassert(result). - 4
Keep scenarios focused
One scenario = one claim about the route's behavior. If you're adding branches, add more scenarios — don't inflate a single one.
CI
Use dawn verify as the integrity gate (it covers app, routes, typegen, and deps in one call), then run dawn test:
- run: pnpm exec dawn verify
- run: pnpm exec dawn testdawn verify runs typegen and check internally, plus the deps check (missing packages, missing env vars) that bare dawn check && dawn typegen does not. dawn test exits non-zero on any failure and outputs a diff per mismatched scenario.