@openwop/openwop-conformance 2.37.0 → 2.37.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.
@@ -87,15 +87,68 @@ const journal: LedgerEntry[] = [];
87
87
  * dispositions throws: RFC 0148 §A says **exactly one** disposition per
88
88
  * requirement, and a silent last-write-wins would let a later soft-skip
89
89
  * overwrite an earlier real failure — the failure mode in reverse.
90
+ *
91
+ * `extras.fold` is the one exception, for the per-`it` rows `setup.ts` records
92
+ * when several `it` legs witness ONE requirement id; see its docblock below.
93
+ */
94
+ /**
95
+ * How certifiable each disposition is, least first. `readLedgerFile` has always
96
+ * resolved a cross-worker disagreement this way — "one worker said it failed"
97
+ * outranks "another said it passed", and an unresolvable disagreement must never
98
+ * round toward certification. `fold` below applies the SAME rule in-worker.
90
99
  */
100
+ const CERTIFIABILITY_RANK: Record<Disposition, number> = {
101
+ 'executed-fail': 0,
102
+ blocked: 1,
103
+ 'executed-pass': 2,
104
+ skipped: 3,
105
+ inapplicable: 4,
106
+ };
107
+
91
108
  export function recordRequirement(
92
109
  requirementId: string,
93
110
  disposition: Disposition,
94
111
  detail?: string,
95
- extras?: { assertionCount?: number; scenarioFile?: string; evidence?: RowEvidence },
112
+ extras?: {
113
+ assertionCount?: number;
114
+ scenarioFile?: string;
115
+ evidence?: RowEvidence;
116
+ /**
117
+ * FOLD instead of throw when this id already has a disposition (2.37.0).
118
+ *
119
+ * The strict contract above — one disposition per requirement per run,
120
+ * contradiction throws — is right for a scenario that classifies ITSELF:
121
+ * two hand-written verdicts for one id is an authoring bug, and the throw
122
+ * is how it surfaces.
123
+ *
124
+ * It is wrong for the per-`it` rows `setup.ts` records, because the corpus
125
+ * deliberately witnesses ONE requirement with SEVERAL `it` legs: 27
126
+ * scenario files hand `req()` a module-level `const ID`, so every leg in
127
+ * the file records under the same id. There the throw is not a guard —
128
+ * `setup.ts` wraps the call in `try {} catch {}` ("never fail a test for
129
+ * bookkeeping"), so the second verdict was silently DISCARDED, and, worse,
130
+ * discarded before it reached the JSONL sink.
131
+ *
132
+ * MEASURED, `v2-run-bulk-cancel.test.ts` on a tier-2 host: leg 1 passed
133
+ * (3 assertions), leg 2 failed on its 7th. The file row recorded
134
+ * `executed-fail` with 10 assertions and the detail "one or more assertions
135
+ * in the file failed"; `openwop.requirement.0170.run-bulk-cancel` recorded
136
+ * `executed-pass` with 3. The failing leg's message — which names the
137
+ * requirement AND prints the offending entry — existed, was computed by
138
+ * `resolveItRecord`, and was thrown away here. The operator had to
139
+ * hand-probe every assertion in the file against production to find out
140
+ * what had failed.
141
+ *
142
+ * Folding by `CERTIFIABILITY_RANK` makes the surviving row the least
143
+ * certifiable of the legs, carrying THAT leg's detail, with the legs'
144
+ * assertion counts summed — the same answer `readLedgerFile` would reach
145
+ * from the sink lines, so the in-memory ledger and the file agree.
146
+ */
147
+ fold?: true;
148
+ },
96
149
  ): void {
97
150
  const prior = ledger.get(requirementId);
98
- if (prior !== undefined && prior.disposition !== disposition) {
151
+ if (prior !== undefined && prior.disposition !== disposition && extras?.fold !== true) {
99
152
  throw new Error(
100
153
  `RFC 0148 §A: ${requirementId} already recorded as '${prior.disposition}', now '${disposition}'. ` +
101
154
  'Exactly one disposition per requirement per run.',
@@ -107,7 +160,7 @@ export function recordRequirement(
107
160
  'Anything other than executed-pass MUST say why, or the ledger records an outcome nobody can act on.',
108
161
  );
109
162
  }
110
- const entry: LedgerEntry = {
163
+ let entry: LedgerEntry = {
111
164
  requirementId,
112
165
  disposition,
113
166
  ...(detail === undefined ? {} : { detail }),
@@ -115,6 +168,27 @@ export function recordRequirement(
115
168
  ...(extras?.scenarioFile === undefined ? {} : { scenarioFile: extras.scenarioFile }),
116
169
  ...(extras?.evidence === undefined || disposition !== 'executed-pass' ? {} : { evidence: extras.evidence }),
117
170
  };
171
+ if (prior !== undefined && extras?.fold === true) {
172
+ // The least-certifiable leg wins the disposition and keeps its own detail;
173
+ // a tie keeps whichever side actually said something. Counts sum, because
174
+ // both legs really did assert against the target for this one requirement.
175
+ const keepPrior = CERTIFIABILITY_RANK[prior.disposition] <= CERTIFIABILITY_RANK[disposition];
176
+ const winner = keepPrior ? prior : entry;
177
+ const loser = keepPrior ? entry : prior;
178
+ const count = (prior.assertionCount ?? 0) + (extras?.assertionCount ?? 0);
179
+ const keptDetail = winner.detail ?? loser.detail;
180
+ const hasCount = prior.assertionCount !== undefined || extras?.assertionCount !== undefined;
181
+ entry = {
182
+ requirementId,
183
+ disposition: winner.disposition,
184
+ ...(keptDetail === undefined ? {} : { detail: keptDetail }),
185
+ ...(hasCount ? { assertionCount: count } : {}),
186
+ ...(winner.scenarioFile === undefined ? {} : { scenarioFile: winner.scenarioFile }),
187
+ // `evidence` is only meaningful on a pass; a fold that lands anywhere
188
+ // else drops it, exactly as the constructor above does.
189
+ ...(winner.disposition === 'executed-pass' && winner.evidence !== undefined ? { evidence: winner.evidence } : {}),
190
+ };
191
+ }
118
192
  ledger.set(requirementId, entry);
119
193
  journal.push(entry);
120
194
  // File sink (RFC 0148 acceptance item 2, S6). The in-memory map lives in a
@@ -159,7 +233,11 @@ export function readLedgerFile(path: string): readonly LedgerEntry[] {
159
233
  }
160
234
  if (typeof e.requirementId !== 'string' || !DISPOSITIONS.includes(e.disposition)) continue;
161
235
  const prior = merged.get(e.requirementId);
162
- if (prior === undefined || rank[e.disposition] < rank[prior.disposition]) merged.set(e.requirementId, e);
236
+ // `<=`, not `<`: a per-`it` FOLD (2.37.0) appends the cumulative row after
237
+ // the leg rows it folded, so on an equal disposition the LAST line is the
238
+ // one carrying the summed `assertionCount`. For genuinely duplicate lines
239
+ // the two are identical and the choice is a no-op.
240
+ if (prior === undefined || rank[e.disposition] <= rank[prior.disposition]) merged.set(e.requirementId, e);
163
241
  }
164
242
  return [...merged.values()].sort((a, b) => a.requirementId.localeCompare(b.requirementId));
165
243
  }
@@ -118,8 +118,13 @@ export function resolveItRecord(
118
118
  * sites were not measured and v1 bundles are read through its EOS.
119
119
  */
120
120
  blockedStands = false,
121
+ /** The `it` title, so a failed row says WHICH leg of the requirement failed. */
122
+ testName?: string,
121
123
  ): { disposition: Disposition; detail?: string } {
122
- if (state === 'fail') return { disposition: 'executed-fail', detail: `the test executed and failed: ${(firstError ?? 'no message').slice(0, 300)}` };
124
+ if (state === 'fail') {
125
+ const where = testName === undefined || testName.trim() === '' ? '' : ` in "${testName.slice(0, 120)}"`;
126
+ return { disposition: 'executed-fail', detail: `the test executed and failed${where}: ${(firstError ?? 'no message').slice(0, 300)}` };
127
+ }
123
128
  if (state === 'pass' && assertionCalls > 0) {
124
129
  // `blockedDespiteAssertions` (soft-skip.ts): the leg says its setup
125
130
  // assertions are not the requirement, and the requirement went unobserved.
@@ -142,14 +147,45 @@ export function resolveItRecord(
142
147
  return { disposition: 'skipped', detail: 'vitest skipped the test (ctx.skip / it.skip) without a recorded gate reason' };
143
148
  }
144
149
 
150
+ /** One failed `it` in a file: its title and its first error message. */
151
+ export interface TestFailure {
152
+ readonly name: string;
153
+ readonly message?: string;
154
+ }
155
+
156
+ /**
157
+ * The `executed-fail` detail for a file row: WHICH cases failed, and what the
158
+ * first one said.
159
+ *
160
+ * Until 2.37.0 this was the fixed string "one or more assertions in the file
161
+ * failed". A tier-2 host read exactly that for `v2-run-bulk-cancel` — 10
162
+ * assertions, no case name, no message, and nothing else in the record for that
163
+ * file — and had to hand-probe every assertion in the file against production to
164
+ * find out what had happened. A bundle row whose only detail is that sentence is
165
+ * undiagnosable by construction, and every future flicker in any scenario had
166
+ * the same problem.
167
+ */
168
+ export function failureDetail(failures: readonly TestFailure[], failedCount: number): string {
169
+ if (failures.length === 0) {
170
+ return `${failedCount} test(s) in the file failed; the runner captured no message`;
171
+ }
172
+ const head = failures[0]!;
173
+ const named = failures.slice(0, 3).map((f) => `"${f.name.slice(0, 120)}"`).join(', ');
174
+ const more = failures.length > 3 ? ` (+${failures.length - 3} more)` : '';
175
+ const msg = head.message === undefined || head.message.trim() === '' ? 'no message' : head.message.slice(0, 400);
176
+ return `${failures.length} test(s) failed — ${named}${more}; first failure: ${msg}`;
177
+ }
178
+
145
179
  /** Worker half: fold a file's per-test states (+ any gate-recorded reason) into
146
180
  * the ONE disposition the file records. */
147
181
  export function fileDisposition(
148
182
  states: readonly FileTestState[],
149
183
  gateReason: 'inapplicable' | 'skipped' | undefined,
150
184
  assertionCount?: number,
185
+ failures: readonly TestFailure[] = [],
151
186
  ): { disposition: Disposition; detail?: string } {
152
- if (states.some((s) => s === 'fail')) return { disposition: 'executed-fail', detail: 'one or more assertions in the file failed' };
187
+ const failed = states.filter((s) => s === 'fail').length;
188
+ if (failed > 0) return { disposition: 'executed-fail', detail: failureDetail(failures, failed) };
153
189
  if (states.some((s) => s === 'pass')) {
154
190
  // A test that early-returned through `behaviorGate` is reported by vitest
155
191
  // as a pass with zero assertions. When EVERY passing test in the file did
@@ -195,6 +231,8 @@ export function resolveFileRecord(
195
231
  assertionCount: number,
196
232
  noted: { kind: 'inapplicable' | 'skipped' | 'blocked'; reason: string } | null,
197
233
  specCoherenceFile?: string,
234
+ /** The failed cases, so an `executed-fail` row NAMES them (2.37.0). */
235
+ failures: readonly TestFailure[] = [],
198
236
  ): { disposition: Disposition; detail?: string } {
199
237
  // A scenario whose subject is the CORPUS, skipped because the published
200
238
  // tarball does not bundle spec/v1/. RFC 0148 §A: `blocked` is defined over
@@ -211,7 +249,7 @@ export function resolveFileRecord(
211
249
  ) {
212
250
  return { disposition: 'inapplicable', detail: SPEC_COHERENCE_DETAIL };
213
251
  }
214
- let { disposition, detail } = fileDisposition(states, gateReason, assertionCount);
252
+ let { disposition, detail } = fileDisposition(states, gateReason, assertionCount, failures);
215
253
  if (disposition === 'executed-pass' && assertionCount === 0) {
216
254
  if (noted !== null) {
217
255
  disposition = noted.kind;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * A webhook receiver that owns its destination — one exercise, one identity.
3
+ *
4
+ * ── Why this file exists (2.37.0) ────────────────────────────────────────────
5
+ * Five surfaces in this suite bind `OPENWOP_WEBHOOK_RECEIVER_PORT`, and on a
6
+ * tunnelled cut four of them registered `resolveRegistrationUrl(...)`, which
7
+ * returns `OPENWOP_WEBHOOK_RECEIVER_URL` VERBATIM — one byte-identical string
8
+ * for every caller. `v2-bound-id-kinds`, `v2-webhook-delivery-shape`,
9
+ * `v2-webhook-durable-delivery` and `webhook-signed-delivery` therefore pointed
10
+ * four subscriptions at ONE destination.
11
+ *
12
+ * A webhook subscription is durable host-side state. It keeps delivering, and
13
+ * RETRYING, after the file that created it has finished, so the collision is not
14
+ * only between two live listeners — it is between an exercise and the leftovers
15
+ * of an earlier one. Whichever receiver held the pinned port read those
16
+ * leftovers as its own traffic, and `v2-webhook-durable-delivery` answers 500 BY
17
+ * DESIGN for the first attempts of every delivery key, so the exercise it landed
18
+ * on saw failures it never caused. `v2-webhook-delivery-shape`'s own
19
+ * `startReceiver` carried a comment describing the symptom from the other end:
20
+ * "the tunnel forwards to the PINNED port — held by the other receiver, which
21
+ * answers 500 by design — so this file's `deliveries` stays empty, its legs
22
+ * soft-skip, and the rows resolve `executed-pass`." A wire-shape scenario that
23
+ * never opened a delivery body went green.
24
+ *
25
+ * This is the class openwop#1513 (two legs, one effect identity) and
26
+ * openwop#1520 (many fakes, one public front) already opened. Those two supply
27
+ * the halves, and this composes them rather than reimplementing either:
28
+ *
29
+ * - ROUTING is `front-mux.ts`. Several receivers can be alive behind one
30
+ * front; each registers its handler under its nonce and calls `routeFronted`
31
+ * first, so a delivery reaches the receiver it was ADDRESSED to whichever
32
+ * listener happens to hold the port.
33
+ * - IDENTITY is here, and it is why the nonce is UNCONDITIONAL rather than
34
+ * `frontedEndpoint`'s "only when this fake does not own the pinned port".
35
+ * Routing alone does not close a webhook: a bare front is one identity
36
+ * shared ACROSS TIME, so a retry aimed at a finished exercise arrives
37
+ * indistinguishable from this one's traffic. With the nonce always present
38
+ * that retry addresses a nonce this listener does not serve, `routeFronted`
39
+ * answers it 404, and it is counted as `foreign()` — never as a delivery.
40
+ *
41
+ * What this does NOT do is relax any gate. The destination is still the
42
+ * operator's own front or loopback; `resolvePublicFront` still refuses a
43
+ * non-https or private front; `receiverBinding` still binds loopback unless the
44
+ * operator declared otherwise. Only the PATH changed.
45
+ *
46
+ * @see lib/front-mux.ts (routing), lib/effect-receiver.ts (the same nonce idea
47
+ * for an outbound effect's Layer-2 identity)
48
+ */
49
+
50
+ import { randomBytes } from 'node:crypto';
51
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
52
+ import { FRONT_MUX_PREFIX, registerBehindFront, routeFronted, unregisterBehindFront } from './front-mux.js';
53
+ import { receiverBinding, resolvePublicFront } from './webhook-receiver.js';
54
+
55
+ /** The operator's public front for the suite's webhook receiver. */
56
+ export const WEBHOOK_FRONT_ENV = 'OPENWOP_WEBHOOK_RECEIVER_URL';
57
+
58
+ type HeaderBag = Record<string, string | string[] | undefined>;
59
+
60
+ /** One request addressed to THIS exercise. */
61
+ export interface ScopedHit {
62
+ /** The path as this receiver sees it — the `/fx/<nonce>` prefix already stripped. */
63
+ readonly path: string;
64
+ readonly method: string;
65
+ readonly headers: HeaderBag;
66
+ readonly body: string;
67
+ }
68
+
69
+ export interface ScopedReceiver {
70
+ readonly server: Server;
71
+ /**
72
+ * The destination to REGISTER with the host: the public front when one is
73
+ * wired, else this listener's own address — this exercise's nonce path
74
+ * appended either way.
75
+ */
76
+ readonly url: string;
77
+ readonly tunnelled: boolean;
78
+ /** The local address this listener actually answers on — for failure detail. */
79
+ readonly localUrl: string;
80
+ /** What makes this exercise's destination, and so its subscription, its own. */
81
+ readonly nonce: string;
82
+ /** The port this listener bound — the pinned one when the operator pinned it. */
83
+ readonly port: number;
84
+ /**
85
+ * Requests that reached this listener bearing some OTHER exercise's nonce, or
86
+ * none at all. Reported so a scenario can say WHY it saw nothing; never handed
87
+ * to the scenario's recorder.
88
+ */
89
+ foreign(): number;
90
+ close(): Promise<void>;
91
+ }
92
+
93
+ /**
94
+ * Start a receiver for exactly one exercise.
95
+ *
96
+ * `respond` is called ONLY for this exercise's own requests, and sees the path
97
+ * with the nonce prefix stripped — exactly what it would see on a listener of
98
+ * its own. It owns the response.
99
+ */
100
+ export async function startScopedReceiver(
101
+ respond: (hit: ScopedHit, res: ServerResponse) => void,
102
+ ): Promise<ScopedReceiver> {
103
+ const nonce = randomBytes(9).toString('hex');
104
+ let foreign = 0;
105
+ const own = (request: IncomingMessage, res: ServerResponse): void => {
106
+ const chunks: Buffer[] = [];
107
+ request.on('data', (c: Buffer) => chunks.push(c));
108
+ request.on('end', () => {
109
+ respond(
110
+ {
111
+ path: request.url ?? '/',
112
+ method: request.method ?? '',
113
+ headers: request.headers,
114
+ body: Buffer.concat(chunks).toString('utf8'),
115
+ },
116
+ res,
117
+ );
118
+ });
119
+ };
120
+ registerBehindFront(WEBHOOK_FRONT_ENV, nonce, own);
121
+ const server = createServer((request: IncomingMessage, res: ServerResponse) => {
122
+ // Counted BEFORE routing: `routeFronted` answers an unknown nonce itself and
123
+ // cannot report that it did, and a request that is not this exercise's must
124
+ // still be visible in the failure detail.
125
+ if (!(request.url ?? '/').startsWith(`${FRONT_MUX_PREFIX}${nonce}`)) foreign += 1;
126
+ if (routeFronted(WEBHOOK_FRONT_ENV, request, res)) return;
127
+ // Not an `/fx/` path at all: a stranger, or a host that dropped the path it
128
+ // was given. Answered, never recorded — a receiver that counts what it was
129
+ // not addressed to is the defect this helper exists to remove.
130
+ request.resume();
131
+ res.writeHead(404, { 'Content-Type': 'application/json' });
132
+ res.end(JSON.stringify({ error: 'this receiver serves one conformance exercise; address its nonce path' }));
133
+ });
134
+ const pinned = Number(process.env['OPENWOP_WEBHOOK_RECEIVER_PORT'] ?? '');
135
+ const bindPort = Number.isInteger(pinned) && pinned > 0 && pinned < 65536 ? pinned : 0;
136
+ const binding = receiverBinding();
137
+ await new Promise<void>((resolve) => server.listen(bindPort, binding.bind, () => resolve()));
138
+ const addr = server.address();
139
+ if (typeof addr !== 'object' || addr === null) throw new Error('receiver address unavailable');
140
+ const origin = `http://${binding.advertise}:${addr.port}`;
141
+ const front = resolvePublicFront(WEBHOOK_FRONT_ENV, origin);
142
+ return {
143
+ server,
144
+ url: `${front.url.replace(/\/+$/, '')}${FRONT_MUX_PREFIX}${nonce}`,
145
+ tunnelled: front.tunnelled,
146
+ localUrl: `${origin}${FRONT_MUX_PREFIX}${nonce}`,
147
+ nonce,
148
+ port: addr.port,
149
+ foreign: () => foreign,
150
+ close: () =>
151
+ new Promise<void>((resolve) => {
152
+ unregisterBehindFront(WEBHOOK_FRONT_ENV, nonce);
153
+ server.close(() => resolve());
154
+ }),
155
+ };
156
+ }
157
+
158
+ /**
159
+ * A destination for an exercise that must REGISTER a subscription but wants no
160
+ * delivery — the mint leg of `v2-bound-id-kinds`, say, which needs a 201 and a
161
+ * bound id and nothing else.
162
+ *
163
+ * Such a leg still has to honour the operator's front, or its registration is
164
+ * refused by an SSRF guard doing its job and the leg records `blocked` on every
165
+ * public cut. But handing it the front VERBATIM gives it the same identity as
166
+ * every other exercise, and a subscription is live from the 201 until the
167
+ * DELETE: anything the host fans out in that window lands on whichever listener
168
+ * holds the port and is read as that exercise's traffic.
169
+ *
170
+ * So it gets a nonce too. No receiver serves it, which is the point: a delivery
171
+ * to this destination is answered 404 by whichever scoped receiver owns the
172
+ * port rather than being absorbed into that exercise's record.
173
+ */
174
+ export function unservedDestination(fallbackUrl: string): { url: string; tunnelled: boolean; nonce: string } {
175
+ const nonce = randomBytes(9).toString('hex');
176
+ const front = resolvePublicFront(WEBHOOK_FRONT_ENV, fallbackUrl);
177
+ return {
178
+ url: `${front.url.replace(/\/+$/, '')}${FRONT_MUX_PREFIX}${nonce}`,
179
+ tunnelled: front.tunnelled,
180
+ nonce,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Why a scoped receiver saw NO request of its own — the cause a `blocked` row
186
+ * must name (RFC 0148 §A: anything other than `executed-pass` MUST say why).
187
+ *
188
+ * A zero here is not by itself evidence that the host failed to deliver, and
189
+ * `foreign()` is what tells the two apart:
190
+ *
191
+ * - `foreign() > 0` — traffic DID reach this listener, addressed to another
192
+ * nonce or to no nonce at all. The path between the host and this process
193
+ * works, so "the host did not deliver" is not what was observed; what was
194
+ * observed is that nothing arrived under THIS exercise's identity. That is
195
+ * unmeasured, not unmet, and callers record `blocked`.
196
+ * - `foreign() === 0` — nothing reached this listener at all. That is still
197
+ * ambiguous (a front not wired to this process, or a host that never called
198
+ * out), and callers keep whichever disposition the leg already carried; this
199
+ * function only supplies the sentence that says which address was in play.
200
+ *
201
+ * Either way the record names the observation rather than a conclusion, so a
202
+ * reader can act on it without hand-probing production.
203
+ */
204
+ export function noDeliveryCause(rx: ScopedReceiver, what = 'delivery'): string {
205
+ const where = rx.tunnelled
206
+ ? `the registered destination is the public front ${rx.url} (${WEBHOOK_FRONT_ENV}) and this listener answers on ${rx.localUrl}, port ${rx.port}`
207
+ : `the registered destination is this listener at ${rx.url}, port ${rx.port}`;
208
+ const strangers = rx.foreign();
209
+ if (strangers > 0) {
210
+ return `no ${what} bearing this exercise's nonce ${rx.nonce} arrived, but ${strangers} request(s) DID reach this listener addressed elsewhere — so the path from the host to this process works and the absence is of this exercise's identity, not of traffic (${where})`;
211
+ }
212
+ return `no ${what} bearing this exercise's nonce ${rx.nonce} arrived, and nothing else reached this listener either — ${where}`;
213
+ }
214
+
215
+ /**
216
+ * True when a zero observation is provably NOT a verdict about the host: other
217
+ * traffic reached this listener, so the absence is of this exercise's identity
218
+ * rather than of delivery. Callers record `blocked` (which denies certification
219
+ * exactly as a failure does, RFC 0168 §E.1) instead of convicting the host.
220
+ */
221
+ export function absenceIsUnmeasured(rx: ScopedReceiver): boolean {
222
+ return rx.foreign() > 0;
223
+ }
@@ -20,6 +20,7 @@
20
20
  * @see spec/v1/trigger-bridge.md
21
21
  * @see spec/v1/profiles.md (§openwop-trigger-bridge)
22
22
  */
23
+ import { randomBytes, randomInt } from 'node:crypto';
23
24
  import { driver } from './driver.js';
24
25
  import { deriveProfiles, type DiscoveryPayload } from './profiles.js';
25
26
 
@@ -70,5 +71,53 @@ export async function driveDelivery(
70
71
  return (res.json as DeliveryResult | undefined) ?? {};
71
72
  }
72
73
 
74
+ /**
75
+ * A dedup key that belongs to ONE exercise (2.37.0).
76
+ *
77
+ * `trigger-bridge.md` §C-1 makes the dedup window a ≥24h FLOOR, so a LITERAL
78
+ * dedup key is not a fixture — it is a durable identity the host is required to
79
+ * remember across runs of this suite. Two exercises that hand the bridge the
80
+ * same key are ONE delivery by the spec's own rule, and the second one's row
81
+ * reads zero deliveries on a host doing exactly what it MUST. That is a suite
82
+ * defect and not a host defect, and it is the same failure the RFC 0158
83
+ * duplicate-delivery row had when two scenario files shared one effect identity
84
+ * (`lib/effect-receiver.ts`).
85
+ *
86
+ * `prefix` keeps a key readable in a host's own log; the random half is what
87
+ * makes it this exercise's. `randomBytes`, not `Date.now()`: two vitest workers
88
+ * can enter the same line in the same millisecond.
89
+ */
90
+ export function freshDedupKey(prefix: string): string {
91
+ return `openwop-conformance-${prefix}-${randomBytes(9).toString('hex')}`;
92
+ }
93
+
94
+ /**
95
+ * The same per-exercise mint for a `stream` source, in the COORDINATE shape
96
+ * §F.5 keys on (2.37.0).
97
+ *
98
+ * `trigger-stream-cdc-sources.test.ts` handed the seam the literal
99
+ * `'events:3:99001'` and then asserted `deliveredCount === 1 || outcome ===
100
+ * 'delivered'` — the identical defect `freshDedupKey` above was written for,
101
+ * one file over, found when the enumeration was re-run against the merged
102
+ * tree. §C-1's window is a ≥24h floor and §F.5 reuses it verbatim, so the
103
+ * second run of that file against the same host, any time that day, hands the
104
+ * bridge broker coordinates it has already delivered; a CONFORMANT host
105
+ * collapses the exercise into the first run's outcome and the assertion
106
+ * convicts it. Cold host passes, warm host fails.
107
+ *
108
+ * `freshDedupKey` would not do here, and the difference is not cosmetic: §F.5
109
+ * says a stream event's dedup key SHOULD derive from
110
+ * `(topic, partition, offset)`, and the leg's own `req()` message asserts over
111
+ * exactly that keying. An opaque token would make the message describe
112
+ * something the call no longer does. So topic and partition stay fixed and the
113
+ * OFFSET is minted — which is precisely what makes a real broker's message a
114
+ * different message.
115
+ */
116
+ export function freshStreamDedupKey(topic = 'events', partition = 3): string {
117
+ // A 2^44 offset space: distinct across every run this suite will make, and
118
+ // still a plausible broker offset rather than an opaque token.
119
+ return `${topic}:${partition}:${randomInt(2 ** 44)}`;
120
+ }
121
+
73
122
  export const SUBSCRIPTION_STATES = ['active', 'paused', 'failed', 'dead-lettered'];
74
123
  export const DELIVERY_OUTCOMES = ['delivered', 'retrying', 'dead-lettered'];
@@ -23,6 +23,7 @@
23
23
  * @see spec/v1/auth-profiles.md §"Subject linking (SAML ⟷ SCIM)"
24
24
  */
25
25
 
26
+ import { randomBytes } from 'node:crypto';
26
27
  import { describe, it, expect } from 'vitest';
27
28
  import { softSkip, seamAbsent } from '../lib/soft-skip.js';
28
29
  import { driver } from '../lib/driver.js';
@@ -109,7 +110,23 @@ describe('auth-subject-link: cross-lane deactivation (RFC 0159 §A.3 — opt-in)
109
110
  if (!idpUrl || !scimUrl) return softSkip('inapplicable', 'opt-in: SAML IdP and/or SCIM endpoint not provided');
110
111
 
111
112
  // 1. Provision a SCIM user carrying an opaque, IdP-stable externalId.
112
- const externalId = 'idp-op-8f3a';
113
+ //
114
+ // MINTED PER EXERCISE (2.37.0). This was the literal `'idp-op-8f3a'`, and
115
+ // the SCIM directory it is provisioned into is the OPERATOR's
116
+ // (`OPENWOP_TEST_SCIM_URL`) — durable state outside this process. Step 3
117
+ // DEACTIVATES that user, and deactivation is the whole point of the leg, so
118
+ // the second run of this file against the same directory provisions an
119
+ // externalId that is already present and already deactivated. Step 2 then
120
+ // asserts "a valid linked SAML assertion authenticates before deactivation"
121
+ // against a subject the host is CORRECT to refuse: cold directory passes,
122
+ // warm directory fails, nothing about the host having changed. Same shape as
123
+ // the shared effect identity in `lib/effect-receiver.ts` (openwop#1513) and
124
+ // the fixed trigger-bridge dedup key.
125
+ //
126
+ // Unwitnessed: both opt-in variables are unset in every cut this suite has
127
+ // run, so the leg has never executed and the collision has never fired. It
128
+ // is read off the code, and the fix costs nothing if the reading is wrong.
129
+ const externalId = `idp-op-${randomBytes(6).toString('hex')}`;
113
130
  const provision = await driver.post('/v1/host/sample/auth/scim/provision', {
114
131
  scimUrl,
115
132
  op: 'create-user',
@@ -38,6 +38,7 @@ import {
38
38
  } from '../lib/triggerBridge.js';
39
39
  import { queryTestEvents, requireEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
40
40
  import { req } from '../lib/requirement-ids.js';
41
+ import { freshDedupKey } from '../lib/triggerBridge.js';
41
42
 
42
43
  const CONTENT_FREE_FORBIDDEN = ['body', 'headers', 'payload', 'secret', 'credentials', 'token', 'apiKey'];
43
44
 
@@ -56,7 +57,21 @@ describe('trigger-bridge-delivery (RFC 0083 §C)', () => {
56
57
  if (!(await isEventLogSeamAvailable())) return seamAbsent('host advertises openwop-trigger-bridge but the event-log seam is absent');
57
58
 
58
59
  // ---- Leg 1: dedup → effectively-once (§C-1) ---------------------------
59
- const dedup = await driveDelivery({ scenario: 'dedup', dedupKey: 'conformance-dedup-key', source: 'queue' });
60
+ //
61
+ // The dedupKey is MINTED PER EXERCISE, and that is load-bearing (2.37.0).
62
+ // It used to be the literal `'conformance-dedup-key'`, and §C-1's dedup
63
+ // window is a ≥24h FLOOR — so the second run of this file against the same
64
+ // host, any time that day, hands the bridge a key it has already delivered.
65
+ // A CONFORMANT host then collapses the whole exercise into the first run's
66
+ // outcome and emits ZERO `delivered` attempts under this run's id, and the
67
+ // `=== 1` below convicts it. Cold host passes, warm host fails, nothing
68
+ // about the host having changed — the same shape as the RFC 0158 row that
69
+ // shared one effect identity between two files (`lib/effect-receiver.ts`).
70
+ // The REPETITION that §C-1 is about happens INSIDE `driveDelivery`'s
71
+ // `scenario: 'dedup'`, so a fresh key per exercise removes the cross-run
72
+ // collision without weakening the clause.
73
+ const dedupKey = freshDedupKey('queue');
74
+ const dedup = await driveDelivery({ scenario: 'dedup', dedupKey, source: 'queue' });
60
75
  if (dedup === null) return seamAbsent('host advertises openwop-trigger-bridge but the delivery seam is unwired');
61
76
 
62
77
  // The profile is derived AND the seam is wired — missing evidence is a
@@ -68,7 +83,7 @@ describe('trigger-bridge-delivery (RFC 0083 §C)', () => {
68
83
  'trigger.delivery.attempted (dedup)',
69
84
  );
70
85
  const deliveredForKey = dedupEvents.filter(
71
- (e) => e.payload.dedupKey === 'conformance-dedup-key' && e.payload.outcome === 'delivered',
86
+ (e) => e.payload.dedupKey === dedupKey && e.payload.outcome === 'delivered',
72
87
  );
73
88
  expect(
74
89
  deliveredForKey.length === 1,
@@ -47,7 +47,7 @@ import { SCHEMAS_DIR, FIXTURES_DIR } from '../lib/paths.js';
47
47
  import { driver } from '../lib/driver.js';
48
48
  import { behaviorGate } from '../lib/behavior-gate.js';
49
49
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
50
- import { driveDelivery } from '../lib/triggerBridge.js';
50
+ import { driveDelivery, freshStreamDedupKey } from '../lib/triggerBridge.js';
51
51
  import { req } from '../lib/requirement-ids.js';
52
52
  import { softSkip } from '../lib/soft-skip.js';
53
53
 
@@ -203,7 +203,22 @@ describe.skipIf(HTTP_SKIP)('trigger-stream-cdc: behavioral ingestion + dedup (ca
203
203
  const external = tb?.ingestion?.externalSources ?? [];
204
204
  if (!behaviorGate('triggerBridge.ingestion', (external.length ?? 0) > 0)) return;
205
205
 
206
- const first = await driveDelivery({ scenario: 'dedup', dedupKey: 'events:3:99001', source: 'stream' });
206
+ // The broker coordinates are MINTED PER EXERCISE (2.37.x), and that is
207
+ // load-bearing. This was the literal `'events:3:99001'`, and §F.5 reuses
208
+ // §C-1's dedup window — a ≥24h FLOOR — verbatim, so the second run of this
209
+ // file against the same host, any time that day, hands the bridge an offset
210
+ // it has already delivered. A CONFORMANT host then collapses the exercise
211
+ // into the first run's outcome and reports neither a `deliveredCount` of 1
212
+ // nor `outcome: 'delivered'` under this run, and the assertion below
213
+ // convicts it: cold host passes, warm host fails, nothing about the host
214
+ // having changed. The repetition §C-1 is about happens INSIDE the seam's
215
+ // `scenario: 'dedup'` (it delivers the key TWICE), so a fresh offset per
216
+ // exercise removes the cross-run collision without weakening the clause.
217
+ // Same defect and same fix as `trigger-bridge-delivery`'s dedup key; the
218
+ // offset rather than an opaque token, because the `(topic,partition,offset)`
219
+ // keying is what the `req()` message below asserts over.
220
+ const dedupKey = freshStreamDedupKey();
221
+ const first = await driveDelivery({ scenario: 'dedup', dedupKey, source: 'stream' });
207
222
  if (first === null) return softSkip('blocked', 'precondition not met — `first === null` returned early (delivery seam unwired — soft-skip) (seam, prior step, or fixture unavailable)'); // delivery seam unwired — soft-skip
208
223
  if (first.outcome === undefined && !external.includes('stream')) return softSkip('blocked', 'precondition not met — `first.outcome === undefined && !external.includes(\'stream\')` returned early (pre-0127 host — soft-skip) (seam, prior step, or fixture unavailable)'); // pre-0127 host — soft-skip
209
224
  expect(