@openwop/openwop-conformance 2.36.1 → 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.
Files changed (38) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +4 -3
  3. package/coverage.md +1 -0
  4. package/dist/lib/requirement-ledger.js +44 -3
  5. package/dist/lib/scenario-disposition.js +37 -8
  6. package/dist/spec-artifacts.lock.json +2 -2
  7. package/package.json +2 -2
  8. package/requirements.json +510 -73
  9. package/scenario-majors.json +14 -2
  10. package/schemas/CORPUS-STAMP.json +27 -27
  11. package/src/lib/a2a-error-info.ts +44 -0
  12. package/src/lib/a2a-fake-peer.ts +50 -7
  13. package/src/lib/effect-receiver.ts +135 -0
  14. package/src/lib/front-mux.ts +102 -0
  15. package/src/lib/mcp-fake-server.ts +15 -4
  16. package/src/lib/oidc-issuer.ts +15 -5
  17. package/src/lib/requirement-ledger.ts +82 -4
  18. package/src/lib/scenario-disposition.ts +41 -3
  19. package/src/lib/scoped-receiver.ts +223 -0
  20. package/src/lib/triggerBridge.ts +49 -0
  21. package/src/scenarios/a2a-1-0-agent-card.test.ts +19 -7
  22. package/src/scenarios/auth-subject-link.test.ts +18 -1
  23. package/src/scenarios/trigger-bridge-delivery.test.ts +17 -2
  24. package/src/scenarios/trigger-stream-cdc-sources.test.ts +17 -2
  25. package/src/scenarios/v2-a2a-client-error-details.test.ts +70 -0
  26. package/src/scenarios/v2-a2a-operation-map.test.ts +84 -2
  27. package/src/scenarios/v2-bound-id-kinds.test.ts +35 -22
  28. package/src/scenarios/v2-durability-recovery.test.ts +83 -35
  29. package/src/scenarios/v2-idempotency-in-flight.test.ts +132 -0
  30. package/src/scenarios/v2-interrupt-resolve-terminal.test.ts +107 -0
  31. package/src/scenarios/v2-mcp-mount-map.test.ts +41 -0
  32. package/src/scenarios/v2-negotiation-authenticated.test.ts +13 -0
  33. package/src/scenarios/v2-sse-last-event-id-cursor.test.ts +133 -0
  34. package/src/scenarios/v2-terminal-event-once.test.ts +15 -21
  35. package/src/scenarios/v2-webhook-delivery-shape.test.ts +31 -34
  36. package/src/scenarios/v2-webhook-durable-delivery.test.ts +63 -47
  37. package/src/scenarios/webhook-signed-delivery.test.ts +55 -42
  38. package/src/setup.ts +24 -4
