@openwop/openwop-conformance 2.42.2 → 2.42.4

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/dist/spec-artifacts.lock.json +2 -2
  4. package/package.json +2 -2
  5. package/requirements.json +353 -133
  6. package/schemas/CORPUS-STAMP.json +10 -10
  7. package/src/lib/agentOrgChart.ts +21 -4
  8. package/src/lib/effect-receiver.ts +23 -2
  9. package/src/lib/toolCatalog.ts +29 -5
  10. package/src/scenarios/agent-manifest-runtime.test.ts +17 -4
  11. package/src/scenarios/agent-org-chart-scoping.test.ts +27 -4
  12. package/src/scenarios/agent-roster-attribution.test.ts +28 -4
  13. package/src/scenarios/agentPackExport.test.ts +33 -0
  14. package/src/scenarios/agentPackInstall.test.ts +32 -0
  15. package/src/scenarios/agentReasoningEvents.test.ts +22 -13
  16. package/src/scenarios/agents-run-tool-allowlist.test.ts +6 -2
  17. package/src/scenarios/ai-envelope-shape.test.ts +9 -8
  18. package/src/scenarios/anonymous-actor-write-gated.test.ts +12 -6
  19. package/src/scenarios/audit-log-integrity.test.ts +37 -0
  20. package/src/scenarios/context-budget-transcript-bound.test.ts +14 -4
  21. package/src/scenarios/context-summarization-replay.test.ts +17 -8
  22. package/src/scenarios/conversationLifecycle.test.ts +12 -1
  23. package/src/scenarios/conversationReplayDeterminism.test.ts +19 -3
  24. package/src/scenarios/cost-attribution.test.ts +12 -0
  25. package/src/scenarios/debug-bundle-truncation.test.ts +16 -3
  26. package/src/scenarios/distillation-shape.test.ts +30 -0
  27. package/src/scenarios/envelope-truncated.test.ts +6 -2
  28. package/src/scenarios/feedback-fork-not-copied.test.ts +28 -7
  29. package/src/scenarios/idempotency-concurrent-claim.test.ts +27 -9
  30. package/src/scenarios/idempotency-key-determinism.test.ts +6 -2
  31. package/src/scenarios/inbound-credential-no-passthrough.test.ts +13 -2
  32. package/src/scenarios/mcp-server-untrusted-args.test.ts +20 -5
  33. package/src/scenarios/mcp-tool-roundtrip.test.ts +7 -21
  34. package/src/scenarios/memory-compaction-provenance-tag.test.ts +11 -3
  35. package/src/scenarios/orchestratorDispatch.test.ts +18 -2
  36. package/src/scenarios/pack-registry.test.ts +8 -1
  37. package/src/scenarios/production-backpressure.test.ts +17 -11
  38. package/src/scenarios/redaction.test.ts +8 -2
  39. package/src/scenarios/replay-fanout-suppression.test.ts +18 -2
  40. package/src/scenarios/runner-ledger.test.ts +7 -2
  41. package/src/scenarios/stream-modes.test.ts +27 -0
  42. package/src/scenarios/tool-catalog-projection.test.ts +35 -12
  43. package/src/scenarios/v2-advertised-fixtures-exist.test.ts +17 -3
  44. package/src/scenarios/v2-artifact-a2a-shape.test.ts +8 -2
  45. package/src/scenarios/v2-bound-id-kinds.test.ts +13 -4
  46. package/src/scenarios/v2-effect-identity-business-key.test.ts +71 -13
  47. package/src/scenarios/v2-effect-seam-manifest.test.ts +3 -9
  48. package/src/scenarios/v2-era-2-append-vocabulary.test.ts +52 -9
  49. package/src/scenarios/v2-idempotency-in-flight.test.ts +16 -0
  50. package/src/scenarios/v2-interop-trace-context.test.ts +23 -0
  51. package/src/scenarios/v2-mcp-tasks.test.ts +33 -6
  52. package/src/scenarios/v2-min-client-version.test.ts +11 -1
  53. package/src/scenarios/v2-mrtr-rounds-ceiling.test.ts +11 -0
  54. package/src/scenarios/v2-poll-cursor-v2.test.ts +28 -5
  55. package/src/scenarios/v2-profiles-derived-only.test.ts +9 -2
  56. package/src/scenarios/v2-run-diff-identical.test.ts +13 -5
  57. package/src/scenarios/v2-run-pause-resume.test.ts +4 -1
  58. package/src/scenarios/v2-sse-last-event-id.test.ts +7 -0
  59. package/src/scenarios/v2-version-header-honored.test.ts +22 -3
  60. package/src/scenarios/v2-webhook-delivery-isolation.test.ts +28 -8
  61. package/src/scenarios/v2-webhook-durable-delivery.test.ts +24 -0
  62. package/src/scenarios/v2-webhook-endpoint-verification.test.ts +25 -6
  63. package/src/scenarios/v2-webhook-message-id-stable.test.ts +10 -2
  64. package/src/scenarios/v2-webhook-unregister-stops-delivery.test.ts +6 -1
  65. package/src/scenarios/version-negotiation.test.ts +12 -1
  66. package/src/scenarios/webhook-signed-delivery.test.ts +18 -3
