# `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 ```ts // 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: ```ts 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 TwoPcParticipantFactory` - `create(): Promise<{ participant: RegisteredTwoPcParticipant; teardown: () => Promise }>` - `function run2pcParticipantConformance(factory): void` - Registers six `describe`/`it` blocks against the surrounding runner. Returns nothing. ## 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.