Current section
Files
Jump to
Current section
Files
docs/scenarios.md
<!-- Generated by scripts/build-sdk-docs.mjs from docs/shared/scenarios.md. Edit the source, not this copy. -->
# Scenario data
Scenario data describes the records a test run needs. The platform generates it from your discover schema, but you write it yourself for integration tests and when debugging. The format is identical across every language SDK.
## The shape: a flat map
Data is a flat map. Top-level keys are model names; each value is an array of records to create for that model.
```json
{
"create": {
"Organization": [{ "_alias": "org", "name": "Test Org", "slug": "test-org" }],
"User": [{ "_alias": "admin", "name": "Admin", "email": "admin@test.com", "organizationId": { "_ref": "org" } }],
"Member": [{ "role": "owner", "organizationId": { "_ref": "org" }, "userId": { "_ref": "admin" } }]
}
}
```
Two rules cover almost everything:
- **A model is created only when it is a top-level key.** There is no nesting. You never place one model's records inside another model's fields. (If you do, the SDK treats that array as opaque field data and hands it to the factory verbatim - it is not created as separate records.)
- **Every cross-model link is an `_alias`/`_ref` pair.** Name a record with `_alias`, then point a foreign key at it with `{ "_ref": "alias" }`. This is the only way to link records.
## How ordering works
The SDK builds a dependency graph purely from the `_alias`/`_ref` edges in the payload and topologically sorts it: a record that another record `_ref`s is created first. Key order in the JSON is irrelevant. There is no relation introspection - the graph comes entirely from the `_ref` edges you write.
By the time your factory's `create` runs, every `_ref` has already been replaced with the real ID of the referenced record. Your factory receives a plain FK value (a string or number), never a `_ref` object and never a temporary ID.
A `_ref` to an alias that no record in the same payload declares fails with `INVALID_BODY` ("references unknown alias(es)").
## Setting foreign keys and the scope field
You set every foreign key yourself, including the scope field. There is no "FK direction" to reason about - every link, up or across, is just a `_ref` from the record holding the column to the record it depends on. Use the exact column name from your schema, whatever you called it (`organizationId`, `orgId`, `tenant_fk`).
```json
{
"create": {
"Organization": [{ "_alias": "org", "name": "Acme", "slug": "acme" }],
"Application": [{ "name": "Marketing Site", "architecture": "WEB", "organizationId": { "_ref": "org" } }],
"Member": [
{ "role": "owner", "organizationId": { "_ref": "org" }, "userId": { "_ref": "alice" } }
],
"User": [{ "_alias": "alice", "name": "Alice", "email": "alice@test.com", "organizationId": { "_ref": "org" } }]
}
}
```
The scope field is a foreign key like any other. The SDK does not inject it - set it explicitly on every model that has it.
## What to include and omit in a record
Include every field your factory's input schema marks required and does not default:
- Required fields with no default value.
- Every foreign key, as a `{ "_ref": "alias" }`.
- The scope field, as a `_ref`.
- Unique fields, with values distinct across records so parallel runs do not collide.
Omit anything the factory or database fills in:
- Primary keys / IDs - generated by your creation code.
- Fields with default values.
- Auto-managed timestamps (`createdAt`, `updatedAt`).
## Built-in placeholder tokens
The platform normally resolves all values before calling `up`, but the SDK also understands three placeholder tokens inside string values, as defense in depth:
| Token | Resolves to |
|-------|-------------|
| `{{testRunId}}` | The current run's ID - useful for making unique values. |
| `{{index}}` | The record's zero-based position within its model array. |
| `{{cycle(a,b,c)}}` | `a`, `b`, `c` in turn, cycling by record index. |
Any other `{{...}}` that reaches the SDK fails loudly with `UNRESOLVED_TOKEN` rather than being inserted as a literal string.
## More link examples
Join table (each row references two aliases):
```json
{
"create": {
"Tag": [
{ "_alias": "tag1", "name": "Critical", "color": "#DC2626", "organizationId": { "_ref": "org" } },
{ "_alias": "tag2", "name": "Flaky", "color": "#EAB308", "organizationId": { "_ref": "org" } }
],
"Test": [{ "_alias": "t1", "name": "Homepage", "testGenerationId": { "_ref": "gen1" } }],
"TestTag": [
{ "testId": { "_ref": "t1" }, "tagId": { "_ref": "tag1" } },
{ "testId": { "_ref": "t1" }, "tagId": { "_ref": "tag2" } }
]
}
}
```
Distinct users for distinct members (the common "one member per user" case): give each `User` its own `_alias`, then point each `Member.userId` at a different one. A shared alias would try to make the same user a member twice and hit a unique constraint.
## The three standard scenarios
When setting up a project, create three scenarios as a baseline. Name files after the shape, not the test.
- **empty** - organization + one user + their membership. Tests empty states, onboarding, and "no data yet" screens.
- **standard** - realistic variety: several users with different roles, at least one record per enum value, descriptive names ("Marketing Website", not "App 1"), enough data to exercise every filter and status.
- **large** - high volume: many explicitly listed records so pagination, sort stability, and filter performance get tested.
Add more targeted scenarios later, only when a specific test needs a data shape these three do not cover. Do not generate dozens up front.