@@ -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(
@@ -0,0 +1,70 @@
1
+ /**
2
+ * RFC 0211 §F — the host as an A2A 1.0 client reads a peer's error in either shape.
3
+ * Target major 2; gated on the v2 `a2a` record and the seams profile.
4
+ *
5
+ * A2A v1.0.1 §9.5 puts a JSON-RPC error's details in `error.data` as an ARRAY of
6
+ * `Any` objects; the suite's fake peer and the reference host shipped a bare
7
+ * `{ reason, domain }` object through 2.36.x. A host's A2A client MUST accept the
8
+ * array and SHOULD accept the object for the rest of 2.x
9
+ * (`spec/v2/core/interop.md` §"The operation mappings", A2A error details).
10
+ *
11
+ * The suite drives the host's client at its own 1.0-only fake peer through the
12
+ * §22 invoke seam with `requestVersion: '99.0'`, so the peer answers
13
+ * `VersionNotSupportedError` (-32009) — once with `data` as `Any[]`, once as the
14
+ * legacy object — and the host MUST project both to `interop_version_unsupported`.
15
+ *
16
+ * Weak by construction, and said so: a host that reads only one shape still
17
+ * identifies the error by its JSON-RPC code, so this leg witnesses that the client
18
+ * tolerates both shapes (does not throw, does not mis-project to `internal_error`),
19
+ * not that it reads `reason` out of the array. `interop_version_unsupported` has
20
+ * `details: null` in `spec/v2/errors.json`, so no projected `supportedVersions` is
21
+ * asserted.
22
+ *
23
+ * Callback-shaped (the host calls the suite's peer): unwitnessable when the host is
24
+ * in a separate network namespace — `../lib/host-callback.ts`.
25
+ *
26
+ * @see RFCS/0211-a2a-error-details-are-errorinfo.md §F
27
+ * @see spec/v2/interop-map.json a2a.errors VersionNotSupportedError (clientProjection)
28
+ */
29
+
30
+ import { describe, it, expect } from 'vitest';
31
+ import { driver } from '../lib/driver.js';
32
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
33
+ import { seamsProfileAdvertised, SEAMS_PREFIX } from '../lib/seams.js';
34
+ import { A2AFakePeer } from '../lib/a2a-fake-peer.js';
35
+ import { readErrorCode } from '../lib/error-envelope.js';
36
+ import { softSkip, seamAbsent } from '../lib/soft-skip.js';
37
+ import { req } from '../lib/requirement-ids.js';
38
+
39
+ export const REQUIRES_HOST_CALLBACK = "the host's A2A client calls a suite-owned 1.0 peer that refuses the requested version with -32009";
40
+
41
+ const ID = 'openwop.requirement.0211.a2a-client-reads-either-shape';
42
+ const DOC = 'spec/v2/core/interop.md §"The operation mappings", A2A error details (RFC 0211 §F)';
43
+
44
+ describe('RFC 0211 §F — v2-a2a-client-error-details (host as A2A 1.0 client; gated on a2a + seams)', () => {
45
+ it('a peer\'s version error projects to interop_version_unsupported whether error.data is an array or a legacy object', async () => {
46
+ const doc = await v2Discovery().catch(() => null);
47
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable');
48
+ const facet = await familyAdvertised('a2a');
49
+ if (!facet) return softSkip('inapplicable', 'a2a is not advertised at major 2 — the host has no A2A client to hold');
50
+ if (!seamsProfileAdvertised(doc)) return softSkip('inapplicable', 'the client exchange is driven through the seams profile — conformance.seamsProfile is not openwop-conformance-seams-v2');
51
+ const peer = new A2AFakePeer({ protocolVersions: ['1.0'] });
52
+ await peer.start();
53
+ const seen: string[] = [];
54
+ try {
55
+ for (const legacy of [false, true]) {
56
+ peer.reset();
57
+ peer.setLegacyErrorData(legacy);
58
+ const res = await driver.post(`${SEAMS_PREFIX}/sample/a2a/invoke`, { peerUrl: peer.hostFacingEndpoint(), authenticated: true, requestVersion: '99.0' });
59
+ if (res.status === 404 || res.status === 403 || res.status === 405) return seamAbsent(`host advertises a2a but ${SEAMS_PREFIX}/sample/a2a/invoke answered ${res.status} (host-sample-test-seams.md §22)`);
60
+ const reached = peer.invocations().some((i) => i.method !== 'GET');
61
+ if (!reached) return softSkip('blocked', `the host never called the suite peer (${legacy ? 'legacy' : 'Any[]'} pass) — the host cannot reach it, so no error was projected`);
62
+ const code = readErrorCode(res.json);
63
+ seen.push(`${legacy ? 'legacy object' : 'Any[]'}: HTTP ${res.status} ${String(code)}`);
64
+ expect(code, req(ID, DOC, `a peer's -32009 MUST project to interop_version_unsupported whichever shape error.data takes (${seen.join('; ')})`)).toBe('interop_version_unsupported');
65
+ }
66
+ } finally {
67
+ await peer.stop();
68
+ }
69
+ });
70
+ });
@@ -25,6 +25,7 @@
25
25
  * @see spec/v2/interop-map.json a2a.operations / a2a.taskState / a2a.errors
26
26
  * @see RFCS/0208-v2-a2a-mcp-operation-mappings.md §C, §D, §E
27
27
  * @see RFCS/0199-outbound-oauth-client-and-credential-interrupt.md §D.1 (the credential leg; interop-map.json a2a.taskState override row)
28
+ * @see RFCS/0211-a2a-error-details-are-errorinfo.md §A–§E (the error-details legs; the isolation comparator)
28
29
  */
29
30
  import { describe, it, expect } from 'vitest';
30
31
  import { readFileSync } from 'node:fs';
@@ -37,6 +38,7 @@ import { softSkip } from '../lib/soft-skip.js';
37
38
  import { SCHEMAS_DIR } from '../lib/paths.js';
38
39
  import { req } from '../lib/requirement-ids.js';
39
40
  import { SEAMS_PREFIX } from '../lib/seams.js';
41
+ import { errorInfos, normaliseErrorData, isOpenwopEnvelope, A2A_ERROR_DOMAIN } from '../lib/a2a-error-info.js';
40
42
 
41
43
  export const HOST_CALLBACK_NOT_REQUIRED = 'the suite is the A2A client: every leg POSTs JSON-RPC to the interface the host\'s own card lists; nothing harness-hosted is handed to the host';
42
44
 
@@ -44,6 +46,8 @@ const DOC = 'spec/v2/core/interop.md §"The operation mappings" (RFC 0208)';
44
46
  const PROFILE = 'a2a-1.0';
45
47
  const SKILL = 'conformance-approval';
46
48
  const R = (slug: string): string => `openwop.requirement.0208.${slug}`;
49
+ const R11 = (slug: string): string => `openwop.requirement.0211.${slug}`;
50
+ const DOC11 = 'spec/v2/core/interop.md §"The operation mappings", A2A error details (RFC 0211)';
47
51
  const ACCEPT = { action: 'accept' };
48
52
 
49
53
  interface MapStateRow { runStatus: string; wire: string; interruptKind?: string }
@@ -184,8 +188,12 @@ describe('RFC 0208 — v2-a2a-operation-map (host as A2A 1.0 server, gated on a2
184
188
  const b = await rpc(t.url, 'GetTask', { id: foreign });
185
189
  expect(a.error?.code, req(R('a2a-unreadable-not-found'), 'a2a.errors TaskNotFoundError', `an unknown task MUST be TaskNotFoundError -32001 (got ${JSON.stringify(a.error ?? a.result)})`)).toBe(-32001);
186
190
  expect(b.error?.code, req(R('a2a-unreadable-not-found'), 'interop.md §"The operation mappings" Isolation', `a task the caller cannot read — here a foreign tenant segment REST refuses 403 — MUST be answered exactly as a nonexistent one, -32001 (got ${JSON.stringify(b.error ?? b.result)}, HTTP ${b.status})`)).toBe(-32001);
187
- const shape = (e: RpcError | undefined): string => JSON.stringify({ http: e === undefined ? null : 'rpc', dataKeys: e?.data && typeof e.data === 'object' ? Object.keys(e.data as object).sort() : typeof e?.data });
188
- expect(shape(b.error) + String(b.status), req(R('a2a-unreadable-not-found'), 'interop.md §"The operation mappings" Isolation (A2A 1.0.1 §3.3.2)', 'the foreign-tenant answer MUST be indistinguishable from the unknown-id answer: same HTTP status and the same error.data shape')).toBe(shape(a.error) + String(a.status));
191
+ // RFC 0211 §D: compare the details per element. `Object.keys(data)` of a one-element
192
+ // array is ["0"] whatever the element discloses, so a key comparison is vacuous.
193
+ const na = normaliseErrorData(a.error?.data, fabricated);
194
+ const nb = normaliseErrorData(b.error?.data, foreign);
195
+ expect(errorInfos(a.error?.data).length, req(R11('a2a-unreadable-not-found-details'), DOC11, `positive control: the unknown-id answer MUST carry error.data as an array with a google.rpc.ErrorInfo — without one the comparison below proves nothing (got ${JSON.stringify(a.error?.data)})`)).toBeGreaterThanOrEqual(1);
196
+ expect(JSON.stringify(nb) + String(b.status), req(R11('a2a-unreadable-not-found-details'), 'interop.md §"The operation mappings" Isolation; RFC 0211 §D (A2A 1.0.1 §3.3.2)', 'the foreign-tenant answer MUST be indistinguishable from the unknown-id answer: same HTTP status and the same normalised error details (@type, reason, domain, sorted metadata keys, minus an echo of the requested id)')).toBe(JSON.stringify(na) + String(a.status));
189
197
  await rpc(t.url, 'CancelTask', { id });
190
198
  });
