@openwop/openwop-conformance 2.42.2 → 2.42.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.
@@ -221,9 +221,14 @@ describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
221
221
  const res = await http(() => driver.get(`/runs/${projectBoundId(runId)}/effects`));
222
222
  if (res === null) return softSkip('blocked', 'GET /runs/{runId}/effects unreachable (fetch failed)');
223
223
  if (res.status === 404 || res.status === 405) return softSkip('inapplicable', `the host advertises idempotency but serves no effect ledger at /runs/{runId}/effects (${res.status})`);
224
- expect(res.status, req(PER_KIND, 'security-defaults.md §Layer-2 effect identity', 'GET /runs/{runId}/effects MUST answer 200')).toBe(200);
225
224
  const effects = ((res.json as { effects?: Array<Record<string, unknown>> } | null)?.effects ?? []);
226
- if (effects.length === 0) return softSkip('inapplicable', 'the noop fixture records no effects — no effectId on the wire to read (a host with an effect-producing fixture witnesses this leg)');
225
+ // Unfailable-leg audit (2026-09-26): the status assert used to precede this
226
+ // empty check, so on every host (the noop fixture records no effects) the
227
+ // leg asserted 200 then soft-skipped — `executed-pass` partial-witness folded
228
+ // into the shared per-kind row with no effectId ever read. The empty 200 now
229
+ // returns before any assertion.
230
+ if (res.status === 200 && effects.length === 0) return softSkip('inapplicable', 'the noop fixture records no effects — no effectId on the wire to read (a host with an effect-producing fixture witnesses this leg)');
231
+ expect(res.status, req(PER_KIND, 'security-defaults.md §Layer-2 effect identity', 'GET /runs/{runId}/effects MUST answer 200')).toBe(200);
227
232
  for (const e of effects) expectBound(e['effectId'], 'effectId', 'GET /runs/{runId}/effects', PER_KIND);
228
233
  });
229
234
 
@@ -244,9 +249,13 @@ describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
244
249
  try {
245
250
  const res = await http(() => driver.get(`/webhooks/${projectBoundId(webhookId)}/dead-letters`));
246
251
  if (res === null) return softSkip('blocked', 'GET /webhooks/{webhookId}/dead-letters unreachable (fetch failed)');
247
- expect(res.status, req(PER_KIND, 'RFC 0188 §A.1', 'a host advertising webhooks.deadLetter MUST serve the dead-letter read (200)')).toBe(200);
248
252
  const rows = ((res.json as { deliveries?: Array<Record<string, unknown>> } | null)?.deliveries ?? []);
249
- if (rows.length === 0) return softSkip('inapplicable', 'the subscription has no dead-lettered delivery in this run — the read is served and the shape is unwitnessed here; v2-webhook-durable-delivery drives an exhaustion and asserts the record');
253
+ // Unfailable-leg audit (2026-09-26): as with effectId above, the 200 assert
254
+ // preceded the empty check, so a fresh subscription (always empty) recorded
255
+ // `executed-pass` partial-witness with no deliveryId read. Empty-200 now
256
+ // returns before any assertion.
257
+ if (res.status === 200 && rows.length === 0) return softSkip('inapplicable', 'the subscription has no dead-lettered delivery in this run — the read is served and the shape is unwitnessed here; v2-webhook-durable-delivery drives an exhaustion and asserts the record');
258
+ expect(res.status, req(PER_KIND, 'RFC 0188 §A.1', 'a host advertising webhooks.deadLetter MUST serve the dead-letter read (200)')).toBe(200);
250
259
  for (const r of rows) expectBound(r['deliveryId'], 'deliveryId', 'GET /webhooks/{webhookId}/dead-letters', PER_KIND);
251
260
  } finally {
252
261
  await http(() => driver.delete(`/webhooks/${projectBoundId(webhookId)}`));
@@ -53,24 +53,36 @@ describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)',
53
53
  const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
54
54
  if (!fixtures.includes(FIXTURE)) return softSkip('inapplicable', `${FIXTURE} fixture not advertised — no run to read`);
55
55
 
56
+ // Unfailable-leg audit (2026-09-26): the noop fixture issues no effects, so
57
+ // the per-row loop asserted nothing on any host, and the trailing
58
+ // `softSkip('inapplicable')` (called without `return`, after four passing
59
+ // setup asserts) recorded `executed-pass` with a `partial-witness:` detail —
60
+ // a host that mis-keyed every effect passed this row. Now no expect passes
61
+ // before the empty-ledger check: a failing setup read still fails, and an
62
+ // empty but well-formed ledger returns `inapplicable` with zero assertions.
56
63
  const create = await driver.post('/runs', { workflowId: FIXTURE });
