@openwop/openwop-conformance 2.23.0 → 2.25.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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.25.0] — 2026-09-19 — a leg that asserted nothing
4
+
5
+ - `v2-webhook-durable-delivery` — the `0188.dead-letter-content-free` leg read a **fresh** subscription's sink, looped over zero rows, and asserted nothing; RFC 0148 §A resolves that silent return to `blocked`, denying certification. It now exhausts a real delivery first and asserts over the records it gets, and records `blocked` with a reason when the sink is empty.
6
+ - Suite pinned to corpus `2.25.0`.
7
+
8
+ ## [2.24.0] — 2026-09-19 — the envelope-kind catalog gets a seat
9
+
10
+ - `v2-envelope-catalog.test.ts` — six corpus witnesses for RFC 0193. The generator leg sabotages a scratch copy (removes a seat file, asserts the refusal names the family), because the committed tree is clean and a bare exit-0 wrapper would pass just as happily with the guard deleted. The optionality leg asserts no seat is `required` — MyndHyve publishes two of these records without one, and the v2 root is closed.
11
+ - Suite pinned to corpus `2.24.0`.
12
+
3
13
  ## [2.23.0] — 2026-09-19 — zero undeclared families
4
14
 
5
15
  **Why a minor.** Three new `spec/v2/core/*.md` documents and `spec/v2/declaration.json`
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  # --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
12
12
  # default resolver refuses. npm 10.9 fails outright with
13
13
  # "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
14
- npm install --legacy-peer-deps @openwop/openwop-conformance@2.23.0 @openwop/spec-artifacts@2.23.0
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.25.0 @openwop/spec-artifacts@2.25.0
15
15
  # or run without install:
16
16
  npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
