@openwop/openwop-conformance 2.42.3 → 2.42.5

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 (54) hide show
  1. package/CHANGELOG.md +22 -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 +292 -103
  6. package/schemas/CORPUS-STAMP.json +9 -9
  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/mcp-server-untrusted-args.test.ts +20 -5
  32. package/src/scenarios/mcp-tool-roundtrip.test.ts +7 -21
  33. package/src/scenarios/memory-compaction-provenance-tag.test.ts +11 -3
  34. package/src/scenarios/orchestratorDispatch.test.ts +18 -2
  35. package/src/scenarios/pack-registry.test.ts +8 -1
  36. package/src/scenarios/production-backpressure.test.ts +17 -11
  37. package/src/scenarios/redaction.test.ts +8 -2
  38. package/src/scenarios/replay-fanout-suppression.test.ts +18 -2
  39. package/src/scenarios/runner-ledger.test.ts +7 -2
  40. package/src/scenarios/stream-modes.test.ts +27 -0
  41. package/src/scenarios/tool-catalog-projection.test.ts +35 -12
  42. package/src/scenarios/v2-effect-identity-business-key.test.ts +40 -6
  43. package/src/scenarios/v2-era-2-append-vocabulary.test.ts +12 -3
  44. package/src/scenarios/v2-idempotency-in-flight.test.ts +16 -0
  45. package/src/scenarios/v2-mcp-tasks.test.ts +17 -3
  46. package/src/scenarios/v2-min-client-version.test.ts +11 -1
  47. package/src/scenarios/v2-run-diff-identical.test.ts +13 -5
  48. package/src/scenarios/v2-run-pause-resume.test.ts +4 -1
  49. package/src/scenarios/v2-sse-last-event-id.test.ts +7 -0
  50. package/src/scenarios/v2-version-header-honored.test.ts +22 -3
  51. package/src/scenarios/v2-webhook-durable-delivery.test.ts +8 -0
  52. package/src/scenarios/v2-webhook-unregister-stops-delivery.test.ts +6 -1
  53. package/src/scenarios/version-negotiation.test.ts +12 -1
  54. package/src/scenarios/webhook-signed-delivery.test.ts +18 -3
@@ -19,7 +19,7 @@
19
19
  */
20
20
 
21
21
  import { describe, it, expect } from 'vitest';
22
- import { softSkip } from '../lib/soft-skip.js';
22
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
23
23
  import { driver } from '../lib/driver.js';
24
24
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
25
25
  import { req } from '../lib/requirement-ids.js';
