Babysitter contract-test-framework
Consumer-driven contract testing for SDK-API compatibility. Generate Pact consumer tests, verify provider contracts, configure Pact broker, and implement can-i-deploy checks.
git clone https://github.com/a5c-ai/babysitter
T=$(mktemp -d) && git clone --depth=1 https://github.com/a5c-ai/babysitter "$T" && mkdir -p ~/.claude/skills && cp -r "$T/library/specializations/sdk-platform-development/skills/contract-test-framework" ~/.claude/skills/a5c-ai-babysitter-contract-test-framework && rm -rf "$T"
library/specializations/sdk-platform-development/skills/contract-test-framework/SKILL.mdcontract-test-framework
You are contract-test-framework - a specialized skill for consumer-driven contract testing between SDKs and APIs, ensuring compatibility and preventing breaking changes through automated verification.
Overview
This skill enables AI-powered contract testing including:
- Generating Pact consumer contracts from SDK usage
- Configuring Pact Broker for contract management
- Provider verification against consumer contracts
- Can-i-deploy safety checks before releases
- Breaking change detection and alerting
- Webhook integration for automated verification
- Support for bidirectional contract testing
Prerequisites
- Node.js 18+ or Python 3.8+
- Pact library for your SDK language
- Pact Broker (PactFlow recommended) or self-hosted
- CI/CD pipeline access
- Consumer SDK and provider API access
Capabilities
1. Consumer Contract Generation for SDKs
Generate contracts from SDK tests:
// tests/contracts/user-api.pact.ts import { PactV3, MatchersV3 } from '@pact-foundation/pact'; import { MyServiceSDK } from '@company/myservice-sdk'; const { like, eachLike, regex, uuid, datetime, integer } = MatchersV3; const provider = new PactV3({ consumer: 'myservice-typescript-sdk', provider: 'myservice-api', logLevel: 'info' }); describe('MyService SDK Contracts', () => { describe('Users API', () => { it('should get user by ID', async () => { const expectedUser = { id: uuid(), email: like('user@example.com'), name: like('John Doe'), createdAt: datetime("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"), status: regex(/active|inactive|pending/, 'active') }; await provider .given('a user with ID exists', { userId: 'user-123' }) .uponReceiving('a request to get user by ID') .withRequest({ method: 'GET', path: '/api/v1/users/user-123', headers: { 'Accept': 'application/json', 'Authorization': regex(/Bearer .+/, 'Bearer test-token') } }) .willRespondWith({ status: 200, headers: { 'Content-Type': 'application/json' }, body: expectedUser }); await provider.executeTest(async (mockServer) => { const sdk = new MyServiceSDK({ baseUrl: mockServer.url, accessToken: 'test-token' }); const user = await sdk.users.get('user-123'); expect(user).toBeDefined(); expect(user.email).toMatch(/@/); }); }); it('should list users with pagination', async () => { await provider .given('users exist') .uponReceiving('a request to list users') .withRequest({ method: 'GET', path: '/api/v1/users', query: { page: '1', limit: '20' } }) .willRespondWith({ status: 200, body: { data: eachLike({ id: uuid(), email: like('user@example.com'), name: like('User Name') }), pagination: { page: integer(1), limit: integer(20), total: integer(100), hasMore: like(true) } } }); await provider.executeTest(async (mockServer) => { const sdk = new MyServiceSDK({ baseUrl: mockServer.url }); const response = await sdk.users.list({ page: 1, limit: 20 }); expect(response.data).toBeInstanceOf(Array); expect(response.pagination.page).toBe(1); }); }); it('should create a new user', async () => { await provider .given('the system is ready') .uponReceiving('a request to create a user') .withRequest({ method: 'POST', path: '/api/v1/users', headers: { 'Content-Type': 'application/json', 'Authorization': regex(/Bearer .+/, 'Bearer test-token') }, body: { email: like('newuser@example.com'), name: like('New User'), password: like('securePassword123') } }) .willRespondWith({ status: 201, body: { id: uuid(), email: like('newuser@example.com'), name: like('New User'), createdAt: datetime("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") } }); await provider.executeTest(async (mockServer) => { const sdk = new MyServiceSDK({ baseUrl: mockServer.url, accessToken: 'test-token' }); const user = await sdk.users.create({ email: 'newuser@example.com', name: 'New User', password: 'securePassword123' }); expect(user.id).toBeDefined(); }); }); it('should handle 404 for non-existent user', async () => { await provider .given('user does not exist', { userId: 'nonexistent' }) .uponReceiving('a request for non-existent user') .withRequest({ method: 'GET', path: '/api/v1/users/nonexistent' }) .willRespondWith({ status: 404, body: { error: { code: like('USER_NOT_FOUND'), message: like('User not found') } } }); await provider.executeTest(async (mockServer) => { const sdk = new MyServiceSDK({ baseUrl: mockServer.url }); await expect(sdk.users.get('nonexistent')) .rejects .toThrow('User not found'); }); }); }); });
2. Multi-SDK Contract Testing
Test contracts for multiple SDK implementations:
# pact-config.yaml consumers: - name: myservice-typescript-sdk language: typescript version: ${GIT_COMMIT} branch: ${GIT_BRANCH} - name: myservice-python-sdk language: python version: ${GIT_COMMIT} branch: ${GIT_BRANCH} - name: myservice-java-sdk language: java version: ${GIT_COMMIT} branch: ${GIT_BRANCH} provider: name: myservice-api baseUrl: http://localhost:3000 broker: url: https://your-broker.pactflow.io token: ${PACT_BROKER_TOKEN} publishResults: true verification: enablePending: true wipPactsSince: '2024-01-01' consumerVersionSelectors: - matchingBranch: true - mainBranch: true - deployedOrReleased: true
3. Provider Verification
Verify API against all SDK contracts:
// tests/contracts/provider-verification.ts import { Verifier } from '@pact-foundation/pact'; import { startServer, stopServer, resetDatabase } from '../test-utils'; describe('Provider Verification', () => { beforeAll(async () => { await startServer(); }); afterAll(async () => { await stopServer(); }); it('should verify all SDK contracts', async () => { const verifier = new Verifier({ provider: 'myservice-api', providerBaseUrl: 'http://localhost:3000', // Pact Broker configuration pactBrokerUrl: process.env.PACT_BROKER_URL, pactBrokerToken: process.env.PACT_BROKER_TOKEN, // Provider version providerVersion: process.env.GIT_COMMIT || '1.0.0', providerVersionBranch: process.env.GIT_BRANCH || 'main', // Consumer selection consumerVersionSelectors: [ { matchingBranch: true }, { mainBranch: true }, { deployedOrReleased: true } ], // State handlers for test setup stateHandlers: { 'a user with ID exists': async (params) => { await resetDatabase(); await db.users.create({ id: params.userId, email: 'user@example.com', name: 'John Doe' }); }, 'users exist': async () => { await resetDatabase(); await db.users.createMany([ { id: 'user-1', email: 'user1@example.com', name: 'User 1' }, { id: 'user-2', email: 'user2@example.com', name: 'User 2' } ]); }, 'user does not exist': async (params) => { await resetDatabase(); // Ensure user doesn't exist await db.users.delete(params.userId).catch(() => {}); }, 'the system is ready': async () => { await resetDatabase(); } }, // Request filters requestFilter: (req, res, next) => { // Add test authentication if (!req.headers.authorization) { req.headers.authorization = 'Bearer test-token'; } next(); }, // Publish results publishVerificationResult: true, enablePending: true, includeWipPactsSince: '2024-01-01' }); await verifier.verifyProvider(); }); });
4. CI/CD Pipeline Integration
Complete GitHub Actions workflow:
name: SDK Contract Testing on: push: branches: [main, develop] pull_request: branches: [main] env: PACT_BROKER_URL: https://your-broker.pactflow.io PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }} jobs: # Consumer SDK contract tests sdk-contracts: runs-on: ubuntu-latest strategy: matrix: sdk: [typescript, python, java] steps: - uses: actions/checkout@v4 - name: Setup SDK environment uses: ./.github/actions/setup-${{ matrix.sdk }} - name: Install dependencies run: | cd sdks/${{ matrix.sdk }} ${{ matrix.sdk == 'typescript' && 'npm ci' || matrix.sdk == 'python' && 'pip install -e .[dev]' || 'mvn install -DskipTests' }} - name: Run contract tests run: | cd sdks/${{ matrix.sdk }} ${{ matrix.sdk == 'typescript' && 'npm run test:contract' || matrix.sdk == 'python' && 'pytest tests/contracts' || 'mvn test -Dtest=*Pact*' }} - name: Publish contracts if: github.event_name == 'push' run: | npx @pact-foundation/pact-cli publish \ sdks/${{ matrix.sdk }}/pacts \ --consumer-app-version ${{ github.sha }} \ --branch ${{ github.ref_name }} \ --broker-base-url $PACT_BROKER_URL \ --broker-token $PACT_BROKER_TOKEN # Provider verification provider-verification: runs-on: ubuntu-latest needs: sdk-contracts steps: - uses: actions/checkout@v4 with: repository: your-org/myservice-api - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm ci - name: Start provider run: npm run start:test & - name: Wait for provider run: npx wait-on http://localhost:3000/health - name: Verify contracts run: npm run test:contract:provider env: PROVIDER_VERSION: ${{ github.sha }} PROVIDER_BRANCH: ${{ github.ref_name }} # Deployment safety check can-i-deploy: runs-on: ubuntu-latest needs: [sdk-contracts, provider-verification] if: github.ref == 'refs/heads/main' strategy: matrix: participant: - myservice-typescript-sdk - myservice-python-sdk - myservice-java-sdk - myservice-api steps: - name: Can I deploy? run: | docker run --rm pactfoundation/pact-cli \ broker can-i-deploy \ --pacticipant ${{ matrix.participant }} \ --version ${{ github.sha }} \ --to-environment production \ --broker-base-url $PACT_BROKER_URL \ --broker-token ${{ secrets.PACT_BROKER_TOKEN }} # Record deployment record-deployment: runs-on: ubuntu-latest needs: can-i-deploy if: github.ref == 'refs/heads/main' strategy: matrix: participant: - myservice-typescript-sdk - myservice-python-sdk - myservice-java-sdk - myservice-api steps: - name: Record deployment run: | docker run --rm pactfoundation/pact-cli \ broker record-deployment \ --pacticipant ${{ matrix.participant }} \ --version ${{ github.sha }} \ --environment production \ --broker-base-url $PACT_BROKER_URL \ --broker-token ${{ secrets.PACT_BROKER_TOKEN }}
5. Webhook Configuration
Set up automated verification webhooks:
# Create webhook for SDK changes pact-broker create-webhook \ 'https://api.github.com/repos/your-org/myservice-api/dispatches' \ --request=POST \ --header 'Accept: application/vnd.github.v3+json' \ --header 'Authorization: Bearer ${GITHUB_TOKEN}' \ --data '{ "event_type": "contract_requiring_verification", "client_payload": { "pact_url": "${pactbroker.pactUrl}", "consumer_name": "${pactbroker.consumerName}", "provider_name": "${pactbroker.providerName}" } }' \ --description "Trigger API verification on SDK contract change" \ --contract-content-changed \ --provider myservice-api \ --broker-base-url https://your-broker.pactflow.io \ --broker-token $PACT_BROKER_TOKEN # Create webhook for verification results pact-broker create-webhook \ 'https://api.github.com/repos/your-org/myservice-sdk/statuses/${pactbroker.consumerVersionNumber}' \ --request=POST \ --header 'Authorization: Bearer ${GITHUB_TOKEN}' \ --data '{ "state": "${pactbroker.verificationResultSuccess ? \"success\" : \"failure\"}", "description": "Contract verification ${pactbroker.verificationResultSuccess ? \"passed\" : \"failed\"}", "context": "pact/provider-verification" }' \ --description "Update SDK commit status on verification" \ --provider-verification-published \ --broker-base-url https://your-broker.pactflow.io \ --broker-token $PACT_BROKER_TOKEN
6. Breaking Change Detection
Detect and handle breaking changes:
// scripts/check-breaking-changes.ts import { PactBrokerClient } from '@pact-foundation/pact'; async function checkBreakingChanges( provider: string, newVersion: string ): Promise<BreakingChangeReport> { const client = new PactBrokerClient({ brokerBaseUrl: process.env.PACT_BROKER_URL!, token: process.env.PACT_BROKER_TOKEN }); // Get current production version const prodVersion = await client.getLatestVersionForEnvironment( provider, 'production' ); // Compare contracts const comparison = await client.compareVersions( provider, prodVersion, newVersion ); const breakingChanges: BreakingChange[] = []; for (const diff of comparison.differences) { if (diff.isBreaking) { breakingChanges.push({ type: diff.type, path: diff.path, description: diff.description, affectedConsumers: diff.consumers }); } } return { hasBreakingChanges: breakingChanges.length > 0, breakingChanges, recommendation: breakingChanges.length > 0 ? 'Major version bump required' : 'Safe to release' }; }
MCP Server Integration
This skill can leverage the following MCP servers:
| Server | Description | Installation |
|---|---|---|
| PactFlow MCP Server | AI-powered contract testing | PactFlow Blog |
| Specmatic MCP Server | Contract testing and mocks | GitHub |
Best Practices
- Consumer-first design - Write consumer tests before implementation
- Meaningful states - Use descriptive provider state names
- Version with git - Use commit SHAs for versions
- Test all SDKs - Ensure all language SDKs have contracts
- Can-i-deploy gates - Block deployments without verification
- Webhook automation - Trigger verification automatically
- Environment tracking - Record deployments per environment
- Pending pacts - Enable for new SDK versions
Process Integration
This skill integrates with the following processes:
- SDK testing patternssdk-testing-strategy.js
- Cross-SDK compatibilitycompatibility-testing.js
- Breaking change managementbackward-compatibility-management.js
- Release coordinationsdk-versioning-release-management.js
Output Format
{ "operation": "verify", "provider": "myservice-api", "providerVersion": "abc123", "consumers": [ { "name": "myservice-typescript-sdk", "version": "def456", "status": "passed", "interactions": 12, "passed": 12, "failed": 0 }, { "name": "myservice-python-sdk", "version": "ghi789", "status": "passed", "interactions": 10, "passed": 10, "failed": 0 } ], "canDeploy": true, "environment": "production", "verificationUrl": "https://broker.pactflow.io/matrix/provider/myservice-api/version/abc123" }
Error Handling
- Handle missing provider states gracefully
- Provide clear mismatch descriptions
- Log full request/response on failures
- Support retry for transient broker failures
- Document breaking changes clearly
Constraints
- Contracts represent consumer needs only
- Provider states must be reproducible
- Broker must be accessible from CI/CD
- Version management is critical
- Breaking changes require coordination across SDKs