17
17
  ```
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.23.0",
4
- "stampSha256": "3d007a8923ab5a43cb71c1bbfba52c0b28c3649284e242cba147e5397d2eec54"
3
+ "version": "2.25.0",
4
+ "stampSha256": "240fb0a1cba70c2a5263b6ef5e150614de639d3cac668c4f7d7875780e0e0a49"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.23.0",
3
+ "version": "2.25.0",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,6 +56,6 @@
56
56
  "@openwop/spec-artifacts": "file:../spec-artifacts"
57
57
  },
58
58
  "peerDependencies": {
59
- "@openwop/spec-artifacts": "2.23.0"
59
+ "@openwop/spec-artifacts": "2.25.0"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
- "files": 578,
6
- "tests": 2192,
7
- "withStableId": 2192,
5
+ "files": 579,
6
+ "tests": 2198,
7
+ "withStableId": 2198,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2146
9
+ "explicitIds": 2152
10
10
  },
11
11
  "records": [
12
12
  {
@@ -29469,7 +29469,8 @@
29469
29469
  "citations": [
29470
29470
  {
29471
29471
  "section": "RFC 0188 §B.1",
29472
- "requirement": "a dead-letter record MUST NOT carry the delivered body, headers or subscription secret — the queue is precisely the traffic the subscriber never received"
29472
+ "requirement": null,
29473
+ "interpolated": true
29473
29474
  }
29474
29475
  ]
29475
29476
  },
@@ -35884,6 +35885,90 @@
35884
35885
  }
35885
35886
  ]
35886
35887
  },
35888
+ {
35889
+ "id": "openwop.it.v2-envelope-catalog.each-of-the-three-carries-its-seat",
35890
+ "file": "v2-envelope-catalog.test.ts",
35891
+ "line": 64,
35892
+ "title": "each of the three carries its seat",
35893
+ "explicitId": "openwop.requirement.0193.seats-present",
35894
+ "citations": [
35895
+ {
35896
+ "section": "RFC 0193 §B",
35897
+ "requirement": null,
35898
+ "interpolated": true
35899
+ }
35900
+ ]
35901
+ },
35902
+ {
35903
+ "id": "openwop.it.v2-envelope-catalog.no-seat-is-required-a-published-record-without-one-stays-valid",
35904
+ "file": "v2-envelope-catalog.test.ts",
35905
+ "line": 73,
35906
+ "title": "no seat is required — a published record without one stays valid",
35907
+ "explicitId": "openwop.requirement.0193.seats-optional",
35908
+ "citations": [
35909
+ {
35910
+ "section": "RFC 0193 §B",
35911
+ "requirement": null,
35912
+ "interpolated": true
35913
+ }
35914
+ ]
35915
+ },
35916
+ {
35917
+ "id": "openwop.it.v2-envelope-catalog.the-generator-refuses-a-family-whose-payload-it-cannot-carry",
35918
+ "file": "v2-envelope-catalog.test.ts",
35919
+ "line": 82,
35920
+ "title": "the generator refuses a family whose payload it cannot carry",
35921
+ "explicitId": "openwop.requirement.0193.generator-refuses",
35922
+ "citations": [
35923
+ {
35924
+ "section": "RFC 0193 §A/§B",
35925
+ "requirement": null,
35926
+ "interpolated": true
35927
+ }
35928
+ ]
35929
+ },
35930
+ {
35931
+ "id": "openwop.it.v2-envelope-catalog.an-absent-catalog-is-stated-to-fail-closed",
35932
+ "file": "v2-envelope-catalog.test.ts",
35933
+ "line": 95,
35934
+ "title": "an absent catalog is stated to fail CLOSED",
35935
+ "explicitId": "openwop.requirement.0193.absence-is-closed",
35936
+ "citations": [
35937
+ {
35938
+ "section": "RFC 0193 §C",
35939
+ "requirement": null,
35940
+ "interpolated": true
35941
+ }
35942
+ ]
35943
+ },
35944
+ {
35945
+ "id": "openwop.it.v2-envelope-catalog.both-refusal-codes-are-registered-at-major-2",
35946
+ "file": "v2-envelope-catalog.test.ts",
35947
+ "line": 106,
35948
+ "title": "both refusal codes are registered at major 2",
35949
+ "explicitId": "openwop.requirement.0193.codes-registered",
35950
+ "citations": [
35951
+ {
35952
+ "section": "RFC 0193 §F",
35953
+ "requirement": null,
35954
+ "interpolated": true
35955
+ }
35956
+ ]
35957
+ },
35958
+ {
35959
+ "id": "openwop.it.v2-envelope-catalog.versioning-axis-6-no-longer-contradicts-the-record",
35960
+ "file": "v2-envelope-catalog.test.ts",
35961
+ "line": 116,
35962
+ "title": "versioning axis 6 no longer contradicts the record",
35963
+ "explicitId": "openwop.requirement.0193.axis-6-agrees",
35964
+ "citations": [
35965
+ {
35966
+ "section": "RFC 0193 §D",
35967
+ "requirement": null,
35968
+ "interpolated": true
35969
+ }
35970
+ ]
35971
+ },
35887
35972
  {
35888
35973
  "id": "openwop.it.v2-eos-clock.the-clock-file-is-current-with-the-matrix-and-the-public-history",
35889
35974
  "file": "v2-eos-clock.test.ts",
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
3
3
  "package": "@openwop/spec-artifacts",
4
- "version": "2.23.0",
5
- "corpusTag": "v2.23.0",
4
+ "version": "2.25.0",
5
+ "corpusTag": "v2.25.0",
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
8
8
  "api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
9
9
  "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
10
10
  "api/openapi.yaml": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "3d800d8f6845460af75a6f96db40a3b1419c9b7471e671e2bc44090871a02d68",
13
- "api/v2/asyncapi.yaml": "f37bbe960632be29c8a86f0678fe39c16640a99bf91d767c02ce4a093c7004db",
14
- "api/v2/openapi.yaml": "f2168eb8552cd21a0aaab004da4257578601867e73755e99de20ad9b771d9a72",
12
+ "api/seams-v2.yaml": "f8a8a952166e13505d28e6859788f3269493fe4c440433442214b282334fef72",
13
+ "api/v2/asyncapi.yaml": "b165823a2a95a9bbefcb1d71d7768b2031342297077e0c89e414c69eec917ed6",
14
+ "api/v2/openapi.yaml": "b131c9fbde1e2d39dca11287f277ee9d51d1b83b684f5713f9c6e277f07636ce",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
@@ -115,8 +115,8 @@
115
115
  "schemas/v2/artifact-type-pack-manifest.schema.json": "85630d1b760d0b41cf5a6fad2acfada39abaf7f0befd95cc4334e1e883e9615d",
116
116
  "schemas/v2/audit-verify-result.schema.json": "a748dfadb5155adfde8a91045e76b0db6695005fe559035d71670fdd55edb3ba",
117
117
  "schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
118
- "schemas/v2/capabilities.schema.json": "213c5cbe7ae7a337418c9183ed8ec39f1bc4dc39650a9b3c06cea9780dca3f24",
119
- "schemas/v2/certification-bundle.schema.json": "9afcd16cf018d9b0d3b31e3b7e2e844d9289fe7b912a363d0df5a78e4fdfc054",
118
+ "schemas/v2/capabilities.schema.json": "50d3f90d6f92637e2f16956c99bc4826f69f28d935fb75fb06ab6fb910078706",
119
+ "schemas/v2/certification-bundle.schema.json": "be5de0cc4362207c27885c51165c92c9541a545c5bd2c59da1d5c28fc265c162",
120
120
  "schemas/v2/channel-presence-payload.schema.json": "1c20ad810cb311d47aa0b95d928490826b2468fd161f884cd29e867053e6782f",
121
121
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
122
122
  "schemas/v2/chat-card-pack-manifest.schema.json": "8df0d9d273ae22efc6876d5586833f1d4c2e551edc3ac6cf6e8f0719329f03dd",
@@ -141,7 +141,7 @@
141
141
  "schemas/v2/envelopes/schema.request.schema.json": "be2c5f7aa881cdd252ccff47ac02193659e1393633a1541f4297383205d384a2",
142
142
  "schemas/v2/envelopes/schema.response.schema.json": "d049a081267c06c28879c331b147f3ed821b14ccfd93005c67aff64db0a8c68e",
143
143
  "schemas/v2/envelopes/ui.a2ui-surface.schema.json": "0a06fbbdc8ba797a6818c79336a98e211ac5ba5747945b4e4098072cd3974d79",
144
- "schemas/v2/error-envelope.schema.json": "b2c6b673c14dd7c0603d0862f4bf0dad00e401c88c418914753bcd744d8400f5",
144
+ "schemas/v2/error-envelope.schema.json": "bb3e6c487dafa951a56ff224a7c029c462f0f1b754a7f51f6729609b4f43a438",
145
145
  "schemas/v2/eval-summary.schema.json": "9d31025752d0f67ad3b8a32e82a5314972c530ef9f780dd82e484ae8c5131198",
146
146
  "schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
147
147
  "schemas/v2/form-content-pack-manifest.schema.json": "461529e4287ec3b15a9fbcde57226a1c6f17ea3ae71b33b418e6f595ff0619fe",
@@ -201,7 +201,7 @@
201
201
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
202
202
  "spec/v1/alias-detectors.json": "40069d5976eeb6ba1384a648e57e5cfd673db3fce36175115fb8293bee9664d4",
203
203
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
204
- "spec/v1/core-standard-manifest.json": "4b924e0e83fb85b9a4ec9400b34239bca6826a3bd2aad0ff44eaf1e7aa23e6e5",
204
+ "spec/v1/core-standard-manifest.json": "c17bb554c788d68fafc3413593dc6686aef0ad65864e925a9d19c4c3833bd0f5",
205
205
  "spec/v1/deprecations.json": "307083ce29c23fd406015951f99a30d78d6187ff061d38dc9732f62191b40f3f",
206
206
  "spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
207
207
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -218,8 +218,8 @@
218
218
  "spec/v2/core/conformance.md": "a451c0a2ba79d93c55e540ccef480eba7de9602d59bbb03eead05daafde0beac",
219
219
  "spec/v2/core/connection-packs.md": "466bdb9dde79d85615ad8281dacfb7bf923ca22d7fb3ed219749e59574c753ed",
220
220
  "spec/v2/core/conversation.md": "e425887c4ba199b8cdc5c99e792689f7bd7d46de492f28670de2e402fa6d9506",
221
- "spec/v2/core/errors.md": "6b6a1480ed889013172d8ce8c6985e321d2f4f73e1d9fae0bac9b961d0dd1a8c",
222
- "spec/v2/core/events.md": "6fa2b5c43f096636d6d14e4a811aff396b51e9d56743fec21b750f6a224bbb62",
221
+ "spec/v2/core/errors.md": "0dff4a0ae102a86f6a17a9a3b16a203292588a0f422ed19d1c0d9213995aa01d",
222
+ "spec/v2/core/events.md": "c98742f5a310eee8287ca8135493502152e88b479200a6544bb3340fed1e4efe",
223
223
  "spec/v2/core/form-content-packs.md": "af3517dd7125106088249c0e32220b0b9fc3e9304c10378a6327cc8ad84b6bf1",
224
224
  "spec/v2/core/headers.md": "14cdf4ae9ee7d80eebfd426d7c3552e577baea0300915cd4bb3a68e8588195a8",
225
225
  "spec/v2/core/host-services.md": "3753f1e034d325bb4bd5751ae6d321fbb16700d4e28032ab4e273d25eb134fc4",
@@ -233,12 +233,12 @@
233
233
  "spec/v2/core/replay.md": "b980a66e2543107240e3dc4f58131b9f4ff52478737e79ade6d5777b26ad1119",
234
234
  "spec/v2/core/runs.md": "136319842bacbbb300178980e4b83444917d5b193cc5fdd92e39a4902ba8d3c0",
235
235
  "spec/v2/core/security-defaults.md": "2a54ceaa4cef02c26ee0fbcf44f822ceeea79537c7d5a51ed300f21aa4fea576",
236
- "spec/v2/core/versioning.md": "27b88cde88f9060567ef7e46f4897b6be8892ade9f4a46c2ab16e314c515eabd",
236
+ "spec/v2/core/versioning.md": "56c1e273c790bcb0a2c3c760ffc7595afc9f5d4a745965e57f36aefcd456da76",
237
237
  "spec/v2/core/webhooks.md": "46c1311c1897236f9bf9db5025c8af31dd84cd19208da3e5e9ff69eec9436965",
238
238
  "spec/v2/core/workflow-chain-packs.md": "df78b76dd79f2267fb6b72c08e0911e5f9d5bb1925c6efdd6deda62fe8a51dc8",
239
- "spec/v2/declaration.json": "ac333c957815380fe263bf22c58cf3a0a896ef29c2bb5a995f3aad670ecf83f7",
239
+ "spec/v2/declaration.json": "0f35205af96f29cacced435d1b606e9e8292c74a5509444bec78f17d315d63bf",
240
240
  "spec/v2/declaration.schema.json": "c86e3083fb9789c1958ecb4576cc6f71aa90e6f339521139238543ed08211dd4",
241
- "spec/v2/errors.json": "aac91b12f954cc90574d310070ae3c651b131055d4389d2c6155242763765195",
241
+ "spec/v2/errors.json": "296117235d21ce4a393371c2f80caf03a4ae93d1f80a89f87cd3f56902cf5b01",
242
242
  "spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
243
243
  "spec/v2/event-codemap.schema.json": "b173f2a9bcc0bf9b62a474ce3b91e1431d597e64fd1560fc58662cd8606eaf9c",
244
244
  "spec/v2/ext/README.md": "ff91a503e0795e1b898e37d9e445add2d5975529723f7b32b2599b60a24c1ae3",
@@ -263,6 +263,7 @@
263
263
  "spec/v2/facets/a2a.schema.json": "6b0bceddec6d18d5115aa7d04720a0cc1ad7a9a0a78f53c208b34becaba5d16e",
264
264
  "spec/v2/facets/aiProviders.schema.json": "0c76e48e5d487b5be3b3ab1f6512a4e206c840ee5087d1336c7d7e2853b58d03",
265
265
  "spec/v2/facets/auth.schema.json": "73440c944ad12c41aab0618aeb799a90a0b59e97fc685527f248afb7d015381a",
266
+ "spec/v2/facets/envelopeStrictness.schema.json": "28be0a27f880bd45ec1222a16155f5e9167bbca57bf0322970fe1eb2654562aa",
266
267
  "spec/v2/facets/heartbeat.schema.json": "81d7fa1895a8b0241bd07cf00a7e7905bef0fd14eb3427325261c62eee5746a6",
267
268
  "spec/v2/facets/interrupt.schema.json": "c76c58c0abd99de619f8c8afad246568f5bc9cce3b7a0d097d16d87eba84be3a",
268
269
  "spec/v2/facets/mcp.schema.json": "f56609fe24ba6160ad29ae31aa16043354ff005e5cb6a0f3871c4d226087cc08",
@@ -270,14 +271,16 @@
270
271
  "spec/v2/facets/replay.schema.json": "0a5aefb0108c708c6794fadd533cf256709704c4f16dbf44cd63fa8980c78018",
271
272
  "spec/v2/facets/runList.schema.json": "6223e1baaebbb9f4faa71bfe968f8d22b4aa88046d0e43e8d5f56409e7f86e90",
272
273
  "spec/v2/facets/sandbox.schema.json": "e75f54486d387b2d089791aa93b5093cd1450f1920753a0f97fc23acdfe978ed",
274
+ "spec/v2/facets/schemaVersions.schema.json": "890d3416c4b66511d51f329abc68e6f467f4338f16807e489fe682999b2b56b9",
275
+ "spec/v2/facets/supportedEnvelopes.schema.json": "644f22cc2236e30a611b5b7f1ba0458ff1925d68b94c514d9672583224e126bd",
273
276
  "spec/v2/facets/webhooks.schema.json": "4bf3f8eb3161a43910efac4c44ccaf9567060c9da52d4cf09bcf3888d1b92192",
274
277
  "spec/v2/facets/workflowChainPacks.schema.json": "cd30907ac92396866a524e4db8d8c1537e3818cfff455d780efd72bcd0ebdac1",
275
278
  "spec/v2/id-field-bindings.json": "e73c06f3055ad30173507ac817a458b96362c8f26209ba105c53653292cf63d3",
276
279
  "spec/v2/path-manifest.json": "c123c9fd77dc1b3f9c2e9346ace80cdae007e6511138894956abda01e61ec1c6",
277
280
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
278
281
  "spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
279
- "spec/v2/release.json": "bdc9e689a96e66a06be04c38674d23a98d7947ca1fa93439f052a9268a3ab193",
282
+ "spec/v2/release.json": "c315b1ff1a7b15dce5e0a0fda70abe67067f39ac5f171668ba8d637867133c51",
280
283
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
281
284
  },
282
- "corpusCommit": "be9c4d375782d69004e55b14bd5fe3b6550d8c72"
285
+ "corpusCommit": "f0ab0fc3a632d984a9cac159b349e908d54b6919"
283
286
  }
@@ -400,22 +400,57 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
400
400
  it('a dead-letter record carries no delivered payload', async () => {
401
401
  // Its own `it`: RFC 0168 §A.1 allows one explicit requirement id per it(),
402
402
  // and `check-req-only` enforces it.
403
- if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
403
+ //
404
+ // THIS LEG USED TO REGISTER A FRESH SUBSCRIPTION AND READ ITS SINK. A fresh
405
+ // subscription has no dead letters by construction, so the loop over
406
+ // `deliveries` ran zero times, the test asserted NOTHING, and RFC 0148 §A
407
+ // resolves a silent return to `blocked` — which denies certification
408
+ // (RFC 0168 §E.1). It went unnoticed because every host recorded
409
+ // `inapplicable` for want of the facet: the first host ever to advertise
410
+ // `webhooks.deadLetter` was refused certification by this leg on its first
411
+ // cut, with 232 other rows passing and zero failing.
412
+ //
413
+ // §B.1 is a claim about what a REAL record carries, so the leg has to make
414
+ // one: exhaust a delivery against a receiver that never succeeds — the same
415
+ // move the sibling 0173 dead-letter leg performs — and then read.
416
+ const doc = await discovery();
417
+ if (!doc) return softSkip('blocked', 'discovery unreachable');
404
418
  const fam = await gateFamily('webhooks');
405
- if (!fam?.['deadLetter']) return softSkip('inapplicable', 'host does not advertise the webhooks.deadLetter facet');
406
- const reg = await driver.post('/webhooks', { url: 'https://subscriber.invalid/hook', events: ['run.completed'] });
407
- if (reg.status !== 201) return softSkip('blocked', `POST /webhooks answered ${reg.status}`);
408
- const webhookId = (reg.json as { webhookId?: unknown } | null)?.webhookId;
409
- if (typeof webhookId !== 'string') return softSkip('blocked', 'the mint returned no webhookId');
410
- const sink = await driver.get(`/webhooks/${projectBoundId(webhookId)}/dead-letters`);
411
- await driver.delete(`/webhooks/${encodeURIComponent(webhookId)}`);
419
+ if (!fam) return softSkip('inapplicable', 'webhooks family not advertised');
420
+ if (!fam['deadLetter']) return softSkip('inapplicable', 'host does not advertise the webhooks.deadLetter facet — RFC 0188 §A.5 makes the read a 404 rather than an obligation');
421
+ if (!fixtureAdvertised(doc, FIXTURE)) return softSkip('inapplicable', `${FIXTURE} fixture not advertised — no delivery to exhaust`);
422
+
423
+ const receiver = await startReceiver(Number.POSITIVE_INFINITY); // never succeeds
424
+ active = receiver.server;
425
+ const sub = await register(receiver.url);
426
+ if (sub === null) return softSkip('blocked', 'registration refused (reason recorded above)');
427
+ const create = await driver.post('/runs', { workflowId: FIXTURE });
428
+ if (create.status !== 201) {
429
+ await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
430
+ return softSkip('blocked', `POST /runs answered ${create.status} — no delivery to exhaust`);
431
+ }
432
+ const runId = (create.json as { runId: string }).runId;
433
+ await waitTerminal(runId, 10_000);
434
+ const policy = advertisedRetryPolicy(doc);
435
+ // Filter by SUBSCRIPTION as well as run, as the sibling leg does: a host
436
+ // that cannot reach loopback shares one tunnelled receiver URL across
437
+ // scenarios, so another scenario's attempts land in this budget otherwise.
438
+ const ours = () => receiver.attempts.filter((a) => a.runId === runId && a.webhookId === sub.webhookId);
439
+ await waitFor(() => ours().length >= (policy?.maxAttempts ?? 2), retryWaitMs(doc));
440
+ await new Promise((r) => setTimeout(r, 1_000));
441
+
442
+ const sink = await driver.get(`/webhooks/${projectBoundId(sub.webhookId)}/dead-letters`);
443
+ await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
412
444
  if (sink.status !== 200) return softSkip('blocked', `the dead-letter read answered ${sink.status}`);
413
445
  const rows = ((sink.json as { deliveries?: Array<Record<string, unknown>> } | null)?.deliveries ?? []);
414
- for (const r of rows) {
415
- expect(
416
- r['body'] === undefined && r['headers'] === undefined && r['secret'] === undefined,
417
- req('openwop.requirement.0188.dead-letter-content-free', 'RFC 0188 §B.1', 'a dead-letter record MUST NOT carry the delivered body, headers or subscription secret — the queue is precisely the traffic the subscriber never received'),
418
- ).toBe(true);
446
+ // An empty sink is NOT a pass. Recording one as a pass is exactly the
447
+ // vacuous witness this leg used to produce; say so instead.
448
+ if (rows.length === 0) {
449
+ return softSkip('blocked', 'the exhausted delivery did not reach the sink inside the retry window — §B.1 is a claim about a real record and there is none here to read');
419
450
  }
451
+ expect(
452
+ rows.every((r) => r['body'] === undefined && r['headers'] === undefined && r['secret'] === undefined),
453
+ req('openwop.requirement.0188.dead-letter-content-free', 'RFC 0188 §B.1', `a dead-letter record MUST NOT carry the delivered body, headers or subscription secret — the queue is precisely the traffic the subscriber never received, so a record carrying any of it turns one read scope into a replay of that traffic for the whole retention window (read ${rows.length} record(s))`),
454
+ ).toBe(true);
420
455
  }, DEAD_LETTER_TEST_TIMEOUT_MS);
421
456
  });