Files
suaveplan/packages/api-stack/api/api-2pc/src/testing/conformance.md
2026-05-27 23:29:53 +01:00

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 TwoPcParticipantFactory
    • create(): Promise<{ participant: RegisteredTwoPcParticipant; teardown: () => Promise<void> }>
  • 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.