@@ -25,7 +25,8 @@ import { describe, it, expect } from 'vitest';
25
25
  import { driver } from '../lib/driver.js';
26
26
  import { v2Discovery, gateFamily, v2Validator } from '../lib/v2.js';
27
27
  import { seamsProfileAdvertised, SEAMS_PREFIX } from '../lib/seams.js';
28
- import { softSkip } from '../lib/soft-skip.js';
28
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
29
+ import { startEffectReceiver } from '../lib/effect-receiver.js';
29
30
  import { req } from '../lib/requirement-ids.js';
30
31
 
31
32
  const FIXTURE = 'conformance-noop';
@@ -53,24 +54,36 @@ describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)',
53
54
  const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
54
55
  if (!fixtures.includes(FIXTURE)) return softSkip('inapplicable', `${FIXTURE} fixture not advertised — no run to read`);
55
56
 
57
+ // Unfailable-leg audit (2026-09-26): the noop fixture issues no effects, so
58
+ // the per-row loop asserted nothing on any host, and the trailing
59
+ // `softSkip('inapplicable')` (called without `return`, after four passing
60
+ // setup asserts) recorded `executed-pass` with a `partial-witness:` detail —
61
+ // a host that mis-keyed every effect passed this row. Now no expect passes
62
+ // before the empty-ledger check: a failing setup read still fails, and an
63
+ // empty but well-formed ledger returns `inapplicable` with zero assertions.
56
64
  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);
65
+ if (create.status !== 201) {
66
+ 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);
67
+ }
58
68
  const runId = (create.json as { runId: string }).runId;
59
69
  await waitTerminal(runId, 10_000);
60
70
 
61
71
  const res = await driver.get(`/runs/${encodeURIComponent(runId)}/effects`);
72
+ const check = v2Validator('effect-ledger-projection')(res.json);
73
+ const body = res.json as { runId?: unknown; effects?: Array<{ effectId?: unknown; keying?: unknown; providerKey?: unknown }> } | null;
74
+ const effects = body?.effects ?? [];
75
+ if (res.status === 200 && check.ok && body?.runId === runId && effects.length === 0) {
76
+ 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)');
77
+ }
62
78
  expect(
63
79
  res.status,
64
80
  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
81
  ).toBe(200);
66
- const check = v2Validator('effect-ledger-projection')(res.json);
67
82
  expect(
68
83
  check.ok,
69
84
  req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json', `the ledger projection MUST validate: ${check.errors}`),
70
85
  ).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 ?? [];
86
+ 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
87
  const ids = new Set<string>();
75
88
  for (const e of effects) {
76
89
  expect(
@@ -90,7 +103,6 @@ describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)',
90
103
  ).toBe(false);
91
104
  }
92
105
  }
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
106
  });
95
107
 