191
199
 
@@ -274,6 +282,80 @@ describe('RFC 0208 — v2-a2a-operation-map (host as A2A 1.0 server, gated on a2
274
282
  await rpc(t.url, 'CancelTask', { id: theirId }, { bearer: other });
275
283
  });
276
284
 
285
+ it('an A2A error\'s data is an array carrying one ErrorInfo with the map row\'s reason and the A2A domain', async () => {
286
+ const t = await target(true);
287
+ if (!t.ok) return skip(t, R11('a2a-error-data-shape'));
288
+ const first = await startApprovalTask(t.url);
289
+ const id = first.task?.id;
290
+ expect(typeof id === 'string' && id.includes('/'), req(R11('a2a-error-data-shape'), DOC, `SendMessage MUST start a task with a tenant-bound id: ${JSON.stringify(first.rpc.error)}`)).toBe(true);
291
+ const tenant = id!.slice(0, id!.indexOf('/'));
292
+ const unknown = await rpc(t.url, 'GetTask', { id: `${tenant}/${randomBytes(16).toString('hex').slice(0, 22)}` });
293
+ await rpc(t.url, 'SendMessage', { message: acceptMessage(id!) });
294
+ await settle(id!, (s) => s !== undefined && TERMINAL.has(s));
295
+ const terminal = await rpc(t.url, 'CancelTask', { id });
296
+ for (const [label, r, reason] of [['GetTask on an unknown id', unknown, 'TASK_NOT_FOUND'], ['CancelTask on a terminal task', terminal, 'TASK_NOT_CANCELABLE']] as const) {
297
+ expect(Array.isArray(r.error?.data), req(R11('a2a-error-data-shape'), `${DOC11} (A2A §9.5)`, `${label}: error.data MUST be an array of objects each carrying @type (got ${JSON.stringify(r.error ?? r.result)})`)).toBe(true);
298
+ const all = (Array.isArray(r.error?.data) ? r.error!.data : []) as unknown[];
299
+ expect(all.every((x) => typeof x === 'object' && x !== null && typeof (x as Record<string, unknown>)['@type'] === 'string'), req(R11('a2a-error-data-shape'), `${DOC11} (A2A §9.5)`, `${label}: every element of error.data MUST carry @type (got ${JSON.stringify(all)})`)).toBe(true);
300
+ const infos = errorInfos(r.error?.data);
301
+ expect(infos.length, req(R11('a2a-error-data-shape'), DOC11, `${label}: error.data MUST include exactly one google.rpc.ErrorInfo (got ${JSON.stringify(all)})`)).toBe(1);
302
+ expect([infos[0]?.reason, infos[0]?.domain], req(R11('a2a-error-data-shape'), `${DOC11}; interop-map.json a2a.errors reason`, `${label}: the ErrorInfo reason MUST be the map row's reason and the domain ${A2A_ERROR_DOMAIN}`)).toEqual([reason, A2A_ERROR_DOMAIN]);
303
+ }
304
+ });
305
+
306
+ it('no response on the card\'s JSON-RPC interface URL is the OpenWOP error envelope', async () => {
307
+ const t = await target(false);
308
+ if (!t.ok) return skip(t, R11('a2a-no-openwop-envelope'));
309
+ const probes: Array<[string, RequestInit]> = [
310
+ ['a body that is not JSON', { method: 'POST', headers: { 'content-type': 'application/json', 'A2A-Version': '1.0', ...(process.env.OPENWOP_API_KEY ? { authorization: `Bearer ${process.env.OPENWOP_API_KEY}` } : {}) }, body: '{"jsonrpc":"2.0","id":1,' }],
311
+ ['a request with no credential', { method: 'POST', headers: { 'content-type': 'application/json', 'A2A-Version': '1.0' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'GetTask', params: { id: 'x/y' } }) }],
312
+ ['an unknown method', { method: 'POST', headers: { 'content-type': 'application/json', 'A2A-Version': '1.0', ...(process.env.OPENWOP_API_KEY ? { authorization: `Bearer ${process.env.OPENWOP_API_KEY}` } : {}) }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'openwop.conformance.NoSuchMethod', params: {} }) }],
313
+ ];
314
+ const seen: string[] = [];
315
+ for (const [label, init] of probes) {
316
+ const res = await fetch(t.url, init);
317
+ const text = await res.text();
318
+ let body: unknown = null;
319
+ try { body = text.length > 0 ? JSON.parse(text) : null; } catch { body = null; }
320
+ seen.push(`${label}: HTTP ${res.status}`);
321
+ expect(isOpenwopEnvelope(body), req(R11('a2a-no-openwop-envelope'), `${DOC11}; RFC 0211 §C`, `${label}: a response on an interface URL the card lists — including a refusal before dispatch — MUST be in the binding's shape, never the OpenWOP { error, message } envelope (HTTP ${res.status}, body ${text.slice(0, 200)})`)).toBe(false);
322
+ }
323
+ expect(seen.length, req(R11('a2a-no-openwop-envelope'), DOC11, 'every probe reached the interface')).toBe(probes.length);
324
+ });
325
+
326
+ it('VersionNotSupportedError carries ErrorInfo VERSION_NOT_SUPPORTED, and supportedVersions is a comma-joined subset of the card', async () => {
327
+ const t = await target(false);
328
+ if (!t.ok) return skip(t, R11('a2a-version-not-supported-shape'));
329
+ const a2a = (await familyAdvertised('a2a'))!;
330
+ const card = (await (await fetch(String(a2a['agentCardUrl']), { headers: { accept: 'application/json', 'A2A-Version': '1.0' } })).json().catch(() => ({}))) as { supportedInterfaces?: Array<{ url?: string; protocolVersion?: string }> };
331
+ const offered = new Set((card.supportedInterfaces ?? []).filter((i) => i.url === t.url).map((i) => String(i.protocolVersion)));
332
+ const r = await rpc(t.url, 'GetTask', { id: 'zz-conformance/unknown' }, { version: '99.0' });
333
+ expect(r.error?.code, req(R11('a2a-version-not-supported-shape'), 'a2a.errors VersionNotSupportedError', `an A2A-Version the interface does not serve MUST be -32009, not -32600 (got ${JSON.stringify(r.error ?? r.result)})`)).toBe(-32009);
334
+ const info = errorInfos(r.error?.data)[0];
335
+ expect(info?.reason, req(R11('a2a-version-not-supported-shape'), DOC11, `the -32009 error MUST carry ErrorInfo reason VERSION_NOT_SUPPORTED (got ${JSON.stringify(r.error?.data)})`)).toBe('VERSION_NOT_SUPPORTED');
336
+ const sv = (info?.metadata as Record<string, unknown> | undefined)?.['supportedVersions'];
337
+ // metadata.supportedVersions is a SHOULD (RFC 0211 §E): absent is not a failure — a client
338
+ // falls back to the card. When present it MUST be a comma-joined subset of the card's versions.
339
+ if (sv !== undefined) {
340
+ expect(typeof sv === 'string' && sv.split(',').every((v) => offered.has(v.trim())), req(R11('a2a-version-not-supported-shape'), `${DOC11}; RFC 0211 §E`, `when present, metadata.supportedVersions MUST be a comma-joined string of versions the card lists for this interface (got ${JSON.stringify(sv)}; card ${JSON.stringify([...offered])})`)).toBe(true);
341
+ }
342
+ });
343
+
344
+ it('an HTTP+JSON interface answers an A2A error with google.rpc.Status carrying the ErrorInfo', async () => {
345
+ if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
346
+ const a2a = await familyAdvertised('a2a');
347
+ if (!a2a || !(Array.isArray(a2a['profiles']) && (a2a['profiles'] as unknown[]).includes(PROFILE))) return softSkip('inapplicable', 'a2a-1.0 is not claimed — no A2A interface to hold');
348
+ const card = (await (await fetch(String(a2a['agentCardUrl']), { headers: { accept: 'application/json', 'A2A-Version': '1.0' } })).json().catch(() => ({}))) as { supportedInterfaces?: Array<{ url?: string; protocolBinding?: string; protocolVersion?: string }> };
349
+ const rest = (card.supportedInterfaces ?? []).find((i) => i.protocolBinding === 'HTTP+JSON' && i.protocolVersion === '1.0');
350
+ if (typeof rest?.url !== 'string') return softSkip('inapplicable', 'the card lists no HTTP+JSON interface at 1.0 — RFC 0211 §B binds only a host that lists one');
351
+ const headers: Record<string, string> = { accept: 'application/a2a+json, application/json', 'A2A-Version': '1.0' };
352
+ if (process.env.OPENWOP_API_KEY) headers['authorization'] = `Bearer ${process.env.OPENWOP_API_KEY}`;
353
+ const res = await fetch(`${rest.url.replace(/\/$/, '')}/tasks/${encodeURIComponent(`zz-conformance/${randomBytes(8).toString('hex')}`)}`, { headers });
354
+ const body = (await res.json().catch(() => ({}))) as { error?: { code?: unknown; status?: unknown; message?: unknown; details?: unknown } };
355
+ expect([typeof body.error?.code, typeof body.error?.status, typeof body.error?.message], req(R11('a2a-httpjson-status'), `${DOC11} (A2A §11.6)`, `an HTTP+JSON A2A error MUST be google.rpc.Status { error: { code, status, message, details } } (HTTP ${res.status}, got ${JSON.stringify(body)})`)).toEqual(['number', 'string', 'string']);
356
+ expect(errorInfos(body.error?.details)[0]?.reason, req(R11('a2a-httpjson-status'), `${DOC11} (A2A §11.6)`, 'the Status details MUST carry the ErrorInfo TASK_NOT_FOUND')).toBe('TASK_NOT_FOUND');
357
+ });
358
+
277
359
  it('a run suspended on a credential interrupt projects to TASK_STATE_AUTH_REQUIRED, naming the provider and carrying connectUrl', async () => {
278
360
  const id = 'openwop.requirement.0199.a2a-auth-required';
279
361
  const t = await target(false);
@@ -27,12 +27,11 @@
27
27
  * @see RFCS/0187-host-found-bindings.md §A
28
28
  */
29
29
  import { describe, it, expect } from 'vitest';
30
- import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
31
30
  import { driver, type OpenWOPResponse } from '../lib/driver.js';
32
31
  import { v2Discovery, gateFamily } from '../lib/v2.js';
33
32
  import { readErrorCode } from '../lib/error-envelope.js';
34
33
  import { softSkip } from '../lib/soft-skip.js';
35
- import { receiverBinding, resolveRegistrationUrl } from '../lib/webhook-receiver.js';
34
+ import { noDeliveryCause, startScopedReceiver, unservedDestination, type ScopedReceiver } from '../lib/scoped-receiver.js';
36
35
  import { scaledTimeoutMs } from '../lib/polling.js';
37
36
  import { req } from '../lib/requirement-ids.js';
38
37
  import { projectBoundId, BOUND_ID } from '../lib/bound-id.js';
@@ -50,20 +49,21 @@ const PER_KIND = 'openwop.requirement.0187.bound-id-kinds.per-kind';
50
49
  const WEBHOOK_EMITTED = 'openwop.requirement.0187.bound-id-kinds.webhook-emitted';
51
50
 
52
51
  type Delivery = { body: string; headers: Record<string, string | string[] | undefined> };
53
- /** The suite's receiver, bound the way every webhook scenario binds it (pinned port / public front honoured). */
54
- async function startReceiver(): Promise<{ server: Server; url: string; deliveries: Delivery[] }> {
52
+ /**
53
+ * The suite's receiver for THIS exercise — pinned port and public front
54
+ * honoured, plus a per-exercise nonce path (2.37.0). Four webhook files used to
55
+ * register the same byte-identical front URL, and a webhook subscription
56
+ * outlives the file that made it, so a sibling's leftovers arrived here
57
+ * indistinguishable from this run's deliveries.
58
+ */
59
+ async function startReceiver(): Promise<ScopedReceiver & { deliveries: Delivery[] }> {
55
60
  const deliveries: Delivery[] = [];
56
- const server = createServer((request: IncomingMessage, res: ServerResponse) => {
57
- const chunks: Buffer[] = [];
58
- request.on('data', (c: Buffer) => chunks.push(c));
59
- request.on('end', () => { deliveries.push({ body: Buffer.concat(chunks).toString('utf8'), headers: request.headers }); res.writeHead(204); res.end(); });
61
+ const rx = await startScopedReceiver((hit, res) => {
62
+ deliveries.push({ body: hit.body, headers: hit.headers });
63
+ res.writeHead(204);
64
+ res.end();
60
65
  });
61
- const pinned = Number(process.env['OPENWOP_WEBHOOK_RECEIVER_PORT'] ?? '');
62
- const bindPort = Number.isInteger(pinned) && pinned > 0 && pinned < 65536 ? pinned : 0;
63
- const binding = receiverBinding();
64
- await new Promise<void>((resolve) => server.listen(bindPort, binding.bind, () => resolve()));
65
- const addr = server.address();
66
- return { server, url: `http://${binding.advertise}:${typeof addr === 'object' && addr ? addr.port : 0}/hook`, deliveries };
66
+ return { ...rx, deliveries };
67
67
  }
68
68
  const header = (d: Delivery, name: string): string | undefined => { const v = d.headers[name]; return Array.isArray(v) ? v[0] : v; };
69
69
 
@@ -89,13 +89,21 @@ describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
89
89
  it('subscriptionId: POST /webhooks mints a bound webhookId, the projected segment is accepted, and a foreign tenant segment is refused 403', async () => {
90
90
  if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
91
91
  if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised — no subscription to mint (gate recorded under openwop.family.webhooks)');
92
- // Route the mint through `resolveRegistrationUrl` so an operator who sets
92
+ // Route the mint through the operator's front so an operator who sets
93
93
  // OPENWOP_WEBHOOK_RECEIVER_URL actually gets a reachable registration here.
94
- // Before this, the blocked note TOLD them to set that variable and this file
94
+ // Before that, the blocked note TOLD them to set the variable and this file
95
95
  // never read it — inoperative advice in a suite where one blocked row denies
96
96
  // every claimed profile (RFC 0168 §E.1). The fallback stays a reserved
97
97
  // `.invalid` host: this leg only needs the mint, never a delivery.
98
- const registration = resolveRegistrationUrl('https://subscriber.invalid/hook');
98
+ //
99
+ // `unservedDestination`, not `resolveRegistrationUrl` (2.37.0): the latter
100
+ // returns the front VERBATIM, so this subscription had the same identity as
101
+ // every other exercise's, and it is live from the 201 until the DELETE
102
+ // below. Anything the host fanned out in that window landed on whichever
103
+ // listener held the pinned port and was read as THAT exercise's traffic. The
104
+ // nonce is served by nobody on purpose — this leg wants no delivery, and a
105
+ // delivery to it is now answered 404 instead of absorbed.
106
+ const registration = unservedDestination('https://subscriber.invalid/hook');
99
107
  const reg = await http(() => driver.post('/webhooks', { url: registration.url, events: ['run.completed'] }));
100
108
  if (reg === null) return softSkip('blocked', 'POST /webhooks unreachable (fetch failed)');
101
109
  if (reg.status === 400 && readErrorCode(reg.json) === 'webhook_url_rejected') return softSkip('blocked', `host SSRF guard rejected the registration URL ${registration.url}${registration.tunnelled ? ' (from OPENWOP_WEBHOOK_RECEIVER_URL)' : ' — set OPENWOP_WEBHOOK_RECEIVER_URL to a public https receiver, which THIS leg now honours'}`);
@@ -136,11 +144,14 @@ describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
136
144
  const rx = await startReceiver();
137
145
  let webhookId: string | null = null;
138
146
  try {
139
- const registration = resolveRegistrationUrl(rx.url);
140
- const reg = await http(() => driver.post('/webhooks', { url: registration.url, events: ['run.completed'] }));
147
+ // `rx.url` is already this exercise's destination — the front (when
148
+ // wired) plus its nonce path — so it is not run through
149
+ // `resolveRegistrationUrl`, which returns the front VERBATIM and would
150
+ // drop the path that makes the subscription ours.
151
+ const reg = await http(() => driver.post('/webhooks', { url: rx.url, events: ['run.completed'] }));
141
152
  if (reg === null) return softSkip('blocked', 'POST /webhooks unreachable (fetch failed)');
142
153
  if (reg.status === 400 && readErrorCode(reg.json) === 'webhook_url_rejected') {
143
- return softSkip('blocked', `host SSRF guard rejected the suite receiver ${registration.url} (webhooks.md §Egress requires it) — set OPENWOP_WEBHOOK_RECEIVER_URL to a public https front for the receiver to witness what the host emits`);
154
+ return softSkip('blocked', `host SSRF guard rejected the suite receiver ${rx.url} (webhooks.md §Egress requires it) — set OPENWOP_WEBHOOK_RECEIVER_URL to a public https front for the receiver to witness what the host emits`);
144
155
  }
145
156
  const minted = (reg.json as { webhookId?: unknown } | null)?.webhookId;
146
157
  if (reg.status !== 201 || typeof minted !== 'string') return softSkip('blocked', `POST /webhooks answered ${reg.status} without a webhookId — the mint leg above owns that obligation; this leg needs an id to compare`);
@@ -155,7 +166,7 @@ describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
155
166
  const deadline = Date.now() + scaledTimeoutMs(20_000);
156
167
  while (ours().length === 0 && Date.now() < deadline) await new Promise((r) => setTimeout(r, 250));
157
168
  const delivery = ours()[0];
158
- if (delivery === undefined) return softSkip('blocked', `no delivery for run ${runId} reached the suite receiver within ${scaledTimeoutMs(20_000)}ms — what the host emits was not observed`);
169
+ if (delivery === undefined) return softSkip('blocked', `no delivery for run ${runId} reached the suite receiver within ${scaledTimeoutMs(20_000)}ms — what the host emits was not observed. ${noDeliveryCause(rx, `run.completed delivery for run ${runId}`)}`);
159
170
  expect(
160
171
  header(delivery, 'openwop-webhook-id'),
161
172
  req(WEBHOOK_EMITTED, 'webhooks.md §Headers (RFC 0187 §A.1)', `the delivery's OpenWOP-Webhook-Id MUST equal the tenant-bound webhookId the host minted (${minted}) — a subscriber identifies its deliveries by the header, not by the 201 it received once`),
@@ -172,7 +183,9 @@ describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
172
183
  }
173
184
  } finally {
174
185
  if (webhookId !== null) await http(() => driver.delete(`/webhooks/${projectBoundId(webhookId as string)}`));
175
- await new Promise<void>((ok) => rx.server.close(() => ok()));
186
+ // Closed through the receiver: `close()` also drops this exercise's
187
+ // nonce from the front-mux registry.
188
+ await rx.close();
176
189
  }
177
190
  });
178
191
 
@@ -64,7 +64,6 @@
64
64
  * @see RFCS/0158-durable-execution-and-disaster-recovery-qualification.md §B.4 §D.9 §E
65
65
  */
66
66
 
67
- import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
68
67
  import { describe, expect, it } from 'vitest';
69
68
  import { driver } from '../lib/driver.js';
70
69
  import { v2Discovery } from '../lib/v2.js';
@@ -72,7 +71,7 @@ import { isFixtureAdvertised } from '../lib/fixtures.js';
72
71
  import { softSkip } from '../lib/soft-skip.js';
73
72
  import { pollUntilTerminal, scaledTimeoutMs } from '../lib/polling.js';
74
73
  import { req } from '../lib/requirement-ids.js';
75
- import { receiverBinding, resolveRegistrationUrl } from '../lib/webhook-receiver.js';
74
+ import { startEffectReceiver, waitForFirstArrival } from '../lib/effect-receiver.js';
76
75
  import { watchForResumption, type Observation, type Watch } from '../lib/durability-watch.js';
77
76
  import { noteEvidence, parseRecoveryBounds, EVIDENCE_NAME_PATTERN } from '../lib/durability-evidence.js';
78
77
 
@@ -267,26 +266,15 @@ function watch(runId: string, budgetMs: number, resumed: (o: Observation) => boo
267
266
  }
268
267
 
269
268
  /**
270
- * The suite's own destination for the one staged effect. Every request that
271
- * reaches it is an INVOCATION — the thing §C says to count. Honours
272
- * OPENWOP_WEBHOOK_RECEIVER_PORT so a tunnelled cut forwards here (the
273
- * certification setting is already `--max-workers 1`, so the pinned port is
274
- * not contended by the webhook files).
269
+ * How long to wait for the ONE legitimate arrival before concluding none came.
270
+ *
271
+ * Separate from `QUIET_WINDOW_MS`, and for the opposite reason: this is a wait
272
+ * FOR something that must happen, so a generous bound weakens nothing, while
273
+ * the quiet window that follows is a wait for something that must NOT happen.
274
+ * Collapsing the two into one blind sleep made the row measure the host's
275
+ * effect latency under load — see `lib/effect-receiver.ts`.
275
276
  */
276
- async function startEffectReceiver(): Promise<{ server: Server; url: string; arrivals: Array<{ method: string; at: number }> }> {
277
- const arrivals: Array<{ method: string; at: number }> = [];
278
- const server = createServer((request: IncomingMessage, res: ServerResponse) => {
279
- request.on('data', () => { /* drain */ });
280
- request.on('end', () => { arrivals.push({ method: request.method ?? '', at: Date.now() }); res.writeHead(204); res.end(); });
281
- });
282
- const pinned = Number(process.env['OPENWOP_WEBHOOK_RECEIVER_PORT'] ?? '');
283
- const bindPort = Number.isInteger(pinned) && pinned > 0 && pinned < 65536 ? pinned : 0;
284
- const binding = receiverBinding();
285
- await new Promise<void>((resolve) => server.listen(bindPort, binding.bind, () => resolve()));
286
- const addr = server.address();
287
- const port = typeof addr === 'object' && addr ? addr.port : 0;
288
- return { server, url: `http://${binding.advertise}:${port}/effect`, arrivals };
289
- }
277
+ const FIRST_ARRIVAL_BUDGET_MS = 20_000;
290
278
 
291
279
  describe('v2-durability-recovery (RFC 0158 §B.4, §D.9, §E — the durable-single-instance rows)', () => {
292
280
  it('accepted work survives a kill before dispatch and dispatches on resume', async () => {
@@ -421,12 +409,29 @@ describe('v2-durability-recovery (RFC 0158 §B.4, §D.9, §E — the durable-sin
421
409
  // (`conformance-noop` records none). The seam is host test infrastructure
422
410
  // (§E) and chooses the work; it MUST stage work that performs EXACTLY ONE
423
411
  // outbound effect, addressed to `effectUrl`, and deliver it twice.
412
+ //
413
+ // 2.37.0 — THE DESTINATION CARRIES A PER-EXERCISE NONCE, and that is what
414
+ // makes this row deterministic. An effect's Layer-2 identity is its
415
+ // BUSINESS identity (idempotency.md §"Layer 2 Keying"): tenant, workflow,
416
+ // node, request digest — and the destination URL is part of that digest,
417
+ // while the runId deliberately is not. Until 2.37.0 this leg and
418
+ // `0194.terminal-once.duplicate-delivery` both handed the seam
419
+ // `resolveRegistrationUrl(...)`, which on a tunnelled cut is
420
+ // OPENWOP_WEBHOOK_RECEIVER_URL verbatim — the same string for both. Same
421
+ // fixture, same node, same URL ⇒ SAME effect identity, so a CONFORMANT
422
+ // host resolved the second exercise to the first's recorded outcome and
423
+ // called out zero times. Whichever leg vitest ran second read zero
424
+ // arrivals: `blocked` on loopback, a hard `executed-fail` on a tunnelled
425
+ // cut. Measured on the v2 reference host — exercise 2's ledger row read
426
+ // `invocationId: "deduplicated-of:<run 1>"`. Nothing but the file order
427
+ // differed between a pass and a fail, and the row was measuring
428
+ // cross-exercise deduplication rather than §C's within-exercise
429
+ // exactly-once. The nonce gives every exercise its own identity.
424
430
  const rx = await startEffectReceiver();
425
431
  try {
426
- const target = resolveRegistrationUrl(rx.url);
427
- const fired = await driver.post(KILL_SEAM, { mode: 'duplicate-delivery', effectUrl: target.url });
432
+ const fired = await driver.post(KILL_SEAM, { mode: 'duplicate-delivery', effectUrl: rx.url });
428
433
  if (fired.status >= 400) {
429
- return softSkip('blocked', `the durability seam answered ${fired.status} for mode=duplicate-delivery with effectUrl ${target.tunnelled ? '(tunnelled)' : rx.url} — the host exposes the route but could not stage a double delivery; if its egress guard refused the receiver, the operator precondition is the webhook rows' own: a publicly-resolvable https front (OPENWOP_WEBHOOK_RECEIVER_URL) or a host run with its private-egress relaxation recorded`);
434
+ return softSkip('blocked', `the durability seam answered ${fired.status} for mode=duplicate-delivery with effectUrl ${rx.tunnelled ? '(tunnelled front)' : rx.url} — the host exposes the route but could not stage a double delivery; if its egress guard refused the receiver, the operator precondition is the webhook rows' own: a publicly-resolvable https front (OPENWOP_WEBHOOK_RECEIVER_URL) or a host run with its private-egress relaxation recorded`);
430
435
  }
431
436
  const runId = (fired.json as { runId?: unknown } | null)?.runId;
432
437
  if (typeof runId !== 'string') {
@@ -434,28 +439,71 @@ describe('v2-durability-recovery (RFC 0158 §B.4, §D.9, §E — the durable-sin
434
439
  }
435
440
 
436
441
  await pollUntilTerminal(runId, { timeoutMs: scaledTimeoutMs(60_000) });
437
- // A LONGER wait is a STRONGER claim: this is a wait for a second arrival
438
- // that must not happen, and the second delivery may trail the first.
442
+ // Wait FOR the one legitimate arrival, then wait OUT the quiet window for
443
+ // a second that must not come. A single blind sleep conflated the two and
444
+ // made a slow-but-correct host read as zero.
445
+ await waitForFirstArrival(rx, scaledTimeoutMs(FIRST_ARRIVAL_BUDGET_MS));
439
446
  await new Promise((r) => setTimeout(r, scaledTimeoutMs(QUIET_WINDOW_MS)));
440
447
 
441
- const arrivals = rx.arrivals.length;
442
- if (arrivals === 0 && !target.tunnelled) {
443
- return softSkip('blocked', `the staged work's effect never reached the suite's receiver at ${rx.url} — for mode=duplicate-delivery the seam MUST aim exactly one outbound effect at the given effectUrl; with nothing landed there is no invocation to count, and the ledger alone cannot witness a double-fire`);
448
+ // Read the host's OWN account first, so a zero can SAY WHY. The two
449
+ // zeroes are different facts and must not share one disposition: nothing
450
+ // was ever sent, or the host resolved this work to an outcome it had
451
+ // already recorded (a `deduplicated-of:` / `replay-of:` invocation) —
452
+ // correct Layer-2 behaviour that leaves §C unmeasured, not refuted.
453
+ const eff = await driver.get(`/runs/${encodeURIComponent(runId)}/effects`);
454
+ const effects = eff.status === 200 ? ((eff.json as { effects?: Array<Record<string, unknown>> } | null)?.effects ?? []) : [];
455
+ const resolvedElsewhere = effects.filter((e) => /^(deduplicated-of|replay-of):/.test(String(e['invocationId'] ?? '')));
456
+ // An effect the host ATTEMPTED and whose transport failed. `released` is
457
+ // the state persistence.md gives a claim that was taken and given back;
458
+ // a host that names it is telling us the invocation was tried and never
459
+ // reached the destination. That is a THIRD kind of zero and it must not
460
+ // be reported as either of the others.
461
+ const attemptedAndFailed = effects.filter((e) => String(e['state'] ?? '') === 'released');
462
+ const arrivals = rx.arrivals();
463
+ if (arrivals === 0) {
464
+ // A BARE FAIL ON ZERO IS UNINFORMATIVE, so the zero is classified from
465
+ // the host's own ledger before any disposition is chosen. Three states
466
+ // wear the same symptom and only one of them is about §C:
467
+ // (a) resolved to an outcome the host had ALREADY recorded — correct
468
+ // Layer-2 keying, and this exercise simply performed no
469
+ // invocation to count;
470
+ // (b) attempted and the transport failed — a missed fire, which is
471
+ // the opposite of a double fire and cannot refute exactly-once;
472
+ // (c) nothing in the ledger at all — the seam never staged the one
473
+ // effect the mode requires, so the exercise did not happen.
474
+ // None of the three is evidence AGAINST §C, so none of them is an
475
+ // `executed-fail`; each is `blocked` with its own cause named. The only
476
+ // thing this row ever fails on is a count that is not 1 with at least
477
+ // one real invocation observed.
478
+ const why = resolvedElsewhere.length > 0
479
+ ? `the host resolved the staged effect to an outcome it had ALREADY RECORDED (${resolvedElsewhere.map((e) => String(e['invocationId'])).join(', ')}) — correct Layer-2 keying (idempotency.md §"Layer 2 Keying"), but it means no invocation happened in THIS exercise, so §C's exactly-once is unmeasured here rather than violated. The destination this suite minted carries a per-exercise nonce precisely so this cannot happen; a host that keys on something coarser than the request MUST stage a fresh business identity per exercise`
480
+ : attemptedAndFailed.length > 0
481
+ ? `the host ATTEMPTED the staged effect and its transport failed — ${attemptedAndFailed.length} ledger row(s) in state \`released\`${attemptedAndFailed.map((e) => ` (${String(e['effectId'] ?? '?')} attempt ${String(e['attempt'] ?? '?')})`).join('')}. A MISSED fire is not an exactly-once violation, so this is not a refutation of §C and is not recorded as one; the operator precondition is a destination the host can actually reach inside its own effect timeout, and a host whose staged work takes no transport retry will show this whenever the round trip exceeds that timeout`
482
+ : `the staged work's effect never reached the suite's receiver at ${rx.localUrl}${rx.tunnelled ? ` (fronted as ${rx.url})` : ''} within ${FIRST_ARRIVAL_BUDGET_MS}ms of terminal, and the host's own effect ledger records no attempt — for mode=duplicate-delivery the seam MUST aim exactly one outbound effect at the given effectUrl; with nothing landed there is no invocation to count, and the ledger alone cannot witness a double-fire${rx.foreign() > 0 ? `. ${rx.foreign()} request(s) DID reach this listener without this exercise's nonce, so the front is wired but the host addressed something else` : ''}`;
483
+ // Zero is never a pass, and — since 2.37.0 — never a bare fail either.
484
+ // Until now a tunnelled cut turned every zero into `executed-fail`
485
+ // ("a mis-wired tunnel must not read as a pass"), which is right about
486
+ // the pass and wrong about the fail: it convicted a host of violating
487
+ // exactly-once on the evidence that it fired too FEW times. The
488
+ // mis-wired tunnel it was defending against is now the `foreign()`
489
+ // count — a front that reaches this listener but carries someone
490
+ // else's nonce — which the reason above names. `blocked` is not a
491
+ // softening: a blocked row DENIES certification (RFC 0168 §E.1)
492
+ // exactly as a failed one does, so the rung is still refused; what
493
+ // changes is that the bundle says which of the three zeroes happened
494
+ // instead of asserting a violation that was never observed.
495
+ return softSkip('blocked', why);
444
496
  }
445
- // With a tunnel declared, zero arrivals is a hard failure, never a skip
446
- // (webhook-receiver.ts): a mis-wired tunnel must not read as a pass.
447
497
  expect(
448
498
  arrivals,
449
- req('openwop.requirement.0158.duplicate-delivery', 'RFC 0158 §C', `the same accepted work delivered twice MUST fire each effect exactly once, counted at the effect's destination — the suite's receiver observed ${arrivals} arrival(s) of the one staged effect for run ${runId}`),
499
+ req('openwop.requirement.0158.duplicate-delivery', 'RFC 0158 §C', `the same accepted work delivered twice MUST fire each effect exactly once, counted at the effect's destination — the suite's receiver observed ${arrivals} arrival(s) bearing this exercise's nonce ${rx.nonce} for run ${runId}${rx.foreign() > 0 ? ` (and ${rx.foreign()} unrelated request(s), not counted)` : ''}`),
450
500
  ).toBe(1);
451
501
 
452
502
  // Secondary, and labelled for what it is: the host's own account agrees
453
503
  // with what landed. On an identity-keyed ledger this can never exceed one
454
504
  // row per identity, so it witnesses that the PROJECTION IS CONSISTENT, not
455
505
  // that no double-fire happened — the arrival count above owns that.
456
- const eff = await driver.get(`/runs/${encodeURIComponent(runId)}/effects`);
457
506
  if (eff.status === 200) {
458
- const effects = (eff.json as { effects?: Array<Record<string, unknown>> } | null)?.effects ?? [];
459
507
  const byIdentity = new Map<string, number>();
460
508
  for (const e of effects) {
461
509
  const id = String(e['effectId'] ?? e['keying'] ?? '');
@@ -469,7 +517,7 @@ describe('v2-durability-recovery (RFC 0158 §B.4, §D.9, §E — the durable-sin
469
517
  ).toBe(true);
470
518
  }
471
519
  } finally {
472
- await new Promise<void>((r) => rx.server.close(() => r()));
520
+ await rx.close();
473
521
  }
474
522
  }, 180_000);
475
523