@openwop/openwop-conformance 2.44.1 → 2.44.3
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 +15 -0
- package/README.md +1 -1
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +8 -13
- package/schemas/CORPUS-STAMP.json +19 -18
- package/src/lib/delivery-isolation-verdict.ts +97 -0
- package/src/lib/row-observation.ts +40 -0
- package/src/lib/webhook-retry-window.ts +56 -0
- package/src/scenarios/v2-webhook-delivery-isolation.test.ts +21 -29
- package/src/scenarios/v2-webhook-durable-delivery.test.ts +21 -1
- package/src/scenarios/v2-webhook-message-id-stable.test.ts +5 -2
- package/src/scenarios/v2-webhook-unregister-stops-delivery.test.ts +3 -1
- package/src/setup.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.44.3] — 2026-09-28 — fifteen more core families get v2 normative homes, and webhook retry rows record the wait path they measured
|
|
4
|
+
|
|
5
|
+
- **Webhook retry rows say which wait path ran and what they measured** (RFC 0225 witness). A passing dead-letter row carried no detail, so a bundle could not show whether its wait came from the host's advertised `retryPolicy.maxElapsedMs`, an operator-raised cap or the 20 s floor, or when the dead-letter arrived. MyndHyve's 2.44.1 cut was the case: it advertises 600000, and it could not be counted as RFC 0225's witness. Rows driven by `retryWaitFor` now carry an informational detail, on passes too. `0173.webhook-durable-delivery.dead-letter` and `0188.dead-letter-content-free` record, for example, `observed: advertised-bound (maxElapsedMs 600000): waited ≤630000ms; 5 attempt(s), last attempt after 225031ms, sink after 231412ms`. The last attempt is a lower bound on the dead-letter time, and the sink read an upper bound. The retry leg, `v2-webhook-unregister-stops-delivery` and `v2-webhook-message-id-stable` record the path and window. The detail is prefixed `observed: `, never `partial-witness: `, so `check-accepted-predicate` counts the row as a full witness. The bundle already carries `detail` on every row inside the witness digest, so the note is signed. New `lib/row-observation.ts`, attached in `setup.ts`; `retryWaitSelection` / `waitObservation` in `lib/webhook-retry-window.ts`; server-free tests in `row-observation.test.ts` and `webhook-retry-window.test.ts`, each sabotage-checked.
|
|
6
|
+
- **The 2.44.3 cycle opens.** Fifteen core families get a v2 normative home (RFC 0189), which changes the packed `@openwop/spec-artifacts` tree. v1-dependent core families fall from 23 to 8. No scenario changes: the rules are restated, none added.
|
|
7
|
+
- `events.md`: `feedback`, `providerUsage`, `envelopes`, and the rest of `heartbeat`.
|
|
8
|
+
- `host-services.md`: `secrets`, `modelCapabilities`, `scheduling`, `queueBus`, `toolHooks`, `httpClient`; `runs.md`: `deadLetter`.
|
|
9
|
+
- New `execution.md`: `selfHostedRunner`, `subWorkflow`, `multiAgent` (the family claim and `executionModel` facet); `identity.md` §1.5: `anonymousActor`.
|
|
10
|
+
|
|
11
|
+
## [2.44.2] — 2026-09-28 — RFC 0215's no-head-of-line leg does not convict a late fan-out, and says when it sampled each count
|
|
12
|
+
|
|
13
|
+
- **The 2.44.2 cycle opens.** A patch: the requirement is unchanged, and one verdict branch that convicted without the contention it names becomes unjudged. `@openwop/spec-artifacts` moves in lockstep at the same exact pin.
|
|
14
|
+
- **`0215.no-head-of-line` no longer fails a host whose `run.started` fan-out is late.** The last branch failed the row as "a bounded dispatcher below the floor" whenever fewer than 8 held attempts had arrived by the time the run was terminal. MyndHyve `00821-qec` hit it twice. In the first cut no held attempt had arrived and the healthy one was delivered. In the second, 2 held attempts had arrived at terminal and 7 later, and the healthy attempt started with 7 held open and none closed. §A forbids an attempt waiting for another subscription's attempt to *finish*. None had finished, so the healthy attempt waited for nothing, and the contention §A.2 names was never established. The row is now `blocked` (unjudged) in that case. A bounded pool still fails: its healthy attempt starts only after a held attempt closes, or never while they stay open. So does a serial dispatcher, through the peak assertion.
|
|
15
|
+
- **The detail names each sampling time.** It read "2 held attempt(s) arrived (at most 7 open at once)", with the 2 sampled at terminal and the 7 at the verdict. It now reports what had arrived and was open at terminal, what had arrived by the verdict and the peak, when the healthy attempt arrived, and whether the host had closed any held attempt. It no longer names a cause the data does not show.
|
|
16
|
+
- **The verdict is a pure function with self-tests** (`lib/delivery-isolation-verdict.ts`). The tests cover pass, the contention-present fail, a 7-slot pool with a 15 s timeout (fail), a serial dispatcher (peak assertion), and both MyndHyve cuts (unjudged). Sabotage-checked: removing the new branch fails the two MyndHyve cases.
|
|
17
|
+
|
|
3
18
|
## [2.44.1] — 2026-09-28 — the webhook retry legs outlast an advertised maxElapsedMs, and four families get v2 normative homes
|
|
4
19
|
|
|
5
20
|
- **The 2.44.1 cycle opens.** `workspace`, `content`, `triggerBridge` and `uiPlugins` get v2 normative homes (RFC 0189), which changes the packed `@openwop/spec-artifacts` tree (`spec/v2/core/host-services.md`, `i18n.md`, `webhooks.md`, `packs.md`, `spec/v2/declaration.json`) after `v2.44.0` was tagged. No scenario changes: the rules are restated from the v1 text, none added or strengthened.
|
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.44.
|
|
14
|
+
npm install --legacy-peer-deps @openwop/openwop-conformance@2.44.3 @openwop/spec-artifacts@2.44.3
|
|
15
15
|
# or run without install:
|
|
16
16
|
npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
|
|
17
17
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.44.
|
|
3
|
+
"version": "2.44.3",
|
|
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.
|
|
59
|
+
"@openwop/spec-artifacts": "2.44.3"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -34246,7 +34246,7 @@
|
|
|
34246
34246
|
{
|
|
34247
34247
|
"id": "openwop.requirement.0215.no-head-of-line",
|
|
34248
34248
|
"file": "v2-webhook-delivery-isolation.test.ts",
|
|
34249
|
-
"line":
|
|
34249
|
+
"line": 119,
|
|
34250
34250
|
"title": null,
|
|
34251
34251
|
"explicitId": "openwop.requirement.0215.no-head-of-line",
|
|
34252
34252
|
"citations": [
|
|
@@ -34273,11 +34273,6 @@
|
|
|
34273
34273
|
"requirement": null,
|
|
34274
34274
|
"interpolated": true
|
|
34275
34275
|
},
|
|
34276
|
-
{
|
|
34277
|
-
"section": "webhooks.md §Durability (RFC 0215 §A.1, §A.2)",
|
|
34278
|
-
"requirement": null,
|
|
34279
|
-
"interpolated": true
|
|
34280
|
-
},
|
|
34281
34276
|
{
|
|
34282
34277
|
"section": "webhooks.md §Durability (RFC 0215 §A.1, §A.2)",
|
|
34283
34278
|
"requirement": null,
|
|
@@ -34360,7 +34355,7 @@
|
|
|
34360
34355
|
{
|
|
34361
34356
|
"id": "openwop.it.v2-webhook-durable-delivery.a-failed-attempt-is-retried-and-the-event-is-delivered-at-least-once",
|
|
34362
34357
|
"file": "v2-webhook-durable-delivery.test.ts",
|
|
34363
|
-
"line":
|
|
34358
|
+
"line": 279,
|
|
34364
34359
|
"title": "a failed attempt is retried and the event is delivered at least once",
|
|
34365
34360
|
"explicitId": "openwop.requirement.0173.webhook-durable-delivery",
|
|
34366
34361
|
"citations": [
|
|
@@ -34397,7 +34392,7 @@
|
|
|
34397
34392
|
{
|
|
34398
34393
|
"id": "openwop.it.v2-webhook-durable-delivery.an-exhausted-delivery-is-dead-lettered-never-dropped",
|
|
34399
34394
|
"file": "v2-webhook-durable-delivery.test.ts",
|
|
34400
|
-
"line":
|
|
34395
|
+
"line": 400,
|
|
34401
34396
|
"title": "an exhausted delivery is dead-lettered, never dropped",
|
|
34402
34397
|
"explicitId": "openwop.requirement.0173.webhook-durable-delivery.dead-letter",
|
|
34403
34398
|
"citations": [
|
|
@@ -34436,7 +34431,7 @@
|
|
|
34436
34431
|
{
|
|
34437
34432
|
"id": "openwop.it.v2-webhook-durable-delivery.the-dead-letter-read-is-served-and-its-records-carry-no-payload",
|
|
34438
34433
|
"file": "v2-webhook-durable-delivery.test.ts",
|
|
34439
|
-
"line":
|
|
34434
|
+
"line": 509,
|
|
34440
34435
|
"title": "the dead-letter read is served and its records carry no payload",
|
|
34441
34436
|
"explicitId": "openwop.requirement.0188.dead-letter-read",
|
|
34442
34437
|
"citations": [
|
|
@@ -34449,7 +34444,7 @@
|
|
|
34449
34444
|
{
|
|
34450
34445
|
"id": "openwop.it.v2-webhook-durable-delivery.after-unregister-the-dead-letter-read-answers-404-not-found-as-for-a-subscriptio",
|
|
34451
34446
|
"file": "v2-webhook-durable-delivery.test.ts",
|
|
34452
|
-
"line":
|
|
34447
|
+
"line": 530,
|
|
34453
34448
|
"title": "after unregister, the dead-letter read answers 404 not_found, as for a subscription that never existed",
|
|
34454
34449
|
"explicitId": "openwop.requirement.0217.dead-letter-read-after-unregister",
|
|
34455
34450
|
"citations": [
|
|
@@ -34476,7 +34471,7 @@
|
|
|
34476
34471
|
{
|
|
34477
34472
|
"id": "openwop.it.v2-webhook-durable-delivery.a-dead-letter-record-carries-no-delivered-payload",
|
|
34478
34473
|
"file": "v2-webhook-durable-delivery.test.ts",
|
|
34479
|
-
"line":
|
|
34474
|
+
"line": 563,
|
|
34480
34475
|
"title": "a dead-letter record carries no delivered payload",
|
|
34481
34476
|
"explicitId": "openwop.requirement.0188.dead-letter-content-free",
|
|
34482
34477
|
"citations": [
|
|
@@ -34620,7 +34615,7 @@
|
|
|
34620
34615
|
{
|
|
34621
34616
|
"id": "openwop.it.v2-webhook-message-id-stable.every-attempt-of-one-delivery-carries-one-webhook-id-and-distinct-deliveries-car",
|
|
34622
34617
|
"file": "v2-webhook-message-id-stable.test.ts",
|
|
34623
|
-
"line":
|
|
34618
|
+
"line": 70,
|
|
34624
34619
|
"title": "every attempt of one delivery carries one webhook-id, and distinct deliveries carry distinct ids",
|
|
34625
34620
|
"explicitId": "openwop.requirement.0201.message-id-stable",
|
|
34626
34621
|
"citations": [
|
|
@@ -34859,7 +34854,7 @@
|
|
|
34859
34854
|
{
|
|
34860
34855
|
"id": "openwop.it.v2-webhook-unregister-stops-delivery.no-attempt-for-an-unregistered-subscription-starts-after-the-204-while-its-contr",
|
|
34861
34856
|
"file": "v2-webhook-unregister-stops-delivery.test.ts",
|
|
34862
|
-
"line":
|
|
34857
|
+
"line": 87,
|
|
34863
34858
|
"title": "no attempt for an unregistered subscription starts after the 204, while its control keeps being retried",
|
|
34864
34859
|
"explicitId": "openwop.requirement.0215.unregister-stops-delivery",
|
|
34865
34860
|
"citations": [
|
|
@@ -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.
|
|
5
|
-
"corpusTag": "v2.44.
|
|
4
|
+
"version": "2.44.3",
|
|
5
|
+
"corpusTag": "v2.44.3",
|
|
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": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "3319d19f95c90c82ad611d3698daa77062760aaed9c703000f0eaa93cdcb3ed3",
|
|
13
|
+
"api/v2/asyncapi.yaml": "fc977f137a3a40ac20c17911f9987e55f1b93ab5e73d039e94ab2749537ad4b3",
|
|
14
|
+
"api/v2/openapi.yaml": "353fe2c7b556dfa12f45f85783e0b1409ae3f6e59755719f08ba525b830189ac",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -204,50 +204,51 @@
|
|
|
204
204
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
205
205
|
"spec/v1/alias-detectors.json": "29f21e0fb2c7a232a92a5c19b4cba3a86bc3b5e16f06780452f0421c66c6ec38",
|
|
206
206
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
207
|
-
"spec/v1/core-standard-manifest.json": "
|
|
207
|
+
"spec/v1/core-standard-manifest.json": "8789a8c30013d9137db12e2cac6bd38b461a0f16147617366263b26bd265a1d8",
|
|
208
208
|
"spec/v1/deprecations.json": "46df807fb4adf0aae24671c20f69975cf3496e3bc3a521cb7e6ea556bdce563c",
|
|
209
209
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
210
210
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
211
211
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
212
212
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
213
|
-
"spec/v1/gaps.json": "
|
|
213
|
+
"spec/v1/gaps.json": "3eb4316a7895eeefd9bb918e496b145d8fc0e39d5cbc5dcc72e2fbefda6803ed",
|
|
214
214
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
215
215
|
"spec/v1/migrations.json": "2efcd9064b2bc65a84a3b3d281aa86a91efd002690c1f4f14e8335c4a18194d1",
|
|
216
216
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
217
217
|
"spec/v1/operation-path-manifest.json": "fada0342815fb129c1ae61f1829bd6e34417717408ce86fcc54280f373c19e74",
|
|
218
218
|
"spec/v1/spec-gaps.json": "ea9612af55bea9dae3e82223d4f714243f8f590b0193f1188257b13491a38afc",
|
|
219
|
-
"spec/v2/README.md": "
|
|
219
|
+
"spec/v2/README.md": "ae7e9dceb3a6a1df75259de43c08244840c8ca8380a3eb4c727ae07dc4bdc4b1",
|
|
220
220
|
"spec/v2/core/capabilities.md": "ad728e8a761153b55d036ece55043ff62d0982121e2e629e5e7cdf560fca3c03",
|
|
221
221
|
"spec/v2/core/conformance.md": "0cfceb926eed87055550a5c23e640210c795fc8cf9dd1b2544db0e2b3842c564",
|
|
222
222
|
"spec/v2/core/connection-packs.md": "1ee129d792c537bfe82393ccb92555d8e6ddc342b1aa781a13f101b27ec38171",
|
|
223
223
|
"spec/v2/core/conversation.md": "354fa57c73d11062e523a44aa75a6353c5c81aefaf306d30c42416a228c82c86",
|
|
224
224
|
"spec/v2/core/errors.md": "6d5a67389de84556f81e2558a30d83959c9283c8c5481ce7c759021e486d84fd",
|
|
225
|
-
"spec/v2/core/events.md": "
|
|
225
|
+
"spec/v2/core/events.md": "8a2767691f604b2adf76399edeeaccfc151c697fc9717dc17cf7dd57372a8c22",
|
|
226
|
+
"spec/v2/core/execution.md": "ec1afd30a2b7dab4a722f4502764cb752685e441ecd6ca61e96ed775163be139",
|
|
226
227
|
"spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
|
|
227
228
|
"spec/v2/core/headers.md": "5cb1624fa63acc81992b6a6d842ec392cf7271aaf2f65d1f83c8c4fa286e6730",
|
|
228
|
-
"spec/v2/core/host-services.md": "
|
|
229
|
+
"spec/v2/core/host-services.md": "244e222ec6f7d4774751bd3cc124bbc9f548fdd84fabca65c19159ff00d49e84",
|
|
229
230
|
"spec/v2/core/i18n.md": "2d5a67bd3b9b956d1144acab2fbc3646cdb50b3621f97d772087eaa953798286",
|
|
230
231
|
"spec/v2/core/idempotency.md": "2084d756789c1faf5121593fb39e67ca36a4c227178e175004742e403178ea49",
|
|
231
|
-
"spec/v2/core/identity.md": "
|
|
232
|
+
"spec/v2/core/identity.md": "582eddbf2e1bad41911e886bd08524683837045404f7b8f6bceccf61792b2765",
|
|
232
233
|
"spec/v2/core/interop.md": "e72e25dff864c83b2980d7366b34054cffa072af0225821199f260f6228d260e",
|
|
233
234
|
"spec/v2/core/interrupt.md": "4c825634acac72900ebd3c5ac5941f55648fa33d2df92913ef16371dcb700a34",
|
|
234
235
|
"spec/v2/core/node-pack-runtimes.md": "e1088cb69b8c3c67256c494063328916c2980ab1aa97a7063de25bab82fdb1e5",
|
|
235
236
|
"spec/v2/core/oauth.md": "c96483a44b62978c596945fbb7d7c863cf6056cd980fdb4cf4e97555e3e10b72",
|
|
236
|
-
"spec/v2/core/overview.md": "
|
|
237
|
+
"spec/v2/core/overview.md": "76fdb48ea68de416d6a63ae3b3ec750cafda01d234ea22a1096426fded7b54f3",
|
|
237
238
|
"spec/v2/core/packs.md": "5929e708bd4e79f6e1beea570af583c80f3b63bb21dec78a5df03c235e73364a",
|
|
238
239
|
"spec/v2/core/persistence.md": "711874f74cb08b03cd118d683056db10a063517762fcea73ec70feb5d983e6a4",
|
|
239
240
|
"spec/v2/core/portability.md": "87c4327f674f714fda6217fd79b99d2ba219ad47dc02f9751bb56c200fb2c17a",
|
|
240
241
|
"spec/v2/core/replay.md": "4d379494057746c6883f6047972e93e621863b998ed291b9a691d3aa57c1ff43",
|
|
241
|
-
"spec/v2/core/runs.md": "
|
|
242
|
+
"spec/v2/core/runs.md": "d0e84ccf7cd7dfa657a8d9a90548dbe1d1f7033be30b405d56c41e36007af977",
|
|
242
243
|
"spec/v2/core/security-defaults.md": "153884da7a2d54c30c81b5e649cfdef31e151b3a2a19dee1bc5a16c35d60a3aa",
|
|
243
244
|
"spec/v2/core/storage.md": "598ad4f3775c297c698a20daf72a7d15a6836c1231ddbca5961865c704f765b5",
|
|
244
245
|
"spec/v2/core/tool-catalog.md": "d16a9004418719c48301804aefa4d7ab6f41112a4ed83727f552aa1b92b418b0",
|
|
245
246
|
"spec/v2/core/versioning.md": "e3b3ab6d76e51035039a32173e9b412628a52c8fe7b16687ff58a2d0d983faa0",
|
|
246
|
-
"spec/v2/core/webhooks.md": "
|
|
247
|
+
"spec/v2/core/webhooks.md": "3f8730e0d15a5e4244e302adb910a0b6f745fbfa8e80920c14118117811ba881",
|
|
247
248
|
"spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
|
|
248
249
|
"spec/v2/corrections.json": "59f647f4d22477f6c2b06dcf913b682dde5d51dd5e607a5d0cf648ab60864d6e",
|
|
249
250
|
"spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
|
|
250
|
-
"spec/v2/declaration.json": "
|
|
251
|
+
"spec/v2/declaration.json": "25ceb30f1b055afa529c2d822e768498e8c3e95bdc652b5edb2404bf75ac96ee",
|
|
251
252
|
"spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
|
|
252
253
|
"spec/v2/errors.json": "fa650cbbdf25a140fdaf82ec5269f95b4585ac4bdde1fa6b0b3ffe48a3f7943c",
|
|
253
254
|
"spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
|
|
@@ -299,9 +300,9 @@
|
|
|
299
300
|
"spec/v2/path-manifest.json": "17298a245c5da9a70aa7ab0b7f2db4277e06209d6fcdd7187a8b4cea179c08f3",
|
|
300
301
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
301
302
|
"spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
|
|
302
|
-
"spec/v2/release.json": "
|
|
303
|
+
"spec/v2/release.json": "f84bac4d5066e20022a2f9011a4f7441e74b261f3e41da59990cf52214896478",
|
|
303
304
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
304
|
-
"spec/v2/surface-baseline.json": "
|
|
305
|
+
"spec/v2/surface-baseline.json": "8acb9fab0aca69abfe40dc56fcf48729aaf0d56a41467962e5c7247f3548eea3"
|
|
305
306
|
},
|
|
306
|
-
"corpusCommit": "
|
|
307
|
+
"corpusCommit": "e8eda2ffa2042f1ab81232cfe4fe5657c9576032"
|
|
307
308
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verdict of RFC 0215's `no-head-of-line` leg, as a pure function of what
|
|
3
|
+
* the leg observed (suite 2.44.2), so the rule can be tested without a host.
|
|
4
|
+
*
|
|
5
|
+
* 2.44.2 fixes two defects that MyndHyve's 2.44.x cuts surfaced:
|
|
6
|
+
*
|
|
7
|
+
* 1. **A conviction without the contention it names.** The last branch failed
|
|
8
|
+
* the row as "a bounded dispatcher below the floor" whenever fewer than
|
|
9
|
+
* `floor` held attempts had arrived by the time the run was terminal. On a
|
|
10
|
+
* host whose `run.started` fan-out arrived late, that convicted a healthy
|
|
11
|
+
* attempt that started while every held attempt that HAD arrived was still
|
|
12
|
+
* open and none had finished (MyndHyve `00821-qec`: 0 held attempts, then 2
|
|
13
|
+
* at terminal and 7 later, the healthy attempt delivered with 7 open). §A
|
|
14
|
+
* forbids an attempt waiting for another subscription's attempt to FINISH; no
|
|
15
|
+
* attempt had finished, so there was nothing it could have waited for. The
|
|
16
|
+
* contention was never established, so the row is unjudged, not failed. A
|
|
17
|
+
* bounded pool still fails: its healthy attempt starts only after a held
|
|
18
|
+
* attempt closes, or never while they stay open.
|
|
19
|
+
*
|
|
20
|
+
* 2. **A detail string that mixed two sampling times.** It read "2 held
|
|
21
|
+
* attempt(s) arrived (at most 7 open at once)": the 2 was sampled at
|
|
22
|
+
* terminal, the 7 at the verdict. The detail now names each time.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface IsolationObservation {
|
|
26
|
+
/** §A.2's floor. */
|
|
27
|
+
readonly floor: number;
|
|
28
|
+
/** When the leg saw the run terminal (the healthy delivery is due by then). */
|
|
29
|
+
readonly terminalAt: number;
|
|
30
|
+
/** When the healthy attempt arrived, or null if it did not within the window. */
|
|
31
|
+
readonly healthyAt: number | null;
|
|
32
|
+
/** Held attempts open when the healthy attempt arrived (-1 if it did not). */
|
|
33
|
+
readonly openAtHealthy: number;
|
|
34
|
+
/** Held attempts that had arrived by `terminalAt`. */
|
|
35
|
+
readonly arrivedAtTerminal: number;
|
|
36
|
+
/** Held attempts still open at `terminalAt`. */
|
|
37
|
+
readonly openAtTerminal: number;
|
|
38
|
+
/** Held attempts that had arrived by the verdict. */
|
|
39
|
+
readonly arrivedByVerdict: number;
|
|
40
|
+
/** The most held attempts open at once, up to the verdict. */
|
|
41
|
+
readonly peakOpen: number;
|
|
42
|
+
/** When the first held attempt closed (the host abandoning it), or Infinity. */
|
|
43
|
+
readonly earliestClose: number;
|
|
44
|
+
readonly windowMs: number;
|
|
45
|
+
readonly delayMs: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type IsolationVerdict =
|
|
49
|
+
/** The healthy attempt started while `floor` held attempts were open. */
|
|
50
|
+
| { readonly kind: 'pass'; readonly detail: string }
|
|
51
|
+
| { readonly kind: 'fail'; readonly message: string; readonly detail: string }
|
|
52
|
+
/** Unjudged, after asserting `peakOpen >= floor` (which fails a pool below the floor). */
|
|
53
|
+
| { readonly kind: 'blocked-after-peak'; readonly message: string; readonly detail: string }
|
|
54
|
+
| { readonly kind: 'blocked'; readonly message: string; readonly detail: string };
|
|
55
|
+
|
|
56
|
+
/** What the leg saw, each count against the time it was sampled. */
|
|
57
|
+
export function isolationDetail(o: IsolationObservation): string {
|
|
58
|
+
const at = (t: number): string => `${t - o.terminalAt}ms after the run was seen terminal`;
|
|
59
|
+
const healthy = o.healthyAt === null
|
|
60
|
+
? `the healthy attempt did not arrive within ${o.windowMs}ms of that`
|
|
61
|
+
: `the healthy attempt arrived ${at(o.healthyAt)}, with ${o.openAtHealthy} held open`;
|
|
62
|
+
const closed = Number.isFinite(o.earliestClose) ? `the first held attempt was closed by the host ${at(o.earliestClose)}` : 'no held attempt was closed by the host';
|
|
63
|
+
return `at terminal, ${o.arrivedAtTerminal} held attempt(s) had arrived and ${o.openAtTerminal} were open; by the verdict, ${o.arrivedByVerdict} had arrived, at most ${o.peakOpen} open at once; ${healthy}; ${closed}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isolationVerdict(o: IsolationObservation): IsolationVerdict {
|
|
67
|
+
const detail = isolationDetail(o);
|
|
68
|
+
const F = o.floor;
|
|
69
|
+
if (o.healthyAt !== null && o.openAtHealthy >= F) return { kind: 'pass', detail };
|
|
70
|
+
if (o.openAtTerminal >= F) {
|
|
71
|
+
// The healthy attempt waited (or never came), and the contention §A.2
|
|
72
|
+
// names was present when it fell due.
|
|
73
|
+
return { kind: 'fail', message: `with ${F} subscriptions' attempts unanswered, a further subscription's attempt MUST still start; it waited for the host to release one`, detail };
|
|
74
|
+
}
|
|
75
|
+
if (o.arrivedAtTerminal >= F || o.earliestClose < o.terminalAt) {
|
|
76
|
+
// Contention was not sustained to the due time. Either the host's own
|
|
77
|
+
// delivery timeout released the held attempts early (its choice), or it
|
|
78
|
+
// never opened F of them at once. peakOpen separates the two: below F the
|
|
79
|
+
// host never had F attempts outstanding at once, which is the defect.
|
|
80
|
+
const released = Number.isFinite(o.earliestClose) ? `${o.terminalAt - o.earliestClose}ms ` : '';
|
|
81
|
+
return { kind: 'blocked-after-peak', message: `unjudged: ${F} held attempts were open at once, but the host closed held attempts ${released}before the run was terminal (its delivery timeout is shorter than this leg's ${o.delayMs}ms delay), so ${F} attempts were not outstanding when the healthy delivery fell due`, detail };
|
|
82
|
+
}
|
|
83
|
+
if (o.healthyAt !== null && !(o.earliestClose < o.healthyAt)) {
|
|
84
|
+
// Fewer than F held attempts had arrived when the healthy delivery fell
|
|
85
|
+
// due, and it started before any held attempt finished: it cannot have
|
|
86
|
+
// waited for one. The held fan-out was late, so the contention the floor
|
|
87
|
+
// names never existed when it mattered.
|
|
88
|
+
return { kind: 'blocked', message: `unjudged: only ${o.arrivedAtTerminal} of the ${F} held attempts had arrived when the healthy delivery fell due, and the healthy attempt started before any held attempt finished, so it did not wait for one; the held run.started deliveries arrived late, and the contention §A.2 names was not established`, detail };
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
kind: 'fail',
|
|
92
|
+
message: o.healthyAt === null
|
|
93
|
+
? `the host never had ${F} subscriptions' attempts outstanding at once, and the healthy attempt did not start within ${o.windowMs}ms while the held attempts stayed open`
|
|
94
|
+
: `the host never had ${F} subscriptions' attempts outstanding at once, and the healthy attempt started only after a held attempt finished`,
|
|
95
|
+
detail,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An informational note for the CURRENT `it`'s row (suite 2.44.3, RFC 0225).
|
|
3
|
+
*
|
|
4
|
+
* `setup.ts` attaches it to the row's `detail` in `afterEach`, whatever the
|
|
5
|
+
* disposition, so a PASSING row can say what it observed. The bundle writer
|
|
6
|
+
* already carries `detail` on every row and the witness digest covers it, so the
|
|
7
|
+
* note is signed with the row. It is prefixed `observed: `, never
|
|
8
|
+
* `partial-witness: `: it describes how a witness was obtained and does not
|
|
9
|
+
* qualify it (`check-accepted-predicate` counts a pass as partial only on that
|
|
10
|
+
* prefix). Same shape as `noteEvidence` (lib/durability-evidence.ts).
|
|
11
|
+
*/
|
|
12
|
+
export const OBSERVATION_PREFIX = 'observed: ';
|
|
13
|
+
|
|
14
|
+
let pending: string | null = null;
|
|
15
|
+
|
|
16
|
+
/** Note what this `it` observed. A second note in the same `it` is appended. */
|
|
17
|
+
export function noteObservation(text: string): void {
|
|
18
|
+
const t = text.trim();
|
|
19
|
+
if (t === '') return;
|
|
20
|
+
pending = pending === null ? t : `${pending} · ${t}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Take (and clear) the pending note. */
|
|
24
|
+
export function takeNotedObservation(): string | null {
|
|
25
|
+
const n = pending;
|
|
26
|
+
pending = null;
|
|
27
|
+
return n;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The detail a row carries once an observation is attached. A row with no
|
|
32
|
+
* detail gets `observed: …`; a row that already has one (a non-pass reason, or
|
|
33
|
+
* a partial-witness marker) keeps it FIRST and gains `· observed: …`, so no
|
|
34
|
+
* prefix a reader filters on ever moves.
|
|
35
|
+
*/
|
|
36
|
+
export function attachObservation(detail: string | undefined, observation: string | null): string | undefined {
|
|
37
|
+
if (observation === null || observation.trim() === '') return detail;
|
|
38
|
+
if (detail === undefined || detail.trim() === '') return `${OBSERVATION_PREFIX}${observation}`;
|
|
39
|
+
return `${detail} · ${OBSERVATION_PREFIX}${observation}`;
|
|
40
|
+
}
|
|
@@ -90,6 +90,62 @@ export function retryTestTimeoutMs(waits: number, slackMs: number): number {
|
|
|
90
90
|
return waits * MAX_RETRY_WAIT_MS + slackMs;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* WHICH branch `retryWaitFor` took, so a row can say so (RFC 0225 witness, suite
|
|
95
|
+
* 2.44.3). A passing dead-letter row used to carry no detail at all, so a
|
|
96
|
+
* bundle could not show whether the wait was derived from the host's advertised
|
|
97
|
+
* `maxElapsedMs` (the RFC 0225 path), from an operator-raised cap, or from the
|
|
98
|
+
* 20 s floor, and a certifier could not tell a witness of the bound from a pass
|
|
99
|
+
* the old default would have produced anyway.
|
|
100
|
+
*
|
|
101
|
+
* advertised-bound the window is the advertised maxElapsedMs + the grace
|
|
102
|
+
* cap the window is the cap (default or operator-raised); when
|
|
103
|
+
* a bound is advertised this means the operator raised the
|
|
104
|
+
* cap ABOVE it, so the bound did not set the wait
|
|
105
|
+
* floor no policy, or backoff none: the 20 s floor
|
|
106
|
+
*/
|
|
107
|
+
export type RetryWaitPath = 'advertised-bound' | 'cap' | 'floor';
|
|
108
|
+
export interface RetryWaitSelection { readonly path: RetryWaitPath; readonly windowMs: number; readonly maxElapsedMs: number | null; readonly capMs: number }
|
|
109
|
+
|
|
110
|
+
export function retryWaitSelection(policy: AdvertisedRetryPolicy | null, capMs: number): RetryWaitSelection {
|
|
111
|
+
const windowMs = retryWaitFor(policy, capMs);
|
|
112
|
+
const bound = advertisedMaxElapsedMs(policy);
|
|
113
|
+
if (policy === null) return { path: 'floor', windowMs, maxElapsedMs: null, capMs };
|
|
114
|
+
if (bound !== null) return { path: bound + ADVERTISED_BOUND_GRACE_MS >= capMs ? 'advertised-bound' : 'cap', windowMs, maxElapsedMs: bound, capMs };
|
|
115
|
+
const backoff = String(policy.backoff ?? '');
|
|
116
|
+
return { path: backoff === 'exponential' || backoff === 'fixed' ? 'cap' : 'floor', windowMs, maxElapsedMs: null, capMs };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** What the leg measured, in ms since the delivery's FIRST attempt reached the receiver. */
|
|
120
|
+
export interface RetryWaitMeasurement { readonly attempts: number; readonly lastAttemptAfterMs: number | null; /** undefined when the leg does not read the sink */ readonly sinkSeenAfterMs?: number | null }
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The informational row detail: the path, the window, and what was measured.
|
|
124
|
+
* The last attempt is a LOWER bound on the dead-lettering time (the host cannot
|
|
125
|
+
* dead-letter before its last attempt); the sink observation is an UPPER bound
|
|
126
|
+
* (the read that first found the record). Never starts with the partial-witness
|
|
127
|
+
* prefix: this describes a witness, it does not qualify one.
|
|
128
|
+
*/
|
|
129
|
+
/** The path and window alone, for a leg that measures no dead-lettering. */
|
|
130
|
+
export function waitPathNote(sel: RetryWaitSelection): string {
|
|
131
|
+
return `${pathLabel(sel)}: waited ≤${sel.windowMs}ms`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function pathLabel(sel: RetryWaitSelection): string {
|
|
135
|
+
return sel.path === 'advertised-bound'
|
|
136
|
+
? `advertised-bound (maxElapsedMs ${sel.maxElapsedMs})`
|
|
137
|
+
: sel.path === 'cap'
|
|
138
|
+
? `cap (${sel.capMs > DEFAULT_RETRY_WAIT_CAP_MS ? `${RETRY_WAIT_ENV} raised to ${sel.capMs}` : `default ${sel.capMs}`}ms; advertised maxElapsedMs ${sel.maxElapsedMs ?? 'none'})`
|
|
139
|
+
: 'floor';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function waitObservation(sel: RetryWaitSelection, m: RetryWaitMeasurement): string {
|
|
143
|
+
const how = pathLabel(sel);
|
|
144
|
+
const last = m.lastAttemptAfterMs === null ? 'no attempt' : `last attempt after ${m.lastAttemptAfterMs}ms`;
|
|
145
|
+
const sink = m.sinkSeenAfterMs === undefined ? '' : m.sinkSeenAfterMs === null ? ', sink not observed' : `, sink after ${m.sinkSeenAfterMs}ms`;
|
|
146
|
+
return `${how}: waited ≤${sel.windowMs}ms; ${m.attempts} attempt(s), ${last}${sink}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
93
149
|
/** What a row says when its window closed before the host's schedule did. Computed, so the numbers a host reads are the ones the run used. */
|
|
94
150
|
export function windowClosedNote(seen: number, maxAttempts: number, waitedMs: number, capMs: number): string {
|
|
95
151
|
const raised = capMs > DEFAULT_RETRY_WAIT_CAP_MS;
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
* host released one to make room), or never within the window, AND the held
|
|
27
27
|
* attempts had stayed open until the run was terminal: the contention the
|
|
28
28
|
* floor names existed when the healthy delivery fell due, and it waited.
|
|
29
|
+
* - blocked (unjudged, suite 2.44.2) — fewer than 8 held attempts had arrived
|
|
30
|
+
* when the healthy delivery fell due, and the healthy attempt started before
|
|
31
|
+
* any held attempt finished: it waited for nothing, and the contention was
|
|
32
|
+
* never established (a late `run.started` fan-out). The rule and its tests
|
|
33
|
+
* are in `lib/delivery-isolation-verdict.ts`.
|
|
29
34
|
* - blocked (unjudged) — the host had all 8 held attempts open at once, but
|
|
30
35
|
* its own delivery timeout closed them before the run was terminal.
|
|
31
36
|
* Contention was not sustained when the healthy delivery fell due, so this
|
|
@@ -56,6 +61,7 @@ import { readErrorCode } from '../lib/error-envelope.js';
|
|
|
56
61
|
import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
|
|
57
62
|
import { req } from '../lib/requirement-ids.js';
|
|
58
63
|
import { RETRY_WAIT_FLOOR_MS } from '../lib/webhook-retry-window.js';
|
|
64
|
+
import { isolationVerdict } from '../lib/delivery-isolation-verdict.js';
|
|
59
65
|
|
|
60
66
|
export const REQUIRES_HOST_CALLBACK = 'the host POSTs webhook deliveries to nine suite-owned subscriptions on the scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL, eight of which the suite holds open';
|
|
61
67
|
|
|
@@ -179,44 +185,30 @@ describe('RFC 0215 §A — one subscription\'s receiver does not hold another\'s
|
|
|
179
185
|
const terminalAt = await waitTerminal(runId, DELAY_MS + 30_000);
|
|
180
186
|
if (terminalAt === null) return blockedDespiteAssertions(`${FIXTURE} run ${runId} did not reach a terminal status — the healthy delivery never fell due`);
|
|
181
187
|
const openAtTerminal = openCount();
|
|
182
|
-
const
|
|
188
|
+
const arrivedAtTerminal = held.size;
|
|
183
189
|
await waitFor(() => healthyAt !== null, WINDOW_MS);
|
|
184
190
|
const earliestClose = Math.min(...[...held.values()].map((h) => h.closedAt ?? Infinity));
|
|
185
|
-
const detail = `${everOpened} held attempt(s) arrived (at most ${peakOpen} open at once); ${openAtTerminal} were still open when the run was seen terminal; `
|
|
186
|
-
+ (healthyAt === null ? `the healthy attempt did not arrive within ${WINDOW_MS}ms of that` : `the healthy attempt arrived ${healthyAt - terminalAt}ms after it, with ${openAtHealthy} held open`);
|
|
187
191
|
|
|
188
|
-
if (healthyAt === null &&
|
|
192
|
+
if (healthyAt === null && held.size === 0) {
|
|
189
193
|
if (absenceIsUnmeasured(rx)) return blockedDespiteAssertions(noDeliveryCause(rx, 'webhook attempt'));
|
|
190
194
|
return blockedDespiteAssertions(`no attempt for any of the ${FLOOR + 1} subscriptions arrived — ${noDeliveryCause(rx, 'webhook attempt')}`);
|
|
191
195
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// never opened FLOOR of them at once. The second is the defect, the first
|
|
202
|
-
// is not, and they differ in WHEN the first held attempt closed.
|
|
203
|
-
//
|
|
204
|
-
// Unfailable-leg audit (2026-09-26): this branch asserted everOpened > 0
|
|
205
|
-
// and then `softSkip('skipped')`, which records `executed-pass` with a
|
|
206
|
-
// `partial-witness:` detail. A serial dispatcher (pool of 1) with a
|
|
207
|
-
// sub-2s delivery timeout landed here and passed — every held attempt
|
|
208
|
-
// arrived eventually (everOpened reached FLOOR), one at a time, while the
|
|
209
|
-
// healthy attempt queued behind them: head-of-line blocking. peakOpen
|
|
210
|
-
// separates the two cases the comment above names: below FLOOR the host
|
|
211
|
-
// never had FLOOR attempts outstanding at once, which is the defect; at
|
|
212
|
-
// FLOOR the host's timeout released them early, which is unjudged.
|
|
196
|
+
const v = isolationVerdict({
|
|
197
|
+
floor: FLOOR, terminalAt, healthyAt, openAtHealthy, arrivedAtTerminal, openAtTerminal,
|
|
198
|
+
arrivedByVerdict: held.size, peakOpen, earliestClose, windowMs: WINDOW_MS, delayMs: DELAY_MS,
|
|
199
|
+
});
|
|
200
|
+
if (v.kind === 'blocked') return blockedDespiteAssertions(`${v.message} — ${v.detail}`);
|
|
201
|
+
if (v.kind === 'blocked-after-peak') {
|
|
202
|
+
// Unfailable-leg audit (2026-09-26): a serial dispatcher (pool of 1)
|
|
203
|
+
// with a sub-2s delivery timeout lands here; below FLOOR at once it
|
|
204
|
+
// fails, at FLOOR the host's timeout released them early (unjudged).
|
|
213
205
|
expect(
|
|
214
206
|
peakOpen,
|
|
215
|
-
req(ID, DOC, `a host MUST sustain ${FLOOR} subscriptions' attempts outstanding at once — at most ${peakOpen} were ever open together, so the held attempts were dispatched below the floor — ${detail}`),
|
|
207
|
+
req(ID, DOC, `a host MUST sustain ${FLOOR} subscriptions' attempts outstanding at once — at most ${peakOpen} were ever open together, so the held attempts were dispatched below the floor — ${v.detail}`),
|
|
216
208
|
).toBeGreaterThanOrEqual(FLOOR);
|
|
217
|
-
return blockedDespiteAssertions(
|
|
218
|
-
} else {
|
|
219
|
-
expect.fail(req(ID, DOC, `the host never had ${FLOOR} subscriptions' attempts outstanding at once, and the healthy attempt waited — a bounded dispatcher below the floor — ${detail}`));
|
|
209
|
+
return blockedDespiteAssertions(`${v.message} — ${v.detail}`);
|
|
220
210
|
}
|
|
211
|
+
if (v.kind === 'fail') expect.fail(req(ID, DOC, `${v.message} — ${v.detail}`));
|
|
212
|
+
expect(openAtHealthy, req(ID, DOC, `an attempt to one subscription MUST NOT wait for attempts to other subscriptions — ${v.detail}`)).toBeGreaterThanOrEqual(FLOOR);
|
|
221
213
|
}, DELAY_MS + 30_000 + RETRY_WAIT_FLOOR_MS + 30_000);
|
|
222
214
|
});
|
|
@@ -35,7 +35,8 @@ import { absenceIsUnmeasured, noDeliveryCause, startScopedReceiver, type ScopedR
|
|
|
35
35
|
import { readErrorCode } from '../lib/error-envelope.js';
|
|
36
36
|
import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
|
|
37
37
|
import { req } from '../lib/requirement-ids.js';
|
|
38
|
-
import { pastAdvertisedBound, retryTestTimeoutMs, retryWaitCapMs, retryWaitFor, windowClosedNote } from '../lib/webhook-retry-window.js';
|
|
38
|
+
import { pastAdvertisedBound, retryTestTimeoutMs, retryWaitCapMs, retryWaitFor, retryWaitSelection, waitObservation, windowClosedNote } from '../lib/webhook-retry-window.js';
|
|
39
|
+
import { noteObservation } from '../lib/row-observation.js';
|
|
39
40
|
|
|
40
41
|
export const REQUIRES_HOST_CALLBACK = 'the host POSTs and retries webhook deliveries to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
|
|
41
42
|
|
|
@@ -180,6 +181,21 @@ function retryWaitMs(doc: Record<string, unknown>): number {
|
|
|
180
181
|
return retryWaitFor(advertisedRetryPolicy(doc), RETRY_WAIT_CAP_MS);
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Suite 2.44.3 (RFC 0225 witness): say, on the row, which wait path ran and what
|
|
186
|
+
* was measured. The first attempt is t0; the last attempt lower-bounds the
|
|
187
|
+
* dead-lettering, and the sink read that found the record upper-bounds it.
|
|
188
|
+
*/
|
|
189
|
+
function observeWait(doc: Record<string, unknown>, attempts: ReadonlyArray<{ readonly at: number }>, sinkSeenAt?: number | null): void {
|
|
190
|
+
const t0 = attempts[0]?.at;
|
|
191
|
+
const last = attempts[attempts.length - 1]?.at;
|
|
192
|
+
noteObservation(waitObservation(retryWaitSelection(advertisedRetryPolicy(doc), RETRY_WAIT_CAP_MS), {
|
|
193
|
+
attempts: attempts.length,
|
|
194
|
+
lastAttemptAfterMs: t0 === undefined || last === undefined ? null : last - t0,
|
|
195
|
+
...(sinkSeenAt === undefined ? {} : { sinkSeenAfterMs: t0 === undefined || sinkSeenAt === null ? null : sinkSeenAt - t0 }),
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
183
199
|
/**
|
|
184
200
|
* The per-test budget, DERIVED from the wait above (suite 2.0.2).
|
|
185
201
|
*
|
|
@@ -287,6 +303,7 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
|
|
|
287
303
|
const ours = () => receiver.attempts.filter((a) => a.runId === runId && a.webhookId === sub.webhookId);
|
|
288
304
|
const retried = await waitFor(() => ours().some((a) => a.status === 204), retryWaitMs(doc));
|
|
289
305
|
const attempts = ours();
|
|
306
|
+
observeWait(doc, attempts);
|
|
290
307
|
// A zero that is PROVABLY not a verdict about the host records `blocked`
|
|
291
308
|
// with its cause, not `executed-fail` (2.37.0). `absenceIsUnmeasured` is
|
|
292
309
|
// true only when other traffic reached this listener — the path from the
|
|
@@ -440,6 +457,7 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
|
|
|
440
457
|
const sink = await driver.get(`/webhooks/${projectBoundId(sub.webhookId)}/dead-letters`);
|
|
441
458
|
inSink = sink.status === 200 && ((sink.json as { deliveries?: unknown[] } | null)?.deliveries ?? []).some((r) => (r as Record<string, unknown>)['runId'] === runId);
|
|
442
459
|
}
|
|
460
|
+
observeWait(doc, ours(), inSink === true ? Date.now() : null);
|
|
443
461
|
await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
|
|
444
462
|
// "Routed to the sink, not dropped" is a claim about an EXHAUSTED delivery,
|
|
445
463
|
// and until 2.34.1 it was asserted whether or not exhaustion had been
|
|
@@ -589,9 +607,11 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
|
|
|
589
607
|
await new Promise((r) => setTimeout(r, 1_000));
|
|
590
608
|
|
|
591
609
|
const sink = await driver.get(`/webhooks/${projectBoundId(sub.webhookId)}/dead-letters`);
|
|
610
|
+
const sinkReadAt = Date.now();
|
|
592
611
|
await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
|
|
593
612
|
if (sink.status !== 200) return blockedDespiteAssertions(`the dead-letter read answered ${sink.status}`);
|
|
594
613
|
const rows = ((sink.json as { deliveries?: Array<Record<string, unknown>> } | null)?.deliveries ?? []);
|
|
614
|
+
observeWait(doc, ours(), rows.length > 0 ? sinkReadAt : null);
|
|
595
615
|
// An empty sink is NOT a pass. Recording one as a pass is exactly the
|
|
596
616
|
// vacuous witness this leg used to produce; say so instead.
|
|
597
617
|
if (rows.length === 0) {
|
|
@@ -33,7 +33,8 @@ import { req } from '../lib/requirement-ids.js';
|
|
|
33
33
|
import { readErrorCode } from '../lib/error-envelope.js';
|
|
34
34
|
import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
|
|
35
35
|
import { hitHeader, mintWhsec, startModalReceiver, STANDARD_WEBHOOKS_ID, type ModalHit } from '../lib/webhook-receiver.js';
|
|
36
|
-
import { retryTestTimeoutMs, retryWaitCapMs, retryWaitFor } from '../lib/webhook-retry-window.js';
|
|
36
|
+
import { retryTestTimeoutMs, retryWaitCapMs, retryWaitFor, retryWaitSelection, waitPathNote } from '../lib/webhook-retry-window.js';
|
|
37
|
+
import { noteObservation } from '../lib/row-observation.js';
|
|
37
38
|
import {
|
|
38
39
|
STANDARD_WEBHOOKS_ALG,
|
|
39
40
|
deliveriesFor,
|
|
@@ -89,7 +90,9 @@ describe('RFC 0201 §C.10 — webhook-id is stable across retries, distinct acro
|
|
|
89
90
|
return m;
|
|
90
91
|
};
|
|
91
92
|
const expectedKeys = subs.length * EVENTS.length;
|
|
92
|
-
const
|
|
93
|
+
const advertised = (g.facet['retryPolicy'] as { backoff?: string; maxElapsedMs?: unknown } | undefined) ?? null;
|
|
94
|
+
const window = retryWaitFor(advertised, CAP_MS);
|
|
95
|
+
noteObservation(waitPathNote(retryWaitSelection(advertised, CAP_MS)));
|
|
93
96
|
await waitFor(() => {
|
|
94
97
|
const m = byKey();
|
|
95
98
|
return m.size >= expectedKeys && [...m.values()].every((a) => a.length >= 2);
|
|
@@ -45,7 +45,8 @@ import { absenceIsUnmeasured, noDeliveryCause, startScopedReceiver, type ScopedR
|
|
|
45
45
|
import { readErrorCode } from '../lib/error-envelope.js';
|
|
46
46
|
import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
|
|
47
47
|
import { req } from '../lib/requirement-ids.js';
|
|
48
|
-
import { retryTestTimeoutMs, retryWaitCapMs, retryWaitFor } from '../lib/webhook-retry-window.js';
|
|
48
|
+
import { retryTestTimeoutMs, retryWaitCapMs, retryWaitFor, retryWaitSelection, waitPathNote } from '../lib/webhook-retry-window.js';
|
|
49
|
+
import { noteObservation } from '../lib/row-observation.js';
|
|
49
50
|
|
|
50
51
|
export const REQUIRES_HOST_CALLBACK = 'the host POSTs and retries webhook deliveries to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
|
|
51
52
|
|
|
@@ -94,6 +95,7 @@ describe('RFC 0215 §B — unregistering stops the attempts (gated on webhooks)'
|
|
|
94
95
|
const policy = advertisedRetryPolicy(doc);
|
|
95
96
|
if (policy?.maxAttempts === 1) return softSkip('inapplicable', 'webhooks.retryPolicy.maxAttempts is 1 — the host schedules no retry, so there is none for unregistering to stop');
|
|
96
97
|
const windowMs = retryWaitFor(policy, CAP_MS);
|
|
98
|
+
noteObservation(waitPathNote(retryWaitSelection(policy, CAP_MS)));
|
|
97
99
|
|
|
98
100
|
const arrivals = new Map<string, number[]>();
|
|
99
101
|
let targetId: string | null = null;
|
package/src/setup.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { PKG_ROOT_PATH } from './lib/paths.js';
|
|
|
38
38
|
import { recordRequirement, hasRequirement, journalLength, journalSince } from './lib/requirement-ledger.js';
|
|
39
39
|
import { requirementIdForFile, resolveFileRecord, resolveItRecord, unrecordedTests, type FileTestState, type FinishedTest, type TestFailure } from './lib/scenario-disposition.js';
|
|
40
40
|
import { softSkipDisposition, softSkipDispositionSince, softSkipMark } from './lib/soft-skip.js';
|
|
41
|
+
import { attachObservation, takeNotedObservation } from './lib/row-observation.js';
|
|
41
42
|
import { ItIdAllocator, takeExplicitRequirementId } from './lib/requirement-ids.js';
|
|
42
43
|
import { SPEC_COHERENCE_SCENARIOS, SPEC_COHERENCE_DETAIL } from './lib/spec-coherence.js';
|
|
43
44
|
import type { DiscoveryPayload } from './lib/profiles.js';
|
|
@@ -472,6 +473,7 @@ afterEach(({ task }) => {
|
|
|
472
473
|
// Suite 2.0.0: src/coherence/ rows feed the corpus ledger (scripts/check-spec-coherence.mjs), not a host bundle.
|
|
473
474
|
if (!/[\\/]src[\\/](scenarios|coherence)[\\/]/.test(filepath)) {
|
|
474
475
|
takeExplicitRequirementId();
|
|
476
|
+
takeNotedObservation();
|
|
475
477
|
return;
|
|
476
478
|
}
|
|
477
479
|
// A leg that soft-skips before its first assertion never calls `req()`, so the
|
|
@@ -506,6 +508,11 @@ afterEach(({ task }) => {
|
|
|
506
508
|
disposition = rec.disposition;
|
|
507
509
|
detail = rec.detail;
|
|
508
510
|
}
|
|
511
|
+
// Suite 2.44.3 (RFC 0225 witness): an informational note the `it` wrote
|
|
512
|
+
// (`noteObservation`) rides on the row's detail, on a pass too — the bundle
|
|
513
|
+
// writer carries it and the witness digest signs it. Taken whatever the
|
|
514
|
+
// disposition, so a note never leaks into the next `it`.
|
|
515
|
+
detail = attachObservation(detail, takeNotedObservation());
|
|
509
516
|
try {
|
|
510
517
|
const evidence = takeNotedEvidence();
|
|
511
518
|
// `fold: true` — several `it` legs of one file legitimately share ONE
|