96
108
  it('the same provider key is presented across two transport retries', async () => {
@@ -98,11 +110,19 @@ describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)',
98
110
  if (!doc) return softSkip('blocked', 'discovery unreachable');
99
111
  if (!(await gateFamily('idempotency'))) return softSkip('inapplicable', 'idempotency family not advertised — no Layer-2 obligation (gate recorded under openwop.family.idempotency)');
100
112
  if (!seamsProfileAdvertised(doc)) return softSkip('inapplicable', 'the retry leg is driven through the suite fixture provider (RFC 0173 §D.2 G4) under the seams profile — seams profile not advertised');
101
- // The seam is catalogued (api/seams-v2.yaml `forceEffectTransportRetry`). An
102
- // unreachable providerUrl forces the transport retry; the witness is the
103
- // host's own Layer-2 ledger (RFC 0173 §C.2), where every attempt of one
104
- // effect MUST carry the same business-identity key.
105
- const fired = await driver.post(`${SEAMS_PREFIX}/sample/test/idempotency/effect-retry`, { providerUrl: 'http://127.0.0.1:1/' });
113
+ // The seam is catalogued (api/seams-v2.yaml `forceEffectTransportRetry`), and
114
+ // its contract names the witness: "the suite fixture provider records the
115
+ // idempotency key of each attempt". Until 2.42.4 this leg handed the host an
116
+ // address nothing listened on (`http://127.0.0.1:1/`), so no suite-owned
117
+ // party ever saw a key, and the only witness was the host's own ledger — a
118
+ // host could record one constant providerKey while sending a fresh key on
119
+ // every attempt. The suite's own receiver now cuts the FIRST attempt's
120
+ // connection (a genuine transport failure, after recording its
121
+ // Idempotency-Key) and answers the second, so the keys compared below are
122
+ // the ones that crossed the wire.
123
+ const rx = await startEffectReceiver({ failFirst: 1 });
124
+ try {
125
+ const fired = await driver.post(`${SEAMS_PREFIX}/sample/test/idempotency/effect-retry`, { providerUrl: rx.url });
106
126
  if (fired.status === 404 || fired.status === 403 || fired.status === 405) {
107
127
  return softSkip('blocked', `the host advertises the seams profile but does not serve ${SEAMS_PREFIX}/sample/test/idempotency/effect-retry (answered ${fired.status}) — the cross-retry keying leg cannot be driven`);
108
128
  }
@@ -117,7 +137,20 @@ describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)',
117
137
  if (attempts.length < 2) {
118
138
  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
139
  }
120
- const keys = new Set(attempts.map((e) => String(e['providerKey'] ?? '')));
140
+ // Unfailable-leg audit (2026-09-26): `providerKey` is optional in the ledger
141
+ // schema and `?? ''` folded a missing key to '', so a host that recorded no
142
+ // provider key on any attempt showed "one distinct key" and passed. Every
143
+ // attempt now MUST carry a non-empty key. Remaining limit: the witness is
144
+ // still the host's own ledger; a suite-owned receiver counting the
145
+ // Idempotency-Key header on the wire (lib/effect-receiver.ts) is the
146
+ // stronger follow-up, not done here.
147
+ for (const a of attempts) {
148
+ expect(
149
+ typeof a['providerKey'] === 'string' && (a['providerKey'] as string).length > 0,
150
+ 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)}`),
151
+ ).toBe(true);
152
+ }
153
+ const keys = new Set(attempts.map((e) => String(e['providerKey'])));
121
154
  expect(
122
155
  keys.size,
123
156
  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)`),
@@ -142,5 +175,30 @@ describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)',
142
175
  keyings.size,
143
176
  req('openwop.requirement.0173.effect-identity-business-key.retry', 'spec/v2/core/idempotency.md §Layer 2: effect identity', `every attempt of one effect MUST declare the same keying — ${attempts.length} attempt(s) declared ${[...keyings].join(', ')}`),
144
177
  ).toBe(1);