57
- expect(create.status, req('openwop.requirement.0173.effect-identity-business-key', 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
64
+ if (create.status !== 201) {
65
+ expect(create.status, req('openwop.requirement.0173.effect-identity-business-key', 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
66
+ }
58
67
  const runId = (create.json as { runId: string }).runId;
59
68
  await waitTerminal(runId, 10_000);
60
69
 
61
70
  const res = await driver.get(`/runs/${encodeURIComponent(runId)}/effects`);
71
+ const check = v2Validator('effect-ledger-projection')(res.json);
72
+ const body = res.json as { runId?: unknown; effects?: Array<{ effectId?: unknown; keying?: unknown; providerKey?: unknown }> } | null;
73
+ const effects = body?.effects ?? [];
74
+ if (res.status === 200 && check.ok && body?.runId === runId && effects.length === 0) {
75
+ return softSkip('inapplicable', 'the noop fixture issued no external effect — the ledger read is well-formed but the per-row keying leg had no rows (an effect-issuing fixture would exercise it)');
76
+ }
62
77
  expect(
63
78
  res.status,
64
79
  req('openwop.requirement.0173.effect-identity-business-key', 'security-defaults.md §Layer-2 effect identity', 'a host advertising `idempotency` MUST serve GET /runs/{runId}/effects with 200 (RFC 0173 §B)'),
65
80
  ).toBe(200);
66
- const check = v2Validator('effect-ledger-projection')(res.json);
67
81
  expect(
68
82
  check.ok,
69
83
  req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json', `the ledger projection MUST validate: ${check.errors}`),
70
84
  ).toBe(true);
71
- const body = res.json as { runId?: unknown; effects?: Array<{ effectId?: unknown; keying?: unknown; providerKey?: unknown }> };
72
- expect(body.runId, req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json runId', 'runId MUST echo the run read')).toBe(runId);
73
- const effects = body.effects ?? [];
85
+ expect(body?.runId, req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json runId', 'runId MUST echo the run read')).toBe(runId);
74
86
  const ids = new Set<string>();
75
87
  for (const e of effects) {
76
88
  expect(
@@ -90,7 +102,6 @@ describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)',
90
102
  ).toBe(false);
91
103
  }
92
104
  }
93
- if (effects.length === 0) softSkip('inapplicable', 'the noop fixture issued no external effect — the per-row keying leg had no rows (an effect-issuing fixture would exercise it)');
94
105
  });
95
106
 
96
107
  it('the same provider key is presented across two transport retries', async () => {
@@ -117,7 +128,20 @@ describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)',
117
128
  if (attempts.length < 2) {
118
129
  return softSkip('blocked', `the seam produced ${attempts.length} ledger row(s) for effect ${String(body.effectId)} — a cross-retry assertion needs at least two attempts`);
119
130
  }
120
- const keys = new Set(attempts.map((e) => String(e['providerKey'] ?? '')));
131
+ // Unfailable-leg audit (2026-09-26): `providerKey` is optional in the ledger
132
+ // schema and `?? ''` folded a missing key to '', so a host that recorded no
133
+ // provider key on any attempt showed "one distinct key" and passed. Every
134
+ // attempt now MUST carry a non-empty key. Remaining limit: the witness is
135
+ // still the host's own ledger; a suite-owned receiver counting the
136
+ // Idempotency-Key header on the wire (lib/effect-receiver.ts) is the
137
+ // stronger follow-up, not done here.
138
+ for (const a of attempts) {
139
+ expect(
140
+ typeof a['providerKey'] === 'string' && (a['providerKey'] as string).length > 0,
141
+ req('openwop.requirement.0173.effect-identity-business-key.retry', 'spec/v2/core/idempotency.md §Layer 2: effect identity', `every attempt of one effect MUST record the provider key it presented — attempt ${String(a['attempt'])} recorded ${JSON.stringify(a['providerKey'] ?? null)}`),
142
+ ).toBe(true);
143
+ }
144
+ const keys = new Set(attempts.map((e) => String(e['providerKey'])));
121
145
  expect(
122
146
  keys.size,
123
147
  req('openwop.requirement.0173.effect-identity-business-key.retry', 'spec/v2/core/idempotency.md §Layer 2: effect identity', `every attempt of one effect MUST present the same provider key across a transport retry — ${attempts.length} attempt(s) presented ${keys.size} distinct key(s)`),
@@ -95,15 +95,9 @@ describe('RFC 0173 §C.1 — effect-seam-manifest (gated on replay)', () => {
95
95
  ).toBe(true);
96
96
  }
97
97
  }
98
- // One row per kind the host declares: the set of kinds the manifest names is
99
- // the host's declaration, and each named kind is backed by at least one row.
100
- const declared = new Set(rows.map((r) => String(r.kind)));
101
- for (const kind of declared) {
102
- expect(
103
- rows.filter((r) => r.kind === kind).length,
104
- req('openwop.requirement.0173.effect-seam-manifest', 'RFC 0173 §C.1', `at least one manifest row per declared kind (${kind})`),
105
- ).toBeGreaterThanOrEqual(1);
106
- }
98
+ // Unfailable-leg audit (2026-09-26): a "one row per declared kind" loop stood
99
+ // here, but it built the declared set FROM the rows, so every host passed it.
100
+ // The manifest has no kind declaration independent of its rows; removed.
107
101
  // RFC 0140 G6: a `branch` re-fire is stated as a permission, and a permission
108
102
  // for a mode the host does not offer is a claim about nothing.
109
103
  const modes = Array.isArray(replay['modes']) ? (replay['modes'] as unknown[]) : [];
@@ -28,7 +28,7 @@
28
28
  import { describe, it, expect } from 'vitest';
29
29
  import { driver, type OpenWOPResponse } from '../lib/driver.js';
30
30
  import { v2Discovery } from '../lib/v2.js';
31
- import { softSkip } from '../lib/soft-skip.js';
31
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
32
32
  import { req } from '../lib/requirement-ids.js';
33
33
  import { readErrorCode } from '../lib/error-envelope.js';
34
34
  import { codemapV1toV2, era2Gate, eventsOf, pollEvents, seedEra2Log, v1FixtureLog, type ReadEvent } from '../lib/era2-seed.js';
@@ -76,12 +76,31 @@ describe('v2-era-2-append-vocabulary (RFC 0176 §A — the writer rule)', () =>
76
76
  return softSkip('blocked', `seedEra2Log reported success but the log reads back empty (0 events) — the seam's return value is not evidence that a log exists, and without a seeded era-2 log the writer rule is unwitnessed here`);
77
77
  }
78
78
 
79
+ // unfailable-leg audit, 2026-09-26: cancel was the ONLY write driven, and
80
+ // it appends run.cancelled (and node.cancelled) — names the codemap spells
81
+ // identically in v1 and v2. A host writing v2 names into an era-2 log
82
+ // therefore produced the same rows as one writing v1 names, and the leg
83
+ // recorded executed-pass either way. The discriminator needs a type the
84
+ // codemap RENAMES. Best effort first: pause then resume the seeded run
85
+ // through the core :pause / :resume operations (runs.md §Pause and
86
+ // resume), which gives a host that records the resume transition a chance
87
+ // to append `run.resuming` (v1) — read back as `run.resume-started` (v2).
88
+ // Neither answer is asserted: the resume transition's event set is not
89
+ // pinned to a renamed type, and a seeded run's pause is host latitude. If
90
+ // no renamed type is appended, the leg records `blocked` below, not a pass.
91
+ const enc = encodeURIComponent(runId);
92
+ const paused = await http(() => driver.post(`/runs/${enc}:pause`, { reason: 'conformance', drainPolicy: 'immediate' }));
93
+ const resumed = paused?.status === 202 ? await http(() => driver.post(`/runs/${enc}:resume`, { reason: 'conformance' })) : null;
94
+
79
95
  // One canonical mutation so the HOST's own writer appends. Cancel is the
80
96
  // universally available terminal transition; a host that refuses it on a
81
- // seeded run records `blocked` rather than a pass.
82
- const cancelled = await http(() => driver.post(`/runs/${encodeURIComponent(runId)}/cancel`, {}));
83
- if (cancelled === null || (cancelled.status !== 200 && cancelled.status !== 202 && cancelled.status !== 204)) {
84
- return softSkip('blocked', `POST /runs/{runId}/cancel answered ${cancelled?.status ?? 'no response'} on a seeded era-2 run — no canonical mutation drove the host's writer, so the append is unwitnessed`);
97
+ // seeded run records `blocked` rather than a pass — unless the pause and
98
+ // resume above were both accepted, which already drove the host's writer.
99
+ const cancelled = await http(() => driver.post(`/runs/${enc}/cancel`, {}));
100
+ const cancelOk = cancelled !== null && (cancelled.status === 200 || cancelled.status === 202 || cancelled.status === 204);
101
+ const pauseResumeOk = paused?.status === 202 && resumed?.status === 202;
102
+ if (!cancelOk && !pauseResumeOk) {
103
+ return softSkip('blocked', `POST /runs/{runId}/cancel answered ${cancelled?.status ?? 'no response'} on a seeded era-2 run (pause ${paused?.status ?? 'no response'}, resume ${resumed?.status ?? 'not attempted'}) — no canonical mutation drove the host's writer, so the append is unwitnessed`);
85
104
  }
86
105
 
87
106
  const after = await pollEvents(runId);
@@ -128,6 +147,19 @@ describe('v2-era-2-append-vocabulary (RFC 0176 §A — the writer rule)', () =>
128
147
  req(ID, DOC, `the appended rows continue the seeded sequence space without a gap or restart (expected ${i}, got ${String(seqs[i])})`),
129
148
  ).toBe(i);
130
149
  }
150
+
151
+ // unfailable-leg audit, 2026-09-26: the vocabulary is only observable
152
+ // through a type the codemap renames. An appended row that reads back under
153
+ // a RENAMED v2 name (v2 ≠ v1) through a successful translated read is the
154
+ // positive witness: the stored spelling was the v1 one the reader maps
155
+ // from. When every appended type is spelled the same in both vocabularies,
156
+ // the rows above hold for a v1-writing and a v2-writing host alike, so the
157
+ // writer rule is unobserved — `blocked`, not a pass.
158
+ const renamedV2 = new Set([...codemapV1toV2()].filter(([v1, v2]) => v1 !== v2).map(([, v2]) => v2));
159
+ const appendedTypes = rows.slice(seedCount).map((r) => String(r.type ?? ''));
160
+ if (!appendedTypes.some((t) => renamedV2.has(t))) {
161
+ return blockedDespiteAssertions(`every type the host appended to the era-2 log (${appendedTypes.join(', ') || 'none'}) is spelled identically in v1 and v2 (spec/v2/event-codemap.json), so a host writing v2 names reads back the same as one writing v1 names — the writer rule is unobserved. The leg needs a canonical mutation that appends a renamed type (e.g. run.resuming → run.resume-started); pause ${paused?.status ?? 'no response'} / resume ${resumed?.status ?? 'not attempted'} on the seeded run appended none, and the seed seam (api/seams-v2.yaml seedEra2EventLog) cannot seed a paused or interrupt-suspended era-2 run to resume.`);
162
+ }
131
163
  });
132
164
 
133
165
  it('the run keeps the era it was created with; an append does not promote it to era 3', async () => {
@@ -139,16 +171,27 @@ describe('v2-era-2-append-vocabulary (RFC 0176 §A — the writer rule)', () =>
139
171
  if (!seeded.ok) return softSkip(seeded.kind, seeded.reason);
140
172
  const runId = seeded.runId;
141
173
 
142
- await http(() => driver.post(`/runs/${encodeURIComponent(runId)}/cancel`, {}));
174
+ // unfailable-leg audit, 2026-09-26: the cancel's answer was ignored, so a
175
+ // host that refused it (no append at all) still had its unappended run's
176
+ // era read as the witness of "an append does not promote".
177
+ const cancelled = await http(() => driver.post(`/runs/${encodeURIComponent(runId)}/cancel`, {}));
178
+ if (cancelled === null || (cancelled.status !== 200 && cancelled.status !== 202 && cancelled.status !== 204)) {
179
+ return softSkip('blocked', `POST /runs/{runId}/cancel answered ${cancelled?.status ?? 'no response'} on a seeded era-2 run — nothing was appended, so whether an append promotes the era is unwitnessed`);
180
+ }
143
181
 
144
182
  const snap = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}`));
145
183
  if (snap === null || snap.status !== 200) {
146
184
  return softSkip('blocked', `GET /runs/{runId} answered ${snap?.status ?? 'no response'} — the era key cannot be read back`);
147
185
  }
148
186
  const era = (snap.json as { eventLogSchemaVersion?: unknown } | null)?.eventLogSchemaVersion;
187
+ // unfailable-leg audit, 2026-09-26: `era === undefined` was accepted, so a
188
+ // host that dropped the era key from the snapshot passed. runs.md §getRun
189
+ // and persistence.md make eventLogSchemaVersion REQUIRED on every v2 run
190
+ // snapshot (schemas/v2/run-snapshot.schema.json `required`); absent ⇒ 2 is
191
+ // the STORAGE rule, not a wire latitude.
149
192
  expect(
150
- era === undefined || era === 2,
151
- req(ID, DOC, `the era key is fixed at run creation: a run seeded in era 2 and then appended to MUST still read as era 2 (got ${String(era)}) — promoting it to 3 would strand the seeded rows in a vocabulary the reader no longer translates`),
152
- ).toBe(true);
193
+ era,
194
+ req(ID, DOC, `the era key is fixed at run creation: a run seeded in era 2 and then appended to MUST still read as era 2 (got ${String(era)}) — promoting it to 3 would strand the seeded rows in a vocabulary the reader no longer translates, and the snapshot MUST carry eventLogSchemaVersion (runs.md §getRun)`),
195
+ ).toBe(2);
153
196
  });
154
197
  });
@@ -327,6 +327,29 @@ describe('RFC 0207 — the receiver rule (host as MCP server / A2A server)', ()
327
327
  try {
328
328
  expect(r.error, req(A2A_MALFORMED, DOC, `a malformed traceparent MUST be ignored, never a request failure — SendMessage answered error ${JSON.stringify(r.error)}`)).toBeUndefined();
329
329
  expect(r.result, req(A2A_MALFORMED, DOC, 'SendMessage MUST answer a normal result')).toBeDefined();
330
+ // unfailable-leg audit, 2026-09-26: only the JSON-RPC envelope was held,
331
+ // so a host that accepted SendMessage but failed (or rejected) the task
332
+ // because it could not parse the traceparent passed. "Ignored" means the
333
+ // task proceeds as if no traceparent were sent — the A2A sibling of the
334
+ // MCP leg's isError check. Read the returned Task (result.task or a bare
335
+ // Task) and, when it names an id, GetTask once more after a beat so a
336
+ // failure recorded just after the response is seen too. A Message result
337
+ // carries no task state and is not a failure.
338
+ const FAILED = new Set(['TASK_STATE_FAILED', 'TASK_STATE_REJECTED', 'failed', 'rejected']);
339
+ const stateOf = (res: Record<string, unknown> | undefined): string | undefined => {
340
+ const task = (res?.['task'] ?? res) as { status?: { state?: unknown } } | undefined;
341
+ const st = task?.status?.state;
342
+ return typeof st === 'string' ? st : undefined;
343
+ };
344
+ const first = stateOf(r.result);
345
+ expect(first !== undefined && FAILED.has(first), req(A2A_MALFORMED, DOC, `a malformed traceparent MUST be ignored — the task SendMessage returned is ${String(first)}; the malformed value failed the task instead of starting a new trace`)).toBe(false);
346
+ const id = ((r.result?.['task'] as { id?: unknown } | undefined)?.id ?? r.result?.['id']) as unknown;
347
+ if (typeof id === 'string') {
348
+ await sleep(500);
349
+ const again = await a2aCall(t.url, 'GetTask', { id }, {}).catch(() => null);
350
+ const later = again?.error === undefined ? stateOf(again?.result) : undefined;
351
+ expect(later !== undefined && FAILED.has(later), req(A2A_MALFORMED, DOC, `a malformed traceparent MUST be ignored — GetTask shows the task ${String(later)} after the SendMessage that carried it`)).toBe(false);
352
+ }
330
353
  } finally {
331
354
  await cancelTask(t.url, r);
332
355
  }
@@ -37,6 +37,7 @@ import { isFixtureAdvertised } from '../lib/fixtures.js';
37
37
  import { loadEnv } from '../lib/env.js';
38
38
  import { softSkip } from '../lib/soft-skip.js';
39
39
  import { req } from '../lib/requirement-ids.js';
40
+ import { scaledTimeoutMs } from '../lib/polling.js';
40
41
 
41
42
  export const HOST_CALLBACK_NOT_REQUIRED = 'the suite is the MCP client: every leg POSTs JSON-RPC to the mount the host advertises in mcp.serverUrls; nothing harness-hosted is handed to the host';
42
43
 
@@ -343,9 +344,21 @@ describe('RFC 0198 — v2-mcp-tasks (MCP Tasks on the server mount; disconnect c
343
344
  expect(key, req(ID_TASK_UPDATE_APPROVER_CHECKED, 'interop-map.json mcp.tasks.status', `the approvers task is input_required (got ${JSON.stringify(t.error ?? t.result)})`)).toBeDefined();
344
345
  const u = await call(m.url, 'tasks/update', { taskId, inputResponses: { [key!]: { action: 'accept', content: { action: 'accept' } } } });
345
346
  expect(u.error === undefined || u.error.code === INVALID_PARAMS, req(ID_TASK_UPDATE_APPROVER_CHECKED, 'interop-map.json mcp.tasks.methods tasks/update', `a non-approver's update is acknowledged or -32602, never an internal error (got ${JSON.stringify(u.error)})`)).toBe(true);
346
- await new Promise((ok) => setTimeout(ok, 750));
347
- expect(await runStatus(unproject(taskId)), req(ID_TASK_UPDATE_APPROVER_CHECKED, `${DOC}; interrupt.md §Approver enforcement; RFC 0198 §E.7`, 'the interrupt stays open: the run is still waiting-approval')).toBe('waiting-approval');
348
- expect((await events(unproject(taskId))).filter((e) => e.type === 'interrupt.resolved').length, req(ID_TASK_UPDATE_APPROVER_CHECKED, 'interrupt.md §Approver enforcement', 'no interrupt.resolved is recorded')).toBe(0);
347
+ // Unfailable-leg audit, 2026-09-26: the negative evidence was read once
348
+ // after a fixed 750 ms, so a host applying the non-approver's update
349
+ // asynchronously after that passed. It is now held for a scaled ~3 s: the
350
+ // run must stay waiting-approval with zero interrupt.resolved on every read.
351
+ const holdUntil = Date.now() + scaledTimeoutMs(3_000);
352
+ let status: unknown;
353
+ let resolvedCount = 0;
354
+ for (;;) {
355
+ status = await runStatus(unproject(taskId));
356
+ resolvedCount = (await events(unproject(taskId))).filter((e) => e.type === 'interrupt.resolved').length;
357
+ if (status !== 'waiting-approval' || resolvedCount !== 0 || Date.now() >= holdUntil) break;
358
+ await new Promise((ok) => setTimeout(ok, 250));
359
+ }
360
+ expect(status, req(ID_TASK_UPDATE_APPROVER_CHECKED, `${DOC}; interrupt.md §Approver enforcement; RFC 0198 §E.7`, 'the interrupt stays open: the run is still waiting-approval')).toBe('waiting-approval');
361
+ expect(resolvedCount, req(ID_TASK_UPDATE_APPROVER_CHECKED, 'interrupt.md §Approver enforcement', 'no interrupt.resolved is recorded')).toBe(0);
349
362
  await call(m.url, 'tasks/cancel', { taskId });
350
363
  });
351
364
 
@@ -97,5 +97,16 @@ describe('RFC 0175 §E.1 — mrtr-rounds-ceiling (gated on mcp + mrtr)', () => {
97
97
  served.length,
98
98
  req('openwop.requirement.0175.mrtr-rounds-ceiling.refused', 'interop.md §The MCP round ceiling', `the host MUST NOT issue a retry beyond the ceiling — the server saw ${served.length} tools/call for a ceiling of ${String(maxRounds)}`),
99
99
  ).toBeLessThanOrEqual((maxRounds as number) + 1);
100
+ // unfailable-leg audit, 2026-09-26: only the upper bound was enforced, so a
101
+ // host that answered 422 mcp_mrtr_rounds_exceeded at round 1 (or refused
102
+ // every MRTR tool result outright) passed. Under the fake's counting, call
103
+ // k (k = 1..maxRounds+1) answers the k-th input_required; a host honouring
104
+ // the ceiling answers the first maxRounds of them (maxRounds retries) and
105
+ // refuses the (maxRounds + 1)-th — exactly maxRounds + 1 tools/call. Fewer
106
+ // means the refusal came BEFORE the ceiling.
107
+ expect(
108
+ served.length,
109
+ req('openwop.requirement.0175.mrtr-rounds-ceiling.refused', 'interop.md §The MCP round ceiling', `the refusal MUST come at the ceiling, not before: rounds 1..${String(maxRounds)} are within maxRounds and MUST be answered, so the server MUST see ${rounds} tools/call (the initial call + ${String(maxRounds)} retries) — it saw ${served.length}`),
110
+ ).toBeGreaterThanOrEqual(rounds);
100
111
  });
