Add a scenario
Add a scenario #
Diátaxis form: how-to. A scenario drives the SUT through its public API to exercise a business workflow. For probing a dependency (component) of the SUT, seeadd-a-component.md↗. For composing probes and scenarios into a run, seeadd-a-segment.md↗. For the vocabulary, seedocs/concepts/glossary.md.
When to use this guide #
You want SQA to verify that the SUT behaves correctly when a real client exercises one of its workflows.
Examples (snappy):
- "Verify that creating a domain with
activate: truetransitions
it through the state machine to a terminal state" → the domain-activation scenario (already shipped).
- "Verify that bulk-importing 50 domains via
POST /domains/batch
succeeds and respects rate limits" → a future bulk-import scenario.
A scenario is not a probe: probes ask "is X up?"; scenarios ask "does the SUT do Y when poked?" If your check runs against a single endpoint with no follow-up state, it's a probe. If it issues a request, then polls or queries to observe what the SUT did next, it's a scenario.
Layout #
One scenario = one file. The phases (drive / observe / verify / cleanup) live as private helpers inside the file, not as separate files.
src/systems/<sut>/<scenario-name>.tsThe file exports one composing function returning Promise<Result>. The composition typically uses sequentialize over four phases that thread state through a closure-scoped accumulator.
This shape comes from the literature:
- **Sam Newman, Building Microservices (2nd ed., 2021), ch. 10
"Semantic Monitoring."** "*With synthetic transactions, we inject fake user behavior into our production system. This fake user behavior has known inputs and expected outputs.*" One transaction = one logical unit.
- **Google, SRE book (2016), ch. 17 "Black-Box Monitoring" and
"Production Probes."** A probe runs "*a protocol check against a target and reports success or failure*" — and probes can "*validate the response payload of the protocol… and even extract and export values as time-series.*" Drive plus payload validation, in one unit.
Splitting drive / observe / verify into separate files (or folders) fragments the transaction and makes shared state harder to thread. Resist the urge.
Recipe #
- Pick the scenario name. Kebab-case, descriptive of the
workflow (domain-activation, bulk-import, webhook-replay).
- Create one file
src/systems/<sut>/<scenario-name>.ts.
- Implement each phase as a private async helper inside the
file. One function per phase. Each helper:
- Returns a
Result(per ADR-0012). Never throws. - Takes primitives only (URLs, IDs, cookies); no
env/
targets reads inside helpers — let the segment composition read those once and pass them down.
- Skip-on-empty for any required arg.
- Tags log lines with
[<sut>.<scenario>.<phase>].
- Compose the phases. Export one
runScenarioName(): Promise<Result> that uses sequentialize(NAME, [...]) to chain the phases. State threading: capture each phase's Result into a closure-scoped priorResults array and read Result.context.<field> for downstream phases.
- Cleanup must be idempotent and tolerant of partial state.
The cleanup phase runs even when earlier phases failed; it should skip cleanly when the prior phase that produced the cleanup handle didn't run. Read the relevant Result.context field; skip if missing.
- Wire as a segment. Add a `step("N", "<scenario-name>",
() => runScenarioName()) entry to src/systems/<sut>/index.ts. By current convention the scenario file is the segment file — no separate segment wrapper. See add-a-segment.md` ↗.
- Run
make check && make test.
Reference implementation #
src/systems/snappy/domain-activation.ts is the worked example — one file, four private phases, one composed runDomainActivation export. PRD-03 describes the design choices end-to-end: docs/prds/03-snappy-activation-segment.md ↗.
Cover-test before merging #
Pretend the next reader knows the codebase but not your scenario. The two questions that catch most issues:
- Does cleanup run cleanly when the create step failed? If not,
every failed run leaves orphaned state in the SUT.
- Are the failure modes obvious from the log message alone? If
not, add the missing field to the structured log payload.