Scenarios
A scenario is a behavioral test contract declared in the spec. Scenarios run against the canonical runtime, not mocks.
Why this exists
Application testing is typically an afterthought, implemented in a separate framework, using test doubles that approximate but do not reproduce the production execution engine. The result is a structural gap: passing tests do not prove the production runtime handles the same scenario correctly.
Test setup also duplicates application setup. Schemas, validation rules, and business invariants are re-declared in test fixtures, which drift from the spec over time.
In Gooi, scenarios and personas are spec sections, co-equal with domain and queries. They run against the same execution pipeline as production invocations, using memory-backed providers instead of real ones. There are no test doubles and no separate test setup.
Personas
A persona is a named customer archetype. Personas provide a simulated user identity and behavioral context for scenarios.
personas:
guest:
description: "Authenticated user who submits normal messages."
traits:
tone: neutral
history: []
tags: [authenticated]
spammer:
description: "Authenticated user attempting spam content."
traits:
tone: aggressive
history: []
tags: [authenticated]
Personas are not user accounts. They are archetypes. The tags field maps to role derivation in access, so a persona tagged [authenticated] receives the authenticated role context.
Scenarios
A scenario is a sequence of trigger, expect, and capture steps.
scenarios:
happy_submit_smoke:
tags: [smoke, happy]
context:
persona: guest
principal:
subject: user_1
steps:
- trigger:
mutation: submit_message
input:
message: "hello from scenario"
- expect:
query: list_messages
guards:
structural:
- description: "Happy path should produce at least one row."
rule:
"!=":
- var: rows.0.id
- null
Step types
trigger: Calls a mutation or query using the scenario's persona and principal context. The runtime processes it through the full execution pipeline.
expect: Calls a query and evaluates guards against the result. The guard language is identical to the guard language used in domain.actions and domain.signals.
capture: Records a value from the current execution context for use in later steps. Useful for time-travel queries.
steps:
- trigger:
mutation: submit_message
input:
message: "first event"
capture:
first_event_at:
var: ctx.now
- expect:
query: get_user_message_state
args:
as_of:
$expr:
var: captured.first_event_at
guards:
structural:
- description: "Time travel should show only the first message."
rule:
"==":
- var: rows.0.message_count
- 1
Running scenarios
import { readFileSync } from "node:fs";
import { defineApp } from "@gooi/app/define";
import { compileApp } from "@gooi/app/compile";
import { runScenarios } from "@gooi/app-testing/run";
const raw = readFileSync("./app.yml", "utf-8");
const spec = defineApp({ yaml: raw });
const bundle = compileApp({ spec });
const results = await runScenarios({ spec, bundle });
for (const result of results) {
console.log(result.scenarioId, result.passed ? "PASS" : "FAIL");
if (!result.passed) {
for (const step of result.steps.filter((s) => !s.passed)) {
console.log(" Step", step.index, "failed:", step.violations);
}
}
}
runScenarios uses the canonical runtime with memory-backed providers. The same access enforcement, replay gating, and signal emission that runs in production runs here.
Integrating with your test runner
@gooi/app-testing works with any test runner that supports async functions.
import { describe, it, expect } from "vitest";
import { runScenarios } from "@gooi/app-testing/run";
describe("scenarios", () => {
it("all scenarios pass", async () => {
const results = await runScenarios({ spec, bundle });
const failed = results.filter((r) => !r.passed);
expect(failed).toHaveLength(0);
});
});
A spec without scenarios is incomplete
The compiler emits a warning when a spec has zero scenarios. This is intentional. A spec that cannot be tested by its own scenarios is a spec that requires external test infrastructure to verify. The scenario section eliminates that external dependency.
What's next
- Installation: install
@gooi/app-testing - Domain Model: the guard language used in
expect.guards.structural - The Runtime: how the execution pipeline that scenarios run against works