Docs

Failure laboratory

Durable systems fail on a schedule you never chose: the crash lands between the commit and the publish, the queue delivers twice, the events arrive backwards. The laboratory lets you choose that schedule, then replay it exactly.

Seven failures worth testing

  • crash - stop dead at a named durability seam.
  • duplicate-delivery - deliver the same batch N times.
  • reorder-events - rotate a batch out of order.
  • delay - advance virtual time, with no real sleeping.
  • expire-budget - report zero remaining at a budget seam.
  • lose-provider-reply - run the call, then lose only its reply.
  • contend-checkpoint - make a checkpoint conflict.

Place the seams, schedule the failure

TS
import { createFailureLab } from "@nifrajs/testing"

const lab = createFailureLab({
  seed: 77,
  schedule: [
    { kind: "duplicate-delivery", point: "relay.batch", copies: 3 },
    { kind: "reorder-events", point: "relay.batch" },
  ],
})

// The adapter under test just names its seams. Same seed, same schedule,
// same result - every run.
const delivered = lab.deliveries("relay.batch", ["evt-a", "evt-b", "evt-c"])

The controller is an off-hot-path port. Your adapter names its seams; the schedule decides what happens at them. Time is virtual, so a 24-hour delay costs nothing and nothing is flaky.

Assert the invariant, not the output

TS
// doc-check: skip - commit/publish/outboxStillPending stand in for the system under test
import { runFailureScenario } from "@nifrajs/testing"

const report = await runFailureScenario(
  {
    name: "crash-after-commit",
    execute: async (lab) => {
      await commit()
      lab.checkpoint("outbox.after-commit") // crashes here, per the schedule
      await publish()
      return "published"
    },
    // The scenario passes only if the invariant survives the failure.
    verify: async ({ error }) => error !== undefined && (await outboxStillPending()),
  },
  { seed: 1, schedule: [{ kind: "crash", point: "outbox.after-commit" }] },
)

report.ok // did the invariant hold?
report.replay // { seed, schedule } - reproduces this exact run

A scenario passes only when verify returns true after the failure. Losing the provider reply is not a bug; losing the money is. That distinction is the whole point.

Evidence is tokens only

Injection evidence records the kind, the point, the occurrence, and the virtual time. Never payloads, provider results, exception messages, or stacks. A failing report carries the error class and the exact replay inputs, so it reproduces from the report alone without leaking what flowed through the system.

Where it fits

The same engine backs nifra levels L4. Contract invariants fuzz the HTTP surface; the laboratory covers the durable paths underneath, where the interesting failures live.