Files
suaveplan/openspec/changes/enterprise-testing-contract/design.md
2026-05-27 23:29:53 +01:00

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-contract package with zero @suaveplan/* runtime deps (testing-infrastructure exception per CLAUDE.md rule 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 to pacts/, 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/pact feature surface (matchers like like/term/eachLike are out of scope for v0; we ship exact-shape recording first and add matcher syntax in a follow-up).
  • Pulling @pact-foundation/pact itself 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-client consumers 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. A fetch-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.
  • verifyProviderAgainstPact is 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 a messaging package lands.
  • Errors via local TestingContractError. Mirrors the TestingError pattern 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 on verifyProviderAgainstPact for 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_TOKEN keeps secrets out of pact files but requires CI secret plumbing; documented as a follow-up in tasks.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).