Integrate Pact.js Utils with TEA
Integrate Pact.js Utils with TEA
Section titled âIntegrate Pact.js Utils with TEAâ@seontechnologies/pactjs-utils wraps @pact-foundation/pact with type-safe helpers for provider states, PactV4 builders, verifier configuration, and request filters. TEA integrates with it through the tea_use_pactjs_utils config flag, which is on by default.
What the Flag Actually Does
Section titled âWhat the Flag Actually Doesâtea_use_pactjs_utils: true does not mean âthe library is available if you ask for itâ, and it does not mean âadd contract tests to this projectâ. It means: whenever TEA writes a Pact artifact, it writes it with these utilities.
The rule lives in the pactjs-utils-mandate knowledge fragment, which every generating and reviewing workflow loads first. It instantiates the same general contract as the Playwright Utils mandate, documented in library-integration-mandate.
Two gates
Section titled âTwo gatesâThe mandate binds only when both hold:
tea_use_pactjs_utilsistrue.@seontechnologies/pactjs-utilsis a dependency in yourpackage.json.
A flag with no install is an intention, not a capability. TEA will not scaffold imports against a package you do not have, and test-review will not deduct for not using one.
The relevance gate
Section titled âThe relevance gateâSeparately from the two gates above, TEA decides whether a Pact suite belongs in your project at all. It scaffolds one only with evidence of a real consumer-provider boundary:
Any one of these settles it:
- An existing
pact/ortests/contract/directory @pact-foundation/pactalready inpackage.jsonPACT_BROKER_*in the environment or.env.example- A microservices layout: two or more independently deployable services in the repo that call each other
- You asked for contract testing
These are weak on their own and need corroboration: an outbound HTTP call, a generated API client, a service URL in .env.example. Most frontends have all three and call a backend that ships in the same deploy. They count only when the called service has no source in this repo and is not started by this repoâs compose file, dev script, or CI â and a second signal is present.
With none of that, TEA creates no Pact artifacts and says why in the summary. A dead contract suite that fails CI for a boundary the project does not have is worse than no suite, so the default-on flag never turns into unwanted scaffolding.
Substitutions
Section titled âSubstitutionsâREQUIRED â drop-in. Generating the raw-Pact equivalent instead is a defect:
| You need | TEA emits | Not |
|---|---|---|
| A provider state on an interaction | .given(...createProviderState({ name, params })) | .given('name', obj as JsonMap) |
Params coerced to Pactâs JsonMap | toJsonMap(value) | Manual casts, per-call-site null and Date handling |
| PactV4 request/response builder callbacks | setJsonContent({ query?, headers?, body? }), setJsonBody | Repeated inline (b) => { b.query(...); ... } lambdas |
| HTTP provider verification options | buildVerifierOptions({ provider, port, ... }) | A hand-assembled 30-line VerifierOptions object |
| Message/Kafka provider verification | buildMessageVerifierOptions({ ... }) | A second hand-assembled options object |
| Broker URL and consumer version selectors | handlePactBrokerUrlAndSelectors(...) | Hand-written env-var branching per flow |
| Provider version tags in CI | getProviderVersionTags() | Hand-written branch/tag extraction per CI platform |
| Auth injection during provider verification | createRequestFilter({ tokenGenerator }) | Bespoke Express middleware, with its Bearer Bearer bug |
| A provider that needs no auth | noOpRequestFilter | An empty inline function |
RECOMMENDED â needs something the project may not have, so TEA proposes it and names what is missing rather than silently hand-rolling the alternative:
zodToPactMatchers(schema)where a Zod schema already exists, instead of a parallel hand-written matcher tree- The
pact-consumer-diinjection, soexecuteTestcalls your real client withmockServer.urlinstead of rawfetch. It needs an optionalbaseUrlon your API context type: two lines of production code
Real exceptions still ship. MatchersV3 used directly for something zodToPactMatchers cannot express is correct and is not a deviation. Where a genuine gap exists, generated code carries // pactjs-utils deviation: <reason> and the workflow summary lists it.
What Never Relaxes
Section titled âWhat Never RelaxesâThe mandate does not soften the correctness rules from the per-utility fragments. They apply with or without the utilities:
- One
pact.addInteraction()perit()block. PactV4âs Rust FFI drops interactions non-deterministically otherwise. Useit.eachfor parameterized cases. - Consumer Vitest config carries
fileParallelism: falseANDpool: 'forks'ANDpoolOptions.forks.singleFork: true. - Provider Vitest config carries the
pool: 'forks'+singleForkpair. - Provider scrutiny before matchers. Response matchers come from provider source, an OpenAPI spec, or broker data, never from consumer-side types alone.
- Postelâs Law. Matchers in
willRespondWithonly; request bodies inwithRequestuse exact values. - A
// Provider endpoint:comment on every interaction.
Canonical Shapes
Section titled âCanonical ShapesâConsumer test
Section titled âConsumer testâimport { PactV4, MatchersV3 } from '@pact-foundation/pact';import { createProviderState, setJsonBody, setJsonContent } from '@seontechnologies/pactjs-utils';import { getMovieById } from '../../src/api/movies-client';
const { integer, string } = MatchersV3;
const pact = new PactV4({ consumer: 'movie-web', provider: 'SampleMoviesAPI', dir: './pacts' });
describe('Movie API Contract', () => { it('returns a movie by id', async () => { // Provider endpoint: server/src/routes/movies.ts -> GET /movies/:id await pact .addInteraction() .given(...createProviderState({ name: 'movie with id 1 exists', params: { id: 1 } })) .uponReceiving('a request for movie 1') .withRequest('GET', '/movies/1', setJsonContent({ headers: { Accept: 'application/json' } })) .willRespondWith(200, setJsonBody({ id: integer(1), name: string('Inception') })) .executeTest(async (mockServer) => { // The real client, pointed at the mock server const movie = await getMovieById(1, { baseUrl: mockServer.url }); expect(movie.name).toBe('Inception'); }); });});One addInteraction() per it(). A second scenario is a second it(), or it.each â never a second chain in the same block.
Where a Zod schema for the response already exists, zodToPactMatchers(MovieSchema) replaces the inline MatchersV3 tree so the schema stays the single source of the shape.
Provider verification
Section titled âProvider verificationâimport { Verifier } from '@pact-foundation/pact';import { buildVerifierOptions, createRequestFilter } from '@seontechnologies/pactjs-utils';import type { StateHandlers } from '@seontechnologies/pactjs-utils';
const stateHandlers: StateHandlers = { 'movie with id 1 exists': { setup: async (params) => db.seed({ movies: [{ id: params?.id ?? 1 }] }), teardown: async () => db.clean('movies'), },};
await new Verifier( buildVerifierOptions({ provider: 'SampleMoviesAPI', port: '3001', includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true', stateHandlers, requestFilter: createRequestFilter({ tokenGenerator: () => process.env.TEST_AUTH_TOKEN ?? 'test-token' }), }),).verifyProvider();State handler names and their params must match the consumerâs createProviderState exactly. That pairing is the contractâs own contract.
Which Workflows Change
Section titled âWhich Workflows Changeâ| Workflow | What the flag changes |
|---|---|
framework | Installs @seontechnologies/pactjs-utils and @pact-foundation/pact, then scaffolds directories, Vitest configs, scripts, CI workflow, and mandated samples â only when the relevance gate opens |
atdd | Red-phase contract scaffolds generated in the mandated style. A scaffold is the file the developer un-skips and keeps |
automate | The API worker emits contract artifacts in the mandated style and reports deviations |
test-design | Pact code examples in design documents match what automate will generate |
test-review | Scores registry row M10 (a configured contract utility bypassed with no stated deviation, MEDIUM), gated on flag plus install |
ci | Adds the contract-test stage and quality gates |
Pact MCP (tea_pact_mcp)
Section titled âPact MCP (tea_pact_mcp)âAlso on by default, and safe without a broker. It gates a runtime capability rather than a dependency, so its second gate is âare the SmartBear MCP tools reachable in this sessionâ.
When they are, TEA prefers real broker data for provider states, the verification matrix, and can-i-deploy. When they are not, it degrades: falls back to provider source or an OpenAPI spec, states in the output that the broker was unreachable, and continues. No workflow blocks on it, nothing retries in a loop, and inferred provider states are never presented as broker data.
Set tea_pact_mcp: 'none' to stop TEA attempting a broker call at all.
Turning It Off
Section titled âTurning It Offâtea_use_pactjs_utils: false # TEA writes raw @pact-foundation/pact insteadtea_pact_mcp: 'none' # TEA never attempts a broker callTurning tea_use_pactjs_utils off does not disable contract testing. It changes which API the generated tests are written against; the determinism rules and provider scrutiny still apply.
Installation
Section titled âInstallationânpm install -D @seontechnologies/pactjs-utils @pact-foundation/pact# peer dependency: @pact-foundation/pact >= 16.2.0, Node.js >= 18For the remote broker flow, set PACT_BROKER_BASE_URL and PACT_BROKER_TOKEN, plus GITHUB_SHA (GitHub Actions sets this) and GITHUB_BRANCH (set it explicitly: ${{ github.head_ref || github.ref_name }}). The local monorepo flow needs no broker.
Related Guides
Section titled âRelated Guidesâ- Integrate Playwright Utils â the same mandate shape for browser and API suites
- TEA Configuration Reference â every key and its default
- Knowledge Base Index â the contract-testing fragments