4.0 KiB
Context
The four packages targeted by this change all sit on a network boundary: oauth-client calls Authorization Code / PKCE / token / discovery endpoints; webhook-client both signs outbound webhook deliveries (consumer of the receiver) and ships a receiver shape (provider for downstream consumers); schema-registry is itself a registry server but is also a consumer of upstream registries during compatibility checks; api-schemas defines the wire shapes that every API plugin consumer sends. Each of these surfaces has a real, reproducible failure mode where the provider shape mutates while consumer expectations do not — exactly the failure class Pact-style contract testing is designed to catch.
The audit (/mnt/s/web/genesis/docs/superpowers/specs/2026-04-25-test-suite-audit.md §2 row "No contract tests") flags this as a critical missing tier. The audit also explicitly names these four packages as the canonical first-wave consumers.
Goals / Non-Goals
Goals:
- A self-contained
@suaveplan/testing-contractpackage with zero@suaveplan/*runtime deps (testing-infrastructure exception perCLAUDE.mdrule 8) that produces and verifies Pact Specification v3 files. - A normative requirement on each of the four consumer packages: ship
*.contract.test.ts, commit pact files topacts/, run provider verification where a server is present. - Optional Pact Broker integration so multi-repo deployments can publish/pull pacts without the file commit.
Non-Goals:
- Replicating the entire
@pact-foundation/pactfeature surface (matchers likelike/term/eachLikeare out of scope for v0; we ship exact-shape recording first and add matcher syntax in a follow-up). - Pulling
@pact-foundation/pactitself as a dependency — it is Node-only and ships a Ruby standalone, neither of which fit the universal package constraint. - Bidirectional contract testing (Pactflow's bi-di flow). Classical consumer-driven only.
Decisions
- Pact Specification v3 as the wire format. Future migration to v4 is plausible but v3 is the broadest-supported version across brokers (Pactflow, OSS Pact Broker) and matches the field shapes our
oauth-client/webhook-clientconsumers already think in. pacts/committed in-repo. Cross-package contract files live next to the consumer, not in a broker-only flow, because the genesis monorepo gives us atomic provider+consumer commits — broker integration is opt-in for downstream multi-repo deployments.recordPactInteraction(httpClient)rather than middleware. Afetch-shaped wrapper composes with every consumer's existing injectable-fetch DI surface (every package on genesis exposes{ fetch?: FetchLike }or equivalent). No instrumentation of the consumer's internals is required.verifyProviderAgainstPactis HTTP-only. Replaying a recorded pact against a live provider URL covers OAuth, webhook receivers, schema registry, and API-schemas consumers. Message contract testing (queues, pubsub) is deferred until amessagingpackage lands.- Errors via local
TestingContractError. Mirrors theTestingErrorpattern in@suaveplan/testing— testing-infrastructure exception keeps the package free of@suaveplan/error.
Risks / Trade-offs
- No matcher syntax in v0 means consumer pacts are exact-shape; provider drift on incidental fields (timestamps, request ids) will trigger spurious failures. Mitigated by an explicit
ignoreFields: string[]option onverifyProviderAgainstPactfor v0; full matcher support tracked as follow-up. - Pact files committed in-repo add line-noise to PRs that touch wire formats. This is the point — wire-format drift becomes reviewable diff, not silent runtime breakage.
- Broker auth via
PACT_BROKER_TOKENkeeps secrets out of pact files but requires CI secret plumbing; documented as a follow-up intasks.md. - Universal-package constraint means the package cannot use
@pact-foundation/pact's native verifier; the in-house verifier must be kept simple and well-tested (100% coverage gate enforces this).