@openwop/openwop-conformance 2.44.7 → 2.44.9

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,14 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.44.9] — 2026-09-29 — a floor gated on a withheld fixture records `blocked`, not nothing
4
+
5
+ - **Nine v1 floor files no longer skip at describe level** (#1686). Each of the following gated every test with `describe.skipIf(!isFixtureAdvertised(…))`: `eventOrdering`, `failure-path`, `idempotency`, `interrupt-clarification`, `replay-fork`, `runs-lifecycle`, `stream-modes`, `stream-modes-buffer` and `stream-modes-mixed`. When every test in a file is describe-skipped, vitest runs none of `setup.ts`'s hooks for it, so the file's floor requirement got no ledger row. `--certify` then counted the floor unclassified and rejected the whole certification, not just the profile. `byok-roundtrip` hit this on openwop-app's 2026-09-28 major-1 cut (`certified: none`) and was fixed in #1708. Each test in the nine files now opens with `if (SKIP) return softSkip('blocked', NO_FIXTURE_REASON)`, so a withheld fixture records `blocked` with the fixture named. That denies only the affected profile. It is `blocked`, not `inapplicable`: the host advertises the capability the floor witnesses, so the requirement applies to the captured discovery, and a withheld fixture is exactly RFC 0148 §A's `blocked` ("a required seam, fixture … was unavailable"). `byok-roundtrip`'s `blocked` (#1708) stands for the same reason. Measured against a stub host that advertises no fixtures, with the ledger on: before, `runs-lifecycle` wrote no ledger row; after, `openwop.floor.runs-lifecycle` is `blocked` with the reason. A test that passed before still passes: the gates are unchanged, only where they sit.
6
+ - **New self-test, `describe-level-skip.test.ts`** (server-free, `test:self`). It fails when a v1 floor file (`PROFILE_FLOOR_SCENARIOS`, `required` plus every `conditional` branch) can skip every test at describe level. A gate on having no target (`!process.env.OPENWOP_BASE_URL`) is exempt, since with no host there is no bundle. A ratchet also holds the 56 non-floor files with the same shape at a ceiling of 56. Those rows resolve to a report-derived `blocked` that names no reason; that is the right disposition, and they certify nothing either way. Sabotage: reverting `runs-lifecycle` to its describe-level gate fails the floor rule, and adding a new all-gated file fails the ratchet.
7
+
8
+ ## [2.44.8] — 2026-09-29 — every subscribed event of a run is delivered
9
+
10
+ - **`0173.webhook-delivery-complete`: every matching event reaches a healthy receiver at least once** (#1781). A new leg in `v2-webhook-durable-delivery` (target major 2, gated on `webhooks` and `conformance-noop`) reads `webhooks.md` §Durability, "deliver each matching event at least once", across a whole run; no row did before. It registers one subscription to `run.started`, `node.started`, `node.completed` and `run.completed` with a receiver that answers `204` to everything, and runs the noop fixture. The expected set is every logged event of a subscribed type (`GET /runs/{runId}/events/poll`), keyed by `sequence`. Each must arrive at least once, deduplicated on `(webhookId, runId, sequence)`; duplicates are counted, never held against the host. The wait is `retryWaitFor`: the advertised `maxElapsedMs` plus the grace (RFC 0225), else the cap or the floor. The row convicts when some of the run's subscribed events arrived and others did not, or when nothing arrived and nothing reached the listener; nothing of this run arriving while other traffic did is `blocked`. A pass carries an `observed:` detail (counts, duplicates, slowest first arrival, wait path). The leg unregisters its subscription. Measured on the v2 reference host (openwop-examples `d0255b4b`): `executed-pass` in 255 ms; with its `run.started` fan-out removed, `executed-fail` naming `run.started (sequence 0)` after the 90 s window, and the file's other five legs stay green, which is the gap: myndhyve#560's lost `run.started` fan-out was convicted only incidentally, under the wrong MUST (#1780). Surveyed against the three hosts' code: each delivers every subscribed type and passes on its certified deployment (MyndHyve's window is its advertised 600000 ms plus the grace).
11
+
3
12
  ## [2.44.7] — 2026-09-29 — RFC 0223 G11 and RFC 0140 G10 close on a certified cut
4
13
 
5
14
  - **The 2.44.7 cycle opens.** The packed `@openwop/spec-artifacts` gaps register changes: RFC 0223 G11 (`0223.reject-loopback-reasks`) and RFC 0140 G10 (`replay.suppression-execution-ordinal`) close on the v2 reference host's certified 2.44.6 cut. No scenario is added or removed.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.44.7",
4
- "stampSha256": "4a05e0a24a939362a2c051c2c142e4aa1403e6e52b672c91c2b9e76a2da28770"
3
+ "version": "2.44.9",
4
+ "stampSha256": "7803a5db18abdf8649235a2665ae9bb801aea6ee7584cdd13ada047fd25a8993"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.44.7",
3
+ "version": "2.44.9",
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.44.7"
59
+ "@openwop/spec-artifacts": "2.44.9"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -3,10 +3,10 @@
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
5
  "files": 649,
6
- "tests": 2494,
7
- "withStableId": 2494,
6
+ "tests": 2495,
7
+ "withStableId": 2495,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2373
9
+ "explicitIds": 2374
10
10
  },
11
11
  "records": [
12
12
  {
@@ -12791,7 +12791,7 @@
12791
12791
  {
12792
12792
  "id": "openwop.it.eventOrdering.events-from-a-single-poll-have-non-decreasing-sequence-numbers",
12793
12793
  "file": "eventOrdering.test.ts",
12794
- "line": 53,
12794
+ "line": 55,
12795
12795
  "title": "events from a single poll have non-decreasing sequence numbers",
12796
12796
  "explicitId": "openwop.it.eventOrdering.events-from-a-single-poll-have-non-decreasing-sequence-numbers",
12797
12797
  "citations": [
@@ -12805,7 +12805,7 @@
12805
12805
  {
12806
12806
  "id": "openwop.it.eventOrdering.repeated-polls-of-a-terminal-run-yield-identical-event-sequences",
12807
12807
  "file": "eventOrdering.test.ts",
12808
- "line": 78,
12808
+ "line": 81,
12809
12809
  "title": "repeated polls of a terminal run yield identical event sequences",
12810
12810
  "explicitId": "openwop.it.eventOrdering.repeated-polls-of-a-terminal-run-yield-identical-event-sequences",
12811
12811
  "citations": [
@@ -12823,7 +12823,7 @@
12823
12823
  {
12824
12824
  "id": "openwop.it.eventOrdering.event-stream-contains-exactly-one-of-run-completed-run-failed-run-cancelled",
12825
12825
  "file": "eventOrdering.test.ts",
12826
- "line": 110,
12826
+ "line": 114,
12827
12827
  "title": "event stream contains exactly one of run.completed / run.failed / run.cancelled",
12828
12828
  "explicitId": "openwop.it.eventOrdering.event-stream-contains-exactly-one-of-run-completed-run-failed-run-cancelled",
12829
12829
  "citations": [
@@ -12836,7 +12836,7 @@
12836
12836
  {
12837
12837
  "id": "openwop.it.eventOrdering.the-terminal-event-is-the-last-event-in-the-stream",
12838
12838
  "file": "eventOrdering.test.ts",
12839
- "line": 131,
12839
+ "line": 136,
12840
12840
  "title": "the terminal event is the LAST event in the stream",
12841
12841
  "explicitId": "openwop.it.eventOrdering.the-terminal-event-is-the-last-event-in-the-stream",
12842
12842
  "citations": [
@@ -13103,7 +13103,7 @@
13103
13103
  {
13104
13104
  "id": "openwop.it.failure-path.post-v1-runs-accepts-the-run-and-run-terminates-as-failed-with-structured-error",
13105
13105
  "file": "failure-path.test.ts",
13106
- "line": 17,
13106
+ "line": 19,
13107
13107
  "title": "POST /v1/runs accepts the run and run terminates as failed with structured error",
13108
13108
  "explicitId": "openwop.it.failure-path.post-v1-runs-accepts-the-run-and-run-terminates-as-failed-with-structured-error",
13109
13109
  "citations": [
@@ -14869,7 +14869,7 @@
14869
14869
  {
14870
14870
  "id": "openwop.it.idempotency.returns-same-runid-twice-and-sets-openwop-idempotent-replay-on-the-replay",
14871
14871
  "file": "idempotency.test.ts",
14872
- "line": 31,
14872
+ "line": 33,
14873
14873
  "title": "returns same runId twice and sets openwop-Idempotent-Replay on the replay",
14874
14874
  "explicitId": "openwop.it.idempotency.returns-same-runid-twice-and-sets-openwop-idempotent-replay-on-the-replay",
14875
14875
  "citations": [
@@ -14894,7 +14894,7 @@
14894
14894
  {
14895
14895
  "id": "openwop.it.idempotency.returns-409-when-the-body-changes-under-the-same-key",
14896
14896
  "file": "idempotency.test.ts",
14897
- "line": 70,
14897
+ "line": 73,
14898
14898
  "title": "returns 409 when the body changes under the same key",
14899
14899
  "explicitId": "openwop.it.idempotency.returns-409-when-the-body-changes-under-the-same-key",
14900
14900
  "citations": [
@@ -15354,7 +15354,7 @@
15354
15354
  {
15355
15355
  "id": "openwop.it.interrupt-clarification.run-suspends-at-ask-answers-payload-drives-terminal-completed",
15356
15356
  "file": "interrupt-clarification.test.ts",
15357
- "line": 20,
15357
+ "line": 22,
15358
15358
  "title": "run suspends at ask, answers payload drives terminal completed",
15359
15359
  "explicitId": "openwop.it.interrupt-clarification.run-suspends-at-ask-answers-payload-drives-terminal-completed",
15360
15360
  "citations": [
@@ -22575,7 +22575,7 @@
22575
22575
  {
22576
22576
  "id": "openwop.it.replay-fork.produces-a-new-run-that-reaches-terminal-completed",
22577
22577
  "file": "replay-fork.test.ts",
22578
- "line": 60,
22578
+ "line": 61,
22579
22579
  "title": "produces a new run that reaches terminal `completed`",
22580
22580
  "explicitId": "openwop.it.replay-fork.produces-a-new-run-that-reaches-terminal-completed",
22581
22581
  "citations": [
@@ -22608,7 +22608,7 @@
22608
22608
  {
22609
22609
  "id": "openwop.it.replay-fork.produces-a-new-run-that-reaches-terminal-completed~2",
22610
22610
  "file": "replay-fork.test.ts",
22611
- "line": 105,
22611
+ "line": 107,
22612
22612
  "title": "produces a new run that reaches terminal `completed`",
22613
22613
  "explicitId": "openwop.it.replay-fork.produces-a-new-run-that-reaches-terminal-completed~2",
22614
22614
  "citations": [
@@ -22621,7 +22621,7 @@
22621
22621
  {
22622
22622
  "id": "openwop.it.replay-fork.rejects-negative-fromseq-with-400",
22623
22623
  "file": "replay-fork.test.ts",
22624
- "line": 142,
22624
+ "line": 145,
22625
22625
  "title": "rejects negative fromSeq with 400",
22626
22626
  "explicitId": "openwop.it.replay-fork.rejects-negative-fromseq-with-400",
22627
22627
  "citations": [
@@ -22634,7 +22634,7 @@
22634
22634
  {
22635
22635
  "id": "openwop.it.replay-fork.rejects-fromseq-beyond-source-event-log-length-with-422",
22636
22636
  "file": "replay-fork.test.ts",
22637
- "line": 161,
22637
+ "line": 165,
22638
22638
  "title": "rejects fromSeq beyond source event log length with 422",
22639
22639
  "explicitId": "openwop.it.replay-fork.rejects-fromseq-beyond-source-event-log-length-with-422",
22640
22640
  "citations": [
@@ -22647,7 +22647,7 @@
22647
22647
  {
22648
22648
  "id": "openwop.it.replay-fork.rejects-replay-mode-with-non-empty-runoptionsoverlay-overlay-is-branch-only",
22649
22649
  "file": "replay-fork.test.ts",
22650
- "line": 182,
22650
+ "line": 187,
22651
22651
  "title": "rejects replay mode with non-empty runOptionsOverlay (overlay is branch-only)",
22652
22652
  "explicitId": "openwop.it.replay-fork.rejects-replay-mode-with-non-empty-runoptionsoverlay-overlay-is-branch-only",
22653
22653
  "citations": [
@@ -22660,7 +22660,7 @@
22660
22660
  {
22661
22661
  "id": "openwop.it.replay-fork.rejects-fork-on-a-non-existent-run-with-404",
22662
22662
  "file": "replay-fork.test.ts",
22663
- "line": 208,
22663
+ "line": 214,
22664
22664
  "title": "rejects fork on a non-existent run with 404",
22665
22665
  "explicitId": "openwop.it.replay-fork.rejects-fork-on-a-non-existent-run-with-404",
22666
22666
  "citations": [
@@ -23735,7 +23735,7 @@
23735
23735
  {
23736
23736
  "id": "openwop.it.runs-lifecycle.post-v1-runs-returns-201-with-runid-per-rest-endpoints-md",
23737
23737
  "file": "runs-lifecycle.test.ts",
23738
- "line": 22,
23738
+ "line": 24,
23739
23739
  "title": "POST /v1/runs returns 201 with runId per rest-endpoints.md",
23740
23740
  "explicitId": "openwop.it.runs-lifecycle.post-v1-runs-returns-201-with-runid-per-rest-endpoints-md",
23741
23741
  "citations": [
@@ -23756,7 +23756,7 @@
23756
23756
  {
23757
23757
  "id": "openwop.it.runs-lifecycle.reaches-terminal-completed-within-bounded-time-per-fixtures-md-noop-spec",
23758
23758
  "file": "runs-lifecycle.test.ts",
23759
- "line": 41,
23759
+ "line": 44,
23760
23760
  "title": "reaches terminal `completed` within bounded time per fixtures.md noop spec",
23761
23761
  "explicitId": "openwop.it.runs-lifecycle.reaches-terminal-completed-within-bounded-time-per-fixtures-md-noop-spec",
23762
23762
  "citations": [
@@ -23773,7 +23773,7 @@
23773
23773
  {
23774
23774
  "id": "openwop.it.runs-lifecycle.get-v1-runs-nonexistentid-returns-404-or-403-per-rest-endpoints-md",
23775
23775
  "file": "runs-lifecycle.test.ts",
23776
- "line": 59,
23776
+ "line": 63,
23777
23777
  "title": "GET /v1/runs/{nonexistentId} returns 404 (or 403) per rest-endpoints.md",
23778
23778
  "explicitId": "openwop.it.runs-lifecycle.get-v1-runs-nonexistentid-returns-404-or-403-per-rest-endpoints-md",
23779
23779
  "citations": [
@@ -25058,7 +25058,7 @@
25058
25058
  {
25059
25059
  "id": "openwop.it.stream-modes-buffer.accepts-bufferms-in-range-and-emits-at-least-one-event-batch-frame",
25060
25060
  "file": "stream-modes-buffer.test.ts",
25061
- "line": 36,
25061
+ "line": 38,
25062
25062
  "title": "accepts bufferMs in range and emits at least one event: batch frame",
25063
25063
  "explicitId": "openwop.it.stream-modes-buffer.accepts-bufferms-in-range-and-emits-at-least-one-event-batch-frame",
25064
25064
  "citations": [
@@ -25079,7 +25079,7 @@
25079
25079
  {
25080
25080
  "id": "openwop.it.stream-modes-buffer.rejects-out-of-range-bufferms-with-400-validation-error",
25081
25081
  "file": "stream-modes-buffer.test.ts",
25082
- "line": 72,
25082
+ "line": 75,
25083
25083
  "title": "rejects out-of-range bufferMs with 400 validation_error",
25084
25084
  "explicitId": "openwop.it.stream-modes-buffer.rejects-out-of-range-bufferms-with-400-validation-error",
25085
25085
  "citations": [
@@ -25092,7 +25092,7 @@
25092
25092
  {
25093
25093
  "id": "openwop.it.stream-modes-buffer.forces-flush-on-terminal-run-completed-arrives-bundled-in-a-batch-before-the-tim",
25094
25094
  "file": "stream-modes-buffer.test.ts",
25095
- "line": 90,
25095
+ "line": 94,
25096
25096
  "title": "forces flush on terminal — run.completed arrives bundled in a batch BEFORE the timer fires",
25097
25097
  "explicitId": "openwop.it.stream-modes-buffer.forces-flush-on-terminal-run-completed-arrives-bundled-in-a-batch-before-the-tim",
25098
25098
  "citations": [
@@ -25110,7 +25110,7 @@
25110
25110
  {
25111
25111
  "id": "openwop.it.stream-modes-buffer.bufferms-0-behaves-identically-to-omitting-per-event-mode",
25112
25112
  "file": "stream-modes-buffer.test.ts",
25113
- "line": 134,
25113
+ "line": 139,
25114
25114
  "title": "bufferMs=0 behaves identically to omitting (per-event mode)",
25115
25115
  "explicitId": "openwop.it.stream-modes-buffer.bufferms-0-behaves-identically-to-omitting-per-event-mode",
25116
25116
  "citations": [
@@ -25123,7 +25123,7 @@
25123
25123
  {
25124
25124
  "id": "openwop.it.stream-modes-mixed.accepts-streammode-updates-messages-and-emits-a-server-closed-stream",
25125
25125
  "file": "stream-modes-mixed.test.ts",
25126
- "line": 34,
25126
+ "line": 36,
25127
25127
  "title": "accepts streamMode=updates,messages and emits a server-closed stream",
25128
25128
  "explicitId": "openwop.it.stream-modes-mixed.accepts-streammode-updates-messages-and-emits-a-server-closed-stream",
25129
25129
  "citations": [
@@ -25144,7 +25144,7 @@
25144
25144
  {
25145
25145
  "id": "openwop.it.stream-modes-mixed.rejects-streammode-values-updates-with-400-unsupported-stream-mode",
25146
25146
  "file": "stream-modes-mixed.test.ts",
25147
- "line": 64,
25147
+ "line": 67,
25148
25148
  "title": "rejects streamMode=values,updates with 400 + unsupported_stream_mode",
25149
25149
  "explicitId": "openwop.it.stream-modes-mixed.rejects-streammode-values-updates-with-400-unsupported-stream-mode",
25150
25150
  "citations": [
@@ -25169,7 +25169,7 @@
25169
25169
  {
25170
25170
  "id": "openwop.it.stream-modes-mixed.rejects-streammode-updates-bogus-one-bad-mode-fails-the-whole-list",
25171
25171
  "file": "stream-modes-mixed.test.ts",
25172
- "line": 98,
25172
+ "line": 102,
25173
25173
  "title": "rejects streamMode=updates,bogus (one bad mode fails the whole list)",
25174
25174
  "explicitId": "openwop.it.stream-modes-mixed.rejects-streammode-updates-bogus-one-bad-mode-fails-the-whole-list",
25175
25175
  "citations": [
@@ -25182,7 +25182,7 @@
25182
25182
  {
25183
25183
  "id": "openwop.it.stream-modes-mixed.mixed-mode-union-updates-debug-sees-every-event-updates-sees",
25184
25184
  "file": "stream-modes-mixed.test.ts",
25185
- "line": 116,
25185
+ "line": 121,
25186
25186
  "title": "mixed mode union: updates,debug sees every event updates sees",
25187
25187
  "explicitId": "openwop.it.stream-modes-mixed.mixed-mode-union-updates-debug-sees-every-event-updates-sees",
25188
25188
  "citations": [
@@ -25196,7 +25196,7 @@
25196
25196
  {
25197
25197
  "id": "openwop.it.stream-modes.emits-at-least-run-started-run-completed-and-server-closes-the-stream",
25198
25198
  "file": "stream-modes.test.ts",
25199
- "line": 35,
25199
+ "line": 37,
25200
25200
  "title": "emits at least run.started + run.completed and server closes the stream",
25201
25201
  "explicitId": "openwop.it.stream-modes.emits-at-least-run-started-run-completed-and-server-closes-the-stream",
25202
25202
  "citations": [
@@ -25217,7 +25217,7 @@
25217
25217
  {
25218
25218
  "id": "openwop.it.stream-modes.returns-400-and-a-structured-error-body",
25219
25219
  "file": "stream-modes.test.ts",
25220
- "line": 60,
25220
+ "line": 63,
25221
25221
  "title": "returns 400 and a structured error body",
25222
25222
  "explicitId": "openwop.it.stream-modes.returns-400-and-a-structured-error-body",
25223
25223
  "citations": [
@@ -25242,7 +25242,7 @@
25242
25242
  {
25243
25243
  "id": "openwop.it.stream-modes.returns-200-emits-at-least-one-event-server-closes-per-stream-modes-md-values",
25244
25244
  "file": "stream-modes.test.ts",
25245
- "line": 90,
25245
+ "line": 94,
25246
25246
  "title": "returns 200 + emits at least one event + server-closes per stream-modes.md §values",
25247
25247
  "explicitId": "openwop.it.stream-modes.returns-200-emits-at-least-one-event-server-closes-per-stream-modes-md-values",
25248
25248
  "citations": [
@@ -25259,7 +25259,7 @@
25259
25259
  {
25260
25260
  "id": "openwop.it.stream-modes.debug-stream-is-a-superset-of-updates-per-stream-modes-md-mode-mapping",
25261
25261
  "file": "stream-modes.test.ts",
25262
- "line": 114,
25262
+ "line": 119,
25263
25263
  "title": "debug stream is a superset of updates per stream-modes.md mode-mapping",
25264
25264
  "explicitId": "openwop.it.stream-modes.debug-stream-is-a-superset-of-updates-per-stream-modes-md-mode-mapping",
25265
25265
  "citations": [
@@ -34418,7 +34418,7 @@
34418
34418
  {
34419
34419
  "id": "openwop.it.v2-webhook-durable-delivery.a-failed-attempt-is-retried-and-the-event-is-delivered-at-least-once",
34420
34420
  "file": "v2-webhook-durable-delivery.test.ts",
34421
- "line": 279,
34421
+ "line": 309,
34422
34422
  "title": "a failed attempt is retried and the event is delivered at least once",
34423
34423
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery",
34424
34424
  "citations": [
@@ -34455,7 +34455,7 @@
34455
34455
  {
34456
34456
  "id": "openwop.it.v2-webhook-durable-delivery.an-exhausted-delivery-is-dead-lettered-never-dropped",
34457
34457
  "file": "v2-webhook-durable-delivery.test.ts",
34458
- "line": 400,
34458
+ "line": 430,
34459
34459
  "title": "an exhausted delivery is dead-lettered, never dropped",
34460
34460
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery.dead-letter",
34461
34461
  "citations": [
@@ -34494,7 +34494,7 @@
34494
34494
  {
34495
34495
  "id": "openwop.it.v2-webhook-durable-delivery.the-dead-letter-read-is-served-and-its-records-carry-no-payload",
34496
34496
  "file": "v2-webhook-durable-delivery.test.ts",
34497
- "line": 509,
34497
+ "line": 539,
34498
34498
  "title": "the dead-letter read is served and its records carry no payload",
34499
34499
  "explicitId": "openwop.requirement.0188.dead-letter-read",
34500
34500
  "citations": [
@@ -34507,7 +34507,7 @@
34507
34507
  {
34508
34508
  "id": "openwop.it.v2-webhook-durable-delivery.after-unregister-the-dead-letter-read-answers-404-not-found-as-for-a-subscriptio",
34509
34509
  "file": "v2-webhook-durable-delivery.test.ts",
34510
- "line": 530,
34510
+ "line": 560,
34511
34511
  "title": "after unregister, the dead-letter read answers 404 not_found, as for a subscription that never existed",
34512
34512
  "explicitId": "openwop.requirement.0217.dead-letter-read-after-unregister",
34513
34513
  "citations": [
@@ -34534,7 +34534,7 @@
34534
34534
  {
34535
34535
  "id": "openwop.it.v2-webhook-durable-delivery.a-dead-letter-record-carries-no-delivered-payload",
34536
34536
  "file": "v2-webhook-durable-delivery.test.ts",
34537
- "line": 563,
34537
+ "line": 593,
34538
34538
  "title": "a dead-letter record carries no delivered payload",
34539
34539
  "explicitId": "openwop.requirement.0188.dead-letter-content-free",
34540
34540
  "citations": [
@@ -34549,6 +34549,33 @@
34549
34549
  }
34550
34550
  ]
34551
34551
  },
34552
+ {
34553
+ "id": "openwop.it.v2-webhook-durable-delivery.every-logged-event-of-a-subscribed-type-reaches-a-healthy-receiver-at-least-once",
34554
+ "file": "v2-webhook-durable-delivery.test.ts",
34555
+ "line": 688,
34556
+ "title": "every logged event of a subscribed type reaches a healthy receiver at least once within the retry bound",
34557
+ "explicitId": "openwop.requirement.0173.webhook-delivery-complete",
34558
+ "citations": [
34559
+ {
34560
+ "section": "webhooks.md §Surfaces",
34561
+ "requirement": null,
34562
+ "interpolated": true
34563
+ },
34564
+ {
34565
+ "section": "webhooks.md §Surfaces",
34566
+ "requirement": "the 201 body MUST carry `webhookId`"
34567
+ },
34568
+ {
34569
+ "section": "runs.md §Create",
34570
+ "requirement": "POST /runs MUST answer 201 for the noop fixture"
34571
+ },
34572
+ {
34573
+ "section": "webhooks.md §Durability",
34574
+ "requirement": null,
34575
+ "interpolated": true
34576
+ }
34577
+ ]
34578
+ },
34552
34579
  {
34553
34580
  "id": "openwop.it.v2-webhook-egress-refusal.a-webhook-registration-naming-a-destination-the-guard-must-refuse-is-refused-400",
34554
34581
  "file": "v2-webhook-egress-refusal.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.44.7",
5
- "corpusTag": "v2.44.7",
4
+ "version": "2.44.9",
5
+ "corpusTag": "v2.44.9",
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": "69464bc0e1e71ef3b543a73d4fa4f67202343d2603c80af8033f6c3619ef1b30",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "99305cbee2bbb0f19fe9fce9894dc956f065b8d015b73fdf260d8caf3e116db1",
13
- "api/v2/asyncapi.yaml": "204f599e5b45249a6a8b3019f135116173684182497f6a733cf5a7fccc20807f",
14
- "api/v2/openapi.yaml": "979fad796d37f5284eeef749cacb824d5412f00058a7d83d47c7f6e838d87d43",
12
+ "api/seams-v2.yaml": "57cf1d472f2bf9bf61bcd2239b9fd43277072d02a367d7a85725aea0e01300db",
13
+ "api/v2/asyncapi.yaml": "40cb3ee66657a736b92c6f79241169e3669486058a65e143d99509d41d691d67",
14
+ "api/v2/openapi.yaml": "08581315aa90da54f9a1c56f8b972a801eec99bda752d92c3ea2b16113b33a92",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -204,7 +204,7 @@
204
204
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
205
205
  "spec/v1/alias-detectors.json": "0b2f28808ffe0b9d98a187775038c3406bb70919747a788a6e4f866e293ae6d3",
206
206
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
207
- "spec/v1/core-standard-manifest.json": "7b446eab4e5028a786e8b097b55671c3a22b61aace20c8e3478040519fbb9eac",
207
+ "spec/v1/core-standard-manifest.json": "cdc802f553a24b2ea8288370a54a9cf9a88d65bb80ec33c258c8fe2d2737edd1",
208
208
  "spec/v1/deprecations.json": "a3293072ea951857395d05f0437264e88965f1e9019ff61c55bacfa9c1aaa17c",
209
209
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
210
210
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -303,9 +303,9 @@
303
303
  "spec/v2/path-manifest.json": "17298a245c5da9a70aa7ab0b7f2db4277e06209d6fcdd7187a8b4cea179c08f3",
304
304
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
305
305
  "spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
306
- "spec/v2/release.json": "a0f3cf27cb26a50a217b96d2695cf58d77373c6b315359ac275493a0e01f44c4",
306
+ "spec/v2/release.json": "0219a1874302b4f3f2e8494d3c972cb649483838f53076e612c04bdf298c56dd",
307
307
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
308
- "spec/v2/surface-baseline.json": "03d6ae15f694dc96ec7e3f4a19387e80b939a89c9435c309803085fafcd9b706"
308
+ "spec/v2/surface-baseline.json": "88e9e211ec1c8d532c413b7407a15f15a3001099b9cda77f2bcc694028a72544"
309
309
  },
310
- "corpusCommit": "8d99b484cb25dc85e36ba60a419f5abe7b5c9e21"
310
+ "corpusCommit": "16a175bf4ad0485d8c1eefe97a83bfbc8d1057bc"
311
311
  }
@@ -21,9 +21,11 @@ import { pollUntilTerminal } from '../lib/polling.js';
21
21
  import { isFixtureAdvertised } from '../lib/fixtures.js';
22
22
  import { req } from '../lib/requirement-ids.js';
23
23
  import { blockedDespiteAssertions } from '../lib/soft-skip.js';
24
+ import { softSkip } from '../lib/soft-skip.js';
24
25
 
25
26
  const NOOP_WORKFLOW_ID = 'conformance-noop';
26
27
  const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
28
+ const NO_FIXTURE_REASON = `precondition not met — \`${NOOP_WORKFLOW_ID}\` is not advertised, so this host cannot witness the eventOrdering floor. RFC 0148 §A: \`blocked\`, naming the fixture (#1686: a describe-level skip recorded no disposition, and an unrecorded floor rejected the whole certification)`;
27
29
 
28
30
  interface RawEvent {
29
31
  // Event shape may use `seq` or `sequence` depending on host's event-
@@ -49,8 +51,9 @@ function getSeq(event: RawEvent): number | null {
49
51
  // always yields a readable events[] of at least two. A host that does not was
50
52
  // never observed ordering anything — until 2026-09-27 these recorded a
51
53
  // partial-witness PASS.
52
- describe.skipIf(SKIP_NO_NOOP)('event-ordering: polling returns events in monotonic order', () => {
54
+ describe('event-ordering: polling returns events in monotonic order', () => {
53
55
  it('events from a single poll have non-decreasing sequence numbers', async () => {
56
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
54
57
  const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
55
58
  expect(create.status).toBe(201);
56
59
  const runId = (create.json as { runId: string }).runId;
@@ -76,6 +79,7 @@ describe.skipIf(SKIP_NO_NOOP)('event-ordering: polling returns events in monoton
76
79
  });
77
80
 
78
81
  it('repeated polls of a terminal run yield identical event sequences', async () => {
82
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
79
83
  const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
80
84
  expect(create.status).toBe(201);
81
85
  const runId = (create.json as { runId: string }).runId;
@@ -106,8 +110,9 @@ describe.skipIf(SKIP_NO_NOOP)('event-ordering: polling returns events in monoton
106
110
  });
107
111
  });
108
112
 
109
- describe.skipIf(SKIP_NO_NOOP)('event-ordering: terminal run has at most one terminal event', () => {
113
+ describe('event-ordering: terminal run has at most one terminal event', () => {
110
114
  it('event stream contains exactly one of run.completed / run.failed / run.cancelled', async () => {
115
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
111
116
  const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
112
117
  expect(create.status).toBe(201);
113
118
  const runId = (create.json as { runId: string }).runId;
@@ -129,6 +134,7 @@ describe.skipIf(SKIP_NO_NOOP)('event-ordering: terminal run has at most one term
129
134
  });
130
135
 
131
136
  it('the terminal event is the LAST event in the stream', async () => {
137
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
132
138
  const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
133
139
  expect(create.status).toBe(201);
134
140
  const runId = (create.json as { runId: string }).runId;
@@ -9,12 +9,15 @@ import { driver } from '../lib/driver.js';
9
9
  import { pollUntilTerminal } from '../lib/polling.js';
10
10
  import { isFixtureAdvertised } from '../lib/fixtures.js';
11
11
  import { req } from '../lib/requirement-ids.js';
12
+ import { softSkip } from '../lib/soft-skip.js';
12
13
 
13
14
  const WORKFLOW_ID = 'conformance-failure';
14
15
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
16
+ const NO_FIXTURE_REASON = `precondition not met — \`${WORKFLOW_ID}\` is not advertised, so this host cannot witness the failure-path floor. RFC 0148 §A: \`blocked\`, naming the fixture (#1686: a describe-level skip recorded no disposition, and an unrecorded floor rejected the whole certification)`;
15
17
 
16
- describe.skipIf(SKIP_NO_FIXTURE)('failure: conformance-failure fixture reaches terminal `failed`', () => {
18
+ describe('failure: conformance-failure fixture reaches terminal `failed`', () => {
17
19
  it('POST /v1/runs accepts the run and run terminates as failed with structured error', async () => {
20
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
18
21
  const create = await driver.post('/v1/runs', { workflowId: WORKFLOW_ID });
19
22
  expect(create.status, req('openwop.it.failure-path.post-v1-runs-accepts-the-run-and-run-terminates-as-failed-with-structured-error',
20
23
  'rest-endpoints.md',
@@ -19,16 +19,19 @@ import { driver } from '../lib/driver.js';
19
19
  import { isFixtureAdvertised } from '../lib/fixtures.js';
20
20
  import { readErrorCode } from '../lib/error-envelope.js';
21
21
  import { req } from '../lib/requirement-ids.js';
22
+ import { softSkip } from '../lib/soft-skip.js';
22
23
 
23
24
  const WORKFLOW_ID = 'conformance-idempotent';
24
25
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
26
+ const NO_FIXTURE_REASON = `precondition not met — \`${WORKFLOW_ID}\` is not advertised, so this host cannot witness the idempotency floor. RFC 0148 §A: \`blocked\`, naming the fixture (#1686: a describe-level skip recorded no disposition, and an unrecorded floor rejected the whole certification)`;
25
27
 
26
28
  function freshKey(suffix: string): string {
27
29
  return `openwop-conformance-${Date.now()}-${Math.random().toString(36).slice(2, 8)}-${suffix}`;
28
30
  }
29
31
 
30
- describe.skipIf(SKIP_NO_FIXTURE)('idempotency: same key + same body replays per idempotency.md §Layer 1', () => {
32
+ describe('idempotency: same key + same body replays per idempotency.md §Layer 1', () => {
31
33
  it('returns same runId twice and sets openwop-Idempotent-Replay on the replay', async () => {
34
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
32
35
  const key = freshKey('replay');
33
36
  const body = { workflowId: WORKFLOW_ID, inputs: { nonce: 'abc-123' } };
34
37
 
@@ -66,8 +69,9 @@ describe.skipIf(SKIP_NO_FIXTURE)('idempotency: same key + same body replays per
66
69
  });
67
70
  });
68
71
 
69
- describe.skipIf(SKIP_NO_FIXTURE)('idempotency: same key + different body conflicts per idempotency.md §Layer 1', () => {
72
+ describe('idempotency: same key + different body conflicts per idempotency.md §Layer 1', () => {
70
73
  it('returns 409 when the body changes under the same key', async () => {
74
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
71
75
  const key = freshKey('conflict');
72
76
 
73
77
  const first = await driver.post(
@@ -11,13 +11,16 @@ import { driver } from '../lib/driver.js';
11
11
  import { pollUntilStatus, pollUntilTerminal } from '../lib/polling.js';
12
12
  import { isFixtureAdvertised } from '../lib/fixtures.js';
13
13
  import { req } from '../lib/requirement-ids.js';
14
+ import { softSkip } from '../lib/soft-skip.js';
14
15
 
15
16
  const WORKFLOW_ID = 'conformance-clarification';
16
17
  const NODE_ID = 'ask';
17
18
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
19
+ const NO_FIXTURE_REASON = `precondition not met — \`${WORKFLOW_ID}\` is not advertised, so this host cannot witness the interrupt-clarification floor. RFC 0148 §A: \`blocked\`, naming the fixture (#1686: a describe-level skip recorded no disposition, and an unrecorded floor rejected the whole certification)`;
18
20
 
19
- describe.skipIf(SKIP_NO_FIXTURE)('interrupt: clarification answers resume to `completed`', () => {
21
+ describe('interrupt: clarification answers resume to `completed`', () => {
20
22
  it('run suspends at ask, answers payload drives terminal completed', async () => {
23
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
21
24
  const create = await driver.post('/v1/runs', { workflowId: WORKFLOW_ID });
22
25
  expect(create.status).toBe(201);
23
26
  const runId = (create.json as { runId: string }).runId;
@@ -33,6 +33,7 @@ import { req } from '../lib/requirement-ids.js';
33
33
 
34
34
  const SOURCE_WORKFLOW_ID = 'conformance-noop';
35
35
  const SKIP_NO_NOOP = !isFixtureAdvertised(SOURCE_WORKFLOW_ID);
36
+ const NO_FIXTURE_REASON = `precondition not met — \`${SOURCE_WORKFLOW_ID}\` is not advertised, so this host cannot witness the replay-fork floor. RFC 0148 §A: \`blocked\`, naming the fixture (#1686: a describe-level skip recorded no disposition, and an unrecorded floor rejected the whole certification)`;
36
37
 
37
38
  async function fetchReplayModes(): Promise<readonly string[]> {
38
39
  const res = await driver.get('/.well-known/openwop', { authenticated: false });
@@ -56,8 +57,9 @@ async function startAndFinishNoop(): Promise<string> {
56
57
  return runId;
57
58
  }
58
59
 
59
- describe.skipIf(SKIP_NO_NOOP)('replay: fork from fromSeq=0 in replay mode', () => {
60
+ describe('replay: fork from fromSeq=0 in replay mode', () => {
60
61
  it('produces a new run that reaches terminal `completed`', async (ctx) => {
62
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
61
63
  const modes = await fetchReplayModes();
62
64
  if (!modes.includes('replay')) {
63
65
  // Visible skip — earlier this was a silent `return` that
@@ -101,8 +103,9 @@ describe.skipIf(SKIP_NO_NOOP)('replay: fork from fromSeq=0 in replay mode', () =
101
103
  });
102
104
  });
103
105
 
104
- describe.skipIf(SKIP_NO_NOOP)('replay: fork from fromSeq=0 in branch mode with empty overlay', () => {
106
+ describe('replay: fork from fromSeq=0 in branch mode with empty overlay', () => {
105
107
  it('produces a new run that reaches terminal `completed`', async (ctx) => {
108
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
106
109
  const modes = await fetchReplayModes();
107
110
  if (!modes.includes('branch')) {
108
111
  softSkip('inapplicable', "host does not advertise the `branch` fork mode — this leg's rule has no path to apply");
@@ -130,7 +133,7 @@ describe.skipIf(SKIP_NO_NOOP)('replay: fork from fromSeq=0 in branch mode with e
130
133
  });
131
134
  });
132
135
 
133
- describe.skipIf(SKIP_NO_NOOP)('replay: validation errors', () => {
136
+ describe('replay: validation errors', () => {
134
137
  // Earlier each of these tests had a silent `return;` early-exit
135
138
  // when the host advertised no replay modes (or only the wrong mode for
136
139
  // the assertion). That collapsed unexercised paths into vacuous green
@@ -140,6 +143,7 @@ describe.skipIf(SKIP_NO_NOOP)('replay: validation errors', () => {
140
143
  // "exercised the validation path and got the expected error code."
141
144
 
142
145
  it('rejects negative fromSeq with 400', async (ctx) => {
146
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
143
147
  const modes = await fetchReplayModes();
144
148
  if (modes.length === 0) {
145
149
  softSkip('inapplicable', "host advertises no usable fork mode for this leg");
@@ -159,6 +163,7 @@ describe.skipIf(SKIP_NO_NOOP)('replay: validation errors', () => {
159
163
  });
160
164
 
161
165
  it('rejects fromSeq beyond source event log length with 422', async (ctx) => {
166
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
162
167
  const modes = await fetchReplayModes();
163
168
  if (modes.length === 0) {
164
169
  softSkip('inapplicable', "host advertises no usable fork mode for this leg");
@@ -180,6 +185,7 @@ describe.skipIf(SKIP_NO_NOOP)('replay: validation errors', () => {
180
185
  });
181
186
 
182
187
  it('rejects replay mode with non-empty runOptionsOverlay (overlay is branch-only)', async (ctx) => {
188
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
183
189
  const modes = await fetchReplayModes();
184
190
  if (!modes.includes('replay')) {
185
191
  // The rule under test (`replay` + non-empty `runOptionsOverlay`
@@ -206,6 +212,7 @@ describe.skipIf(SKIP_NO_NOOP)('replay: validation errors', () => {
206
212
  });
207
213
 
208
214
  it('rejects fork on a non-existent run with 404', async (ctx) => {
215
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
209
216
  const modes = await fetchReplayModes();
210
217
  if (modes.length === 0) {
211
218
  softSkip('inapplicable', "host advertises no usable fork mode for this leg");
@@ -14,12 +14,15 @@ import { driver } from '../lib/driver.js';
14
14
  import { pollUntilTerminal } from '../lib/polling.js';
15
15
  import { isFixtureAdvertised } from '../lib/fixtures.js';
16
16
  import { req } from '../lib/requirement-ids.js';
17
+ import { softSkip } from '../lib/soft-skip.js';
17
18
 
18
19
  const NOOP_WORKFLOW_ID = 'conformance-noop';
19
20
  const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
21
+ const NO_FIXTURE_REASON = `precondition not met — \`${NOOP_WORKFLOW_ID}\` is not advertised, so this host cannot witness the runs-lifecycle floor. RFC 0148 §A: \`blocked\`, naming the fixture (#1686: a describe-level skip recorded no disposition, and an unrecorded floor rejected the whole certification)`;
20
22
 
21
- describe.skipIf(SKIP_NO_NOOP)('run lifecycle: conformance-noop fixture', () => {
23
+ describe('run lifecycle: conformance-noop fixture', () => {
22
24
  it('POST /v1/runs returns 201 with runId per rest-endpoints.md', async () => {
25
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
23
26
  const res = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
24
27
 
25
28
  expect(res.status, req('openwop.it.runs-lifecycle.post-v1-runs-returns-201-with-runid-per-rest-endpoints-md',
@@ -39,6 +42,7 @@ describe.skipIf(SKIP_NO_NOOP)('run lifecycle: conformance-noop fixture', () => {
39
42
  });
40
43
 
41
44
  it('reaches terminal `completed` within bounded time per fixtures.md noop spec', async () => {
45
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
42
46
  const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
43
47
  expect(create.status).toBe(201);
44
48
  const runId = (create.json as { runId: string }).runId;
@@ -57,6 +61,7 @@ describe.skipIf(SKIP_NO_NOOP)('run lifecycle: conformance-noop fixture', () => {
57
61
  });
58
62
 
59
63
  it('GET /v1/runs/{nonexistentId} returns 404 (or 403) per rest-endpoints.md', async () => {
64
+ if (SKIP_NO_NOOP) return softSkip('blocked', NO_FIXTURE_REASON);
60
65
  const res = await driver.get('/v1/runs/openwop-conformance-this-run-id-does-not-exist');
61
66
  expect(
62
67
  [403, 404].includes(res.status),
@@ -23,17 +23,20 @@ import { subscribe } from '../lib/sse.js';
23
23
  import { pollUntilTerminal } from '../lib/polling.js';
24
24
  import { isFixtureAdvertised } from '../lib/fixtures.js';
25
25
  import { req } from '../lib/requirement-ids.js';
26
+ import { softSkip } from '../lib/soft-skip.js';
26
27
 
27
28
  const WORKFLOW_ID = 'conformance-delay';
28
29
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
30
+ const NO_FIXTURE_REASON = `precondition not met — \`${WORKFLOW_ID}\` is not advertised, so this host cannot witness the stream-modes-buffer floor. RFC 0148 §A: \`blocked\`, naming the fixture (#1686: a describe-level skip recorded no disposition, and an unrecorded floor rejected the whole certification)`;
29
31
 
30
32
  interface RunEventDoc {
31
33
  readonly type: string;
32
34
  readonly sequence: number;
33
35
  }
34
36
 
35
- describe.skipIf(SKIP_NO_FIXTURE)('stream-modes-buffer: ?bufferMs= aggregation hint', () => {
37
+ describe('stream-modes-buffer: ?bufferMs= aggregation hint', () => {
36
38
  it('accepts bufferMs in range and emits at least one event: batch frame', async () => {
39
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
37
40
  const create = await driver.post('/v1/runs', { workflowId: WORKFLOW_ID });
38
41
  expect(create.status).toBe(201);
39
42
  const runId = (create.json as { runId: string }).runId;
@@ -70,6 +73,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-modes-buffer: ?bufferMs= aggregation hi
70
73
  });
71
74
 
72
75
  it('rejects out-of-range bufferMs with 400 validation_error', async () => {
76
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
73
77
  const create = await driver.post('/v1/runs', { workflowId: WORKFLOW_ID });
74
78
  const runId = (create.json as { runId: string }).runId;
75
79
 
@@ -88,6 +92,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-modes-buffer: ?bufferMs= aggregation hi
88
92
  });
89
93
 
90
94
  it('forces flush on terminal — run.completed arrives bundled in a batch BEFORE the timer fires', async () => {
95
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
91
96
  const create = await driver.post('/v1/runs', { workflowId: WORKFLOW_ID });
92
97
  const runId = (create.json as { runId: string }).runId;
93
98
 
@@ -132,6 +137,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-modes-buffer: ?bufferMs= aggregation hi
132
137
  });
133
138
 
134
139
  it('bufferMs=0 behaves identically to omitting (per-event mode)', async () => {
140
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
135
141
  const create = await driver.post('/v1/runs', { workflowId: WORKFLOW_ID });
136
142
  const runId = (create.json as { runId: string }).runId;
137
143
 
@@ -22,16 +22,19 @@ import { subscribe, type SseEvent } from '../lib/sse.js';
22
22
  import { pollUntilTerminal } from '../lib/polling.js';
23
23
  import { isFixtureAdvertised } from '../lib/fixtures.js';
24
24
  import { req } from '../lib/requirement-ids.js';
25
+ import { softSkip } from '../lib/soft-skip.js';
25
26
 
26
27
  const WORKFLOW_ID = 'conformance-delay';
27
28
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
29
+ const NO_FIXTURE_REASON = `precondition not met — \`${WORKFLOW_ID}\` is not advertised, so this host cannot witness the stream-modes-mixed floor. RFC 0148 §A: \`blocked\`, naming the fixture (#1686: a describe-level skip recorded no disposition, and an unrecorded floor rejected the whole certification)`;
28
30
 
29
31
  function eventTypes(events: readonly SseEvent[]): string[] {
30
32
  return events.map((e) => e.event);
31
33
  }
32
34
 
33
- describe.skipIf(SKIP_NO_FIXTURE)('stream-modes-mixed: comma-separated subsets', () => {
35
+ describe('stream-modes-mixed: comma-separated subsets', () => {
34
36
  it('accepts streamMode=updates,messages and emits a server-closed stream', async () => {
37
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
35
38
  const create = await driver.post('/v1/runs', {
36
39
  workflowId: WORKFLOW_ID,
37
40
  inputs: { delayMs: 500 },
@@ -62,6 +65,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-modes-mixed: comma-separated subsets',
62
65
  });
63
66
 
64
67
  it('rejects streamMode=values,updates with 400 + unsupported_stream_mode', async () => {
68
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
65
69
  const create = await driver.post('/v1/runs', {
66
70
  workflowId: WORKFLOW_ID,
67
71
  inputs: { delayMs: 100 },
@@ -96,6 +100,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-modes-mixed: comma-separated subsets',
96
100
  });
97
101
 
98
102
  it('rejects streamMode=updates,bogus (one bad mode fails the whole list)', async () => {
103
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
99
104
  const create = await driver.post('/v1/runs', {
100
105
  workflowId: WORKFLOW_ID,
101
106
  inputs: { delayMs: 100 },
@@ -114,6 +119,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-modes-mixed: comma-separated subsets',
114
119
  });
115
120
 
116
121
  it('mixed mode union: updates,debug sees every event updates sees', async () => {
122
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
117
123
  // Run twice — once with updates only, once with updates,debug.
118
124
  // The mixed-mode response MUST be a superset of the updates-only
119
125
  // response (union semantics).
@@ -12,9 +12,11 @@ import { driver } from '../lib/driver.js';
12
12
  import { subscribe, type SseEvent } from '../lib/sse.js';
13
13
  import { isFixtureAdvertised } from '../lib/fixtures.js';
14
14
  import { req } from '../lib/requirement-ids.js';
15
+ import { softSkip } from '../lib/soft-skip.js';
15
16
 
16
17
  const WORKFLOW_ID = 'conformance-delay';
17
18
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
19
+ const NO_FIXTURE_REASON = `precondition not met — \`${WORKFLOW_ID}\` is not advertised, so this host cannot witness the stream-modes floor. RFC 0148 §A: \`blocked\`, naming the fixture (#1686: a describe-level skip recorded no disposition, and an unrecorded floor rejected the whole certification)`;
18
20
 
19
21
  async function startDelayRun(delayMs: number): Promise<string> {
20
22
  const create = await driver.post('/v1/runs', {
@@ -31,8 +33,9 @@ function eventTypes(events: readonly SseEvent[]): string[] {
31
33
  return events.map((e) => e.event);
32
34
  }
33
35
 
34
- describe.skipIf(SKIP_NO_FIXTURE)('stream-modes: updates (default) closes on terminal event', () => {
36
+ describe('stream-modes: updates (default) closes on terminal event', () => {
35
37
  it('emits at least run.started + run.completed and server closes the stream', async () => {
38
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
36
39
  const runId = await startDelayRun(1_000);
37
40
  const { events, closedBy } = await subscribe(
38
41
  `/v1/runs/${encodeURIComponent(runId)}/events?streamMode=updates`,
@@ -56,8 +59,9 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-modes: updates (default) closes on term
56
59
  });
57
60
  });
58
61
 
59
- describe.skipIf(SKIP_NO_FIXTURE)('stream-modes: invalid streamMode is rejected', () => {
62
+ describe('stream-modes: invalid streamMode is rejected', () => {
60
63
  it('returns 400 and a structured error body', async () => {
64
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
61
65
  const runId = await startDelayRun(1_000);
62
66
  const res = await driver.get(
63
67
  `/v1/runs/${encodeURIComponent(runId)}/events?streamMode=does-not-exist`,
@@ -86,8 +90,9 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-modes: invalid streamMode is rejected',
86
90
  });
87
91
  });
88
92
 
89
- describe.skipIf(SKIP_NO_FIXTURE)('stream-modes: values mode is reachable + closes on terminal', () => {
93
+ describe('stream-modes: values mode is reachable + closes on terminal', () => {
90
94
  it('returns 200 + emits at least one event + server-closes per stream-modes.md §values', async () => {
95
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
91
96
  const runId = await startDelayRun(1_000);
92
97
  const result = await subscribe(
93
98
  `/v1/runs/${encodeURIComponent(runId)}/events?streamMode=values`,
@@ -110,8 +115,9 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-modes: values mode is reachable + close
110
115
  });
111
116
  });
112
117
 
113
- describe.skipIf(SKIP_NO_FIXTURE)('stream-modes: debug emits at least as many events as updates', () => {
118
+ describe('stream-modes: debug emits at least as many events as updates', () => {
114
119
  it('debug stream is a superset of updates per stream-modes.md mode-mapping', async () => {
120
+ if (SKIP_NO_FIXTURE) return softSkip('blocked', NO_FIXTURE_REASON);
115
121
  const runIdUpdates = await startDelayRun(1_000);
116
122
  const updatesResult = await subscribe(
117
123
  `/v1/runs/${encodeURIComponent(runIdUpdates)}/events?streamMode=updates`,
@@ -8,6 +8,10 @@
8
8
  * (`spec/v2/core/webhooks.md` §Durability; security-defaults.md §Webhook
9
9
  * durability; RFC 0173 §B row C6.3).
10
10
  *
11
+ * The last leg (suite 2.44.8, openwop#1781) reads "deliver each matching event
12
+ * at least once" across a whole run, with a receiver that never fails: see
13
+ * `webhook-delivery-complete` below.
14
+ *
11
15
  * How the receiver is driven: the suite boots its own HTTP receiver (the same
12
16
  * shape `webhook-signed-delivery.test.ts` uses) in a FAILING mode — it answers
13
17
  * `500` to the first N attempts for a delivery key and `204` afterwards — so
@@ -35,7 +39,7 @@ import { absenceIsUnmeasured, noDeliveryCause, startScopedReceiver, type ScopedR
35
39
  import { readErrorCode } from '../lib/error-envelope.js';
36
40
  import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
37
41
  import { req } from '../lib/requirement-ids.js';
38
- import { pastAdvertisedBound, retryTestTimeoutMs, retryWaitCapMs, retryWaitFor, retryWaitSelection, waitObservation, windowClosedNote } from '../lib/webhook-retry-window.js';
42
+ import { pastAdvertisedBound, retryTestTimeoutMs, retryWaitCapMs, retryWaitFor, retryWaitSelection, waitObservation, waitPathNote, windowClosedNote } from '../lib/webhook-retry-window.js';
39
43
  import { noteObservation } from '../lib/row-observation.js';
40
44
 
41
45
  export const REQUIRES_HOST_CALLBACK = 'the host POSTs and retries webhook deliveries to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
@@ -43,7 +47,7 @@ export const REQUIRES_HOST_CALLBACK = 'the host POSTs and retries webhook delive
43
47
  const FIXTURE = 'conformance-noop';
44
48
  const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
45
49
 
46
- interface Attempt { readonly key: string; readonly runId: string | null; readonly webhookId: string; readonly status: number; readonly at: number }
50
+ interface Attempt { readonly key: string; readonly runId: string | null; readonly webhookId: string; readonly sequence: number | null; readonly status: number; readonly at: number }
47
51
 
48
52
  /**
49
53
  * A receiver that fails the first `failFirst` attempts for each delivery key
@@ -75,7 +79,7 @@ async function startReceiver(failFirst: number): Promise<ScopedReceiver & { atte
75
79
  const n = (seen.get(key) ?? 0) + 1;
76
80
  seen.set(key, n);
77
81
  const status = n <= failFirst ? 500 : 204;
78
- attempts.push({ key, runId, webhookId, status, at: Date.now() });
82
+ attempts.push({ key, runId, webhookId, sequence: typeof sequence === 'number' ? sequence : null, status, at: Date.now() });
79
83
  res.writeHead(status);
80
84
  res.end();
81
85
  });
@@ -275,6 +279,32 @@ async function register(rx: ScopedReceiver): Promise<{ webhookId: string } | nul
275
279
  return { webhookId: webhookId as string };
276
280
  }
277
281
 
282
+ /**
283
+ * `webhook-delivery-complete` (suite 2.44.8, openwop#1781): the lifecycle types
284
+ * a one-node run emits. The EXPECTED deliveries are still read from the host's
285
+ * own log, so the suite assumes nothing about what the fixture emits.
286
+ */
287
+ const COMPLETE_ID = 'openwop.requirement.0173.webhook-delivery-complete';
288
+ const COMPLETE_TYPES = ['run.started', 'node.started', 'node.completed', 'run.completed'] as const;
289
+ const COMPLETE_TERMINAL_WAIT_MS = 30_000;
290
+
291
+ /** The run's logged events of a subscribed type, by sequence; null when the log is unreadable or carries no sequences. */
292
+ async function subscribedLog(runId: string): Promise<Map<number, string> | null> {
293
+ const res = await driver.get(`/runs/${encodeURIComponent(runId)}/events/poll?timeout=1`);
294
+ if (res.status !== 200) return null;
295
+ const events = (res.json as { events?: Array<{ type?: unknown; sequence?: unknown }> } | null)?.events;
296
+ if (!Array.isArray(events)) return null;
297
+ const want = new Map<number, string>();
298
+ for (const e of events) {
299
+ if (typeof e.type !== 'string' || !(COMPLETE_TYPES as readonly string[]).includes(e.type)) continue;
300
+ if (typeof e.sequence !== 'number') return null;
301
+ want.set(e.sequence, e.type);
302
+ }
303
+ return want;
304
+ }
305
+
306
+ const bareId = (id: string): string => (id.includes('/') ? id.slice(id.indexOf('/') + 1) : id);
307
+
278
308
  describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () => {
279
309
  it('a failed attempt is retried and the event is delivered at least once', async () => {
280
310
  const doc = await discovery();
@@ -626,4 +656,93 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
626
656
  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))`),
627
657
  ).toBe(true);
628
658
  }, DEAD_LETTER_TEST_TIMEOUT_MS);
659
+ /**
660
+ * openwop#1781: "deliver each matching event at least once" binds EVERY
661
+ * matching event, and no row read it that way. `v2-webhook-delivery-shape`
662
+ * records `blocked` on a missing delivery; `v2-webhook-standard-webhooks-delivery`
663
+ * fails only on a missing `run.completed`; the legs above subscribe to
664
+ * `run.completed` alone and drive retries after a FAILING receiver. So a host
665
+ * that lost part of a run's fan-out certified green. Measured on a tier-2
666
+ * host: its `run.started` fan-out was fire-and-forget work that Cloud Run CPU
667
+ * throttling starved once the appending request ended (myndhyve#560); the
668
+ * only row that reddened was `0215.no-head-of-line`, under the wrong MUST,
669
+ * and #1780 correctly stopped it convicting.
670
+ *
671
+ * One subscription to the four lifecycle types, a receiver that answers 204
672
+ * to everything, one noop run. Every logged event of a subscribed type must
673
+ * arrive at least once, deduplicated on `(webhookId, runId, sequence)` — the
674
+ * triple webhooks.md §Verification names. Duplicates are counted in the
675
+ * `observed:` detail and never held against the host: at-least-once permits
676
+ * them. The wait is `retryWaitFor`: the advertised `maxElapsedMs` plus the
677
+ * grace when the host states one (RFC 0225), else the cap or the floor. It
678
+ * only bounds how long a missing event is waited for; a healthy receiver
679
+ * needs no retry.
680
+ *
681
+ * Convicts only when some of this run's subscribed events arrived and others
682
+ * did not, or when nothing arrived and nothing else reached the listener (the
683
+ * rule the first leg applies). Nothing of this run arriving while other
684
+ * traffic did is `blocked`: the path works, and what is absent is this
685
+ * exercise's identity. Negative control: the v2 reference host with its
686
+ * `run.started` fan-out removed fails this leg naming `run.started`.
687
+ */
688
+ it('every logged event of a subscribed type reaches a healthy receiver at least once within the retry bound', async () => {
689
+ const doc = await discovery();
690
+ if (!doc) return softSkip('blocked', 'discovery unreachable');
691
+ if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised (gate recorded under openwop.family.webhooks)');
692
+ if (!fixtureAdvertised(doc, FIXTURE)) return softSkip('inapplicable', `${FIXTURE} fixture not advertised — no run to deliver`);
693
+
694
+ const receiver = await startReceiver(0); // 204 to every attempt
695
+ active = receiver;
696
+ const reg = await driver.post('/webhooks', { url: receiver.url, events: [...COMPLETE_TYPES] });
697
+ if (reg.status === 400 && readErrorCode(reg.json) === 'webhook_url_rejected') {
698
+ if (!receiver.tunnelled) return softSkip('blocked', 'host SSRF guard rejected the loopback receiver (webhooks.md §Egress requires it); set OPENWOP_WEBHOOK_RECEIVER_URL to a public https tunnel in front of the suite receiver');
699
+ expect.fail(`host rejected the operator-supplied public https receiver (${receiver.url}) with webhook_url_rejected — a public https destination is legitimate under webhooks.md §Egress`);
700
+ }
701
+ expect(reg.status, req(COMPLETE_ID, 'webhooks.md §Surfaces', `POST /webhooks MUST answer 201 { webhookId } for events [${COMPLETE_TYPES.join(', ')}], each a v2 event type name`)).toBe(201);
702
+ const webhookId = (reg.json as { webhookId?: unknown } | null)?.webhookId;
703
+ expect(typeof webhookId, req(COMPLETE_ID, 'webhooks.md §Surfaces', 'the 201 body MUST carry `webhookId`')).toBe('string');
704
+ const sub = webhookId as string;
705
+ try {
706
+ const create = await driver.post('/runs', { workflowId: FIXTURE });
707
+ expect(create.status, req(COMPLETE_ID, 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
708
+ const runId = (create.json as { runId: string }).runId;
709
+ const createdAt = Date.now();
710
+ const status = await waitTerminal(runId, COMPLETE_TERMINAL_WAIT_MS);
711
+ if (status === null || !TERMINAL.has(status)) return softSkip('blocked', `the ${FIXTURE} run did not reach a terminal status inside ${COMPLETE_TERMINAL_WAIT_MS}ms (last ${status ?? 'unreadable'}), so its event log, the expected set, is not final`);
712
+ const want = await subscribedLog(runId);
713
+ if (want === null) return softSkip('blocked', 'GET /runs/{runId}/events/poll did not yield the run\'s events with a numeric sequence, so the expected deliveries cannot be keyed (webhooks.md §Verification dedupes on sequence)');
714
+ if (want.size === 0) return softSkip('blocked', `the run's event log holds no event of a subscribed type [${COMPLETE_TYPES.join(', ')}]: nothing to deliver, nothing measured`);
715
+
716
+ const ours = (): Attempt[] => receiver.attempts.filter((a) => a.runId !== null && bareId(a.runId) === bareId(runId) && bareId(a.webhookId) === bareId(sub));
717
+ const missing = (): number[] => {
718
+ const got = new Set(ours().map((a) => a.sequence));
719
+ return [...want.keys()].filter((q) => !got.has(q)).sort((a, b) => a - b);
720
+ };
721
+ const policy = advertisedRetryPolicy(doc);
722
+ const selection = retryWaitSelection(policy, RETRY_WAIT_CAP_MS);
723
+ const deadline = Date.now() + retryWaitFor(policy, RETRY_WAIT_CAP_MS);
724
+ while (missing().length > 0 && Date.now() < deadline) await new Promise((r) => setTimeout(r, 250));
725
+
726
+ const arrivals = ours();
727
+ const firstAt = new Map<number, number>();
728
+ for (const a of arrivals) if (a.sequence !== null && want.has(a.sequence) && !firstAt.has(a.sequence)) firstAt.set(a.sequence, a.at);
729
+ const slowest = firstAt.size === 0 ? null : Math.max(...[...firstAt.values()].map((t) => t - createdAt));
730
+ const duplicates = arrivals.filter((a) => a.sequence !== null && want.has(a.sequence)).length - firstAt.size;
731
+ const lost = missing();
732
+ noteObservation(`${firstAt.size}/${want.size} subscribed event(s) delivered, ${duplicates} duplicate arrival(s); slowest first arrival ${slowest === null ? 'none' : `${slowest}ms after the run was created`}; ${waitPathNote(selection)}`);
733
+
734
+ if (arrivals.length === 0 && absenceIsUnmeasured(receiver)) {
735
+ return blockedDespiteAssertions(noDeliveryCause(receiver, 'delivery for this run'));
736
+ }
737
+ const lostList = lost.map((q) => `${want.get(q)} (sequence ${q})`).join(', ');
738
+ expect(
739
+ lost,
740
+ req(COMPLETE_ID, 'webhooks.md §Durability', `the host MUST deliver each matching event at least once; best-effort delivery is not a conforming mode (RFC 0173 §B). ${lost.length} of ${want.size} subscribed event(s) in this run's log never reached the healthy receiver inside ${selection.windowMs}ms: ${lostList}. ${firstAt.size > 0 ? `${firstAt.size} other(s) did arrive, so the receiver was reachable.` : noDeliveryCause(receiver, 'delivery for this run')}`),
741
+ ).toEqual([]);
742
+ } finally {
743
+ // Four lifecycle types on a live subscription would keep fanning every later
744
+ // run's events at a closed nonce, each retried to exhaustion.
745
+ try { await driver.delete(`/webhooks/${encodeURIComponent(sub)}`); } catch { /* best effort: the verdict is already recorded */ }
746
+ }
747
+ }, retryTestTimeoutMs(1, COMPLETE_TERMINAL_WAIT_MS + WAIT_SLACK_MS));
629
748
  });