178
+ // The wire half: what the provider actually received.
179
+ const deadline = Date.now() + 10_000;
180
+ while (rx.arrivals() < 2 && Date.now() < deadline) await new Promise((r) => setTimeout(r, 100));
181
+ const wire = rx.all().filter((a) => a.mine);
182
+ if (wire.length < 2) {
183
+ return blockedDespiteAssertions(`the suite's fixture provider saw ${wire.length} attempt(s) at ${rx.url} (local ${rx.localUrl}${rx.tunnelled ? ', tunnelled' : ''}) while the ledger records ${attempts.length} — the host's attempts did not reach the suite's receiver, so the keys on the wire are unobserved`);
184
+ }
185
+ for (const a of wire) {
186
+ expect(
187
+ typeof a.idempotencyKey === 'string' && a.idempotencyKey.length > 0,
188
+ req('openwop.requirement.0173.effect-identity-business-key.retry', 'spec/v2/core/idempotency.md §Layer 2: effect identity', `every attempt MUST present an Idempotency-Key to the provider — an attempt at ${a.path} carried ${JSON.stringify(a.idempotencyKey ?? null)}`),
189
+ ).toBe(true);
190
+ }
191
+ const wireKeys = new Set(wire.map((a) => a.idempotencyKey));
192
+ expect(
193
+ wireKeys.size,
194
+ req('openwop.requirement.0173.effect-identity-business-key.retry', 'spec/v2/core/idempotency.md §Layer 2: effect identity', `the provider MUST see the same Idempotency-Key on every attempt of one effect — ${wire.length} attempt(s) on the wire carried ${wireKeys.size} distinct key(s)`),
195
+ ).toBe(1);
196
+ expect(
197
+ [...wireKeys][0],
198
+ req('openwop.requirement.0173.effect-identity-business-key.retry', 'spec/v2/core/idempotency.md §Layer 2: effect identity', 'the key the ledger records MUST be the key the provider received — a ledger that disagrees with the wire is not a witness'),
199
+ ).toBe([...keys][0]);
200
+ } finally {
201
+ await rx.close();
202
+ }
145
203
  });
146
204
  });
@@ -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
  });
@@ -106,6 +106,12 @@ describe('v2 idempotency-in-flight (idempotency.md Concurrency, RFC 0213 §B)',
106
106
  const r = await theRace();
107
107
  if (r.kind === 'blocked') return softSkip('blocked', r.reason);
108
108
  expect(r.successes.length, req(ID_ONE, DOC, `at least one of ${N} same-key creates MUST complete (statuses: ${r.all.map((x) => x.status).join(',')})`)).toBeGreaterThan(0);
109
+ // unfailable-leg audit wave 2, 2026-09-27: non-string runIds were FILTERED
110
+ // before the distinct-count, so a host answering the winner `201 {runId}`
111
+ // and every loser `201 {}` (a success that names no run — possibly a second
112
+ // one) passed. Every success MUST name the run it created or replayed.
113
+ const withoutRunId = r.successes.filter((x) => typeof (x.json as { runId?: unknown } | null)?.runId !== 'string');
114
+ expect(withoutRunId.length, req(ID_ONE, DOC, `every same-key success MUST carry the string runId of the one run processed (${withoutRunId.length} of ${r.successes.length} did not; statuses ${withoutRunId.map((x) => x.status).join(',')})`)).toBe(0);
109
115
  const runIds = new Set(r.successes.map((x) => (x.json as { runId?: unknown } | null)?.runId).filter((x): x is string => typeof x === 'string'));
110
116
  expect(runIds.size, req(ID_ONE, DOC, `a host MUST NOT process two same-key requests: distinct runIds ${[...runIds].join(', ')}`)).toBe(1);
111
117
  }, 60_000);
