smart-pact
v1.0.8
Published
Zero-maintenance contract testing: auto-generates Pact contracts from real traffic, catches data-level regressions, manages service dependencies, and integrates into any CI/CD pipeline.
Maintainers
Readme
SmartPact
A zero-maintenance wrapper around
@pact-foundation/pactthat solves every major pain point of consumer-driven contract testing at scale.
SmartPact adds an intelligent layer on top of standard Pact by:
- Auto-generating contracts from real HTTP traffic — no manual interaction writing
- Detecting data regressions beyond structural checks (the #1 gap in plain Pact)
- Auto-discovering all consumer-provider relationships from contract files
- Notifying teams on Slack/webhook when contracts change or break
- Managing test data using real captured payloads, not synthetic mocks
- Supporting async/message contracts (Kafka, RabbitMQ, etc.)
- Integrating into CI/CD with a single command and proper exit codes
- Providing a click-to-approve workflow for provider API changes
The 8 Problems SmartPact Solves
| # | Pact Limitation | SmartPact Solution |
|---|---|---|
| 1 | Manual contract creation & maintenance | TrafficRecorder auto-captures real traffic |
| 2 | Only tests scenarios you manually defined | Records ALL interactions, 24/7 |
| 3 | Catches structure failures, misses data errors | RegressionDetector does deep field/type/value diff |
| 4 | Manual cross-team communication on changes | Notifier sends Slack alerts automatically |
| 5 | No auto-discovery of service dependencies | DependencyGraphManager builds live graph from contracts |
| 6 | Synthetic test data diverges from production | TestDataManager uses real captured payloads |
| 7 | Immature async/message support | AsyncMessageRecorder + AsyncContractVerifier |
| 8 | Hard to integrate into CI/CD pipelines | CiRunner with GitHub Actions, GitLab, Bitbucket support |
Installation
npm install smart-pact @pact-foundation/pactQuick Start
1. Create a SmartPact instance
import { SmartPact } from 'smart-pact';
const smartPact = new SmartPact({
pactDir: './pacts',
recordingsDir: './recordings',
notifications: {
slack: {
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
channel: '#contract-alerts',
mentionOnBreaking: '@channel',
},
},
});
await smartPact.start();
``
### 2. Record real traffic (Express middleware)
```typescript
// Attach to your Express app — captures every request/response automatically
app.use(smartPact.middleware('frontend', 'user-api'));3. Or record outgoing Axios calls
import axios from 'axios';
smartPact.recordAxios(axios, 'frontend', 'user-api');4. Generate contracts from recordings
// Run once after traffic is captured (or in CI)
const contractPath = await smartPact.generateContract('frontend', 'user-api');
// → ./pacts/frontend-user-api.json (fully auto-generated)5. Verify with deep regression detection
const report = await smartPact.verify('frontend', 'user-api', 'http://user-api:3000');
console.log(report.passed); // interactions that passed all checks
console.log(report.failed); // interactions with breaking regressions
console.log(report.findings); // detailed field-level diffs6. See your dependency map
smartPact.printDependencyMap();
// ┌────────────────────────────────────┐
// Service Dependency Map
// ─────────────────────
// ✅ user-api
// └─ consumed by: frontend (v1.2.0, 8 interactions)
// └─ consumed by: mobile (v1.1.0, 5 interactions)
// ✅ payment-api
// └─ consumed by: frontend (v2.0.0, 3 interactions)7. Run CI/CD verification
// In your CI script
const suite = await smartPact.runCi({
providerUrls: {
'user-api': 'http://user-api:3000',
'payment-api': 'http://payment-api:3001',
},
failOnBreaking: true, // exit code 1 on breaking regressions
});Regression Detection
SmartPact goes far beyond what standard Pact checks. It detects:
const report = await smartPact.verify('frontend', 'user-api', 'http://user-api:3000');
for (const finding of report.findings) {
console.log(finding.kind); // 'field_removed' | 'type_changed' | 'value_changed' | 'field_added' | 'status_changed'
console.log(finding.severity); // 'breaking' | 'warning' | 'info'
console.log(finding.path); // e.g. 'data.user.phone'
console.log(finding.expected); // what the contract said
console.log(finding.actual); // what the provider actually returned
}Example findings:
breaking field_removed data.user.phone (was "+234...", now missing)
breaking type_changed data.price (was number, now string)
breaking status_changed status (expected 200, got 404)
warning value_changed data.user.name (was "Alice", now "Alice Smith")
info field_added data.user.avatarUrl (new field, non-breaking)Standard Pact would pass all of the above as long as the top-level structure matches. SmartPact catches them all.
Async / Message Queue Contracts
// Producer side — record messages as they're published
const recorder = smartPact.asyncRecorder('order-service', 'notification-service');
// In your order creation code:
await orderService.createOrder(payload);
await recorder.record('order.created', { orderId: 123, userId: 456, status: 'pending' });
// Auto-generate contract from recorded messages
await recorder.generateContract('./pacts');// Consumer side — verify your handler processes the contract's messages
const verifier = smartPact.asyncVerifier();
const results = await verifier.verify(contract, async (topic, payload) => {
await notificationService.handle(topic, payload);
});Provider Approval Workflow
When a provider changes its API, no more "update the Pact file and tell everyone":
// Provider team: "We're changing the /users endpoint"
const approvals = await smartPact.requestApproval(
'user-api',
['data.user.phone', 'data.user.address'], // changed fields
'1.0.0', '2.0.0'
);
// → Slack messages sent to all consumer teams automatically
// Consumer team: approve with one call
await smartPact.approveChange(approvals[0].id, 'team-mobile');
// Or reject it to block the provider deploy
await smartPact.rejectChange(approvals[0].id, 'team-frontend', 'We rely on the phone field');
// See what needs your attention
const pending = await smartPact.getPendingApprovals('frontend');Test Data with Real Payloads
const data = smartPact.testData();
// Derive provider state fixtures from real traffic
const fixtures = data.deriveFixtures(recorder.getInteractions());
await data.saveFixtures(fixtures);
// → fixtures.json contains actual responses captured from production/staging
// Build real-data mocks to stub downstream dependencies
const mocks = data.buildMocks(recorder.getInteractions());
const mockServer = await smartPact.startMockServer('frontend', 'user-api', 4010);
// → Mock server serves real payloads on http://localhost:4010CI/CD Integration
GitHub Actions
- name: Run SmartPact contract verification
run: node scripts/smart-pact-ci.js
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}// scripts/smart-pact-ci.ts
import { CiRunner, writeGitHubActionsOutput } from 'smart-pact';
const runner = new CiRunner();
const suite = await runner.runAndExit({
pactDir: './pacts',
providerUrls: {
'user-api': process.env.USER_API_URL!,
'payment-api': process.env.PAYMENT_API_URL!,
},
failOnBreaking: true,
});
writeGitHubActionsOutput(suite);Output in GitHub Actions:
╔═══════════════════════════════════════╗
║ SmartPact CI Runner ║
║ Zero-maintenance contract testing ║
╚═══════════════════════════════════════╝
─────────────────────────────────────────
SmartPact Results (1243ms)
─────────────────────────────────────────
✅ frontend → user-api (8 interactions, 0 breaking)
❌ mobile → payment-api (3 interactions, 1 breaking)
⚠️ [data.price] Type changed: number → string
─────────────────────────────────────────
Total: 2 ✅ 1 ❌ 1
Exit code: 1
─────────────────────────────────────────API Reference
SmartPact
| Method | Description |
|---|---|
| start() | Start dependency graph watcher |
| middleware(consumer, provider) | Express middleware for recording |
| recordAxios(axios, consumer, provider) | Axios interceptor for outgoing requests |
| generateContract(consumer, provider) | Generate Pact file from recordings |
| verify(consumer, provider, url) | Verify + deep regression check |
| printDependencyMap() | ASCII graph of all service relationships |
| getImpactedServices(provider) | Transitive consumers of a provider |
| requestApproval(provider, fields, from, to) | Notify consumers of API change |
| approveChange(id, approvedBy) | Consumer approves a change |
| rejectChange(id, rejectedBy, reason) | Consumer rejects a change |
| getPendingApprovals(consumer?) | Get pending approvals for a consumer |
| asyncRecorder(producer, consumer) | Create async message recorder |
| asyncVerifier() | Create async contract verifier |
| testData() | Access TestDataManager |
| startMockServer(consumer, provider, port) | Start real-data mock server |
| runCi(opts) | Run full CI verification pipeline |
Configuration
interface SmartPactConfig {
pactDir: string; // Where to store pact JSON files
recordingsDir: string; // Where to store traffic recordings
logLevel?: 'debug' | 'info' | 'warn' | 'error';
deduplicateInteractions?: boolean; // Merge duplicate interactions (default: true)
versionBump?: 'patch' | 'minor' | 'major'; // Contract version strategy
notifications?: {
slack?: {
webhookUrl: string;
channel?: string;
mentionOnBreaking?: string; // e.g. '@channel'
};
webhook?: {
url: string;
headers?: Record<string, string>;
};
console?: boolean; // Log to stdout (default: true)
};
}Architecture
SmartPact (facade)
│
├── TrafficRecorder ← Captures real HTTP traffic (Express / Axios)
├── ContractGenerator ← Builds Pact JSON from recordings + matchers
├── DependencyGraphManager ← Auto-discovers service relationships
├── RegressionDetector ← Deep field/type/value diff against live provider
├── Notifier ← Slack + webhook alerts
├── TestDataManager ← Real-data fixtures + mock server
├── AsyncMessageRecorder ← Records pub/sub messages
├── AsyncContractVerifier ← Verifies consumer handlers against async contracts
├── ContractManager ← Orchestrates lifecycle + approval workflow
└── CiRunner ← Full CI pipeline runnerLicense
MIT
