autotel-pact 0.2.0
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/README.md +385 -0
- package/dist/audit.cjs +412 -0
- package/dist/audit.cjs.map +1 -0
- package/dist/audit.d.cts +25 -0
- package/dist/audit.d.ts +25 -0
- package/dist/audit.js +403 -0
- package/dist/audit.js.map +1 -0
- package/dist/auto-wrap.cjs +255 -0
- package/dist/auto-wrap.cjs.map +1 -0
- package/dist/auto-wrap.d.cts +57 -0
- package/dist/auto-wrap.d.ts +57 -0
- package/dist/auto-wrap.js +248 -0
- package/dist/auto-wrap.js.map +1 -0
- package/dist/broker.cjs +84 -0
- package/dist/broker.cjs.map +1 -0
- package/dist/broker.d.cts +23 -0
- package/dist/broker.d.ts +23 -0
- package/dist/broker.js +80 -0
- package/dist/broker.js.map +1 -0
- package/dist/cli.cjs +662 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +656 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +967 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +200 -0
- package/dist/index.d.ts +200 -0
- package/dist/index.js +932 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger-BuBmfWNc.d.ts +22 -0
- package/dist/ledger-D88TzN1c.d.cts +22 -0
- package/dist/processor.cjs +200 -0
- package/dist/processor.cjs.map +1 -0
- package/dist/processor.d.cts +58 -0
- package/dist/processor.d.ts +58 -0
- package/dist/processor.js +193 -0
- package/dist/processor.js.map +1 -0
- package/dist/provider.cjs +219 -0
- package/dist/provider.cjs.map +1 -0
- package/dist/provider.d.cts +41 -0
- package/dist/provider.d.ts +41 -0
- package/dist/provider.js +213 -0
- package/dist/provider.js.map +1 -0
- package/dist/tag.cjs +50 -0
- package/dist/tag.cjs.map +1 -0
- package/dist/tag.d.cts +9 -0
- package/dist/tag.d.ts +9 -0
- package/dist/tag.js +48 -0
- package/dist/tag.js.map +1 -0
- package/dist/types-BHGiwqcp.d.cts +157 -0
- package/dist/types-BHGiwqcp.d.ts +157 -0
- package/package.json +108 -0
- package/schemas/README.md +24 -0
- package/schemas/audit-matrix-v0.2.0.json +78 -0
- package/schemas/ledger-entry-v0.2.0.json +77 -0
- package/src/attrs.test.ts +35 -0
- package/src/attrs.ts +53 -0
- package/src/audit.test.ts +189 -0
- package/src/audit.ts +251 -0
- package/src/auto-wrap.test.ts +149 -0
- package/src/auto-wrap.ts +283 -0
- package/src/broker.test.ts +175 -0
- package/src/broker.ts +118 -0
- package/src/cli.test.ts +148 -0
- package/src/cli.ts +287 -0
- package/src/index.ts +94 -0
- package/src/labels.ts +25 -0
- package/src/ledger-normalize.test.ts +141 -0
- package/src/ledger-normalize.ts +82 -0
- package/src/ledger.test.ts +92 -0
- package/src/ledger.ts +156 -0
- package/src/pact-file.test.ts +124 -0
- package/src/pact-file.ts +65 -0
- package/src/processor.test.ts +90 -0
- package/src/processor.ts +191 -0
- package/src/tag.test.ts +72 -0
- package/src/tag.ts +21 -0
- package/src/types.ts +169 -0
- package/src/wrapper-http.test.ts +133 -0
- package/src/wrapper-http.ts +194 -0
- package/src/wrapper-provider.test.ts +132 -0
- package/src/wrapper-provider.ts +163 -0
- package/src/wrapper.test.ts +176 -0
- package/src/wrapper.ts +221 -0
package/src/wrapper.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { span as autotelSpan, getActiveSpan } from 'autotel';
|
|
2
|
+
import { buildPactAttributes, outcomeAttribute } from './attrs.js';
|
|
3
|
+
import { appendLedgerEntry, type LedgerOptions } from './ledger.js';
|
|
4
|
+
import {
|
|
5
|
+
LEDGER_ENTRY_SPEC,
|
|
6
|
+
type InteractionLedgerEntry,
|
|
7
|
+
type PactInteractionMeta,
|
|
8
|
+
type PactKind,
|
|
9
|
+
} from './types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Minimal structural type for `MessageConsumerPact`. We don't declare
|
|
13
|
+
* `config` here because pact-js marks it `private` — listing it on a
|
|
14
|
+
* public structural type would prevent users passing the real class.
|
|
15
|
+
* Inside the wrapper we read `(pact as MessageConsumerPactWithConfig).config`
|
|
16
|
+
* at runtime, with `opts.consumer` / `opts.provider` as override fallbacks.
|
|
17
|
+
*/
|
|
18
|
+
export interface MessageConsumerPactLike {
|
|
19
|
+
verify: (handler: (message: ReifiedMessage) => Promise<unknown>) => Promise<unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* Optional fluent metadata appender. Pact-JS's `MessageConsumerPact`
|
|
22
|
+
* provides this; we use it (when present) to write `interactionId`
|
|
23
|
+
* into the pact file's `messages[].metadata` block so the audit can
|
|
24
|
+
* key on the same id from both sides.
|
|
25
|
+
*
|
|
26
|
+
* The value type is `string` because that is the only thing autotel-pact
|
|
27
|
+
* passes. A narrower-than-pact-js type keeps real `MessageConsumerPact`
|
|
28
|
+
* assignable here by parameter contravariance.
|
|
29
|
+
*/
|
|
30
|
+
withMetadata?: (metadata: Record<string, string>) => MessageConsumerPactLike;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface MessageConsumerPactWithConfig extends MessageConsumerPactLike {
|
|
34
|
+
config?: { consumer?: string; provider?: string };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ReifiedMessage {
|
|
38
|
+
contents: unknown;
|
|
39
|
+
description?: string;
|
|
40
|
+
metadata?: Record<string, unknown>;
|
|
41
|
+
providerStates?: Array<{ name: string }>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type PactMessageHandler<R = unknown> = (
|
|
45
|
+
message: ReifiedMessage,
|
|
46
|
+
) => R | Promise<R>;
|
|
47
|
+
|
|
48
|
+
export interface WithPactInteractionOptions extends LedgerOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Path (relative to cwd) of the pact file this interaction belongs to.
|
|
51
|
+
* Stamped on the span as `pact.contract.file` and surfaced in the audit.
|
|
52
|
+
*/
|
|
53
|
+
contractFile?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Override the span name. Defaults to `pact.interaction`.
|
|
56
|
+
*/
|
|
57
|
+
spanName?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Consumer name. Only needed if the supplied pact instance doesn't expose
|
|
60
|
+
* `.config.consumer` (e.g. a custom pact-like wrapper).
|
|
61
|
+
*/
|
|
62
|
+
consumer?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Provider name. Same caveat as `consumer`.
|
|
65
|
+
*/
|
|
66
|
+
provider?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Stable identity for this interaction. Recommended whenever you might
|
|
69
|
+
* rename the human-readable `expectsToReceive` description in the future —
|
|
70
|
+
* the audit matches on `interactionId` first, so renames don't break
|
|
71
|
+
* continuity. Conventional form: `domain.event.vN` (e.g. `order.created.v1`).
|
|
72
|
+
*/
|
|
73
|
+
interactionId?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveParticipants(
|
|
77
|
+
pact: MessageConsumerPactLike,
|
|
78
|
+
opts: WithPactInteractionOptions,
|
|
79
|
+
): { consumer: string; provider: string } {
|
|
80
|
+
const cfg = (pact as MessageConsumerPactWithConfig).config;
|
|
81
|
+
const consumer = opts.consumer ?? cfg?.consumer;
|
|
82
|
+
const provider = opts.provider ?? cfg?.provider;
|
|
83
|
+
if (!consumer || !provider) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'autotel-pact: could not resolve consumer/provider from the Pact instance. ' +
|
|
86
|
+
'Pass `{ consumer, provider }` in the options object.',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return { consumer, provider };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Wrap a Pact-Message `verify()` call so that:
|
|
94
|
+
* 1. An autotel span opens around the verification.
|
|
95
|
+
* 2. Span attributes capture consumer / provider / interaction / states.
|
|
96
|
+
* 3. A ledger entry records that this interaction was actually exercised.
|
|
97
|
+
*
|
|
98
|
+
* The handler arg to `verify()` is wrapped so we can read the reified
|
|
99
|
+
* message (the only place description + states are exposed by Pact-JS).
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const pact = new MessageConsumerPact({ consumer, provider, dir });
|
|
104
|
+
* pact.given('an order exists').expectsToReceive('OrderCreated').withContent({...});
|
|
105
|
+
*
|
|
106
|
+
* await withPactInteraction(pact, (msg) => orderHandler(msg.contents));
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export async function withPactInteraction<R>(
|
|
110
|
+
pact: MessageConsumerPactLike,
|
|
111
|
+
handler: PactMessageHandler<R>,
|
|
112
|
+
opts: WithPactInteractionOptions = {},
|
|
113
|
+
): Promise<R> {
|
|
114
|
+
const start = process.hrtime.bigint();
|
|
115
|
+
const spanName = opts.spanName ?? 'pact.interaction';
|
|
116
|
+
const kind: PactKind = 'message';
|
|
117
|
+
const { consumer, provider } = resolveParticipants(pact, opts);
|
|
118
|
+
|
|
119
|
+
// When the caller asked for a stable interaction id, write it into the
|
|
120
|
+
// pact file's message metadata so the audit matches the same identity
|
|
121
|
+
// on both sides. Without this, the ledger entry carries the id but the
|
|
122
|
+
// pact file does not — producing one STALE row (description-keyed
|
|
123
|
+
// contracted) and one SHADOW row (id-keyed observed) for what is really
|
|
124
|
+
// a single interaction.
|
|
125
|
+
if (opts.interactionId && typeof pact.withMetadata === 'function') {
|
|
126
|
+
pact.withMetadata({ interactionId: opts.interactionId });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let captured: ReifiedMessage | undefined;
|
|
130
|
+
let handlerResult: R | undefined;
|
|
131
|
+
|
|
132
|
+
// Run the verify inside an autotel span. We populate attributes once we
|
|
133
|
+
// have the reified message; until then `pact.consumer` / `pact.provider`
|
|
134
|
+
// are known but the description/states are not.
|
|
135
|
+
return autotelSpan(spanName, async (span) => {
|
|
136
|
+
span.setAttributes({
|
|
137
|
+
'pact.consumer': consumer,
|
|
138
|
+
'pact.provider': provider,
|
|
139
|
+
'pact.kind': kind,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await pact.verify(async (reified) => {
|
|
144
|
+
captured = reified;
|
|
145
|
+
const meta: PactInteractionMeta = {
|
|
146
|
+
consumer,
|
|
147
|
+
provider,
|
|
148
|
+
description: reified.description ?? '<unknown>',
|
|
149
|
+
states: (reified.providerStates ?? []).map((s) => s.name),
|
|
150
|
+
kind,
|
|
151
|
+
interactionId: opts.interactionId,
|
|
152
|
+
};
|
|
153
|
+
span.setAttributes(buildPactAttributes(meta, { contractFile: opts.contractFile }));
|
|
154
|
+
|
|
155
|
+
handlerResult = (await handler(reified)) as R;
|
|
156
|
+
return handlerResult;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
span.setAttributes(outcomeAttribute('passed'));
|
|
160
|
+
writeLedgerForOutcome({
|
|
161
|
+
consumer,
|
|
162
|
+
provider,
|
|
163
|
+
captured,
|
|
164
|
+
kind,
|
|
165
|
+
outcome: 'passed',
|
|
166
|
+
start,
|
|
167
|
+
opts,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return handlerResult as R;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
span.setAttributes(outcomeAttribute('failed'));
|
|
173
|
+
writeLedgerForOutcome({
|
|
174
|
+
consumer,
|
|
175
|
+
provider,
|
|
176
|
+
captured,
|
|
177
|
+
kind,
|
|
178
|
+
outcome: 'failed',
|
|
179
|
+
start,
|
|
180
|
+
opts,
|
|
181
|
+
error: error instanceof Error ? error.message : String(error),
|
|
182
|
+
});
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function writeLedgerForOutcome(args: {
|
|
189
|
+
consumer: string;
|
|
190
|
+
provider: string;
|
|
191
|
+
captured: ReifiedMessage | undefined;
|
|
192
|
+
kind: PactKind;
|
|
193
|
+
outcome: 'passed' | 'failed';
|
|
194
|
+
start: bigint;
|
|
195
|
+
opts: WithPactInteractionOptions;
|
|
196
|
+
error?: string;
|
|
197
|
+
}): void {
|
|
198
|
+
const { consumer, provider, captured, kind, outcome, start, opts, error } = args;
|
|
199
|
+
const ctx = getActiveSpan()?.spanContext();
|
|
200
|
+
const entry: InteractionLedgerEntry = {
|
|
201
|
+
type: 'interaction',
|
|
202
|
+
spec: LEDGER_ENTRY_SPEC,
|
|
203
|
+
consumer,
|
|
204
|
+
provider,
|
|
205
|
+
interaction: captured?.description ?? '<unknown>',
|
|
206
|
+
interaction_id: opts.interactionId,
|
|
207
|
+
states: (captured?.providerStates ?? []).map((s) => s.name),
|
|
208
|
+
kind,
|
|
209
|
+
source: 'test',
|
|
210
|
+
role: 'consumer',
|
|
211
|
+
outcome,
|
|
212
|
+
duration_ms: Number(process.hrtime.bigint() - start) / 1e6,
|
|
213
|
+
observed_at: new Date().toISOString(),
|
|
214
|
+
trace_id: ctx?.traceId,
|
|
215
|
+
span_id: ctx?.spanId,
|
|
216
|
+
run_id: process.env.AUTOTEL_PACT_RUN_ID,
|
|
217
|
+
git_sha: process.env.GIT_SHA ?? process.env.GITHUB_SHA,
|
|
218
|
+
error,
|
|
219
|
+
};
|
|
220
|
+
appendLedgerEntry(entry, opts);
|
|
221
|
+
}
|