Move scenario definitions into one catalog, keep registered blocks under test-blocks, and split the supertest scheduler probe into start/assert/stop blocks.
4.4 KiB
Supertest Architecture
Supertest separates lifecycle orchestration from assertions:
- Topology provisions and mutates nodes, then emits named lifecycle hooks.
- Hook is a point in that lifecycle with static guarantees and optional aborting gates.
- Test block is one reusable assertion/preparation unit run at a hook.
- Scenario binds blocks to hooks on one topology.
- Group is an ordered list of scenarios.
The active registries live in src/scenarios.ts, with scenario catalog data in src/scenario-definitions.ts and reusable blocks in src/test-blocks/. CLI listings are generated from those registries, so new entries should appear in supertest scenario|topology|block list without hand-written help changes.
Topologies
Topologies live in src/topologies/ and implement Topology from src/architecture.ts.
A topology owns cloud shape and lifecycle:
export const exampleTopology: Topology = {
name: "example",
description: "Provision one primary node.",
estimatedDuration: "20m",
roles: ["primary"],
hooks: [
{
name: "primary-ready",
guarantees: { minNodes: 1, roles: ["primary"] },
gates: [initPrimaryGate("example-primary-ready")]
}
],
async drive(ctx, emit) {
// provision / mutate nodes
const snapshot = await ctx.captureSnapshot("primary-ready")
await emit("primary-ready", createHookPayload({ hook: "primary-ready", nodes, snapshot }))
}
}
Guidance:
- Emit hooks in lifecycle order; runner summaries preserve emit order.
- Capture the lifecycle snapshot in the topology and pass it in
HookPayload.snapshot. - Put only “healthy enough to proceed” checks in hook gates. Gate failure aborts the run.
- Keep role names stable and semantic (
primary,join,edge) so blocks can target roles, not node ids.
Test blocks
Registered scenario blocks live in src/test-blocks/, grouped by category. Blocks implement TestBlock.
export const createFakeUsersBlock: TestBlock = {
name: "create-fake-users",
description: "Create fake users through the plugin API.",
requires: { minNodes: 1, roles: ["primary"] },
async run(ctx, hook, state, params) {
const count = requireIntegerParam(params, "count")
const primary = requireHookNode(hook.byRole("primary")[0], "primary").server
// assertions / preparation
}
}
Guidance:
- Blocks are soft when attached by a scenario: failures are recorded, later blocks/hooks still run, and the final scenario result fails.
- Blocks are aborting when attached as topology gates.
- Prefer
hook.nodes,hook.byRole(...), andhook.snapshotover taking a redundant snapshot at the start of a block. - Capture additional snapshots only after the block materially changes the system or needs a distinct diagnostic artifact.
- Validate params inside the block with small local helpers; avoid a schema framework until it pays for itself.
- Use
RunStatefor prepare/verify handoff across hooks.
Scenarios
Scenarios are small data objects collected in src/scenario-definitions.ts:
export const exampleScenario: Scenario = {
name: "example-load",
description: "Provision one node and create fake users.",
topology: "example",
blocks: {
"primary-ready": ["http3-health", { block: "create-fake-users", params: { count: 1000 } }]
}
}
Scenario block bindings can be plain block names or { block, params } objects. Params are per binding, so the same block can be reused with different values in different scenarios or hooks.
Guidance:
- A scenario should not provision nodes directly; choose or add a topology instead.
- Bind only blocks whose
requiresfit the hook guarantees. The runner checks this before provisioning. - Keep scenario names stable: users and scripts run
supertest scenario run <name>.
Groups
Groups are ScenarioGroup entries registered in src/scenarios.ts.
Guidance:
- Use groups for coverage bundles (
tribes,sender,kobold,all), not for lifecycle logic. - Keep
allas the deduplicated union of normal automated groups. - Leave manual or intentionally redundant scenarios out of
allunless they are safe and useful in routine runs.
Runner results
At the end of every run, the runner prints a structured scenario summary:
- scenario name
- final result
- hooks in chronological order
- each gate/block with
pass,fail, ornot-run - total runtime
A soft block failure does not stop lifecycle progression, but it does make the final scenario result and process exit fail.