Skip to content
🤖 Consolidated, AI-optimized BMAD docs: llms-full.txt. Fetch this plain text file for complete context.
🚀 Build your own BMad modules and share them with the community! Get started or submit to the marketplace.

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.

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.

The mandate binds only when both hold:

  1. tea_use_pactjs_utils is true.
  2. @seontechnologies/pactjs-utils is a dependency in your package.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.

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/ or tests/contract/ directory
  • @pact-foundation/pact already in package.json
  • PACT_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.

REQUIRED — drop-in. Generating the raw-Pact equivalent instead is a defect:

You needTEA emitsNot
A provider state on an interaction.given(...createProviderState({ name, params })).given('name', obj as JsonMap)
Params coerced to Pact’s JsonMaptoJsonMap(value)Manual casts, per-call-site null and Date handling
PactV4 request/response builder callbackssetJsonContent({ query?, headers?, body? }), setJsonBodyRepeated inline (b) => { b.query(...); ... } lambdas
HTTP provider verification optionsbuildVerifierOptions({ provider, port, ... })A hand-assembled 30-line VerifierOptions object
Message/Kafka provider verificationbuildMessageVerifierOptions({ ... })A second hand-assembled options object
Broker URL and consumer version selectorshandlePactBrokerUrlAndSelectors(...)Hand-written env-var branching per flow
Provider version tags in CIgetProviderVersionTags()Hand-written branch/tag extraction per CI platform
Auth injection during provider verificationcreateRequestFilter({ tokenGenerator })Bespoke Express middleware, with its Bearer Bearer bug
A provider that needs no authnoOpRequestFilterAn 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-di injection, so executeTest calls your real client with mockServer.url instead of raw fetch. It needs an optional baseUrl on 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.

The mandate does not soften the correctness rules from the per-utility fragments. They apply with or without the utilities:

  • One pact.addInteraction() per it() block. PactV4’s Rust FFI drops interactions non-deterministically otherwise. Use it.each for parameterized cases.
  • Consumer Vitest config carries fileParallelism: false AND pool: 'forks' AND poolOptions.forks.singleFork: true.
  • Provider Vitest config carries the pool: 'forks' + singleFork pair.
  • 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 willRespondWith only; request bodies in withRequest use exact values.
  • A // Provider endpoint: comment on every interaction.
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.

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.

WorkflowWhat the flag changes
frameworkInstalls @seontechnologies/pactjs-utils and @pact-foundation/pact, then scaffolds directories, Vitest configs, scripts, CI workflow, and mandated samples — only when the relevance gate opens
atddRed-phase contract scaffolds generated in the mandated style. A scaffold is the file the developer un-skips and keeps
automateThe API worker emits contract artifacts in the mandated style and reports deviations
test-designPact code examples in design documents match what automate will generate
test-reviewScores registry row M10 (a configured contract utility bypassed with no stated deviation, MEDIUM), gated on flag plus install
ciAdds the contract-test stage and quality gates

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.

_bmad/tea/config.yaml
tea_use_pactjs_utils: false # TEA writes raw @pact-foundation/pact instead
tea_pact_mcp: 'none' # TEA never attempts a broker call

Turning 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.

Terminal window
npm install -D @seontechnologies/pactjs-utils @pact-foundation/pact
# peer dependency: @pact-foundation/pact >= 16.2.0, Node.js >= 18

For 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.