@openwop/openwop-conformance 2.0.1 → 2.0.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
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.3] — 2026-09-06 — a selector that asked the wrong question, and a window that convicted a host for its own width
|
|
4
|
+
|
|
5
|
+
**`v2-effect-seam-no-refire` selected on a branch permission to witness a replay obligation.** The filter was `guarded === true && branchReFires === false`. `replay.md:78`: *"A host MAY suppress branch effects and MUST NOT report that as replay suppression."* Replay suppression is unconditional (§Suppression rule 1) and does not vary with `branchReFires`, which states only what a **branch** re-fires by design. Every `guarded: true` row is a valid target; the filter now selects on `guarded` alone. `branchReFires` is also optional, so `=== false` additionally excluded rows merely silent on it. Found by the reference host, whose ten `branchReFires: true` rows are all honest and which was about to build `fireEffectSeam` for a scenario that would have kept recording `inapplicable` afterwards.
|
|
6
|
+
|
|
7
|
+
**`v2-webhook-durable-delivery`'s at-least-once leg convicted a host for the suite's window.** The 204 arrives on attempt `FAIL_FIRST + 1`, costing the **sum** of the first `FAIL_FIRST` backoff intervals — `30 + 60 = 90 s` on exponential-from-30s, against a cap of exactly `90_000`. The interval is not advertised (`retryPolicy` is `additionalProperties: false` over `{ maxAttempts, backoff }`), so the wait is underivable and any cap is a guess. The leg now records **`blocked`** with the arithmetic, not `executed-fail`.
|
|
8
|
+
|
|
9
|
+
**The cost:** a host that retries forever and never succeeds also records `blocked` now. That is a missed detection traded for a false conviction, and it returns only by advertising the interval — an RFC and a 2.1.0.
|
|
10
|
+
|
|
11
|
+
Both defects encoded a condition the obligation does not have: one a flag the requirement never mentions, one a deadline it never sets.
|
|
12
|
+
|
|
13
|
+
## [2.0.2] — 2026-09-06 — 2.0.1's own fix could not run: the derived wait exceeded the harness timeout that governs it
|
|
14
|
+
|
|
15
|
+
A regression in 2.0.1, measured by the host that reported the defect 2.0.1 fixed. On a 2.0.1 re-cut one row moved, the wrong way: `0173.webhook-durable-delivery.dead-letter` went `executed-pass` → `executed-fail`.
|
|
16
|
+
|
|
17
|
+
2.0.1 widened the retry wait to a 90 s cap and left both `it()` blocks on the harness default (`vitest.config.ts` `testTimeout: 30_000`, no per-test override). **A wait longer than the timeout that governs it can never elapse** — the test dies at 30 s with "Test timed out in 30000ms". The dead-letter leg is worse than one wait: `waitTerminal` plus **two** sequential `retryWaitMs` waits, up to 191 s inside a 30 s budget. And it took a passing row with it — `dead-letter` passed at rc.67 by observing `attempts.length > 1` inside the old 20 s window, and 2.0.1 moved that leg onto `retryWaitMs` too.
|
|
18
|
+
|
|
19
|
+
Both `it()` blocks now take a timeout **derived from the wait constant** — `RETRY_WAIT_CAP_MS + WAIT_SLACK_MS`, and twice the cap for the two-wait leg — rather than a second literal, so a later change to the wait carries its own budget.
|
|
20
|
+
|
|
21
|
+
The shape is the defect 2.0.1 fixed, displaced one layer: 2.0.1 stopped the scenario blaming a host for a deadline *the scenario* chose, then let *the harness* choose a shorter one silently, on exactly the durable hosts the widening was written to help.
|
|
22
|
+
|
|
3
23
|
## [2.0.1] — 2026-09-05 — the webhook durability scenario read the wrong carrier and imposed its own deadline
|
|
4
24
|
|
|
5
25
|
Two defects in `v2-webhook-durable-delivery`, both reported by a host running against 2.0.0.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.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.0.
|
|
59
|
+
"@openwop/spec-artifacts": "2.0.3"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -26623,7 +26623,7 @@
|
|
|
26623
26623
|
"explicitId": "openwop.requirement.0173.effect-seam-no-refire",
|
|
26624
26624
|
"citations": [
|
|
26625
26625
|
{
|
|
26626
|
-
"section": "spec/v2/core/replay.md §
|
|
26626
|
+
"section": "spec/v2/core/replay.md §Suppression",
|
|
26627
26627
|
"requirement": null,
|
|
26628
26628
|
"interpolated": true
|
|
26629
26629
|
}
|
|
@@ -29006,7 +29006,7 @@
|
|
|
29006
29006
|
{
|
|
29007
29007
|
"id": "openwop.it.v2-webhook-durable-delivery.a-failed-attempt-is-retried-and-the-event-is-delivered-at-least-once",
|
|
29008
29008
|
"file": "v2-webhook-durable-delivery.test.ts",
|
|
29009
|
-
"line":
|
|
29009
|
+
"line": 232,
|
|
29010
29010
|
"title": "a failed attempt is retried and the event is delivered at least once",
|
|
29011
29011
|
"explicitId": "openwop.requirement.0173.webhook-durable-delivery",
|
|
29012
29012
|
"citations": [
|
|
@@ -29023,10 +29023,6 @@
|
|
|
29023
29023
|
"requirement": null,
|
|
29024
29024
|
"interpolated": true
|
|
29025
29025
|
},
|
|
29026
|
-
{
|
|
29027
|
-
"section": "webhooks.md §Durability",
|
|
29028
|
-
"requirement": "at-least-once: after the failing attempts the retry MUST land (the receiver answered 204 to the third attempt for the key)"
|
|
29029
|
-
},
|
|
29030
29026
|
{
|
|
29031
29027
|
"section": "webhooks.md §Durability",
|
|
29032
29028
|
"requirement": null,
|
|
@@ -29041,7 +29037,7 @@
|
|
|
29041
29037
|
{
|
|
29042
29038
|
"id": "openwop.it.v2-webhook-durable-delivery.an-exhausted-delivery-is-dead-lettered-never-dropped",
|
|
29043
29039
|
"file": "v2-webhook-durable-delivery.test.ts",
|
|
29044
|
-
"line":
|
|
29040
|
+
"line": 313,
|
|
29045
29041
|
"title": "an exhausted delivery is dead-lettered, never dropped",
|
|
29046
29042
|
"explicitId": "openwop.requirement.0173.webhook-durable-delivery.dead-letter",
|
|
29047
29043
|
"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.0.
|
|
5
|
-
"corpusTag": "v2.0.
|
|
4
|
+
"version": "2.0.3",
|
|
5
|
+
"corpusTag": "v2.0.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": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
|
|
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": "371368d382f64c6025b3c328f1c0d8ebbc5f3a3f974b3bf889e281a891af9186",
|
|
13
|
+
"api/v2/asyncapi.yaml": "3ab2d59c0a9b64bfeb4b1e8eebdb332de83d324ac66bc36a98fd59d7838e574a",
|
|
14
|
+
"api/v2/openapi.yaml": "6213c7997bc9700077e21096f555688e0be5061a24436bbe972c4d42afb9419f",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
200
200
|
"spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
|
|
201
201
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
202
|
-
"spec/v1/core-standard-manifest.json": "
|
|
202
|
+
"spec/v1/core-standard-manifest.json": "0f6e3852415ea4a3dbc9359d78f94bffa28d71a7d21da9271ebe50475b3133e0",
|
|
203
203
|
"spec/v1/deprecations.json": "1d5acb69a9b8ccb57275a95605f74aef1d920685f8407c9d382a46b59dc803bb",
|
|
204
204
|
"spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
|
|
205
205
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
@@ -230,8 +230,8 @@
|
|
|
230
230
|
"spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
|
|
231
231
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
232
232
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
233
|
-
"spec/v2/release.json": "
|
|
233
|
+
"spec/v2/release.json": "02478dd3e9e5ba5fc54e1e14a4e2ee0f8d539a02c99bfa62cd9f4cd78cee7d0d",
|
|
234
234
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
235
235
|
},
|
|
236
|
-
"corpusCommit": "
|
|
236
|
+
"corpusCommit": "c600da7988e0efab5d3122c33435988dbfb11c4a"
|
|
237
237
|
}
|
|
@@ -56,8 +56,27 @@ describe('RFC 0173 §C.2 — effect-seam-no-refire (gated on replay, seam-driven
|
|
|
56
56
|
const manifest = await driver.get(MANIFEST_PATH);
|
|
57
57
|
const seamRows: SeamRow[] = manifest.status === 200 && manifest.json && typeof manifest.json === 'object'
|
|
58
58
|
? ((manifest.json as Manifest).seams ?? []) : [];
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// Select ANY guarded row (suite 2.0.3). Until 2.0.2 this read
|
|
60
|
+
// `r.guarded === true && r.branchReFires === false` — it selected on a
|
|
61
|
+
// BRANCH permission to witness a REPLAY obligation, which `replay.md:78`
|
|
62
|
+
// forbids in one sentence: "A host MAY suppress branch effects and MUST NOT
|
|
63
|
+
// report that as replay suppression." §Suppression rule 1 is unconditional
|
|
64
|
+
// ("a node that performs an external side effect … MUST NOT perform it")
|
|
65
|
+
// and does not vary with `branchReFires`, whose own schema description is
|
|
66
|
+
// "RFC 0140 G6 — a `branch` fork re-fires this seam by design".
|
|
67
|
+
//
|
|
68
|
+
// Two defects in one line. `branchReFires` is also OPTIONAL (`required:
|
|
69
|
+
// ["seam","kind","guarded","guardedBy"]`), so `=== false` additionally
|
|
70
|
+
// excluded every row that is merely silent on the permission.
|
|
71
|
+
//
|
|
72
|
+
// Found by the reference host, which had ten honest `branchReFires: true`
|
|
73
|
+
// rows — every seam it owns does re-fire on a branch, by design — and was
|
|
74
|
+
// about to build `fireEffectSeam` to satisfy a scenario that would have
|
|
75
|
+
// gone on recording `inapplicable` after the work landed. It asked instead
|
|
76
|
+
// of inventing a `false` row, which would have made this pass by lying
|
|
77
|
+
// about the seam.
|
|
78
|
+
const target = seamRows.find((r) => r.guarded === true);
|
|
79
|
+
if (!target) return softSkip('inapplicable', `no manifest row is guarded — nothing to witness suppression on (${seamRows.length} row(s) at ${MANIFEST_PATH})`);
|
|
61
80
|
const fired = await driver.post(`${SEAMS_PREFIX}/sample/effect-seams/fire`, { seam: String(target.seam) });
|
|
62
81
|
if (fired.status === 404 || fired.status === 403 || fired.status === 405) {
|
|
63
82
|
return softSkip('blocked', `the host advertises the seams profile but does not serve ${SEAMS_PREFIX}/sample/effect-seams/fire (answered ${fired.status}) — the no-re-fire leg cannot be driven`);
|
|
@@ -98,7 +117,7 @@ describe('RFC 0173 §C.2 — effect-seam-no-refire (gated on replay, seam-driven
|
|
|
98
117
|
if (forkEffects.status !== 200) return softSkip('blocked', `GET /runs/{runId}/effects answered ${forkEffects.status} on the replay fork`);
|
|
99
118
|
expect(
|
|
100
119
|
countOf(forkEffects),
|
|
101
|
-
req('openwop.requirement.0173.effect-seam-no-refire', 'spec/v2/core/replay.md §
|
|
120
|
+
req('openwop.requirement.0173.effect-seam-no-refire', 'spec/v2/core/replay.md §Suppression', `seam ${String(target.seam)} is guarded, so a replay fork MUST NOT issue a further attempt through it — suppression is unconditional for mode: replay (§Suppression rule 1) and does not depend on branchReFires, which states only what a BRANCH may re-fire (§Branch: a host "MUST NOT report that as replay suppression"). The fork's effect ledger (${countOf(forkEffects)}) cannot exceed the parent's (${countOf(parentEffects)})`),
|
|
102
121
|
).toBeLessThanOrEqual(countOf(parentEffects));
|
|
103
122
|
});
|
|
104
123
|
});
|
|
@@ -175,6 +175,42 @@ function retryWaitMs(doc: Record<string, unknown>): number {
|
|
|
175
175
|
return backoff === 'exponential' || backoff === 'fixed' ? RETRY_WAIT_CAP_MS : RETRY_WAIT_FLOOR_MS;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The per-test budget, DERIVED from the wait above (suite 2.0.2).
|
|
180
|
+
*
|
|
181
|
+
* 2.0.1 raised the derived wait to 90 s and left the `it()` blocks on the
|
|
182
|
+
* harness default (`vitest.config.ts` `testTimeout: 30_000`). A wait longer
|
|
183
|
+
* than the timeout that governs it can never elapse: on exactly the durable
|
|
184
|
+
* hosts the widening was written to help, the test died at 30 s with "Test
|
|
185
|
+
* timed out in 30000ms" — and took `dead-letter` with it, which had passed at
|
|
186
|
+
* the old 20 s window. Measured by a host on 2.0.1 (`00337-dgw`): one row moved
|
|
187
|
+
* `executed-pass -> executed-fail` and it was this one.
|
|
188
|
+
*
|
|
189
|
+
* The shape is the defect 2.0.1 itself fixed, one layer out: 2.0.1 stopped the
|
|
190
|
+
* scenario blaming a host for a deadline the SCENARIO chose, and then let the
|
|
191
|
+
* HARNESS choose a shorter one silently. So the budget is computed from
|
|
192
|
+
* `RETRY_WAIT_CAP_MS` rather than written as a second literal — a later change
|
|
193
|
+
* to the wait carries its own timeout, the way the advert is sourced from the
|
|
194
|
+
* constant the delivery loop reads. `WAIT_SLACK_MS` covers `waitTerminal`,
|
|
195
|
+
* registration and the HTTP round trips around the waits.
|
|
196
|
+
*/
|
|
197
|
+
/**
|
|
198
|
+
* How many attempts the receiver refuses before answering 204 (suite 2.0.3).
|
|
199
|
+
*
|
|
200
|
+
* The retry leg's success witness lands on attempt `FAIL_FIRST + 1`, so this
|
|
201
|
+
* number decides how much backoff the scenario has to sit through. It is a
|
|
202
|
+
* named constant rather than a literal at the `startReceiver` call because the
|
|
203
|
+
* blocked-reason quotes it: the message a host reads must be computed from the
|
|
204
|
+
* receiver the suite actually started, not from a number typed twice.
|
|
205
|
+
*/
|
|
206
|
+
const FAIL_FIRST = 2;
|
|
207
|
+
|
|
208
|
+
const WAIT_SLACK_MS = 30_000;
|
|
209
|
+
/** One `retryWaitMs` wait (the retry leg). */
|
|
210
|
+
const RETRY_TEST_TIMEOUT_MS = RETRY_WAIT_CAP_MS + WAIT_SLACK_MS;
|
|
211
|
+
/** Two sequential `retryWaitMs` waits (the dead-letter leg: observe a retry, then exhaust). */
|
|
212
|
+
const DEAD_LETTER_TEST_TIMEOUT_MS = RETRY_WAIT_CAP_MS * 2 + WAIT_SLACK_MS;
|
|
213
|
+
|
|
178
214
|
/** Register the suite receiver; null (with a note) when the host's SSRF guard refuses a loopback URL. */
|
|
179
215
|
async function register(url: string): Promise<{ webhookId: string } | null> {
|
|
180
216
|
const registration = resolveRegistrationUrl(url);
|
|
@@ -199,7 +235,7 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
|
|
|
199
235
|
if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised (gate recorded under openwop.family.webhooks)');
|
|
200
236
|
if (!fixtureAdvertised(doc, FIXTURE)) return softSkip('inapplicable', `${FIXTURE} fixture not advertised — no run to deliver`);
|
|
201
237
|
|
|
202
|
-
const receiver = await startReceiver(
|
|
238
|
+
const receiver = await startReceiver(FAIL_FIRST); // 500, 500, then 204
|
|
203
239
|
active = receiver.server;
|
|
204
240
|
const sub = await register(receiver.url);
|
|
205
241
|
if (sub === null) return softSkip('blocked', 'registration refused (reason recorded above)');
|
|
@@ -221,10 +257,37 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
|
|
|
221
257
|
attempts.length,
|
|
222
258
|
req('openwop.requirement.0173.webhook-durable-delivery', 'webhooks.md §Durability', `a 500 from the subscriber MUST be retried — ${failedThenSucceeded} failed attempt(s) were answered and the host made ${attempts.length} attempt(s) in total; one attempt is best-effort delivery, which is not a conforming mode (RFC 0173 §B)`),
|
|
223
259
|
).toBeGreaterThan(1);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
260
|
+
// At-least-once: the retry MUST eventually land. When it has not landed
|
|
261
|
+
// inside our window this records `blocked`, NOT `executed-fail` — suite
|
|
262
|
+
// 2.0.3, and this is the third time this file has had to learn it.
|
|
263
|
+
//
|
|
264
|
+
// The receiver answers 204 only on attempt `FAIL_FIRST + 1`, so reaching it
|
|
265
|
+
// costs the SUM of the first FAIL_FIRST backoff intervals, not the largest
|
|
266
|
+
// one. On an exponential-from-30s policy that is 30 + 60 = 90 s, which is
|
|
267
|
+
// exactly RETRY_WAIT_CAP_MS — a host loses by the width of one delivery.
|
|
268
|
+
// The obvious fix is to derive the wait from the intervals, and it cannot
|
|
269
|
+
// be built: `spec/v2/facets/webhooks.schema.json` `retryPolicy` is
|
|
270
|
+
// `additionalProperties: false` over exactly { maxAttempts, backoff }.
|
|
271
|
+
// THE BASE INTERVAL IS NOT ON THE WIRE, so the suite cannot compute the
|
|
272
|
+
// time to the Nth attempt, and any cap I pick is 2.0.1's 20-second
|
|
273
|
+
// deadline again with a bigger literal.
|
|
274
|
+
//
|
|
275
|
+
// So the honest disposition is `blocked`: the host took the obligation on
|
|
276
|
+
// (it advertises webhooks and we observed it retry) and the suite could not
|
|
277
|
+
// measure the outcome (RFC 0148 §A). Not `inapplicable` — that would claim
|
|
278
|
+
// it never took the obligation on.
|
|
279
|
+
//
|
|
280
|
+
// THE COST, stated rather than hidden: a host that retries forever and
|
|
281
|
+
// never succeeds now also records `blocked` instead of failing. This trades
|
|
282
|
+
// a false conviction for a missed detection. Detection comes back by
|
|
283
|
+
// putting the interval on the wire — an additive `retryPolicy` field so the
|
|
284
|
+
// sum is derivable — which is normative surface, an RFC and a 2.1.0, not a
|
|
285
|
+
// patch. Recorded here so the trade is visible at the assertion rather than
|
|
286
|
+
// only in a changelog.
|
|
287
|
+
if (!retried) {
|
|
288
|
+
const policyNote = advertisedRetryPolicy(doc);
|
|
289
|
+
return softSkip('blocked', `the retry was observed (${attempts.length} attempts) but the receiver's 204 did not land inside the ${retryWaitMs(doc)}ms window: it answers 204 only on attempt ${FAIL_FIRST + 1}, which costs the SUM of the first ${FAIL_FIRST} backoff intervals, and webhooks.retryPolicy carries only { maxAttempts, backoff${policyNote ? `: ${String(policyNote.backoff)}` : ''} } — the base interval is not advertised, so the suite cannot derive how long to wait. Unmeasured, not unmet (RFC 0148 §A).`);
|
|
290
|
+
}
|
|
228
291
|
// Backoff: the retry MUST NOT be a tight loop — consecutive attempts for one
|
|
229
292
|
// key are spaced. Only asserted when the host advertises a non-`none` backoff.
|
|
230
293
|
const policy = advertisedRetryPolicy(doc);
|
|
@@ -245,7 +308,7 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
|
|
|
245
308
|
|
|
246
309
|
const del = await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
|
|
247
310
|
expect(del.status, req('openwop.requirement.0173.webhook-durable-delivery', 'webhooks.md §Surfaces', 'DELETE /webhooks/{webhookId} MUST answer 204')).toBe(204);
|
|
248
|
-
});
|
|
311
|
+
}, RETRY_TEST_TIMEOUT_MS);
|
|
249
312
|
|
|
250
313
|
it('an exhausted delivery is dead-lettered, never dropped', async () => {
|
|
251
314
|
const doc = await discovery();
|
|
@@ -285,5 +348,5 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
|
|
|
285
348
|
// (no GET /webhooks/{webhookId}/dead-letters). Without a normative read the
|
|
286
349
|
// routing to the sink is not observable from the suite.
|
|
287
350
|
return softSkip('blocked', 'no normative dead-letter read surface for webhook deliveries in api/v2/openapi.yaml (a GET /webhooks/{webhookId}/dead-letters projection is needed) — exhaustion was observed, routing to the sink was not');
|
|
288
|
-
});
|
|
351
|
+
}, DEAD_LETTER_TEST_TIMEOUT_MS);
|
|
289
352
|
});
|