101
112
  });
@@ -14,7 +14,7 @@ import { describe, it, expect } from 'vitest';
14
14
  import { driver, type OpenWOPResponse } from '../lib/driver.js';
15
15
  import { v2Discovery } from '../lib/v2.js';
16
16
  import { readErrorCode } from '../lib/error-envelope.js';
17
- import { softSkip } from '../lib/soft-skip.js';
17
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
18
18
  import { req } from '../lib/requirement-ids.js';
19
19
 
20
20
  const DOC = 'spec/v2/core/events.md §Poll';
@@ -32,19 +32,24 @@ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse
32
32
  try { return await fn(); } catch { return null; }
33
33
  }
34
34
 
35
- async function terminalRun(): Promise<{ runId: string } | { reason: string }> {
35
+ async function terminalRun(): Promise<{ runId: string; terminal: boolean } | { reason: string }> {
36
36
  if (!(await discovery())) return { reason: 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0' };
37
37
  const created = await http(() => driver.post('/runs', { workflowId: NOOP_WORKFLOW_ID }));
38
38
  if (created === null) return { reason: 'POST /runs unreachable (fetch failed)' };
39
39
  const runId = (created.json as { runId?: unknown } | undefined)?.runId;
40
40
  if (created.status !== 201 || typeof runId !== 'string') return { reason: `POST /runs {workflowId: ${NOOP_WORKFLOW_ID}} answered ${created.status} ${readErrorCode(created.json) ?? ''} — the smallest valid create was refused (fixture not seeded?)`.trim() };
41
+ // unfailable-leg audit, 2026-09-26: the helper used to return the run even
42
+ // when the deadline lapsed with it still running, so the shape leg could not
43
+ // hold a host to isTerminal:true. It now reports whether it actually saw a
44
+ // terminal snapshot status.
41
45
  const deadline = Date.now() + TIMEOUT_MS;
46
+ let terminal = false;
42
47
  while (Date.now() < deadline) {
43
48
  const snap = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}`));
44
- if (snap !== null && TERMINAL.has(String((snap.json as { status?: unknown } | undefined)?.status))) break;
49
+ if (snap !== null && TERMINAL.has(String((snap.json as { status?: unknown } | undefined)?.status))) { terminal = true; break; }
45
50
  await new Promise((r) => setTimeout(r, 250));
46
51
  }
47
- return { runId };
52
+ return { runId, terminal };
48
53
  }
49
54
 
50
55
  async function poll(runId: string, query: string): Promise<OpenWOPResponse | null> {
@@ -60,6 +65,7 @@ describe('v2 poll-cursor-v2 (RFC 0171 §E.2)', () => {
60
65
  it('the response is the closed { runId, events, lastSequence, status, isTerminal }', async () => {
61
66
  const r = await terminalRun();
62
67
  if ('reason' in r) return softSkip('blocked', r.reason);
68
+ if (!r.terminal) return softSkip('blocked', `the ${NOOP_WORKFLOW_ID} run did not reach a terminal snapshot status within ${TIMEOUT_MS} ms — isTerminal/status/lastSequence cannot be held to a settled log`);
63
69
  const res = await poll(r.runId, '');
64
70
  if (res === null) return softSkip('blocked', 'GET /runs/{runId}/events/poll unreachable (fetch failed)');
65
71
  expect(res.status, req('openwop.requirement.0171.poll-cursor-v2.shape', DOC, 'pollRunEvents MUST answer 200')).toBe(200);
@@ -70,8 +76,25 @@ describe('v2 poll-cursor-v2 (RFC 0171 §E.2)', () => {
70
76
  expect(Number.isInteger(body?.lastSequence) && (body?.lastSequence as number) >= -1, req('openwop.requirement.0171.poll-cursor-v2.shape', DOC, 'lastSequence is the highest sequence in the log (−1 when empty)')).toBe(true);
71
77
  expect(typeof body?.status, req('openwop.requirement.0171.poll-cursor-v2.shape', DOC, 'status is the snapshot status')).toBe('string');
72
78
  expect(typeof body?.isTerminal, req('openwop.requirement.0171.poll-cursor-v2.shape', DOC, 'isTerminal is a boolean')).toBe('boolean');
79
+ // unfailable-leg audit, 2026-09-26: status and isTerminal were checked only
80
+ // for type, so a host answering isTerminal:false (or status "running") for
81
+ // a run whose snapshot is terminal passed. The run was observed terminal
82
+ // above, so the poll MUST say so.
83
+ expect(TERMINAL.has(String(body?.status)), req('openwop.requirement.0171.poll-cursor-v2.shape', DOC, `status is the snapshot status: the run was observed terminal, so status MUST be one of ${[...TERMINAL].join('/')}; got ${String(body?.status)}`)).toBe(true);
84
+ expect(body?.isTerminal, req('openwop.requirement.0171.poll-cursor-v2.shape', DOC, 'isTerminal is whether the run is terminal: the run was observed terminal, so isTerminal MUST be true')).toBe(true);
85
+ // unfailable-leg audit, 2026-09-26: the old check was max(seqs) ≤
86
+ // lastSequence, so a host answering max+1 (or any larger number) passed.
87
+ // lastSequence is the highest sequence IN THE LOG; a cursor-less poll
88
+ // carries the whole log, so once the log is confirmed quiescent (a second
89
+ // whole-log read returns the same sequences) lastSequence MUST equal the
90
+ // highest returned sequence, or −1 when the log is empty.
73
91
  const seqs = sequences(body);
74
- if (seqs.length > 0) expect(Math.max(...seqs), req('openwop.requirement.0171.poll-cursor-v2.shape', DOC, 'lastSequence MUST equal the highest sequence returned when the response carries the whole log')).toBeLessThanOrEqual(body?.lastSequence as number);
92
+ const settled = await poll(r.runId, '');
93
+ const seqsAfter = settled !== null && settled.status === 200 ? sequences(settled.json) : null;
94
+ if (seqsAfter === null || seqsAfter.join(',') !== seqs.join(',')) {
95
+ return blockedDespiteAssertions(`the event log grew between two whole-log reads (before: [${seqs.join(',')}], after: [${seqsAfter?.join(',') ?? 'unreadable'}]) — a run at terminal STATUS is still appending, so lastSequence cannot be compared to a settled log. Re-run against a quiescent log.`);
96
+ }
97
+ expect(body?.lastSequence, req('openwop.requirement.0171.poll-cursor-v2.shape', DOC, `lastSequence MUST equal the highest sequence in the log (−1 when empty); the whole-log poll returned [${seqs.join(', ')}]`)).toBe(seqs.length > 0 ? Math.max(...seqs) : -1);
75
98
  });
76
99
 
77
100
  it('omitting the cursor returns from the first event (sequence 0)', async () => {
@@ -28,8 +28,13 @@ async function discovery(): Promise<Record<string, unknown> | null> {
28
28
  try { return await v2Discovery(); } catch { return null; }
29
29
  }
30
30
 
31
+ // Unfailable-leg audit, 2026-09-26: this checked `status` alone, so a bare
32
+ // `{status}` object passed as a capability record. capabilities.md §2 makes
33
+ // `status`, `since` and `witness` all REQUIRED, so all three are checked.
31
34
  function isRecord(v: unknown): v is Record<string, unknown> {
32
- return v !== null && typeof v === 'object' && !Array.isArray(v) && typeof (v as Record<string, unknown>)['status'] === 'string';
35
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
36
+ const r = v as Record<string, unknown>;
37
+ return typeof r['status'] === 'string' && typeof r['since'] === 'string' && typeof r['witness'] === 'string';
33
38
  }
34
39
 
35
40
  /** RFC 0169 §C.1 — the predicate, evaluated over the document alone. */
@@ -60,7 +65,9 @@ describe('v2 profiles-derived-only (RFC 0169 §C.1, §C.3)', () => {
60
65
  expect(isRecord(doc[f]), req('openwop.requirement.0169.profiles-derived-only.derivable', DOC, `profile ${p.id} lists family ${f}; the host advertises it, so it MUST be one capability record {status, since, witness, …}`)).toBe(true);
61
66
  }
62
67
  }
63
- expect(typeof derive(doc, p), req('openwop.requirement.0169.profiles-derived-only.derivable', DOC, `profile ${p.id} MUST be decidable from the document alone`)).toBe('boolean');
68
+ // Unfailable-leg audit, 2026-09-26: a `typeof derive(...) === 'boolean'`
69
+ // assertion stood here; `derive` always returns a boolean, so it passed
70
+ // every host. Decidability is carried by the record check above.
64
71
  }
65
72
  const core = rows.find((p) => p.id === 'openwop-discovery-core');
66
73
  if (core !== undefined) {
@@ -26,11 +26,13 @@
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
- * - partial-witness — the host's own delivery timeout closed held attempts
30
- * before the run was terminal. Contention was not sustained when the
31
- * healthy delivery fell due, so this run of the leg cannot judge §A. The
32
- * timeout is the host's to choose; a host with a timeout shorter than
33
- * DELAY_MS is simply not measured by this instrument.
29
+ * - blocked (unjudged) — the host had all 8 held attempts open at once, but
30
+ * its own delivery timeout closed them before the run was terminal.
31
+ * Contention was not sustained when the healthy delivery fell due, so this
32
+ * run of the leg cannot judge §A. The timeout is the host's to choose; a
33
+ * host with a timeout shorter than DELAY_MS is simply not measured by this
34
+ * instrument. If fewer than 8 were EVER open at once (a serial or small
35
+ * pool), it fails instead (2026-09-26, unfailable-leg audit).
34
36
  * - blocked — fewer than 8 held attempts ever arrived AND the healthy one
35
37
  * never did either: nothing reached the suite, which says nothing about
36
38
  * isolation (`noDeliveryCause`).
@@ -121,6 +123,10 @@ describe('RFC 0215 §A — one subscription\'s receiver does not hold another\'s
121
123
  let healthyId: string | null = null;
122
124
  let healthyAt: number | null = null;
123
125
  let openAtHealthy = -1;
126
+ // The most held attempts ever open at once. Sampled at each held arrival:
127
+ // the open count only rises there (closes only lower it), so the max over
128
+ // arrivals is the true peak.
129
+ let peakOpen = 0;
124
130
  const openCount = (): number => [...held.values()].filter((h) => h.closedAt === null).length;
125
131
 
126
132
  const rx = await startScopedReceiver((hit, res) => {
@@ -137,6 +143,7 @@ describe('RFC 0215 §A — one subscription\'s receiver does not hold another\'s
137
143
  }
138
144
  const h: Held = { webhookId, res, openedAt: Date.now(), closedAt: null };
139
145
  held.set(webhookId, h);
146
+ peakOpen = Math.max(peakOpen, openCount());
140
147
  // The HOST closing the socket before we answer is the host abandoning the attempt.
141
148
  res.on('close', () => { if (h.closedAt === null) h.closedAt = Date.now(); });
142
149
  });
@@ -175,7 +182,7 @@ describe('RFC 0215 §A — one subscription\'s receiver does not hold another\'s
175
182
  const everOpened = held.size;
176
183
  await waitFor(() => healthyAt !== null, WINDOW_MS);
177
184
  const earliestClose = Math.min(...[...held.values()].map((h) => h.closedAt ?? Infinity));
178
- const detail = `${everOpened} held attempt(s) arrived; ${openAtTerminal} were still open when the run was seen terminal; `
185
+ const detail = `${everOpened} held attempt(s) arrived (at most ${peakOpen} open at once); ${openAtTerminal} were still open when the run was seen terminal; `
179
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`);
180
187
 
181
188
  if (healthyAt === null && everOpened === 0) {
@@ -193,8 +200,21 @@ describe('RFC 0215 §A — one subscription\'s receiver does not hold another\'s
193
200
  // delivery timeout released the held attempts early (its choice), or it
194
201
  // never opened FLOOR of them at once. The second is the defect, the first
195
202
  // is not, and they differ in WHEN the first held attempt closed.
196
- expect(everOpened, req(ID, DOC, 'the held subscriptions\' attempts MUST have been dispatched')).toBeGreaterThan(0);
197
- return softSkip('skipped', `partial-witness: the host closed held attempts ${Number.isFinite(earliestClose) ? `${terminalAt - earliestClose}ms ` : ''}before the run was terminal (its delivery timeout is shorter than this leg's ${DELAY_MS}ms delay), so ${FLOOR} attempts were not outstanding when the healthy delivery fell due — ${detail}`);
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.
213
+ expect(
214
+ 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}`),
216
+ ).toBeGreaterThanOrEqual(FLOOR);
217
+ return blockedDespiteAssertions(`unjudged: ${FLOOR} held attempts were open at once, but the host closed held attempts ${Number.isFinite(earliestClose) ? `${terminalAt - earliestClose}ms ` : ''}before the run was terminal (its delivery timeout is shorter than this leg's ${DELAY_MS}ms delay), so ${FLOOR} attempts were not outstanding when the healthy delivery fell due — ${detail}`);
198
218
  } else {
199
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}`));
200
220
  }
@@ -348,6 +348,22 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
348
348
  req('openwop.requirement.0173.webhook-durable-delivery', 'webhooks.md §Durability', `retry attempts for ${key} MUST be spaced by the advertised ${policy.backoff} backoff (attempt ${i + 1} followed attempt ${i} after ${times[i]! - times[i - 1]!}ms)`),
349
349
  ).toBeGreaterThan(0);
350
350
  }
351
+ // Unfailable-leg audit (2026-09-26): `> 0 ms` alone let a host advertising
352
+ // `backoff: exponential` that retried in a tight loop (any constant
353
+ // spacing, even a few ms) pass. An exponential backoff MUST widen, so with
354
+ // three or more attempts the second gap is at least 1.5x the first (not
355
+ // 2x: jitter tolerance). Only the first two gaps are compared, so a host
356
+ // whose schedule hits its cap later is not convicted. `fixed` gets no
357
+ // equivalent: its base interval is not on the wire (see the comment above
358
+ // the `!retried` branch), so there is no floor to measure against.
359
+ if (policy.backoff === 'exponential' && times.length >= 3) {
360
+ const gap1 = times[1]! - times[0]!;
361
+ const gap2 = times[2]! - times[1]!;
362
+ expect(
363
+ gap2,
364
+ req('openwop.requirement.0173.webhook-durable-delivery', 'webhooks.md §Durability', `retry attempts for ${key} MUST follow the advertised exponential backoff — the second gap (${gap2}ms) MUST be at least 1.5x the first (${gap1}ms); a constant spacing is not exponential`),
365
+ ).toBeGreaterThanOrEqual(1.5 * gap1);
366
+ }
351
367
  }
352
368
  } else {
353
369
  softSkip('inapplicable', 'no advertised retryPolicy.backoff other than none — the spacing leg is not asserted');
@@ -38,9 +38,10 @@
38
38
  import { afterEach, describe, it, expect } from 'vitest';
39
39
  import { req } from '../lib/requirement-ids.js';
40
40
  import { readErrorCode } from '../lib/error-envelope.js';
41
- import { softSkip } from '../lib/soft-skip.js';
41
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
42
42
  import { v2Validator } from '../lib/v2.js';
43
- import { hitHeader, mintWhsec, startModalReceiver, verifyStandardWebhooks } from '../lib/webhook-receiver.js';
43
+ import { scaledTimeoutMs } from '../lib/polling.js';
44
+ import { hitHeader, mintWhsec, startModalReceiver, verifyStandardWebhooks, type ModalHit } from '../lib/webhook-receiver.js';
44
45
  import {
45
46
  STANDARD_WEBHOOKS_ALG,
46
47
  deliveriesFor,
@@ -53,6 +54,9 @@ import {
53
54
  } from '../lib/standard-webhooks.js';
54
55
 
55
56
  const REFUSED = ['no-echo', 'wrong-echo', 'redirect'] as const;
57
+ /** A second echo subscription, registered AFTER the refused modes (routed as `echo`, told apart by this trailing segment). */
58
+ const LATE_CONTROL = 'late-control';
59
+ const isLateControl = (h: ModalHit): boolean => h.path.split('?')[0]!.endsWith(`/echo/${LATE_CONTROL}`);
56
60
 
57
61
  let closeReceiver: (() => Promise<void>) | null = null;
58
62
  afterEach(async () => {
@@ -80,10 +84,18 @@ describe('RFC 0201 §D — endpoint verification refuses without consent (opted-
80
84
  const res = await registerSw(body(mode));
81
85
  refused.set(mode, { status: res.status, code: readErrorCode(res.json), id: (res.json as { webhookId?: unknown } | null)?.webhookId });
82
86
  }
87
+ // Unfailable-leg audit, 2026-09-26: the persisted-nothing check synced only
88
+ // on the FIRST echo subscription plus a 1 s grace, so a host that answered
89
+ // 400 but kept the rows, delivering serially in registration order, could
90
+ // land those deliveries after the window. A second echo control registered
91
+ // AFTER the refused modes sits behind them in that order.
92
+ const late = await registerSw({ ...body('echo'), url: `${rx.urlFor('echo').url}/${LATE_CONTROL}` });
83
93
 
84
94
  const ID = 'openwop.requirement.0201.endpoint-verification';
85
95
  expect(ok.status, req(ID, 'RFC 0201 §D.14', `an endpoint that echoes the challenge MUST be registered 201 (got ${ok.status} ${readErrorCode(ok.json) ?? ''})`)).toBe(201);
86
96
  const echoId = (ok.json as { webhookId: string }).webhookId;
97
+ expect(late.status, req(ID, 'RFC 0201 §D.14', `a second echoing endpoint (the late control) MUST be registered 201 (got ${late.status} ${readErrorCode(late.json) ?? ''})`)).toBe(201);
98
+ const lateId = (late.json as { webhookId: string }).webhookId;
87
99
  for (const [mode, r] of refused) {
88
100
  expect(r.status, req(ID, 'RFC 0201 §D.14', `a ${mode} endpoint MUST be refused 400 before any 201 (got ${r.status})`)).toBe(400);
89
101
  expect(r.code, req(ID, 'RFC 0201 §D.14', `a ${mode} refusal MUST carry webhook_endpoint_unverified`)).toBe('webhook_endpoint_unverified');
@@ -91,7 +103,7 @@ describe('RFC 0201 §D — endpoint verification refuses without consent (opted-
91
103
 
92
104
  const validate = v2Validator('webhook-verification');
93
105
  for (const mode of ['echo', ...REFUSED]) {
94
- const vr = rx.hits.filter((h) => h.mode === mode && h.verification);
106
+ const vr = rx.hits.filter((h) => h.mode === mode && h.verification && !isLateControl(h));
95
107
  expect(vr.length, req(ID, 'RFC 0201 §D.13–§D.14', `the host MUST send exactly one verification request to the ${mode} endpoint and MUST NOT retry it (saw ${vr.length})`)).toBe(1);
96
108
  const h = vr[0]!;
97
109
  expect(h.method, req(ID, 'RFC 0201 §D.13', 'the verification request MUST be a POST')).toBe('POST');
@@ -113,12 +125,19 @@ describe('RFC 0201 §D — endpoint verification refuses without consent (opted-
113
125
  // delivery; by then any persisted refused subscription would have one too.
114
126
  const run = await driveRun();
115
127
  expect(run.status, req(ID, 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
116
- await waitFor(() => deliveriesFor(rx.hits, echoId, run.runId).length > 0, 15_000);
128
+ await waitFor(() => deliveriesFor(rx.hits, echoId, run.runId).length > 0 && deliveriesFor(rx.hits, lateId, run.runId).length > 0, 15_000);
117
129
  expect(deliveriesFor(rx.hits, echoId, run.runId).length, req(ID, 'webhooks.md §Durability', 'the verified subscription MUST receive run.completed — the sync point for the persisted-nothing check')).toBeGreaterThan(0);
118
- await new Promise((r) => setTimeout(r, 1_000));
130
+ // The late control is a SYNC POINT, not a requirement: a public receiver
131
+ // front that drops its extra path segment would starve it through no fault
132
+ // of the host. Unobserved ⇒ the persisted-nothing check is unjudged, never
133
+ // failed and never passed.
134
+ if (deliveriesFor(rx.hits, lateId, run.runId).length === 0) {
135
+ return blockedDespiteAssertions(`the late-control subscription (${lateId}) received no run.completed within the wait — the second sync point for the persisted-nothing check is missing, so that check is unjudged`);
136
+ }
137
+ await new Promise((r) => setTimeout(r, scaledTimeoutMs(3_000)));
119
138
  for (const mode of REFUSED) {
120
139
  const late = rx.hits.filter((h) => h.mode === mode && !h.verification);
121
140
  expect(late.length, req(ID, 'RFC 0201 §D.14', `a refused (${mode}) registration MUST persist no subscription — it received ${late.length} later request(s)`)).toBe(0);
122
141
  }
123
- }, 60_000);
142
+ }, 75_000);
124
143
  });