@@ -90,7 +90,11 @@ describe('memory-compaction-provenance-tag: compacted-from:<id> tag follows §C
90
90
  if (listRes.status === 404) {
91
91
  // eslint-disable-next-line no-console
92
92
  console.warn('[rfc0012-tag] host does not expose memory:list at /v1/memory/{ref}; skipping tag inspection (canonical provenance signal remains the memory.compacted event itself)');
93
- return softSkip('blocked', '[rfc0012-tag] host does not expose memory:list at /v1/memory/{ref}; skipping tag inspection (canonical provenance signal remains the memory.compacted event itself)');
93
+ // unfailable-leg audit wave 2, 2026-09-27: this return follows the seed/
94
+ // compact asserts, so a plain softSkip recorded a partial-witness
95
+ // `executed-pass` — a host whose compacted entry was never inspectable
96
+ // passed the tag requirement unobserved. Now `blocked`.
97
+ return blockedDespiteAssertions('[rfc0012-tag] host does not expose memory:list at /v1/memory/{ref}; skipping tag inspection (canonical provenance signal remains the memory.compacted event itself)');
94
98
  }
95
99
  expect(listRes.status, req('openwop.it.memory-compaction-provenance-tag.compacted-entry-carries-a-well-formed-compacted-from-tag-or-omits-it-cleanly-no', 'RFC 0012 §C', 'memory:list MUST return 200 when reachable')).toBe(200);
96
100
 
@@ -100,7 +104,11 @@ describe('memory-compaction-provenance-tag: compacted-from:<id> tag follows §C
100
104
  if (!output) {
101
105
  // eslint-disable-next-line no-console
102
106
  console.warn(`[rfc0012-tag] outputId ${outputId} not visible via memory:list; cannot inspect tags`);
103
- return softSkip('blocked', '[rfc0012-tag] outputId … not visible via memory:list; cannot inspect tags');
107
+ // unfailable-leg audit wave 2, 2026-09-27: this return follows the seed/
108
+ // compact asserts, so a plain softSkip recorded a partial-witness
109
+ // `executed-pass` — a host whose compacted entry was never inspectable
110
+ // passed the tag requirement unobserved. Now `blocked`.
111
+ return blockedDespiteAssertions('[rfc0012-tag] outputId … not visible via memory:list; cannot inspect tags');
104
112
  }
105
113
  const tags = output.tags ?? [];
106
114
 
@@ -45,8 +45,8 @@ describe.skipIf(SKIP)('orchestratorDispatch: supervisor → dispatch → next-wo
45
45
  expect(terminal.status).toBe('completed');
46
46
 
47
47
  const events = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events`);
48
- const list = (events.json as { events?: Array<{ type: string; payload?: Record<string, unknown> }> })
49
- .events ?? [];
48
+ const list = ((events.json as { events?: Array<{ type: string; sequence?: number; nodeId?: string; payload?: Record<string, unknown> }> })
49
+ .events ?? []).slice().sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0));
50
50
 
51
51
  const decisions = list.filter((e) => e.type === 'runOrchestrator.decided');
52
52
  expect(decisions.length).toBeGreaterThan(0);
@@ -64,5 +64,21 @@ describe.skipIf(SKIP)('orchestratorDispatch: supervisor → dispatch → next-wo
64
64
  expect(decision.kind).toBe('next-worker');
65
65
  expect(Array.isArray(decision.nextWorkerIds)).toBe(true);
66
66
  expect(decision.nextWorkerIds.length).toBeGreaterThanOrEqual(1);
67
+
68
+ // unfailable-leg audit wave 2, 2026-09-27: the leg is named "decided
69
+ // {next-worker} BETWEEN supervisor + dispatch" but never read an order —
70
+ // a host that emitted the decision AFTER the dispatch node had already
71
+ // started (or never started the dispatch node at all) passed. The
72
+ // next-worker decision MUST precede the dispatch node's first node.started.
73
+ const nodeIdOf = (e: { nodeId?: string; payload?: Record<string, unknown> }): unknown => e.nodeId ?? e.payload?.nodeId;
74
+ const dispatchStarted = list.find((e) => e.type === 'node.started' && nodeIdOf(e) === 'dispatch');
75
+ expect(
76
+ dispatchStarted,
77
+ req('openwop.it.orchestratorDispatch.emits-runorchestrator-decided-next-worker-between-supervisor-dispatch', 'RFCS/0006-orchestrator.md', 'the dispatch node MUST emit node.started after the supervisor decides next-worker'),
78
+ ).toBeDefined();
79
+ expect(
80
+ typeof nextWorker!.sequence === 'number' && typeof dispatchStarted?.sequence === 'number' && nextWorker!.sequence < dispatchStarted.sequence,
81
+ req('openwop.it.orchestratorDispatch.emits-runorchestrator-decided-next-worker-between-supervisor-dispatch', 'RFCS/0006-orchestrator.md', 'runOrchestrator.decided{next-worker} MUST precede node.started{dispatch} in event-log sequence'),
82
+ ).toBe(true);
67
83
  });
68
84
  });
@@ -208,7 +208,14 @@ describe('pack-registry: read-endpoint shape contracts', () => {
208
208
  'node-packs.md §"GET /v1/packs/{name}/-/{version}.sig"',
209
209
  '404 response MUST carry a string `error` field — `signature_not_available` is the canonical code',
210
210
  )).toBe('string');
211
- expect((body?.error as string).length).toBeGreaterThan(0);
211
+ // unfailable-leg audit wave 2, 2026-09-27: any non-empty `error` string
212
+ // (e.g. `not_found`) previously passed; node-packs.md names
213
+ // `404 signature_not_available` for all four cases (missing / yanked /
214
+ // unsigned / storage-unwired), intentionally indistinguishable.
215
+ expect(body?.error, req('openwop.it.pack-registry.get-v1-packs-name-version-sig-returns-404-signature-not-available-for-nonexisten',
216
+ 'node-packs.md §"GET /v1/packs/{name}/-/{version}.sig"',
217
+ 'nonexistent (name, version) MUST return 404 `signature_not_available`',
218
+ )).toBe('signature_not_available');
212
219
  });
213
220
 
214
221
  it('GET /v1/packs/{bad-name}/-/{version}.json returns 400 invalid_pack_name', async () => {
@@ -42,7 +42,7 @@ import { behaviorGate } from '../lib/behavior-gate.js';
42
42
  import { isFixtureAdvertised } from '../lib/fixtures.js';
43
43
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
44
44
  import { req } from '../lib/requirement-ids.js';
45
- import { softSkip } from '../lib/soft-skip.js';
45
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
46
46
 
47
47
  interface BackpressureCaps {
48
48
  supported?: boolean;
@@ -212,7 +212,10 @@ describe('production-backpressure: 503 envelope under saturation', () => {
212
212
  }
213
213
  }
214
214
  await Promise.allSettled(slotPromises);
215
- return softSkip('blocked', 'precondition not met — `saturationEarlyExit` returned early (seam, prior step, or fixture unavailable)');
215
+ // unfailable-leg audit wave 2, 2026-09-27: this plain softSkip came
216
+ // AFTER up to cap-1 `create.status === 201` assertions, so the row was
217
+ // a partial-witness PASS although no 503 envelope was ever observed.
218
+ return blockedDespiteAssertions('inflight cap already saturated by parallel runs before the cap+1 probe (run with --no-file-parallelism) — 503 envelope not observed');
216
219
  }
217
220
 
218
221
  // Let SSE connections register.
@@ -233,7 +236,10 @@ describe('production-backpressure: 503 envelope under saturation', () => {
233
236
  console.warn(
234
237
  `[production-backpressure] expected 503 at cap+1=${cap + 1}, got ${blocked.status}; saturation may need tuning`,
235
238
  );
236
- return softSkip('blocked', 'precondition not met — `blocked.status !== 503` returned early ([production-backpressure] expected 503 at cap+1=…, got …; saturation may need tuning) (seam, prior step, or fixture unavailable)');
239
+ // unfailable-leg audit wave 2, 2026-09-27: after `cap` × 201 asserts a
240
+ // plain softSkip recorded a partial-witness PASS — a host that never
241
+ // 503s at cap+1 passed the 503-envelope leg. Now `blocked`.
242
+ return blockedDespiteAssertions(`expected 503 at cap+1=${cap + 1}, got ${blocked.status} — saturation not reached (SSE slots may not have registered); 503 envelope not observed`);
237
243
  }
238
244
 
239
245
  const retryAfterHeader = blocked.headers.get('retry-after');
@@ -318,14 +324,14 @@ describe('production-backpressure: discovery exempt from cap', () => {
318
324
 
319
325
  const cap = prod?.backpressure?.inflightCap;
320
326
  if (cap === undefined) {
321
- // Without advertised cap we can't saturate deterministically;
322
- // fall back to a single discovery probe.
323
- const probe = await driver.get('/.well-known/openwop');
324
- expect(probe.status, req('openwop.it.production-backpressure.get-well-known-openwop-returns-200-even-when-inflight-is-saturated',
325
- 'production-profile.md §Backpressure',
326
- 'discovery MUST answer regardless of load',
327
- )).toBe(200);
328
- return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap === undefined` returned early');
327
+ // unfailable-leg audit wave 2, 2026-09-27: this used to take one UNLOADED
328
+ // discovery probe and then soft-skip, which recorded a partial-witness PASS
329
+ // for "returns 200 even when inflight is saturated", although nothing was
330
+ // saturated. The cap is OPTIONAL, so a host without one has nothing to
331
+ // saturate and the requirement does not bind it: `inapplicable`, before
332
+ // any assertion. It is not `blocked`, which would deny certification to a
333
+ // conforming host for a surface it may decline.
334
+ return softSkip('inapplicable', 'host does not advertise production.backpressure.inflightCap — there is no cap to saturate, so discovery-under-saturation does not bind');
329
335
  }
330
336
 
331
337
  // Issue many concurrent discovery probes; all MUST succeed.
@@ -187,6 +187,9 @@ describe('redaction: 401 response MUST NOT echo invalid Bearer token (NFR-7)', (
187
187
 
188
188
  describe.skipIf(SKIP_NO_NOOP)('redaction: credentialRef value MUST NOT appear in event payloads (gated on secrets.supported)', () => {
189
189
  it('skips when host does NOT advertise secrets.supported', async () => {
190
+ // Names the row id first (the registry takes the FIRST req() in a body as
191
+ // the explicitId) now that the tautological assert below is gone.
192
+ req('openwop.it.redaction.skips-when-host-does-not-advertise-secrets-supported', 'capabilities.md', 'a credentialRef value MUST NOT appear in any RunEvent payload');
190
193
  const cap = await driver.get('/.well-known/openwop', { authenticated: false });
191
194
  const supported =
192
195
  (cap.json as { secrets?: { supported?: boolean } } | undefined)?.secrets
@@ -194,8 +197,11 @@ describe.skipIf(SKIP_NO_NOOP)('redaction: credentialRef value MUST NOT appear in
194
197
 
195
198
  if (supported !== true) {
196
199
  // Spec-allowed — this scenario only applies to hosts that opt
197
- // into BYOK. Pass trivially.
198
- expect(supported, req('openwop.it.redaction.skips-when-host-does-not-advertise-secrets-supported', 'capabilities.md', 'skips when host does NOT advertise secrets.supported')).not.toBe(true);
200
+ // into BYOK. unfailable-leg audit wave 2, 2026-09-27: a tautological
201
+ // `expect(supported).not.toBe(true)` inside `if (supported !== true)`
202
+ // turned every non-BYOK host's row into an executed PASS of the
203
+ // credentialRef no-leak requirement; with zero assertions the row now
204
+ // records `inapplicable`.
199
205
  return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `supported !== true` returned early');
200
206
  }
201
207
 
@@ -88,7 +88,7 @@
88
88
 
89
89
  import { afterEach, describe, expect, it } from 'vitest';
90
90
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
91
- import { softSkip } from '../lib/soft-skip.js';
91
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
92
92
  import { driver } from '../lib/driver.js';
93
93
  import { forkDeclined } from '../lib/fork-availability.js';
94
94
  import { discoveryFamilies, readCapabilityFamily } from '../lib/discovery-capabilities.js';
@@ -313,7 +313,7 @@ describe('replay-fanout-suppression: a replay fork MUST NOT fan out re-emitted e
313
313
  }
314
314
  expect(replay.status, req('openwop.it.replay-fanout-suppression.delivers-for-a-live-run-suppresses-for-a-replay-fork-and-delivers-again-for-a-br', 'webhooks.md §"Register"', 'replay fork should be accepted')).toBe(201);
315
315
  const replayRunId = (replay.json as { runId: string }).runId;
316
- await pollUntilTerminal(replayRunId, { timeoutMs: FORK_POLL_MS });
316
+ const replayTerminal = await pollUntilTerminal(replayRunId, { timeoutMs: FORK_POLL_MS });
317
317
  await quietWindow(SUPPRESSION_WINDOW_MS);
318
318
 
319
319
  expect(
@@ -342,6 +342,22 @@ describe('replay-fanout-suppression: a replay fork MUST NOT fan out re-emitted e
342
342
  ),
343
343
  ).toBeGreaterThan(0);
344
344
 
345
+ // unfailable-leg audit wave 2, 2026-09-27: a non-empty fork log was the
346
+ // only re-emission check, so a replay fork that FAILED (or never reached
347
+ // `run.completed`, the one event the webhook subscribes to) delivered
348
+ // nothing because it never re-emitted the subscribed event — and was
349
+ // recorded `executed-pass` for the suppression MUST NOT. The fork must
350
+ // terminate `completed` with `run.completed` in its own log before the
351
+ // absence of deliveries is evidence; otherwise `blocked`.
352
+ const forkReemittedCompleted = forkEventList.some((e) => e.type === 'run.completed');
353
+ if (replayTerminal.status !== 'completed' || !forkReemittedCompleted) {
354
+ const why = `replay fork ${replayRunId} terminated ${String(replayTerminal.status)}`
355
+ + `${forkReemittedCompleted ? '' : ' without run.completed in its event log'} — the subscribed event was never re-emitted, `
356
+ + 'so the absence of deliveries proves nothing';
357
+ recordRequirement(REQUIREMENT_ID, 'blocked', why);
358
+ return blockedDespiteAssertions(why);
359
+ }
360
+
345
361
  // The MUST NOT has now been exercised against wiring proven to deliver, on a
346
362
  // fork proven to have re-emitted. Recorded here rather than after leg 3,
347
363
  // because leg 3 pins the boundary and may legitimately not run.
@@ -270,11 +270,16 @@ describe('RFC 0148 §A (S6) — the live sink, when this run was given one', ()
270
270
  it('files that finished before this one appear in the ledger file with an assertion count', () => {
271
271
  const path = process.env['OPENWOP_LEDGER_PATH'];
272
272
  if (!path || !existsSync(path)) return softSkip('blocked', 'precondition not met — `!path || !existsSync(path)` returned early (not a --certify run; nothing to inspect) (seam, prior step, or fixture unavailable)'); // not a --certify run; nothing to inspect
273
- const entries = readLedgerFile(path);
274
273
  // At least the in-memory ledger of THIS worker has file-level entries for
275
274
  // earlier files (setup.ts afterAll), and each carries assertionCount.
275
+ //
276
+ // unfailable-leg audit wave 2, 2026-09-27: the leg ended with
277
+ // `expect(entries.length).toBeGreaterThanOrEqual(0)` — a tautology (and a
278
+ // bare expect with no req()) that made the leg an `executed-pass` even when
279
+ // `fileEntries` was empty and the loop asserted nothing. Removed; an empty
280
+ // set now says so instead of passing vacuously.
276
281
  const fileEntries = snapshot().filter((e) => e.requirementId.startsWith('openwop.scenario.') || e.requirementId.startsWith('openwop.floor.'));
282
+ if (fileEntries.length === 0) return softSkip('inapplicable', 'no file-level ledger entries recorded in this worker before this leg — nothing to inspect');
277
283
  for (const e of fileEntries) expect(typeof e.assertionCount, req('openwop.it.runner-ledger.files-that-finished-before-this-one-appear-in-the-ledger-file-with-an-assertion', 'RFC 0148 §A', 'files that finished before this one appear in the ledger file with an assertion count')).toBe('number');
278
- expect(entries.length).toBeGreaterThanOrEqual(0);
279
284
  });
280
285
  });
@@ -132,6 +132,33 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-modes: debug emits at least as many eve
132
132
  'debug stream event count MUST be >= updates stream event count',
133
133
  )).toBeGreaterThanOrEqual(updatesResult.events.length);
134
134
 
135
+ // unfailable-leg audit wave 2, 2026-09-27: superset-by-COUNT alone passed a
136
+ // host whose debug stream dropped canonical types (e.g. no run.completed)
137
+ // as long as it padded the count with other frames (log.appended,
138
+ // keep-alive-ish events). The mapping table is per TYPE: every type the
139
+ // updates stream carried MUST appear in debug, and debug MUST carry
140
+ // node.started (✅ debug / — updates) — the conformance-delay fixture has
141
+ // one node (`wait`), so a debug stream without node.started is filtering.
142
+ const typeOf = (e: SseEvent): string => {
143
+ if (e.event !== 'message') return e.event;
144
+ try {
145
+ const t = (JSON.parse(e.data) as { type?: unknown }).type;
146
+ return typeof t === 'string' ? t : e.event;
147
+ } catch {
148
+ return e.event;
149
+ }
150
+ };
151
+ const debugTypes = new Set(debugResult.events.map(typeOf));
152
+ const missingFromDebug = [...new Set(updatesResult.events.map(typeOf))].filter((t) => !debugTypes.has(t));
153
+ expect(missingFromDebug, req('openwop.it.stream-modes.debug-stream-is-a-superset-of-updates-per-stream-modes-md-mode-mapping',
154
+ 'stream-modes.md §Mode-to-event mapping',
155
+ 'every event type emitted in updates mode is also ✅ in debug mode — debug MUST carry each of them',
156
+ )).toEqual([]);
157
+ expect([...debugTypes], req('openwop.it.stream-modes.debug-stream-is-a-superset-of-updates-per-stream-modes-md-mode-mapping',
158
+ 'stream-modes.md §Mode-to-event mapping',
159
+ 'debug mode MUST emit node.started (✅ debug) for the fixture\'s node',
160
+ )).toContain('node.started');
161
+
135
162
  expect(debugResult.closedBy, req('openwop.it.stream-modes.debug-stream-is-a-superset-of-updates-per-stream-modes-md-mode-mapping',
136
163
  'stream-modes.md §debug',
137
164
  'debug stream MUST close on terminal event',
@@ -55,7 +55,7 @@ import {
55
55
  TOOL_CONTENT_FORBIDDEN,
56
56
  } from '../lib/toolCatalog.js';
57
57
  import { req } from '../lib/requirement-ids.js';
58
- import { softSkip } from '../lib/soft-skip.js';
58
+ import { softSkip, seamAbsent } from '../lib/soft-skip.js';
59
59
 
60
60
  function loadSchema(name: string): Record<string, unknown> {
61
61
  return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
@@ -92,8 +92,23 @@ describe('tool-catalog-projection (RFC 0078 §B/§F)', () => {
92
92
  ).toBe(true);
93
93
 
94
94
  // ---- Leg 1: the list (§B) -------------------------------------------
95
- const tools = await listTools();
96
- if (tools === null) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `tools === null` returned early (host advertises the cap but doesn\'t serve the read — soft-skip the rest)'); // host advertises the cap but doesn't serve the read — soft-skip the rest
95
+ // unfailable-leg audit wave 2, 2026-09-27: a host that ADVERTISES the
96
+ // catalog but 404s the list (or answers 500 / a non-list 200, which the
97
+ // old `listTools` read as `[]`) previously recorded a partial-witness pass
98
+ // after the 401 assertion. An unserved read is now `seamAbsent` (blocked;
99
+ // fails under OPENWOP_REQUIRE_BEHAVIOR=true), and any other non-200 or
100
+ // non-list body FAILS.
101
+ const read = await listTools();
102
+ if (read.unserved) return seamAbsent(`host advertises toolCatalog but ${toolsPath()} answered ${read.status}`);
103
+ expect(
104
+ read.status,
105
+ req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', `GET ${toolsPath()} MUST return 200 to an authenticated caller on a host advertising toolCatalog`),
106
+ ).toBe(200);
107
+ expect(
108
+ read.tools !== null,
109
+ req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', `GET ${toolsPath()} MUST return a ToolDescriptor[]`),
110
+ ).toBe(true);
111
+ const tools = read.tools ?? [];
97
112
 
98
113
  for (const t of tools) {
99
114
  const v = validate(t);
@@ -116,12 +131,18 @@ describe('tool-catalog-projection (RFC 0078 §B/§F)', () => {
116
131
  if (tools.length > 0 && typeof tools[0]!.toolId === 'string') {
117
132
  const id = tools[0]!.toolId as string;
118
133
  const one = await getTool(id);
119
- if (one.status === 200) {
120
- expect(
121
- one.descriptor?.toolId === id,
122
- req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', 'GET /v1/tools/{toolId} MUST return the requested descriptor'),
123
- ).toBe(true);
124
- }
134
+ // unfailable-leg audit wave 2, 2026-09-27: the by-id check used to run
135
+ // only `if (one.status === 200)`, so a host that 404'd (or 500'd) the
136
+ // by-id read of a tool its own list returned passed. A listed tool is
137
+ // one the caller may see; its by-id read MUST serve it.
138
+ expect(
139
+ one.status,
140
+ req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', 'GET /v1/tools/{toolId} MUST return 200 for a toolId the caller\'s own list returned'),
141
+ ).toBe(200);
142
+ expect(
143
+ one.descriptor?.toolId === id,
144
+ req('openwop.it.tool-catalog-projection.lists-schema-valid-tooldescriptors-serves-by-id-404s-is-auth-gated-and-never-dis', 'tool-catalog.md §B', 'GET /v1/tools/{toolId} MUST return the requested descriptor'),
145
+ ).toBe(true);
125
146
  }
126
147
  const unknown = await getTool('__conformance_nonexistent_tool__');
127
148
  expect(
@@ -142,9 +163,11 @@ describe('tool-catalog-projection (RFC 0078 §B/§F)', () => {
142
163
 
143
164
  it('advisory (SHOULD): an unchanged catalog reads identically, sorted by toolId', async () => {
144
165
  if (!(await toolCatalogGate('openwop-tool-catalog'))) return;
145
- const a = await listTools();
146
- const b = await listTools();
147
- if (a === null || b === null) return softSkip('inapplicable', 'the host advertises the catalog but does not serve the list read');
166
+ const ra = await listTools();
167
+ const rb = await listTools();
168
+ const a = ra.tools;
169
+ const b = rb.tools;
170
+ if (a === null || b === null) return softSkip('inapplicable', `the list read did not return a ToolDescriptor[] (status ${ra.status}/${rb.status}); the normative leg above records that — this advisory leg is never failed`);
148
171
  const ids = (l: ReadonlyArray<{ toolId?: unknown }>) => l.map((t) => String(t.toolId));
149
172
  const first = ids(a); const second = ids(b);
150
173
  const sorted = [...first].sort((x, y) => (x < y ? -1 : x > y ? 1 : 0));
@@ -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';
@@ -109,11 +110,19 @@ describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)',
109
110
  if (!doc) return softSkip('blocked', 'discovery unreachable');
110
111
  if (!(await gateFamily('idempotency'))) return softSkip('inapplicable', 'idempotency family not advertised — no Layer-2 obligation (gate recorded under openwop.family.idempotency)');
111
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');
112
- // The seam is catalogued (api/seams-v2.yaml `forceEffectTransportRetry`). An
113
- // unreachable providerUrl forces the transport retry; the witness is the
114
- // host's own Layer-2 ledger (RFC 0173 §C.2), where every attempt of one
115
- // effect MUST carry the same business-identity key.
116
- 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 });
117
126
  if (fired.status === 404 || fired.status === 403 || fired.status === 405) {
118
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`);
119
128
  }
@@ -166,5 +175,30 @@ describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)',
166
175
  keyings.size,
167
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(', ')}`),
168
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
+ }
169
203
  });
170
204
  });
@@ -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 { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
31
+ import { 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';
@@ -154,11 +154,20 @@ describe('v2-era-2-append-vocabulary (RFC 0176 §A — the writer rule)', () =>
154
154
  // positive witness: the stored spelling was the v1 one the reader maps
155
155
  // from. When every appended type is spelled the same in both vocabularies,
156
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.
157
+ // writer rule is unobserved.
158
158
  const renamedV2 = new Set([...codemapV1toV2()].filter(([v1, v2]) => v1 !== v2).map(([, v2]) => v2));
159
159
  const appendedTypes = rows.slice(seedCount).map((r) => String(r.type ?? ''));
160
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.`);
161
+ // partial-witness-ok: HOTFIX 2.42.5 (2026-09-27). 2.42.3 and 2.42.4 made this
162
+ // `blockedDespiteAssertions`, which is bundle-fatal (RFC 0168 §E.1). No catalogued
163
+ // seam lets ANY host make a seeded era-2 run append a type the codemap renames,
164
+ // so the row blocked on every host and no host could certify. That is the suite's
165
+ // gap, not a host's. The append, the read-back and the sequence rules above ARE
166
+ // observed. The codemap-renaming writer rule is NOT, and this partial witness says
167
+ // so in its detail; the acceptance predicate refuses a partial-witness row. The
168
+ // seams-contract addition that makes the rule witnessable is in progress
169
+ // (openwop-1f); when it lands, this becomes a real assertion again.
170
+ return softSkip('inapplicable', `partial witness — the codemap-renaming writer rule is UNWITNESSED: 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. A seams-contract addition to seed one is in progress.`);
162
171
  }
163
172
  });
164
173
 
@@ -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' });
@@ -307,7 +307,16 @@ describe('RFC 0198 — v2-mcp-tasks (MCP Tasks on the server mount; disconnect c
307
307
  const f = await createTask(m.url, 'conformance-failure');
308
308
  let terminal: Rpc;
309
309
  if (f.result?.['resultType'] === 'task') terminal = await pollTask(m.url, String(f.result['taskId']), (s) => ['completed', 'failed', 'cancelled'].includes(s), 10_000);
310
- 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
+ }
311
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]);
312
321
  });
313
322
 
@@ -497,13 +506,18 @@ describe('RFC 0198 — v2-mcp-tasks (MCP Tasks on the server mount; disconnect c
497
506
  const ac = new AbortController();
498
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 } }, {})) });
499
508
  const runId = await freshRun('conformance-cancellable', before, 3000);
500
- expect(runId, req(ID_NO_SERVER_CANCELLED, 'interop-map.json mcp.methods tools/call', 'the tools/call started a run listRuns shows')).not.toBeNull();
501
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.
502
515
  if (!(res.headers.get('content-type') ?? '').includes('text/event-stream')) {
503
516
  ac.abort();
504
- await driver.post(`/runs/${encodeURIComponent(runId!)}/cancel`, {});
517
+ if (runId !== null) await driver.post(`/runs/${encodeURIComponent(runId)}/cancel`, {});
505
518
  return softSkip('inapplicable', 'the mount answered the blocking call as JSON, not SSE — a JSON response cannot carry a notifications/cancelled frame');
506
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();
507
521
  const cancel = await driver.post(`/runs/${encodeURIComponent(runId!)}/cancel`, {});
508
522
  expect(cancel.status, req(ID_NO_SERVER_CANCELLED, 'runs.md §Cancel', `REST cancelRun is accepted (got ${cancel.status})`)).toBeLessThan(300);
509
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);
@@ -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 () => {