Skip to main content
A conformance contract asserts an adapter behaves; a fixture asserts the contract still contains those assertions.

Quick Start

1

Run the contracts against your adapter

Each port ships a runnable contract. Register your adapter and both the fake and the shipping adapter are held to the same cases.
2

Prove the contract can still fail

The fixture spawns the real contract against a deliberately broken adapter, one break at a time. It takes a single mode string.
The six break modes plus the none control:
3

Check the floor and the control

contracts.test.ts counts passing cases from a real none run and asserts a floor per contract: secrets โ‰ฅ 9, storage โ‰ฅ 11, time โ‰ฅ 8. The none control must pass green, so a fixture that failed for an unrelated reason cannot masquerade as proof.
This runs automatically inside contracts.test.ts under npm test. There is no flag to enable and no opt-in.

Why Two Layers

The contracts assert on adapter behaviour; the fixture asserts the contracts still contain those assertions. A contract with no broken-implementation test is documentation with a test() around it. Deleting assert.ok(fired >= 2) from the time contract โ€” the one assertion that catches setInterval becoming setTimeout, which stops every polling loop in the app after a single tick โ€” left the suite at 1035 pass, 0 fail. Fourteen assertions across all four contracts could be deleted for free.

The Six Break Modes

Each mode breaks the adapter in one named way, and the matching contract case must go red by name. A none control runs unbroken, so a fixture that failed for an unrelated reason โ€” a syntax error, a missing import โ€” cannot masquerade as proof.

The Shrink Floor

A case with no break mode could still be deleted, so contracts.test.ts counts passing cases from a real run of the fixture โ€” not a regex over source text โ€” and asserts a floor. Raise these numbers when you add cases. A drop means a contract lost coverage, and that is exactly the event worth a red build.
Node 22 emits TAP when stdout is a pipe; Node 24 emits the spec reporter. The fixture is spawned with --test-reporter=tap so a test grepping for not ok behaves the same on both.

Why The Fixture Builds Broken Adapters Inline

adapters may not import testing โ€” enforced by tools/depgraph.mjs โ€” so the fixture builds its broken adapters inline, the same way engines/src/contract-fixture.ts does.

Adding A New Case Or Break Mode

1

Add the contract case with a stable name

The name is matched by regex, so keep it stable once other code depends on it.
2

Add the break mode in contract-fixture.ts

Guard the defect behind a mode === "..." branch.
3

Add the pair to the ADAPTER_BREAKS table

contracts.test.ts maps each mode to the case name it must redden.
4

Bump the shrink floor for that contract by one

A new case raises the floor by one, so a later deletion is caught.

Best Practices

An inline broken adapter that re-implements the assertion proves an assertion of that shape would catch the defect โ€” not that the contract still contains it. Spawn the real contract instead.
Without the unbroken control, a fixture that failed everything โ€” a syntax error, a runner that cannot start โ€” would satisfy every break mode while proving nothing.
A regex over test( is satisfied by a case that asserts nothing. Counting passing cases from a real none run pins behaviour, not shape.
Spawn the fixture with --test-reporter=tap so a test grepping for not ok behaves identically on Node 22 and Node 24.

Storage & Secrets

The two ports the secrets and storage break modes pin.

Time & Pacing

The port the two time break modes pin.

Shell & Adapters

The shell contract that sits in the same file.

Mobile Engines

The parallel fixture pattern one directory over.