6.1 KiB
ADDED Requirements
Requirement: Package isolation
The @suaveplan/testing-contract package dependencies, peerDependencies, and devDependencies MUST NOT list any @suaveplan/* scoped package other than @suaveplan/types. External catalog deps only (zod, build tooling). This mirrors the testing-infrastructure carve-out documented in CLAUDE.md rule 8.
Scenario: Dep-graph guard
- WHEN
bunx turbo ls --filter=@suaveplan/testing-contract...is run - THEN no
@suaveplan/*package other than@suaveplan/typesappears in the forward closure of@suaveplan/testing-contract
Requirement: Pact Specification v3 file shape
Pact files emitted and consumed by the package SHALL conform to Pact Specification v3 — a JSON document with consumer.name, provider.name, interactions[] (each carrying description, optional providerStates[], request, response), and metadata.pactSpecification.version === "3.0.0". Field order in serialised output MUST be deterministic so file diffs are reviewable.
Scenario: Round-trip preserves byte equality
- WHEN a pact file is read via
readPactFile, then written viawritePactFileto a new path - THEN the new file is byte-identical to the original
Scenario: Spec version asserted on read
- WHEN
readPactFileis given a file whosemetadata.pactSpecification.version !== "3.0.0" - THEN the function throws
TestingContractErrorwithcode === "TESTING_CONTRACT.PACT_FILE.UNSUPPORTED_VERSION"
Requirement: Consumer-side recording API
defineConsumerInteraction({ provider, request, response, state? }) SHALL register an expected interaction. recordPactInteraction(httpClient) SHALL wrap a FetchLike such that every request issued through the wrapped client is matched against registered interactions and accumulated into a pact file emitted by flushPact(path). Unmatched interactions and interactions with no observed traffic MUST both be reported, distinctly.
Scenario: Recorded interaction lands in pact file
- WHEN
defineConsumerInteraction({ provider:"P", request:{ method:"GET", path:"/x" }, response:{ status:200, body:{ ok:true } } })is registered AND arecordPactInteraction-wrapped fetch hitsGET /xANDflushPact("p.json")runs - THEN
p.jsoncontains exactly one interaction matching the registered shape
Scenario: Unmatched traffic surfaces as error
- WHEN the wrapped fetch issues a request that matches no registered interaction AND
flushPactis called - THEN
flushPactthrowsTestingContractErrorwithcode === "TESTING_CONTRACT.RECORD.UNMATCHED_REQUEST"ANDcontext.methodandcontext.pathidentify the unmatched request
Scenario: Unused interaction surfaces as error
- WHEN an interaction is registered AND the wrapped fetch issues no matching request AND
flushPactis called - THEN
flushPactthrowsTestingContractErrorwithcode === "TESTING_CONTRACT.RECORD.UNUSED_INTERACTION"ANDcontext.descriptionidentifies the interaction
Requirement: Provider-side verification
verifyProviderAgainstPact(pactFile, providerUrl, options) SHALL replay each interaction in the pact file against the supplied providerUrl, asserting that each response's status, headers (where recorded), and body shape match the recorded shape modulo options.ignoreFields. The function MUST use an injectable fetchFn. State handlers registered via options.stateHandlers MUST run before each interaction whose providerStates[] includes the matching state name.
Scenario: Provider response matches recorded shape
- WHEN the provider returns
{ status:200, body:{ ok:true } }for the interactionGET /xANDverifyProviderAgainstPactis called - THEN the function resolves with
{ passed: true, results: [{ description, ok: true }] }
Scenario: Provider response diverges
- WHEN the provider returns
{ status:500 }instead of the recorded200 - THEN the function throws
TestingContractErrorwithcode === "TESTING_CONTRACT.VERIFY.MISMATCH"ANDcontext.diffcontains the structured path → expected/actual diff
Scenario: State handler runs before matching interaction
- WHEN an interaction declares
providerStates:[{ name:"user-exists" }]ANDoptions.stateHandlers["user-exists"]is registered - THEN the registered handler is awaited exactly once before the interaction's request is replayed
Requirement: Pact Broker integration
publishPact(brokerUrl, version) SHALL POST the pact file to the broker's pacts/provider/<P>/consumer/<C>/version/<v> endpoint. pullPactsForVerification(brokerUrl, providerName) SHALL GET the broker's verification endpoint and return PactFile[]. Both functions MUST authenticate via the PACT_BROKER_TOKEN env var (Bearer scheme); when unset, both MUST throw before any HTTP call.
Scenario: Publish includes Bearer token
- WHEN
PACT_BROKER_TOKEN=abcis set ANDpublishPact("https://broker", "1.2.3")runs - THEN the issued POST has header
Authorization: Bearer abc
Scenario: Missing token aborts before HTTP
- WHEN
PACT_BROKER_TOKENis unset ANDpublishPact(...)is called - THEN the function throws
TestingContractErrorwithcode === "TESTING_CONTRACT.BROKER.MISSING_TOKEN"AND no HTTP request is issued
Requirement: Error code prefix
Every error thrown by the package SHALL be a TestingContractError whose code begins with TESTING_CONTRACT.. Plain Error / TypeError MUST NOT be thrown from any public code path.
Scenario: Code namespace enforced
- WHEN any public function throws
- THEN the thrown value
instanceof TestingContractError === trueANDerror.code.startsWith("TESTING_CONTRACT.") === true
Requirement: Coverage gate
Istanbul statement, branch, function, and line coverage across packages/testing/testing-contract/src/ (excluding *.bench.ts and *.schemas.ts) MUST be 100%. The pre-publish gate MUST hard-fail below 100%.
Scenario: Coverage gate trips below 100%
- WHEN any line, branch, function, or statement is uncovered
- THEN the pre-publish gate fails AND the package MUST NOT be published