@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
@@ -194,7 +194,13 @@ describe('mcp-tool-roundtrip: server wire shape', () => {
194
194
  );
195
195
  return softSkip('blocked', 'precondition not met — `!probe` returned early (seam, prior step, or fixture unavailable)');
196
196
  }
197
- if (!probe.isReal) getMcpFakeServer()!.reset();
197
+ // unfailable-leg audit wave 2, 2026-09-27: with only the in-process fake
198
+ // configured, this leg probed the SUITE'S OWN fake MCP server and recorded
199
+ // `executed-pass` — every host passed it, conforming or not, because the
200
+ // host under test was never contacted. The fake-path self-check is now
201
+ // `inapplicable` before any assertion (the host-mediated leg below is the
202
+ // one that exercises the host against the fake).
203
+ if (!probe.isReal) return softSkip('inapplicable', 'suite fixture self-test — host not exercised (only the in-process MCP fake is configured; set OPENWOP_MCP_REAL_SERVER_URL for real-server interop evidence)');
198
204
 
199
205
  // Per MCP `initialize` spec, params MUST carry protocolVersion +
200
206
  // capabilities + clientInfo. The in-process fake accepts empty
@@ -258,26 +264,6 @@ describe('mcp-tool-roundtrip: server wire shape', () => {
258
264
  `[mcp-tool-roundtrip] real-server interop OK against ${probe.url} ` +
259
265
  `(tool=${first?.name}, isError=${callResult.isError === true})`,
260
266
  );
261
- } else {
262
- // Fake-server path: deterministic echo tool, assert verbatim.
263
- expect(listResult.tools?.some((t) => t.name === 'echo')).toBe(true);
264
-
265
- const call = await postJsonRpc(
266
- probe.url,
267
- 'tools/call',
268
- { name: 'echo', arguments: { text: 'hello-from-conformance' } },
269
- 3,
270
- );
271
- expect(call.status).toBe(200);
272
- const callResult = (call.json.result ?? {}) as {
273
- content?: ReadonlyArray<{ type?: string; text?: string }>;
274
- };
275
- expect(callResult.content?.[0]?.type).toBe('text');
276
- expect(callResult.content?.[0]?.text).toBe('hello-from-conformance');
277
-
278
- const fake = getMcpFakeServer()!;
279
- const methods = fake.invocations().map((i) => i.method);
280
- expect(methods).toEqual(['initialize', 'tools/list', 'tools/call']);
281
267
  }
282
268
  });
283
269
  });
