3.7 KiB
testing/
(no submodules)
Purpose
Provides run2pcParticipantConformance(factory) — a runnable test
suite that pins the contract every 2PC writer participant MUST honour.
Adapter authors drop a single import + invocation into a
tests/conformance/run-2pc-participant.test.ts file in their package;
the harness handles the rest.
Shipping the harness on this package's ./testing subpath keeps
participants accountable to the same prepare/commit/rollback semantics
the coordinator depends on at runtime. Without this harness, drift
between participant implementations would only surface during
integration testing — too late.
Features
- Six pinned invariants (engine identifier, voter mode, prepare-then- prepare-SQL, commit-prepared, rollback-prepared, idempotency-on- second-rollback).
- Per-scenario factory: every test starts from a fresh participant + teardown so the suite is isolation-safe under parallel runs.
- Uses
@suaveplan/testing/runner— works under both Vitest and Bun:test without re-importing. - Generates unique gids per scenario so re-runs do not collide.
Usage: Basic
// In an adapter package's tests/conformance/run-2pc-participant.test.ts
import { run2pcParticipantConformance } from "@suaveplan/api-2pc/testing";
import { createMyParticipant, teardownMyParticipant } from "../helpers";
run2pcParticipantConformance({
async create() {
const participant = await createMyParticipant();
return {
participant,
teardown: () => teardownMyParticipant(participant),
};
},
});
Usage: Advanced
The harness exposes no surface beyond run2pcParticipantConformance —
deliberately. Adapter-specific assertions belong in adapter test
files, not in the conformance harness. If your adapter needs to
exercise behaviour the suite does not cover (e.g. cancellation,
custom serialization), add a sibling test file:
import { describe, it } from "@suaveplan/testing/runner";
import { run2pcParticipantConformance } from "@suaveplan/api-2pc/testing";
describe("my-participant", () => {
run2pcParticipantConformance({ create: createMyParticipant });
it("cancels a stuck prepare on signal abort", { timeout: 30_000 }, async () => {
// adapter-specific assertion
});
});
API Reference
interface TwoPcParticipantFactorycreate(): Promise<{ participant: RegisteredTwoPcParticipant; teardown: () => Promise<void> }>
function run2pcParticipantConformance(factory): void- Registers six
describe/itblocks against the surrounding runner. Returns nothing.
- Registers six
Implementation Notes
The harness deliberately does NOT use a shared Postgres connection
across scenarios. Each scenario calls factory.create() to get a
fresh participant; the factory is expected to wire its own connection
(directly to a Postgres container, against a shared pool, or against
an env-bridge passthrough URL). This isolation prevents one
scenario's leftover prepared transactions from contaminating the
next.
Gids embed Date.now() + a random suffix so re-runs within the same
millisecond never collide on the underlying pg_prepared_xacts
table. The cleanup ROLLBACK PREPARED at the end of the
prepare-only scenario is best-effort: failure to roll back leaves
the gid prepared until the next teardown sweep, but the harness's
own teardown handle is the primary cleanup path.
The harness is intentionally NOT exported from the package root (./
subpath) — only ./testing. This mirrors the convention used by
sibling packages (@suaveplan/i18n-shared/testing,
@suaveplan/api-host/testing) so production code can not accidentally
depend on test-only symbols.