autotel-pact 6.0.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -4
- package/src/attrs.test.ts +0 -35
- package/src/attrs.ts +0 -53
- package/src/audit.test.ts +0 -189
- package/src/audit.ts +0 -251
- package/src/auto-wrap.test.ts +0 -149
- package/src/auto-wrap.ts +0 -283
- package/src/broker.test.ts +0 -175
- package/src/broker.ts +0 -118
- package/src/cli.test.ts +0 -148
- package/src/cli.ts +0 -287
- package/src/index.ts +0 -94
- package/src/labels.ts +0 -25
- package/src/ledger-normalize.test.ts +0 -141
- package/src/ledger-normalize.ts +0 -82
- package/src/ledger.test.ts +0 -92
- package/src/ledger.ts +0 -156
- package/src/pact-file.test.ts +0 -124
- package/src/pact-file.ts +0 -65
- package/src/processor.test.ts +0 -90
- package/src/processor.ts +0 -191
- package/src/tag.test.ts +0 -72
- package/src/tag.ts +0 -21
- package/src/types.ts +0 -169
- package/src/wrapper-http.test.ts +0 -133
- package/src/wrapper-http.ts +0 -194
- package/src/wrapper-provider.test.ts +0 -132
- package/src/wrapper-provider.ts +0 -163
- package/src/wrapper.test.ts +0 -176
- package/src/wrapper.ts +0 -221
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autotel-pact",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.1",
|
|
4
4
|
"description": "Runtime evidence for Pact contracts — autotel bridge that records which contract interactions were actually exercised, and audits 'contracted but never observed'.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,7 +52,6 @@
|
|
|
52
52
|
},
|
|
53
53
|
"files": [
|
|
54
54
|
"dist",
|
|
55
|
-
"src",
|
|
56
55
|
"schemas",
|
|
57
56
|
"README.md"
|
|
58
57
|
],
|
|
@@ -70,7 +69,7 @@
|
|
|
70
69
|
"license": "MIT",
|
|
71
70
|
"peerDependencies": {
|
|
72
71
|
"@pact-foundation/pact": ">=16",
|
|
73
|
-
"autotel": "4.1
|
|
72
|
+
"autotel": "4.2.1"
|
|
74
73
|
},
|
|
75
74
|
"peerDependenciesMeta": {
|
|
76
75
|
"@pact-foundation/pact": {
|
|
@@ -84,7 +83,7 @@
|
|
|
84
83
|
"tsdown": "^0.22.2",
|
|
85
84
|
"typescript": "^6.0.3",
|
|
86
85
|
"vitest": "^4.1.8",
|
|
87
|
-
"autotel": "4.1
|
|
86
|
+
"autotel": "4.2.1"
|
|
88
87
|
},
|
|
89
88
|
"repository": {
|
|
90
89
|
"type": "git",
|
package/src/attrs.test.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { buildPactAttributes, outcomeAttribute, PACT_ATTRS } from './attrs.js';
|
|
3
|
-
import type { PactInteractionMeta } from './types.js';
|
|
4
|
-
|
|
5
|
-
const meta: PactInteractionMeta = {
|
|
6
|
-
consumer: 'OrderShipper',
|
|
7
|
-
provider: 'OrderService',
|
|
8
|
-
description: 'an OrderCreated event',
|
|
9
|
-
states: ['an order exists'],
|
|
10
|
-
kind: 'message',
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
describe('buildPactAttributes', () => {
|
|
14
|
-
it('emits the canonical pact.* attribute set', () => {
|
|
15
|
-
const attrs = buildPactAttributes(meta);
|
|
16
|
-
expect(attrs[PACT_ATTRS.CONSUMER]).toBe('OrderShipper');
|
|
17
|
-
expect(attrs[PACT_ATTRS.PROVIDER]).toBe('OrderService');
|
|
18
|
-
expect(attrs[PACT_ATTRS.KIND]).toBe('message');
|
|
19
|
-
expect(attrs[PACT_ATTRS.INTERACTION_DESCRIPTION]).toBe('an OrderCreated event');
|
|
20
|
-
expect(attrs[PACT_ATTRS.INTERACTION_STATES]).toEqual(['an order exists']);
|
|
21
|
-
expect(attrs[PACT_ATTRS.CONTRACT_FILE]).toBeUndefined();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('includes pact.contract.file when supplied', () => {
|
|
25
|
-
const attrs = buildPactAttributes(meta, { contractFile: 'pacts/OrderShipper-OrderService.json' });
|
|
26
|
-
expect(attrs[PACT_ATTRS.CONTRACT_FILE]).toBe('pacts/OrderShipper-OrderService.json');
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('outcomeAttribute', () => {
|
|
31
|
-
it('returns a single-key record for the outcome', () => {
|
|
32
|
-
expect(outcomeAttribute('passed')).toEqual({ [PACT_ATTRS.OUTCOME]: 'passed' });
|
|
33
|
-
expect(outcomeAttribute('failed')).toEqual({ [PACT_ATTRS.OUTCOME]: 'failed' });
|
|
34
|
-
});
|
|
35
|
-
});
|
package/src/attrs.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { PactInteractionMeta, PactOutcome } from './types.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Attribute keys for Pact interactions. Centralised so the namespace is
|
|
5
|
-
* a single source of truth and is forward-compatible with eventual OTel
|
|
6
|
-
* semantic conventions.
|
|
7
|
-
*/
|
|
8
|
-
export const PACT_ATTRS = {
|
|
9
|
-
CONSUMER: 'pact.consumer',
|
|
10
|
-
PROVIDER: 'pact.provider',
|
|
11
|
-
KIND: 'pact.kind',
|
|
12
|
-
INTERACTION_DESCRIPTION: 'pact.interaction.description',
|
|
13
|
-
INTERACTION_ID: 'pact.interaction.id',
|
|
14
|
-
INTERACTION_STATES: 'pact.interaction.states',
|
|
15
|
-
CONTRACT_FILE: 'pact.contract.file',
|
|
16
|
-
OUTCOME: 'pact.outcome',
|
|
17
|
-
} as const;
|
|
18
|
-
|
|
19
|
-
export type PactAttributeKey = (typeof PACT_ATTRS)[keyof typeof PACT_ATTRS];
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Build the set of attributes to stamp on a span when an interaction is
|
|
23
|
-
* about to be exercised. `outcome` is added later by the wrapper.
|
|
24
|
-
*/
|
|
25
|
-
export function buildPactAttributes(
|
|
26
|
-
meta: PactInteractionMeta,
|
|
27
|
-
opts: { contractFile?: string } = {},
|
|
28
|
-
): Record<string, string | string[]> {
|
|
29
|
-
const attrs: Record<string, string | string[]> = {
|
|
30
|
-
[PACT_ATTRS.CONSUMER]: meta.consumer,
|
|
31
|
-
[PACT_ATTRS.PROVIDER]: meta.provider,
|
|
32
|
-
[PACT_ATTRS.KIND]: meta.kind,
|
|
33
|
-
[PACT_ATTRS.INTERACTION_DESCRIPTION]: meta.description,
|
|
34
|
-
[PACT_ATTRS.INTERACTION_STATES]: meta.states,
|
|
35
|
-
};
|
|
36
|
-
if (opts.contractFile) {
|
|
37
|
-
attrs[PACT_ATTRS.CONTRACT_FILE] = opts.contractFile;
|
|
38
|
-
}
|
|
39
|
-
if (meta.interactionId) {
|
|
40
|
-
attrs[PACT_ATTRS.INTERACTION_ID] = meta.interactionId;
|
|
41
|
-
}
|
|
42
|
-
return attrs;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Helper that returns just the outcome attribute — stamped after the
|
|
47
|
-
* handler resolves or rejects.
|
|
48
|
-
*/
|
|
49
|
-
export function outcomeAttribute(
|
|
50
|
-
outcome: PactOutcome,
|
|
51
|
-
): Record<string, string> {
|
|
52
|
-
return { [PACT_ATTRS.OUTCOME]: outcome };
|
|
53
|
-
}
|
package/src/audit.test.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { computeAuditMatrix, runAuditSync } from './audit.js';
|
|
6
|
-
import type { InteractionLedgerEntry, PactFile } from './types.js';
|
|
7
|
-
import { AUDIT_MATRIX_SPEC, LEDGER_ENTRY_SPEC } from './types.js';
|
|
8
|
-
|
|
9
|
-
const NOW = new Date('2026-06-01T00:00:00.000Z');
|
|
10
|
-
const DAY = 24 * 60 * 60 * 1000;
|
|
11
|
-
|
|
12
|
-
function entry(overrides: Partial<InteractionLedgerEntry>): InteractionLedgerEntry {
|
|
13
|
-
return {
|
|
14
|
-
type: 'interaction',
|
|
15
|
-
spec: LEDGER_ENTRY_SPEC,
|
|
16
|
-
consumer: 'A',
|
|
17
|
-
provider: 'B',
|
|
18
|
-
interaction: 'evt',
|
|
19
|
-
states: [],
|
|
20
|
-
kind: 'message',
|
|
21
|
-
source: 'test',
|
|
22
|
-
role: 'consumer',
|
|
23
|
-
outcome: 'passed',
|
|
24
|
-
duration_ms: 1,
|
|
25
|
-
observed_at: NOW.toISOString(),
|
|
26
|
-
...overrides,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe('computeAuditMatrix', () => {
|
|
31
|
-
it('marks contracted-and-seen-in-test as OK', () => {
|
|
32
|
-
const m = computeAuditMatrix({
|
|
33
|
-
contracted: [{ consumer: 'A', provider: 'B', interaction: 'evt', kind: 'message' }],
|
|
34
|
-
ledger: [entry({})],
|
|
35
|
-
now: NOW,
|
|
36
|
-
});
|
|
37
|
-
expect(m.counts).toMatchObject({ contracted_and_test_seen: 1, contracted_not_test_seen: 0 });
|
|
38
|
-
expect(m.rows[0]).toMatchObject({ contracted: true, test_seen: true, observed: true });
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('flags contracted-but-not-seen-in-test (STALE)', () => {
|
|
42
|
-
const m = computeAuditMatrix({
|
|
43
|
-
contracted: [{ consumer: 'A', provider: 'B', interaction: 'evt', kind: 'message' }],
|
|
44
|
-
ledger: [],
|
|
45
|
-
now: NOW,
|
|
46
|
-
});
|
|
47
|
-
expect(m.counts.contracted_not_test_seen).toBe(1);
|
|
48
|
-
expect(m.rows[0]).toMatchObject({ contracted: true, test_seen: false });
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('flags seen-but-not-contracted', () => {
|
|
52
|
-
const m = computeAuditMatrix({
|
|
53
|
-
contracted: [],
|
|
54
|
-
ledger: [entry({})],
|
|
55
|
-
now: NOW,
|
|
56
|
-
});
|
|
57
|
-
expect(m.counts.test_or_prod_seen_not_contracted).toBe(1);
|
|
58
|
-
expect(m.rows[0]).toMatchObject({ contracted: false, test_seen: true });
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('tracks prod_seen separately from test_seen', () => {
|
|
62
|
-
const m = computeAuditMatrix({
|
|
63
|
-
contracted: [{ consumer: 'A', provider: 'B', interaction: 'evt', kind: 'message' }],
|
|
64
|
-
ledger: [entry({ source: 'production' })],
|
|
65
|
-
now: NOW,
|
|
66
|
-
});
|
|
67
|
-
expect(m.rows[0]).toMatchObject({
|
|
68
|
-
test_seen: false,
|
|
69
|
-
prod_seen: true,
|
|
70
|
-
});
|
|
71
|
-
expect(m.counts.contracted_not_test_seen).toBe(1);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('sets provider_verified only for provider role passed entries', () => {
|
|
75
|
-
const m = computeAuditMatrix({
|
|
76
|
-
contracted: [{ consumer: 'A', provider: 'B', interaction: 'evt', kind: 'message' }],
|
|
77
|
-
ledger: [
|
|
78
|
-
entry({ role: 'provider', outcome: 'passed' }),
|
|
79
|
-
],
|
|
80
|
-
now: NOW,
|
|
81
|
-
});
|
|
82
|
-
expect(m.rows[0]!.provider_verified).toBe(true);
|
|
83
|
-
expect(m.rows[0]!.test_seen).toBe(false);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('ignores provider_verification_run for per-interaction columns', () => {
|
|
87
|
-
const m = computeAuditMatrix({
|
|
88
|
-
contracted: [{ consumer: 'A', provider: 'B', interaction: 'evt', kind: 'message' }],
|
|
89
|
-
ledger: [
|
|
90
|
-
{
|
|
91
|
-
type: 'provider_verification_run',
|
|
92
|
-
spec: LEDGER_ENTRY_SPEC,
|
|
93
|
-
consumer: 'A',
|
|
94
|
-
provider: 'B',
|
|
95
|
-
outcome: 'failed',
|
|
96
|
-
source: 'test',
|
|
97
|
-
role: 'provider',
|
|
98
|
-
observed_at: NOW.toISOString(),
|
|
99
|
-
error: 'setup failed',
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
now: NOW,
|
|
103
|
-
});
|
|
104
|
-
expect(m.rows[0]!.provider_verified).toBe(false);
|
|
105
|
-
expect(m.verification_failures).toHaveLength(1);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('applies broker_verified at pact-pair level', () => {
|
|
109
|
-
const m = computeAuditMatrix({
|
|
110
|
-
contracted: [
|
|
111
|
-
{ consumer: 'A', provider: 'B', interaction: 'evt1', kind: 'message' },
|
|
112
|
-
{ consumer: 'A', provider: 'B', interaction: 'evt2', kind: 'message' },
|
|
113
|
-
],
|
|
114
|
-
ledger: [],
|
|
115
|
-
brokerVerifications: [
|
|
116
|
-
{ consumer: 'A', provider: 'B', success: true, verifiedAt: NOW.toISOString() },
|
|
117
|
-
],
|
|
118
|
-
now: NOW,
|
|
119
|
-
});
|
|
120
|
-
expect(m.rows.every((r) => r.broker_verified)).toBe(true);
|
|
121
|
-
expect(m.counts.broker_verified).toBe(2);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('excludes ledger entries outside the window', () => {
|
|
125
|
-
const oldEntry = entry({
|
|
126
|
-
observed_at: new Date(NOW.getTime() - 20 * DAY).toISOString(),
|
|
127
|
-
});
|
|
128
|
-
const m = computeAuditMatrix({
|
|
129
|
-
contracted: [{ consumer: 'A', provider: 'B', interaction: 'evt', kind: 'message' }],
|
|
130
|
-
ledger: [oldEntry],
|
|
131
|
-
windowDays: 14,
|
|
132
|
-
now: NOW,
|
|
133
|
-
});
|
|
134
|
-
expect(m.counts.contracted_not_test_seen).toBe(1);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('emits v0.2 audit matrix spec', () => {
|
|
138
|
-
const m = computeAuditMatrix({
|
|
139
|
-
contracted: [],
|
|
140
|
-
ledger: [],
|
|
141
|
-
now: NOW,
|
|
142
|
-
});
|
|
143
|
-
expect(m.spec).toBe(AUDIT_MATRIX_SPEC);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe('runAuditSync (integration)', () => {
|
|
148
|
-
let workDir: string;
|
|
149
|
-
let originalCwd: string;
|
|
150
|
-
|
|
151
|
-
beforeEach(() => {
|
|
152
|
-
originalCwd = process.cwd();
|
|
153
|
-
workDir = mkdtempSync(path.join(tmpdir(), 'autotel-pact-audit-'));
|
|
154
|
-
process.chdir(workDir);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
afterEach(() => {
|
|
158
|
-
process.chdir(originalCwd);
|
|
159
|
-
rmSync(workDir, { recursive: true, force: true });
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('reads pacts and ledger from disk and produces a matrix', () => {
|
|
163
|
-
mkdirSync('pacts', { recursive: true });
|
|
164
|
-
const pact: PactFile = {
|
|
165
|
-
consumer: { name: 'OrderShipper' },
|
|
166
|
-
provider: { name: 'OrderService' },
|
|
167
|
-
messages: [
|
|
168
|
-
{ description: 'an OrderCreated event' },
|
|
169
|
-
{ description: 'an OrderCancelled event' },
|
|
170
|
-
],
|
|
171
|
-
};
|
|
172
|
-
writeFileSync('pacts/OrderShipper-OrderService.json', JSON.stringify(pact));
|
|
173
|
-
|
|
174
|
-
mkdirSync('.autotel-pact', { recursive: true });
|
|
175
|
-
const observed = entry({
|
|
176
|
-
consumer: 'OrderShipper',
|
|
177
|
-
provider: 'OrderService',
|
|
178
|
-
interaction: 'an OrderCreated event',
|
|
179
|
-
observed_at: new Date().toISOString(),
|
|
180
|
-
});
|
|
181
|
-
writeFileSync('.autotel-pact/ledger-x.jsonl', JSON.stringify(observed) + '\n');
|
|
182
|
-
|
|
183
|
-
const m = runAuditSync({});
|
|
184
|
-
|
|
185
|
-
expect(m.counts.contracted).toBe(2);
|
|
186
|
-
expect(m.counts.contracted_and_test_seen).toBe(1);
|
|
187
|
-
expect(m.counts.contracted_not_test_seen).toBe(1);
|
|
188
|
-
});
|
|
189
|
-
});
|
package/src/audit.ts
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { fetchBrokerVerifications, type BrokerConfig } from './broker.js';
|
|
3
|
-
import { readLedger, type LedgerOptions } from './ledger.js';
|
|
4
|
-
import {
|
|
5
|
-
interactionsFromPactFile,
|
|
6
|
-
listPactFiles,
|
|
7
|
-
parsePactFile,
|
|
8
|
-
} from './pact-file.js';
|
|
9
|
-
import {
|
|
10
|
-
AUDIT_MATRIX_SPEC,
|
|
11
|
-
isInteractionLedgerEntry,
|
|
12
|
-
isProviderVerificationRun,
|
|
13
|
-
type AuditMatrix,
|
|
14
|
-
type AuditRow,
|
|
15
|
-
type BrokerVerification,
|
|
16
|
-
type InteractionLedgerEntry,
|
|
17
|
-
type LedgerRecord,
|
|
18
|
-
type PactInteractionKey,
|
|
19
|
-
type ProviderVerificationRunEntry,
|
|
20
|
-
} from './types.js';
|
|
21
|
-
|
|
22
|
-
export interface AuditOptions extends LedgerOptions {
|
|
23
|
-
pactsDir?: string;
|
|
24
|
-
windowDays?: number;
|
|
25
|
-
broker?: BrokerConfig;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const DEFAULT_PACTS_DIR = './pacts';
|
|
29
|
-
const DEFAULT_WINDOW_DAYS = 14;
|
|
30
|
-
|
|
31
|
-
export function keyOf(k: PactInteractionKey): string {
|
|
32
|
-
const identity = k.interactionId ?? k.interaction;
|
|
33
|
-
return `${k.consumer}::${k.provider}::${k.kind}::${identity}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function pairKey(consumer: string, provider: string): string {
|
|
37
|
-
return `${consumer}::${provider}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function inWindow(observedAt: string, cutoff: number): boolean {
|
|
41
|
-
const t = Date.parse(observedAt);
|
|
42
|
-
return Number.isFinite(t) && t >= cutoff;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Compute the audit matrix from pact files, ledger, and optional broker data.
|
|
47
|
-
*/
|
|
48
|
-
export function computeAuditMatrix(input: {
|
|
49
|
-
contracted: PactInteractionKey[];
|
|
50
|
-
ledger: LedgerRecord[];
|
|
51
|
-
brokerVerifications?: BrokerVerification[];
|
|
52
|
-
windowDays?: number;
|
|
53
|
-
now?: Date;
|
|
54
|
-
}): AuditMatrix {
|
|
55
|
-
const windowDays = input.windowDays ?? DEFAULT_WINDOW_DAYS;
|
|
56
|
-
const now = input.now ?? new Date();
|
|
57
|
-
const cutoff = now.getTime() - windowDays * 24 * 60 * 60 * 1000;
|
|
58
|
-
|
|
59
|
-
const verificationFailures: ProviderVerificationRunEntry[] = [];
|
|
60
|
-
const recentInteractions: InteractionLedgerEntry[] = [];
|
|
61
|
-
|
|
62
|
-
for (const entry of input.ledger) {
|
|
63
|
-
if (!inWindow(entry.observed_at, cutoff)) continue;
|
|
64
|
-
if (isProviderVerificationRun(entry)) {
|
|
65
|
-
verificationFailures.push(entry);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (isInteractionLedgerEntry(entry)) {
|
|
69
|
-
recentInteractions.push(entry);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const brokerByPair = new Map<string, BrokerVerification>();
|
|
74
|
-
for (const b of input.brokerVerifications ?? []) {
|
|
75
|
-
brokerByPair.set(pairKey(b.consumer, b.provider), b);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const testSeenByKey = new Map<string, InteractionLedgerEntry[]>();
|
|
79
|
-
const prodSeenByKey = new Map<string, InteractionLedgerEntry[]>();
|
|
80
|
-
const providerVerifiedByKey = new Map<string, InteractionLedgerEntry[]>();
|
|
81
|
-
const anyObservedByKey = new Map<string, InteractionLedgerEntry[]>();
|
|
82
|
-
|
|
83
|
-
for (const entry of recentInteractions) {
|
|
84
|
-
const k = keyOf({
|
|
85
|
-
consumer: entry.consumer,
|
|
86
|
-
provider: entry.provider,
|
|
87
|
-
interaction: entry.interaction,
|
|
88
|
-
kind: entry.kind,
|
|
89
|
-
interactionId: entry.interaction_id,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const push = (map: Map<string, InteractionLedgerEntry[]>) => {
|
|
93
|
-
const arr = map.get(k) ?? [];
|
|
94
|
-
arr.push(entry);
|
|
95
|
-
map.set(k, arr);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
push(anyObservedByKey);
|
|
99
|
-
|
|
100
|
-
if (entry.source === 'test' && entry.role === 'consumer') {
|
|
101
|
-
push(testSeenByKey);
|
|
102
|
-
}
|
|
103
|
-
if (entry.source === 'production') {
|
|
104
|
-
push(prodSeenByKey);
|
|
105
|
-
}
|
|
106
|
-
if (entry.role === 'provider' && entry.outcome === 'passed') {
|
|
107
|
-
push(providerVerifiedByKey);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const contractedByKey = new Map<string, PactInteractionKey>();
|
|
112
|
-
for (const c of input.contracted) {
|
|
113
|
-
contractedByKey.set(keyOf(c), c);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const rows: AuditRow[] = [];
|
|
117
|
-
|
|
118
|
-
function pushRow(parts: PactInteractionKey, k: string, isContracted: boolean): void {
|
|
119
|
-
const testObs = testSeenByKey.get(k) ?? [];
|
|
120
|
-
const prodObs = prodSeenByKey.get(k) ?? [];
|
|
121
|
-
const providerObs = providerVerifiedByKey.get(k) ?? [];
|
|
122
|
-
const allObs = anyObservedByKey.get(k) ?? [];
|
|
123
|
-
const latest = allObs.toSorted((a, b) =>
|
|
124
|
-
b.observed_at.localeCompare(a.observed_at),
|
|
125
|
-
)[0];
|
|
126
|
-
const broker = brokerByPair.get(pairKey(parts.consumer, parts.provider));
|
|
127
|
-
|
|
128
|
-
rows.push({
|
|
129
|
-
consumer: parts.consumer,
|
|
130
|
-
provider: parts.provider,
|
|
131
|
-
interaction: parts.interaction,
|
|
132
|
-
interaction_id: parts.interactionId ?? latest?.interaction_id,
|
|
133
|
-
kind: parts.kind,
|
|
134
|
-
contracted: isContracted,
|
|
135
|
-
observed: testObs.length > 0 || prodObs.length > 0,
|
|
136
|
-
test_seen: testObs.length > 0,
|
|
137
|
-
prod_seen: prodObs.length > 0,
|
|
138
|
-
provider_verified: providerObs.length > 0,
|
|
139
|
-
broker_verified: broker?.success === true,
|
|
140
|
-
broker_verified_at: broker?.verifiedAt,
|
|
141
|
-
broker_error: broker?.error,
|
|
142
|
-
last_observed_at: latest?.observed_at,
|
|
143
|
-
last_outcome: latest?.outcome,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
for (const [k, contracted] of contractedByKey) {
|
|
148
|
-
pushRow(contracted, k, true);
|
|
149
|
-
}
|
|
150
|
-
for (const [k, observations] of anyObservedByKey) {
|
|
151
|
-
if (contractedByKey.has(k)) continue;
|
|
152
|
-
const first = observations[0]!;
|
|
153
|
-
pushRow(
|
|
154
|
-
{
|
|
155
|
-
consumer: first.consumer,
|
|
156
|
-
provider: first.provider,
|
|
157
|
-
interaction: first.interaction,
|
|
158
|
-
kind: first.kind,
|
|
159
|
-
interactionId: first.interaction_id,
|
|
160
|
-
},
|
|
161
|
-
k,
|
|
162
|
-
false,
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
rows.sort((a, b) =>
|
|
167
|
-
a.consumer.localeCompare(b.consumer) ||
|
|
168
|
-
a.provider.localeCompare(b.provider) ||
|
|
169
|
-
a.interaction.localeCompare(b.interaction),
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
const counts = {
|
|
173
|
-
total: rows.length,
|
|
174
|
-
contracted: rows.filter((r) => r.contracted).length,
|
|
175
|
-
observed: rows.filter((r) => r.observed).length,
|
|
176
|
-
contracted_and_test_seen: rows.filter((r) => r.contracted && r.test_seen).length,
|
|
177
|
-
contracted_not_test_seen: rows.filter((r) => r.contracted && !r.test_seen).length,
|
|
178
|
-
test_or_prod_seen_not_contracted: rows.filter(
|
|
179
|
-
(r) => !r.contracted && (r.test_seen || r.prod_seen),
|
|
180
|
-
).length,
|
|
181
|
-
test_seen: rows.filter((r) => r.test_seen).length,
|
|
182
|
-
prod_seen: rows.filter((r) => r.prod_seen).length,
|
|
183
|
-
provider_verified: rows.filter((r) => r.provider_verified).length,
|
|
184
|
-
broker_verified: rows.filter((r) => r.broker_verified).length,
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const matrix: AuditMatrix = {
|
|
188
|
-
spec: AUDIT_MATRIX_SPEC,
|
|
189
|
-
rows,
|
|
190
|
-
counts,
|
|
191
|
-
window_days: windowDays,
|
|
192
|
-
generated_at: now.toISOString(),
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
if (verificationFailures.length > 0) {
|
|
196
|
-
matrix.verification_failures = verificationFailures;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return matrix;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export async function runAudit(opts: AuditOptions = {}): Promise<AuditMatrix> {
|
|
203
|
-
const pactsDir = path.resolve(process.cwd(), opts.pactsDir ?? DEFAULT_PACTS_DIR);
|
|
204
|
-
const contracted: PactInteractionKey[] = [];
|
|
205
|
-
const pairs = new Set<string>();
|
|
206
|
-
|
|
207
|
-
for (const file of listPactFiles(pactsDir)) {
|
|
208
|
-
const pact = parsePactFile(file);
|
|
209
|
-
if (!pact) continue;
|
|
210
|
-
const interactions = interactionsFromPactFile(pact);
|
|
211
|
-
contracted.push(...interactions);
|
|
212
|
-
const consumer = pact.consumer?.name;
|
|
213
|
-
const provider = pact.provider?.name;
|
|
214
|
-
if (consumer && provider) {
|
|
215
|
-
pairs.add(pairKey(consumer, provider));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const ledger = readLedger(opts);
|
|
220
|
-
|
|
221
|
-
let brokerVerifications: BrokerVerification[] | undefined;
|
|
222
|
-
if (opts.broker) {
|
|
223
|
-
brokerVerifications = await fetchBrokerVerifications(opts.broker, [...pairs].map((p) => {
|
|
224
|
-
const [consumer, provider] = p.split('::');
|
|
225
|
-
return { consumer: consumer!, provider: provider! };
|
|
226
|
-
}));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return computeAuditMatrix({
|
|
230
|
-
contracted,
|
|
231
|
-
ledger,
|
|
232
|
-
brokerVerifications,
|
|
233
|
-
windowDays: opts.windowDays,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Sync audit without broker (backward compatible for tests). */
|
|
238
|
-
export function runAuditSync(opts: Omit<AuditOptions, 'broker'> = {}): AuditMatrix {
|
|
239
|
-
const pactsDir = path.resolve(process.cwd(), opts.pactsDir ?? DEFAULT_PACTS_DIR);
|
|
240
|
-
const contracted: PactInteractionKey[] = [];
|
|
241
|
-
for (const file of listPactFiles(pactsDir)) {
|
|
242
|
-
const pact = parsePactFile(file);
|
|
243
|
-
if (pact) contracted.push(...interactionsFromPactFile(pact));
|
|
244
|
-
}
|
|
245
|
-
const ledger = readLedger(opts);
|
|
246
|
-
return computeAuditMatrix({
|
|
247
|
-
contracted,
|
|
248
|
-
ledger,
|
|
249
|
-
windowDays: opts.windowDays,
|
|
250
|
-
});
|
|
251
|
-
}
|