@@ -115,6 +121,16 @@ describe('v2 idempotency-in-flight (idempotency.md Concurrency, RFC 0213 §B)',
115
121
  if (r.kind === 'blocked') return softSkip('blocked', r.reason);
116
122
  const unmarked = r.successes.filter((x) => x.headers.get('openwop-idempotent-replay') !== 'true');
117
123
  expect(unmarked.length, req(ID_LOSER, DOC, `exactly one success is the winner; every other MUST carry OpenWOP-Idempotent-Replay: true (${unmarked.length} of ${r.successes.length} unmarked)`)).toBe(1);
124
+ // unfailable-leg audit wave 2, 2026-09-27: a marked replay was never
125
+ // compared to the winner, so a loser answered `201` + the replay header
126
+ // over a DIFFERENT (or no) runId passed. §Concurrency: the loser receives
127
+ // "the winner's response" — the same runId.
128
+ const winnerRunId = (unmarked[0]?.json as { runId?: unknown } | null)?.runId;
129
+ const replayRunIds = r.successes.filter((x) => x !== unmarked[0]).map((x) => (x.json as { runId?: unknown } | null)?.runId);
130
+ expect(
131
+ { winnerIsString: typeof winnerRunId === 'string', mismatched: replayRunIds.filter((id) => id !== winnerRunId).map(String) },
132
+ req(ID_LOSER, DOC, `a loser that waits MUST receive the winner's response — every marked replay MUST carry the winner's runId (${String(winnerRunId)})`),
133
+ ).toEqual({ winnerIsString: true, mismatched: [] });
118
134
  for (const x of r.refusals) {
119
135
  const code = readErrorCode(x.json);
120
136
  expect({ status: x.status, code }, req(ID_LOSER, DOC, `a loser that is not a replay MUST be 409 idempotency_in_flight (got ${x.status} ${String(code)})`)).toEqual({ status: 409, code: 'idempotency_in_flight' });
@@ -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
 
@@ -306,7 +307,16 @@ describe('RFC 0198 — v2-mcp-tasks (MCP Tasks on the server mount; disconnect c
306
307
  const f = await createTask(m.url, 'conformance-failure');
307
308
  let terminal: Rpc;
308
309
  if (f.result?.['resultType'] === 'task') terminal = await pollTask(m.url, String(f.result['taskId']), (s) => ['completed', 'failed', 'cancelled'].includes(s), 10_000);
309
- else return softSkip('inapplicable', 'conformance-failure finished before the host answered, so it was answered CallToolResult (RFC 0198 §B.4) — no task to project');
310
+ else {
311
+ // unfailable-leg audit wave 2, 2026-09-27: this branch returned
312
+ // softSkip('inapplicable') after the input_required asserts — a
313
+ // partial-witness PASS at both majors even when the synchronous answer
314
+ // was NOT a tool-error outcome. The synchronous CallToolResult of a
315
+ // failed run MUST still carry isError true; the task projection itself
316
+ // was not observed, so the row is `blocked` (stands at major 2).
317
+ expect((f.result as { isError?: unknown } | undefined)?.isError, req(ID_TASK_STATUS_PROJECTION, 'RFC 0198 §B.4; ext-tasks §Task Execution Errors', `a failed run answered synchronously is a CallToolResult with isError true (got ${JSON.stringify(f.error ?? f.result)})`)).toBe(true);
318
+ return softSkip('blocked', 'conformance-failure finished before the host answered, so it was answered CallToolResult (RFC 0198 §B.4) — the failed-run task-status projection was not observed');
319
+ }
310
320
  expect([terminal.result?.['status'], (terminal.result?.['result'] as { isError?: unknown } | undefined)?.isError], req(ID_TASK_STATUS_PROJECTION, 'interop-map.json mcp.tasks.status failed; ext-tasks §Task Execution Errors', `a failed run is a tool outcome: completed with result.isError true, never failed (got ${JSON.stringify(terminal.result)})`)).toEqual(['completed', true]);
311
321
  });
312
322
 
@@ -343,9 +353,21 @@ describe('RFC 0198 — v2-mcp-tasks (MCP Tasks on the server mount; disconnect c
343
353
  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
354
  const u = await call(m.url, 'tasks/update', { taskId, inputResponses: { [key!]: { action: 'accept', content: { action: 'accept' } } } });
345
355
  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);
356
+ // Unfailable-leg audit, 2026-09-26: the negative evidence was read once
357
+ // after a fixed 750 ms, so a host applying the non-approver's update
358
+ // asynchronously after that passed. It is now held for a scaled ~3 s: the
359
+ // run must stay waiting-approval with zero interrupt.resolved on every read.
360
+ const holdUntil = Date.now() + scaledTimeoutMs(3_000);
361
+ let status: unknown;
362
+ let resolvedCount = 0;
363
+ for (;;) {
364
+ status = await runStatus(unproject(taskId));
365
+ resolvedCount = (await events(unproject(taskId))).filter((e) => e.type === 'interrupt.resolved').length;
366
+ if (status !== 'waiting-approval' || resolvedCount !== 0 || Date.now() >= holdUntil) break;
367
+ await new Promise((ok) => setTimeout(ok, 250));
368
+ }
369
+ 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');
370
+ expect(resolvedCount, req(ID_TASK_UPDATE_APPROVER_CHECKED, 'interrupt.md §Approver enforcement', 'no interrupt.resolved is recorded')).toBe(0);
349
371
  await call(m.url, 'tasks/cancel', { taskId });
350
372
  });
