Add a system (SUT)
Add a new system under test #
Diátaxis form: how-to. Vocabulary followsdocs/concepts/glossary.md: a system under test is one folder undersrc/systems/; an external system is one folder undersrc/components/; a segment composes probes for one system. For the why behind the directory name matching the discipline name (src/systems/, notsrc/apps/), see ADR-0009. For the purity contract every system function obeys, see ADR-0012.
When to use this guide #
You want SQA to verify something other than snappy — e.g. conveyor, data-factory, or a third-party service.
Recipe #
- Create
src/systems/<name>/. Pick a verb-friendly name; the
directory becomes the runtime identifier.
- Add
src/systems/<name>/index.tsexporting arunX()function
that returns Promise<Result>. The function is pure (per ADR-0012) — it composes its segments with parallelize or sequentialize (per ADR-0015) and never throws:
```ts // src/systems/conveyor/index.ts import { sequentialize, type Result } from "@lib/result.ts"; import { step } from "@lib/step.ts"; import { runPreflight } from "./preflight.ts";
const NAME = "conveyor";
export async function runConveyor(): Promise<Result> { return sequentialize(NAME, [ () => step("1", "preflight", () => runPreflight()).then((s) => s.value), // () => step("2", "data probes", () => runDataProbes()).then((s) => s.value), ]); } ```
sequentialize at the system level (rather than parallelize) is the usual choice: preflight establishes reachability before later segments run, so segments are ordered. Inside a segment, parallelize is the default for independent children — see add-a-segment.md ↗.
- Add the first segment file (usually
preflight.ts). Follow
- Wire dispatch in
src/index.ts. The runner already has an
SQA_SYSTEM dispatch table (introduced when the mock system landed); adding a system means adding two lines:
```ts // src/index.ts (current shape) import { runConveyor } from "@systems/conveyor/index.ts"; // <— new import { runMock } from "@systems/mock/index.ts"; import { runSnappyApi } from "@systems/snappy/index.ts";
const SYSTEMS: Record<string, () => Promise<Result>> = { "snappy": runSnappyApi, mock: runMock, conveyor: runConveyor, // <— new }; ```
Then:
- Add
"conveyor"to theSQA_SYSTEMzod enum in
src/config/env.ts.
- Mention
conveyornext tosnappyandmockin
.env.example's SQA_SYSTEM block and in the README's env-var table.
- The runner already handles trace start/stop, exit-code
mapping, and pretty-vs-JSON output — you don't need to change any of that, only the system function it dispatches.
- Add system-specific Make targets if the run needs distinct
ergonomics (e.g. make run-conveyor). The existing make run-mock is the template.
What the runner gives you for free #
A new system folder doesn't have to wire any of the cross-cutting machinery — it's already in place:
- Tracing. Every
step()opens a child span; trace IDs and
span IDs propagate via AsyncLocalStorage. ADR-0014.
- Outcomes. Every leaf returns a
Resultenvelope with
pass / warn / fail / error / skip. The runner derives the exit code via exitCodeFor(). ADR-0012.
- Timing.
step()stamps observed timestamps onto every
child Result. ADR-0013.
- Output. Pretty-mode runs get a summary block; JSON mode
runs get a structured "done" record. The system function doesn't choose; the runner does. ADR-0016.
So the system folder is small on purpose: it lists segments in their execution order, picks parallelize or sequentialize, and returns the composite Result. Everything else is one layer down.
Naming reminder #
The folder under src/systems/ is the system under test — the discipline-level noun SQA is built around. The folders under src/components/ are external systems the probes talk to (S3, an HTTP endpoint, a queue). One system under test composes many external systems. See the architecture vocabulary note ↗ and ADR-0009.