Files
self 5da888b3de refactor: consolidate scenario catalog
Move scenario definitions into one catalog, keep registered blocks under test-blocks, and split the supertest scheduler probe into start/assert/stop blocks.
2026-06-28 19:06:35 +02:00

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(...), and hook.snapshot over 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 RunState for 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 requires fit 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 all as the deduplicated union of normal automated groups.
  • Leave manual or intentionally redundant scenarios out of all unless 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, or not-run
  • total runtime

A soft block failure does not stop lifecycle progression, but it does make the final scenario result and process exit fail.