351
373
 
@@ -484,13 +506,18 @@ describe('RFC 0198 — v2-mcp-tasks (MCP Tasks on the server mount; disconnect c
484
506
  const ac = new AbortController();
485
507
  const resP = fetch(m.url, { method: 'POST', signal: ac.signal, headers: headersFor('tools/call', 'conformance-cancellable', undefined, 'text/event-stream, application/json'), body: JSON.stringify(envelope('tools/call', { name: 'conformance-cancellable', arguments: { delayMs: 20_000 } }, {})) });
486
508
  const runId = await freshRun('conformance-cancellable', before, 3000);
487
- expect(runId, req(ID_NO_SERVER_CANCELLED, 'interop-map.json mcp.methods tools/call', 'the tools/call started a run listRuns shows')).not.toBeNull();
488
509
  const res = await resP;
510
+ // unfailable-leg audit wave 2, 2026-09-27: the runId assert used to run
511
+ // BEFORE this content-type gate, so a JSON-answering mount (which cannot
512
+ // carry notifications/cancelled at all) recorded a partial-witness PASS
513
+ // for "carries no notifications/cancelled". The gate now precedes any
514
+ // assertion, so the JSON mount is cleanly inapplicable.
489
515
  if (!(res.headers.get('content-type') ?? '').includes('text/event-stream')) {
490
516
  ac.abort();
491
- await driver.post(`/runs/${encodeURIComponent(runId!)}/cancel`, {});
517
+ if (runId !== null) await driver.post(`/runs/${encodeURIComponent(runId)}/cancel`, {});
492
518
  return softSkip('inapplicable', 'the mount answered the blocking call as JSON, not SSE — a JSON response cannot carry a notifications/cancelled frame');
493
519
  }
520
+ expect(runId, req(ID_NO_SERVER_CANCELLED, 'interop-map.json mcp.methods tools/call', 'the tools/call started a run listRuns shows')).not.toBeNull();
494
521
  const cancel = await driver.post(`/runs/${encodeURIComponent(runId!)}/cancel`, {});
495
522
  expect(cancel.status, req(ID_NO_SERVER_CANCELLED, 'runs.md §Cancel', `REST cancelRun is accepted (got ${cancel.status})`)).toBeLessThan(300);
496
523
  const msgs: Array<Record<string, unknown>> = [];
@@ -18,7 +18,7 @@ import { describe, it, expect } from 'vitest';
18
18
  import { driver, type OpenWOPResponse } from '../lib/driver.js';
19
19
  import { v2Discovery, v2Validator } from '../lib/v2.js';
20
20
  import { readErrorCode } from '../lib/error-envelope.js';
21
- import { softSkip } from '../lib/soft-skip.js';
21
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
22
22
  import { req } from '../lib/requirement-ids.js';
23
23
 
24
24
  const DOC = 'spec/v2/core/versioning.md §1.5';
@@ -40,6 +40,16 @@ describe('v2 min-client-version (RFC 0172 §A.5 — gated on minClientVersion)',
40
40
  expect(typeof floor === 'string' && VERSION.test(floor), req('openwop.requirement.0172.min-client-version', DOC, 'minClientVersion MUST use the <major>.<minor> grammar (axis 15, as axis 1)')).toBe(true);
41
41
  const res = await http(() => driver.get('/.well-known/openwop', { authenticated: false, headers: { 'OpenWOP-Client-Version': '0.0.1' } }));
42
42
  if (res === null) return softSkip('blocked', 'GET /.well-known/openwop with OpenWOP-Client-Version unreachable (fetch failed)');
43
+ // unfailable-leg audit wave 2, 2026-09-27: every non-426 answer — including
44
+ // a 4xx/5xx that refused the client in an unregistered form — recorded
45
+ // `inapplicable` AFTER the grammar assert, i.e. a partial-witness PASS of
46
+ // the 426 refusal row. Only a SERVED request (2xx) is the MAY exercised; any
47
+ // other non-426 answer is `blocked`. Not asserted as a failure:
48
+ // `OpenWOP-Client-Version` is not yet declared in api/v2/openapi.yaml /
49
+ // headers.md, so a non-2xx here is not provably a version refusal.
50
+ if (res.status !== 426 && (res.status < 200 || res.status >= 300)) {
51
+ return blockedDespiteAssertions(`the host advertises minClientVersion ${String(floor)} and answered ${res.status} to a client announcing 0.0.1 — neither served (the MAY) nor refused with the registered 426 client_version_unsupported; the refusal form was not observed`);
52
+ }
43
53
  if (res.status !== 426) return softSkip('inapplicable', `the host advertises minClientVersion ${String(floor)} but served a client announcing 0.0.1 (${res.status}) — refusal is a MAY; nothing further is observable`);
44
54
  expect(readErrorCode(res.json), req('openwop.requirement.0172.min-client-version', DOC, 'a 426 refusal MUST carry client_version_unsupported')).toBe('client_version_unsupported');
45
55
  const r = v2Validator('error-envelope')(res.json);
@@ -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) {
@@ -12,7 +12,10 @@
12
12
  * `schemas/v2/run-diff-response.schema.json` and echo both ids in `a` / `b`
13
13
  * (a host answering `{}` to any pair fails there); and, when
14
14
  * `conformance-failure` is advertised, a noop-vs-failure diff MUST diverge.
15
- * Without that fixture the divergence control records `inapplicable`.
15
+ * Without that fixture the divergence control cannot run and the leg records
16
+ * `blocked` (`blockedDespiteAssertions`): a host answering a constant
17
+ * `{a, b, divergedAtSeq: null, eventDiffs: []}` to every pair would otherwise
18
+ * pass on the identical-logs assertions alone.
16
19
  *
17
20
  * @see spec/v2/core/runs.md §Diff and ancestry
18
21
  * @see schemas/v2/run-diff-response.schema.json
@@ -23,7 +26,7 @@ import { driver, type OpenWOPResponse } from '../lib/driver.js';
23
26
  import { v2Discovery, v2Validator } from '../lib/v2.js';
24
27
  import { isFixtureAdvertised } from '../lib/fixtures.js';
25
28
  import { readErrorCode } from '../lib/error-envelope.js';
26
- import { softSkip } from '../lib/soft-skip.js';
29
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
27
30
  import { req } from '../lib/requirement-ids.js';
28
31
 
29
32
  const ID = 'openwop.requirement.0170.run-diff-identical';
@@ -65,10 +68,15 @@ describe('v2 run-diff-identical (runs.md §Diff and ancestry)', () => {
65
68
  expect([d.a, d.b], req(ID, DOC, 'the response MUST name the two runs it compared')).toEqual([a.runId, b.runId]);
66
69
  expect(d.divergedAtSeq, req(ID, DOC, `identical logs MUST yield divergedAtSeq: null (got ${String(d.divergedAtSeq)}) — eventId, runId, timestamp and other run-scoped fields MUST be excluded from the comparison`)).toBeNull();
67
70
  expect(d.eventDiffs, req(ID, DOC, 'identical logs MUST yield empty eventDiffs')).toEqual([]);
68
- if (!isFixtureAdvertised(FAILURE)) return softSkip('inapplicable', `${FAILURE} fixture not advertised — the divergence control (a noop-vs-failure diff MUST diverge) cannot run`);
69
- const f = await createSettled(FAILURE); if ('reason' in f) return softSkip('blocked', f.reason);
71
+ // unfailable-leg audit wave 2, 2026-09-27: without the divergence control,
72
+ // these returns were plain softSkips AFTER the identical-logs asserts, so a
73
+ // host answering a constant `divergedAtSeq: null, eventDiffs: []` to every
74
+ // pair recorded a (partial-witness) pass. The identical-logs result is only
75
+ // evidence once a different pair is seen to diverge — else `blocked`.
76
+ if (!isFixtureAdvertised(FAILURE)) return blockedDespiteAssertions(`${FAILURE} fixture not advertised — the divergence control (a noop-vs-failure diff MUST diverge) cannot run, so the identical-logs result is not evidence`);
77
+ const f = await createSettled(FAILURE); if ('reason' in f) return blockedDespiteAssertions(f.reason);
70
78
  const dv = await http(() => driver.get(`/runs/${enc(a.runId)}:diff?against=${enc(f.runId)}`));
71
- if (dv === null || dv.status !== 200) return softSkip('blocked', `the divergence control diff answered ${dv?.status ?? 'no response'}`);
79
+ if (dv === null || dv.status !== 200) return blockedDespiteAssertions(`the divergence control diff answered ${dv?.status ?? 'no response'}`);
72
80
  const x = dv.json as Diff;
73
81
  expect(typeof x.divergedAtSeq === 'number' && Array.isArray(x.eventDiffs) && x.eventDiffs.length > 0, req(ID, DOC, `a noop log and a failure log MUST diverge (divergedAtSeq ${String(x.divergedAtSeq)}, ${Array.isArray(x.eventDiffs) ? x.eventDiffs.length : 0} diff(s)) — the identical-logs pass above is only evidence if a different pair does not also read identical`)).toBe(true);
74
82
  }, 45_000);
@@ -71,7 +71,10 @@ describe('v2 run-pause-resume (runs.md §Pause and resume)', () => {
71
71
  if (resume === null) return softSkip('blocked', 'POST /runs/{runId}:resume unreachable (fetch failed)');
72
72
  expect(resume.status, req(ID, DOC, `resume on a run that is not paused MUST answer 409 — got ${resume.status} ${readErrorCode(resume.json) ?? ''}`.trim())).toBe(409);
73
73
  expect(readErrorCode(resume.json), req(ID, DOC, `a resume refused because the run is terminal MUST carry run_terminal (got ${String(readErrorCode(resume.json))})`)).toBe('run_terminal');
74
- if (!isFixtureAdvertised(DELAY)) return softSkip('inapplicable', `${DELAY} fixture not advertised — the positive pause/resume leg cannot run, so the two 409s above are witnessed without their control (a host answering 409 to everything would pass them)`);
74
+ // unfailable-leg audit wave 2, 2026-09-27: 'inapplicable' after the 409
75
+ // asserts resolved to a partial-witness PASS — a host answering 409 to
76
+ // every pause/resume passed. `blocked` stands at major 2.
77
+ if (!isFixtureAdvertised(DELAY)) return softSkip('blocked', `${DELAY} fixture not advertised — the positive pause/resume leg cannot run, so the two 409s above are witnessed without their control (a host answering 409 to everything would pass them)`);
75
78
  });
76
79
 
77
80
  it('a running run pauses (202 paused), refuses a second pause (409), resumes (202 running), and the log carries run.paused then run.resumed', async () => {