Anatomy of a Kerno test
What Kerno writes into your repository for each entry point, which file to read, and which one to leave alone.
Kerno writes its tests into your repository so they live with your code and travel through review like anything else. Each entry point gets its own folder.
The important thing to know first: every scenario exists as two files. The .scenario.md is written for you, and the .scenario.ts is written for the agent that runs it. When you want to know what a test does, read the Markdown.
What is in an entry point's folder
For POST /api/auth/login, Kerno creates .kerno/scenarios/endpoints/POST/api/auth/login/:
<name>.scenario.md
The scenario in plain English. One per scenario
Yes
<name>.scenario.ts
The same scenario as executable TypeScript, which is what actually runs
Rarely
preconditions.ts
Checks that run before the entry point's scenarios, confirming the entry point is reachable and testable
Rarely
preconditions.md
The same checks in plain English
Rarely
plan.json
The plan the Markdown is rendered from
No
report.json
Results from the most recent run
No
<name>.scenario.blocked
Marks a scenario that could not run
No
Everything here is committed alongside your code.
The Markdown, which is the one to read
Each .scenario.md opens with an HTML comment marking it as generated, then follows the same section order every time. This is a real one, with the credential masked:
## Successful login with valid credentials returns tokens and user data
**Kind:** HAPPY_PATH | **Actor:** unauthenticated visitor | **Scope:** POST /api/auth/login
### Preconditions
- The database contains an active user with email `admin@example.com` and password hash matching `<password>`
### Trigger
POST /api/auth/login with JSON body `{ "email": "admin@example.com", "password": "<password>" }`
### Main success scenario
1. The endpoint validates the email format and password presence
2. The endpoint finds the active user by email
3. The endpoint verifies the plaintext password against the stored hash
4. The endpoint generates a JWT access token and a refresh token
5. The endpoint updates the user's `refreshToken` column and `lastLoginAt` timestamp
6. The endpoint responds 200 with JSON containing `user`, `token`, and `refresh_token`
### Postconditions
- Response status is `200`
- Response body `user.email` equals `"admin@example.com"`
- Response body `user.id` is a UUID string
- Response body `token` is a non-empty string
- The `token` value decodes as a valid JWT (does not throw on decode)
- A `user` table row with email `admin@example.com` exists with a non-null `refreshToken` column
- The same row's `lastLoginAt` column is set to a timestamp within the last 10 seconds
### Notes
The endpoint is throttled at 5 requests per 60 seconds per the `@Throttle` decorator. The
stored hash may be bcrypt or scrypt; the service auto-migrates to the current algorithm on
successful login.Reading top to bottom you get what is worth knowing about any scenario. Sections marked optional appear only when the plan has them.
Kind / Actor / Scope
What sort of case this is, who is making the request, and which entry point it hits
Preconditions
The state that has to exist before the request is sent
Trigger
The exact request
Main success scenario
What your code is expected to do, step by step
Postconditions
Every assertion, in plain English. This is the baseline
Minimal guarantees
Optional. What must hold even when the scenario fails
Cleanup
Optional. The teardown that runs after the assertions
Notes
Optional. What Kerno noticed and deliberately decided about, including what it chose to leave unasserted
The Postconditions section is the one to read closely. It is the baseline in words, so when a validation run reports a diff, the assertion that moved is written there.
The Notes section is worth a look when a scenario surprises you. Kerno records the judgement calls it made, such as an optional field it saw and chose to leave out of the assertions.
The TypeScript, which the agent maintains
The .scenario.ts is what the sandbox executes. It is a Vitest file that talks to your running application over HTTP and, in white box mode, to your datastores directly.
A scenario runs in up to four phases, and each one is a ctx call:
ctx.arrange
Sets up the state the Preconditions describe
ctx.act
Sends the request
ctx.assert
Checks the Postconditions
ctx.cleanUp
Removes whatever the scenario created
Two details explain why the generated code looks the way it does.
Assertions are soft. expect.soft lets a run finish and report every mismatch at once, so one validation tells you everything that moved instead of stopping at the first difference.
Every assertion carries a message. The second argument to each expect is the plain-English form of that postcondition, which is what you see in a run report when it fails.
Flow tests
A user flow gets one test that walks the journey in order. It lives under .kerno/scenarios/flows/<flow>/, beside a flow.json that records the flow's description, its steps and the outcome it should reach. In the TypeScript, each step of the journey runs inside its own ctx.step(...), and the test checks the outcome at the end. When a journey breaks, the result names the step where it failed.
Preconditions
preconditions.ts runs before the entry point's scenarios and checks, read-only, that testing the entry point is possible at all. What it checks depends on the entry point.
HTTP route. The application URL is set, the route is mounted and answering, and any credential the scenarios need can be obtained.
MCP tool. One read-only call to the tool answers.
Background consumer. Only the environment and seed data below, since a consumer is triggered through its broker and nothing signs in.
User flow. The service answers and the flow's actor can sign in.
Each also checks that the environment variables the run needs are set, and routes, tools and consumers check that their seed data exists. Reaching your databases and other dependencies is the environment healthcheck's job.
When a precondition fails, the run stops before any scenario runs and ends with an error naming the reason, which tells you nothing was tested. In an interactive run, Kerno first asks whether to retry with your guidance or abort. Validation runs reuse the tests on disk and skip preconditions.
Changing a test
Ask Kerno rather than editing the files.
Kerno re-plans, re-renders the Markdown, and rewrites the TypeScript together, so the two halves stay in step. An edit you make by hand is overwritten the next time that entry point is generated or updated.
Related
Baseline tests for what a baseline records and how results are reported.
Testing modes for the dials that change what gets generated.
Ports and file paths for where everything lives on disk.
Last updated