@@ -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));
@@ -65,9 +65,16 @@ describe('v2-advertised-fixtures-exist (RFC 0168 §C.3)', () => {
65
65
  const ids = advertisedIds(doc);
66
66
  if (ids.length === 0) return softSkip('inapplicable', 'the host advertises no fixtures[]');
67
67
 
68
- // Deterministic sample: the first N by sort order, so a failure is
68
+ // Deterministic sample spread across the sorted list, so a failure is
69
69
  // reproducible and a host cannot pass by luck of ordering.
70
- const sample = [...ids].sort().slice(0, SAMPLE);
70
+ // Unfailable-leg audit, 2026-09-26: the sample was the first N by sort
71
+ // order, so an unseeded fixture sorting 6th or later was never attempted.
72
+ // Indices round(i*(n-1)/(SAMPLE-1)), deduped, reach both ends and between.
73
+ const sorted = [...new Set(ids)].sort();
74
+ const picks = sorted.length <= SAMPLE
75
+ ? sorted.map((_, i) => i)
76
+ : Array.from({ length: SAMPLE }, (_, i) => Math.round((i * (sorted.length - 1)) / (SAMPLE - 1)));
77
+ const sample = [...new Set(picks)].map((i) => sorted[i]!);
71
78
  const unreachable: string[] = [];
72
79
  let attempted = 0;
73
80
  for (const id of sample) {
@@ -79,7 +86,14 @@ describe('v2-advertised-fixtures-exist (RFC 0168 §C.3)', () => {
79
86
  // 201 is the claim honoured. A 4xx that names the workflow as unknown is
80
87
  // the drift this leg exists to catch; any other status is a different
81
88
  // problem and is not judged here.
82
- if (res.status !== 201) unreachable.push(`${id} → ${res.status}`);
89
+ // The comment above always said only an unknown-workflow refusal is
90
+ // judged, but the code counted ANY non-201. The 2026-09-26 audit's wider
91
+ // sample then hit fixtures that need an Idempotency-Key or specific
92
+ // inputs (a validation 4xx) and failed a conforming host. v2 has no
93
+ // dedicated unknown-workflow code, so the signal is 404, or a 4xx whose
94
+ // details.field names workflowId.
95
+ const field = (res.json as { details?: { field?: unknown } } | null)?.details?.field;
96
+ if (res.status === 404 || (res.status >= 400 && res.status < 500 && field === 'workflowId')) unreachable.push(`${id} → ${res.status}`);
83
97
  }
84
98
  if (attempted === 0) return softSkip('blocked', 'no advertised fixture could be attempted');
85
99
  expect(
@@ -118,12 +118,18 @@ describe('RFC 0205 §A.3 — a url Part does not resolve beyond the caller\'s au
118
118
  const tenantB = process.env['OPENWOP_TEST_TENANT_B_API_KEY'];
119
119
  if (!tenantB) return softSkip('blocked', 'the artifact carries a url Part but OPENWOP_TEST_TENANT_B_API_KEY is not supplied — the second-tenant leg cannot run');
120
120
  const base = loadEnv().baseUrl;
121
+ // unfailable-leg audit, 2026-09-26: the probes use redirect:'manual' and
122
+ // only failed on a 2xx, so a url that answered 302 to a public pre-signed
123
+ // object-store URL — readable by tenant B and by anyone — passed. A 3xx is
124
+ // the host resolving the url for that caller; the only answers that do not
125
+ // resolve it are a refusal (401/403) or a non-disclosing 404.
126
+ const REFUSED = new Set([401, 403, 404]);
121
127
  for (const u of urls) {
122
128
  const abs = new URL(u, base).toString();
123
129
  const asB = await fetch(abs, { headers: { Authorization: `Bearer ${tenantB}` }, redirect: 'manual' });
124
- expect(asB.status >= 200 && asB.status < 300, req('openwop.requirement.0205.artifact-url-part-scoped', 'RFC 0205 §A.3', `${abs} MUST NOT answer 2xx to tenant B's credential (got ${asB.status})`)).toBe(false);
130
+ expect(REFUSED.has(asB.status), req('openwop.requirement.0205.artifact-url-part-scoped', 'RFC 0205 §A.3', `${abs} MUST NOT resolve for tenant B's credential: expected 401/403/404, got ${asB.status}${asB.status >= 300 && asB.status < 400 ? ` (a redirect to ${asB.headers.get('location') ?? 'no Location'} resolves the url)` : ''}`)).toBe(true);
125
131
  const anon = await fetch(abs, { redirect: 'manual' });
126
- expect(anon.status >= 200 && anon.status < 300, req('openwop.requirement.0205.artifact-url-part-scoped', 'RFC 0205 §A.3', `${abs} MUST NOT answer 2xx with no credential (got ${anon.status})`)).toBe(false);
132
+ expect(REFUSED.has(anon.status), req('openwop.requirement.0205.artifact-url-part-scoped', 'RFC 0205 §A.3', `${abs} MUST NOT resolve with no credential: expected 401/403/404, got ${anon.status}${anon.status >= 300 && anon.status < 400 ? ` (a redirect to ${anon.headers.get('location') ?? 'no Location'} resolves the url)` : ''}`)).toBe(true);
127
133
  }
128
134
  });
129
135
  });
@@ -221,9 +221,14 @@ describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
221
221
  const res = await http(() => driver.get(`/runs/${projectBoundId(runId)}/effects`));
222
222
  if (res === null) return softSkip('blocked', 'GET /runs/{runId}/effects unreachable (fetch failed)');
223
223
  if (res.status === 404 || res.status === 405) return softSkip('inapplicable', `the host advertises idempotency but serves no effect ledger at /runs/{runId}/effects (${res.status})`);
224
- expect(res.status, req(PER_KIND, 'security-defaults.md §Layer-2 effect identity', 'GET /runs/{runId}/effects MUST answer 200')).toBe(200);
225
224
  const effects = ((res.json as { effects?: Array<Record<string, unknown>> } | null)?.effects ?? []);
226
- if (effects.length === 0) return softSkip('inapplicable', 'the noop fixture records no effects — no effectId on the wire to read (a host with an effect-producing fixture witnesses this leg)');
225
+ // Unfailable-leg audit (2026-09-26): the status assert used to precede this
226
+ // empty check, so on every host (the noop fixture records no effects) the
227
+ // leg asserted 200 then soft-skipped — `executed-pass` partial-witness folded
228
+ // into the shared per-kind row with no effectId ever read. The empty 200 now
229
+ // returns before any assertion.
230
+ if (res.status === 200 && effects.length === 0) return softSkip('inapplicable', 'the noop fixture records no effects — no effectId on the wire to read (a host with an effect-producing fixture witnesses this leg)');
231
+ expect(res.status, req(PER_KIND, 'security-defaults.md §Layer-2 effect identity', 'GET /runs/{runId}/effects MUST answer 200')).toBe(200);
227
232
  for (const e of effects) expectBound(e['effectId'], 'effectId', 'GET /runs/{runId}/effects', PER_KIND);
228
233
  });
229
234
 
@@ -244,9 +249,13 @@ describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
244
249
  try {
245
250
  const res = await http(() => driver.get(`/webhooks/${projectBoundId(webhookId)}/dead-letters`));
246
251
  if (res === null) return softSkip('blocked', 'GET /webhooks/{webhookId}/dead-letters unreachable (fetch failed)');
247
- expect(res.status, req(PER_KIND, 'RFC 0188 §A.1', 'a host advertising webhooks.deadLetter MUST serve the dead-letter read (200)')).toBe(200);
248
252
  const rows = ((res.json as { deliveries?: Array<Record<string, unknown>> } | null)?.deliveries ?? []);
249
- if (rows.length === 0) return softSkip('inapplicable', 'the subscription has no dead-lettered delivery in this run — the read is served and the shape is unwitnessed here; v2-webhook-durable-delivery drives an exhaustion and asserts the record');
253
+ // Unfailable-leg audit (2026-09-26): as with effectId above, the 200 assert
254
+ // preceded the empty check, so a fresh subscription (always empty) recorded
255
+ // `executed-pass` partial-witness with no deliveryId read. Empty-200 now
256
+ // returns before any assertion.
257
+ if (res.status === 200 && rows.length === 0) return softSkip('inapplicable', 'the subscription has no dead-lettered delivery in this run — the read is served and the shape is unwitnessed here; v2-webhook-durable-delivery drives an exhaustion and asserts the record');
258
+ expect(res.status, req(PER_KIND, 'RFC 0188 §A.1', 'a host advertising webhooks.deadLetter MUST serve the dead-letter read (200)')).toBe(200);
250
259
  for (const r of rows) expectBound(r['deliveryId'], 'deliveryId', 'GET /webhooks/{webhookId}/dead-letters', PER_KIND);
251
260
  } finally {
252
261
  await http(() => driver.delete(`/webhooks/${projectBoundId(webhookId)}`));