@openwop/openwop-conformance 2.42.5 → 2.42.7

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 (123) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +3 -3
  3. package/coverage.md +1 -1
  4. package/dist/lib/soft-skip.js +13 -0
  5. package/dist/spec-artifacts.lock.json +2 -2
  6. package/package.json +2 -2
  7. package/requirements.json +430 -162
  8. package/scenario-majors.json +16 -3
  9. package/schemas/CORPUS-STAMP.json +64 -63
  10. package/src/lib/audit-checkpoint.ts +72 -0
  11. package/src/lib/era2-seed.ts +30 -0
  12. package/src/lib/ext-claims.ts +96 -0
  13. package/src/lib/soft-skip.ts +14 -0
  14. package/src/scenarios/a2a-peer-authority.test.ts +6 -13
  15. package/src/scenarios/a2a-task-roundtrip.test.ts +6 -34
  16. package/src/scenarios/a2a-version-negotiation.test.ts +6 -12
  17. package/src/scenarios/a2ui-surface-delta-transport.test.ts +23 -12
  18. package/src/scenarios/agent-channel-dispatch.test.ts +13 -4
  19. package/src/scenarios/agent-live-invocation-bracket.test.ts +1 -1
  20. package/src/scenarios/agent-manifest-runtime.test.ts +3 -0
  21. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -2
  22. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +2 -2
  23. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -2
  24. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -2
  25. package/src/scenarios/aiproviders-selfhosted-honesty.test.ts +2 -2
  26. package/src/scenarios/append-ordering.test.ts +26 -21
  27. package/src/scenarios/artifact-auth.test.ts +3 -1
  28. package/src/scenarios/artifact-type-registration-source.test.ts +2 -2
  29. package/src/scenarios/audit-checkpoint-signature.test.ts +63 -0
  30. package/src/scenarios/audit-checkpoint-vectors.test.ts +93 -0
  31. package/src/scenarios/auth-mtls.test.ts +7 -6
  32. package/src/scenarios/auth-subject-link-key-class.test.ts +3 -3
  33. package/src/scenarios/auth-subject-link.test.ts +19 -18
  34. package/src/scenarios/blob-presign-expiry.test.ts +2 -2
  35. package/src/scenarios/byok-roundtrip.test.ts +4 -1
  36. package/src/scenarios/compensation-behavior.test.ts +10 -5
  37. package/src/scenarios/conformance-advertised-seam-required.test.ts +6 -0
  38. package/src/scenarios/connection-pack-apihosts.test.ts +2 -2
  39. package/src/scenarios/connection-provider-resolution.test.ts +2 -2
  40. package/src/scenarios/context-budget-transcript-bound.test.ts +7 -3
  41. package/src/scenarios/context-summarization-replay.test.ts +2 -0
  42. package/src/scenarios/cost-attribution.test.ts +3 -1
  43. package/src/scenarios/debugBundle.test.ts +12 -4
  44. package/src/scenarios/discovery.test.ts +6 -2
  45. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +2 -2
  46. package/src/scenarios/dispatch-input-mapping.test.ts +2 -2
  47. package/src/scenarios/dispatch-output-mapping.test.ts +15 -2
  48. package/src/scenarios/distillation-token-budget.test.ts +2 -2
  49. package/src/scenarios/durability-poison-exhaustion.test.ts +2 -2
  50. package/src/scenarios/envelope-nl-to-format-engaged.test.ts +2 -2
  51. package/src/scenarios/envelope-recovery-applied.test.ts +2 -2
  52. package/src/scenarios/envelope-refusal-shape.test.ts +2 -2
  53. package/src/scenarios/envelope-retry-attempted.test.ts +2 -2
  54. package/src/scenarios/envelope-retry-exhausted.test.ts +2 -2
  55. package/src/scenarios/envelope-tier-one-subset-static.test.ts +1 -1
  56. package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +2 -2
  57. package/src/scenarios/eventOrdering.test.ts +17 -10
  58. package/src/scenarios/form-content-instantiation.test.ts +2 -2
  59. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +2 -2
  60. package/src/scenarios/highConcurrency.test.ts +9 -9
  61. package/src/scenarios/idempotency-concurrent-claim.test.ts +5 -5
  62. package/src/scenarios/idempotency-key-determinism.test.ts +8 -8
  63. package/src/scenarios/idempotency.test.ts +6 -6
  64. package/src/scenarios/idempotencyRetry.test.ts +16 -11
  65. package/src/scenarios/interrupt-token-matrix.test.ts +24 -11
  66. package/src/scenarios/mcp-cache-tenant-scope.test.ts +3 -3
  67. package/src/scenarios/mcp-current-auth-boundary.test.ts +9 -6
  68. package/src/scenarios/mcp-extension-opacity.test.ts +6 -5
  69. package/src/scenarios/mcp-mrtr-roundtrip.test.ts +9 -8
  70. package/src/scenarios/mcp-version-negotiation.test.ts +5 -10
  71. package/src/scenarios/media-url-inline-cap.test.ts +3 -2
  72. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -0
  73. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -2
  74. package/src/scenarios/memory-consolidation-idempotent.test.ts +2 -2
  75. package/src/scenarios/memory-injection-budget.test.ts +2 -2
  76. package/src/scenarios/multi-party-conversation-behavioral.test.ts +2 -2
  77. package/src/scenarios/multi-region-idempotency-behavior.test.ts +9 -9
  78. package/src/scenarios/multi-region-idempotency.test.ts +2 -2
  79. package/src/scenarios/pack-registry-isolation.test.ts +13 -12
  80. package/src/scenarios/pack-registry.test.ts +3 -2
  81. package/src/scenarios/pause-resume.test.ts +30 -11
  82. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +43 -34
  83. package/src/scenarios/provider-usage.test.ts +2 -2
  84. package/src/scenarios/redaction.test.ts +7 -5
  85. package/src/scenarios/redactionAdversarial.test.ts +5 -4
  86. package/src/scenarios/registry-public.test.ts +5 -1
  87. package/src/scenarios/replay-divergence-at-refusal.test.ts +10 -7
  88. package/src/scenarios/replay-fanout-suppression.test.ts +7 -4
  89. package/src/scenarios/replay-fork-arbitrary.test.ts +2 -2
  90. package/src/scenarios/replay-side-effect-suppression.test.ts +2 -2
  91. package/src/scenarios/replayDeterminism.test.ts +9 -4
  92. package/src/scenarios/runner-ledger.test.ts +4 -0
  93. package/src/scenarios/runtime-capabilities.test.ts +2 -0
  94. package/src/scenarios/speech-synthesis-roundtrip.test.ts +1 -1
  95. package/src/scenarios/streamReconnect.test.ts +2 -2
  96. package/src/scenarios/subrun-approval-gate.test.ts +2 -2
  97. package/src/scenarios/subworkflow-input-mapping.test.ts +2 -2
  98. package/src/scenarios/tool-catalog-projection.test.ts +10 -7
  99. package/src/scenarios/tool-session-lifecycle.test.ts +1 -1
  100. package/src/scenarios/trigger-ingestion.test.ts +2 -2
  101. package/src/scenarios/v2-a2ui-v09-surface.test.ts +15 -0
  102. package/src/scenarios/v2-effect-identity-business-key.test.ts +12 -0
  103. package/src/scenarios/v2-era-2-append-vocabulary.test.ts +39 -24
  104. package/src/scenarios/v2-ext-family-claims.test.ts +62 -0
  105. package/src/scenarios/v2-ext-rest-transport.test.ts +151 -0
  106. package/src/scenarios/v2-idempotency-in-flight.test.ts +3 -0
  107. package/src/scenarios/v2-legacy-profiles-absent.test.ts +4 -6
  108. package/src/scenarios/v2-mcp-tasks.test.ts +1 -1
  109. package/src/scenarios/v2-min-client-version.test.ts +2 -0
  110. package/src/scenarios/v2-refresh-sla.test.ts +6 -5
  111. package/src/scenarios/v2-sse-last-event-id-cursor.test.ts +3 -3
  112. package/src/scenarios/v2-sse-last-event-id.test.ts +2 -0
  113. package/src/scenarios/v2-version-header-honored.test.ts +3 -0
  114. package/src/scenarios/v2-webhook-durable-delivery.test.ts +2 -0
  115. package/src/scenarios/v2-webhook-secret-rotation.test.ts +8 -1
  116. package/src/scenarios/verifier-gating.test.ts +2 -2
  117. package/src/scenarios/version-fold.test.ts +2 -2
  118. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +7 -6
  119. package/src/scenarios/webhook-signed-delivery.test.ts +2 -0
  120. package/src/scenarios/workflow-chain-host-expansion.test.ts +10 -1
  121. package/src/scenarios/workflow-variable-format.test.ts +2 -2
  122. package/src/scenarios/workload-identity-chain-bounds.test.ts +6 -3
  123. package/vectors/audit-checkpoint-v1.json +260 -0
@@ -48,7 +48,7 @@ import { SCHEMAS_DIR } from '../lib/paths.js';
48
48
  import { driver } from '../lib/driver.js';
49
49
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
50
50
  import { req } from '../lib/requirement-ids.js';
51
- import { softSkip } from '../lib/soft-skip.js';
51
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
52
52
 
53
53
  const ajv = new Ajv2020({ strict: false, allErrors: true });
54
54
 
@@ -518,28 +518,33 @@ describe.skipIf(HTTP_SKIP)('a2ui-surface-delta-transport: live host delta transp
518
518
  const surfaceB = fullSurfaceMaterializedAfterUpdates();
519
519
 
520
520
  // Baseline (surface A) through the host's REAL surface-emit path.
521
+ // The two emits and the JSON event read are SETUP for this leg, and each is
522
+ // an early return rather than an assertion, so that a host on which the
523
+ // agreement is unobservable records that honestly instead of a
524
+ // partial-witness PASS (until 2026-09-27 the emit statuses were asserted
525
+ // first). JSON content negotiation on the events endpoint is OPTIONAL
526
+ // (stream-modes.md §"Content negotiation") and delivering a delta frame is
527
+ // MAY even under the advertisement (§"A2UI delta transport"), so an
528
+ // SSE-only host, or one that sends no frame, is conformant and out of reach
529
+ // of this leg: `inapplicable`, never `blocked`.
521
530
  const emitA = await driver.post('/v1/host/sample/a2ui/emit-surface', { runId, surface: surfaceA });
522
531
  if (emitA.status === 404 || emitA.status === 405) return softSkip('blocked', 'precondition not met — `emitA.status === 404 || emitA.status === 405` returned early (seam absent — soft-skip the live leg) (seam, prior step, or fixture unavailable)'); // seam absent — soft-skip the live leg
523
- expect(
524
- emitA.status,
525
- req('openwop.it.a2ui-surface-delta-transport.drives-the-emit-surface-seam-a-a2uidelta-1-subscriber-reconstruction-equals-the', 'RFC 0114 §15', 'the emit-surface seam MUST record the baseline full surface'),
526
- ).toBeLessThan(300);
532
+ if (emitA.status >= 300) return softSkip('blocked', `the emit-surface seam refused the baseline full surface (${emitA.status}) — it MUST record it (RFC 0114 §15), and nothing was transported`);
527
533
 
528
534
  // Second full surface (surface B) — recorded full AND transported as a delta
529
535
  // to any ?a2uiDelta=1 subscriber.
530
536
  const emitB = await driver.post('/v1/host/sample/a2ui/emit-surface', { runId, surface: surfaceB });
531
- if (emitB.status === 404 || emitB.status === 405) return softSkip('blocked', 'precondition not met — `emitB.status === 404 || emitB.status === 405` returned early (seam, prior step, or fixture unavailable)');
532
- expect(emitB.status, req('openwop.it.a2ui-surface-delta-transport.drives-the-emit-surface-seam-a-a2uidelta-1-subscriber-reconstruction-equals-the', 'RFC 0114 §15', 'the second emit MUST succeed')).toBeLessThan(300);
537
+ if (emitB.status >= 300) return softSkip('blocked', `the emit-surface seam accepted the baseline surface, then answered ${emitB.status} for the second — no update was transported`);
533
538
  const refB = (emitB.json as EmitSurfaceResponse)?.surfaceRef;
534
539
 
535
540
  // ?a2uiDelta=1 subscriber: locate the delta frame the host transported.
536
541
  const deltaEvents = await getEvents(runId, true);
537
- if (deltaEvents === null) return softSkip('blocked', 'precondition not met — `deltaEvents === null` returned early (events stream unavailable in JSON — soft-skip) (seam, prior step, or fixture unavailable)'); // events stream unavailable in JSON — soft-skip
542
+ if (deltaEvents === null) return softSkip('inapplicable', 'GET /v1/runs/{runId}/events?a2uiDelta=1 with Accept: application/json did not answer a JSON 200 — the host serves the events endpoint as SSE only, which is conformant (stream-modes.md §"Content negotiation"), and this leg reads JSON');
538
543
  const frames = collectMatching(deltaEvents, validateFrame)
539
544
  .map(readFrame)
540
545
  .filter((f): f is { surfaceRef: string; catalogVersion: string; patch: PatchOp[] } => f !== null)
541
546
  .filter((f) => refB === undefined || f.surfaceRef === refB);
542
- if (frames.length === 0) return softSkip('blocked', 'precondition not met — `frames.length === 0` returned early (host streams SSE-only or buffered the frame — soft-skip) (seam, prior step, or fixture unavailable)'); // host streams SSE-only or buffered the frame — soft-skip
547
+ if (frames.length === 0) return softSkip('inapplicable', 'the ?a2uiDelta=1 JSON read carried no delta frame for the update — delivering one is MAY (stream-modes.md §"A2UI delta transport"), so there is nothing to reconstruct');
543
548
  const frame = frames[frames.length - 1];
544
549
 
545
550
  // catalogVersion on the delta MUST equal the baseline full surface's.
@@ -557,9 +562,13 @@ describe.skipIf(HTTP_SKIP)('a2ui-surface-delta-transport: live host delta transp
557
562
 
558
563
  // Non-negotiating subscriber: the host materializes the FULL surface for the same update.
559
564
  const fullEvents = await getEvents(runId, false);
560
- if (fullEvents === null) return softSkip('blocked', 'precondition not met — `fullEvents === null` returned early (seam, prior step, or fixture unavailable)');
565
+ // The same endpoint just answered JSON for the ?a2uiDelta=1 read, so the host
566
+ // negotiates JSON; failing the plain read leaves the agreement unobserved.
567
+ if (fullEvents === null) return blockedDespiteAssertions('the non-negotiating GET /v1/runs/{runId}/events did not answer 200 with JSON — the materialized full surface was never read');
561
568
  const fulls = collectMatching(fullEvents, validateRecorded);
562
- if (fulls.length === 0) return softSkip('blocked', 'precondition not met — `fulls.length === 0` returned early (soft-skip — no full surface observed on the non-delta stream) (seam, prior step, or fixture unavailable)'); // soft-skip — no full surface observed on the non-delta stream
569
+ // ai-envelope.md §"Delta transport": without ?a2uiDelta=1 the host MUST
570
+ // deliver the materialized full surface, so a conforming host always has one here.
571
+ if (fulls.length === 0) return blockedDespiteAssertions('the non-negotiating event read carried no full ui.a2ui-surface — the delta reconstruction had nothing to be compared against');
563
572
  const materializedFull = fulls[fulls.length - 1];
564
573
 
565
574
  // delta and full agree — the core RFC 0114 transport guarantee, witnessed live.
@@ -590,7 +599,9 @@ describe.skipIf(HTTP_SKIP)('a2ui-surface-delta-transport: live host delta transp
590
599
  surface: { components: [{ component: 'iframe', src: 'https://evil.example/x' }] },
591
600
  };
592
601
  const rejected = await driver.post('/v1/host/sample/a2ui/emit-surface', { runId, surface: outOfCatalog });
593
- if (rejected.status === 404 || rejected.status === 405) return softSkip('blocked', 'precondition not met — `rejected.status === 404 || rejected.status === 405` returned early (seam, prior step, or fixture unavailable)');
602
+ // The seam accepted the baseline (asserted above), so a 404/405 here is the
603
+ // seam vanishing, not absent: the fail-closed rejection was never observed.
604
+ if (rejected.status === 404 || rejected.status === 405) return blockedDespiteAssertions(`the emit-surface seam accepted the baseline surface, then answered ${rejected.status} for the out-of-catalog one — the fail-closed rejection was never observed`);
594
605
  expect(
595
606
  rejected.status,
596
607
  req('openwop.it.a2ui-surface-delta-transport.the-emit-surface-seam-rejects-an-out-of-catalog-surface-fail-closed-real-catalog',
@@ -153,8 +153,9 @@ describe.skipIf(HTTP_SKIP)('agent-channel-dispatch (RFC 0082 §B): production ru
153
153
  { fromSeq: 0, mode: 'replay' },
154
154
  );
155
155
  if (forkDeclined(fork1.status, 'channel-dispatch replay fork 1')) {
156
- ctx.skip();
157
- return softSkip('blocked', 'precondition not met — `forkDeclined(fork1.status, \'channel-dispatch replay fork 1\')` returned early (seam, prior step, or fixture unavailable)');
156
+ // forkDeclined() already noted `blocked` with the status; ctx.skip()
157
+ // throws, so the row is `blocked` — nothing after it runs.
158
+ return ctx.skip();
158
159
  }
159
160
  expect(
160
161
  fork1.status,
@@ -198,6 +199,10 @@ describe.skipIf(HTTP_SKIP)('agent-channel-dispatch (RFC 0082 §B): production ru
198
199
  // evidence; the cross-move proof needs the seam. Honest skip of Leg 3.
199
200
  // eslint-disable-next-line no-console
200
201
  console.warn('[agent-channel-dispatch] deployment seam absent — skipping the channel-move non-re-resolution leg (Leg 3)');
202
+ // partial-witness-ok: Legs 1+2 observed the §B pin on a real run and the
203
+ // replay re-reading it; the deployment-transition seam is OPTIONAL
204
+ // (conformance-only), so a conforming host without it cannot show the
205
+ // channel-move half.
201
206
  return softSkip('blocked', 'precondition not met — `moved === null` returned early ([agent-channel-dispatch] deployment seam absent — skipping the channel-move non-re-resolution leg (Leg 3)) (seam, prior step, or fixture unavailable)');
202
207
  }
203
208
  // Confirm the move is OBSERVABLE: a fresh channel-bound run must now resolve
@@ -209,6 +214,9 @@ describe.skipIf(HTTP_SKIP)('agent-channel-dispatch (RFC 0082 §B): production ru
209
214
  if (typeof movedVersion !== 'string' || movedVersion === pinnedVersion) {
210
215
  // eslint-disable-next-line no-console
211
216
  console.warn('[agent-channel-dispatch] channel did not observably move — skipping Leg 3 strict assertion');
217
+ // partial-witness-ok: Legs 1+2 observed the §B pin and its replay
218
+ // re-read; a promote that does not move the head (canary split, no-op)
219
+ // is conforming, and without a moved head non-re-resolution has no contrast.
212
220
  return softSkip('blocked', 'precondition not met — `typeof movedVersion !== \'string\' || movedVersion === pinnedVersion` returned early ([agent-channel-dispatch] channel did not observably move — skipping Leg 3 strict assertion) (seam, prior step…');
213
221
  }
214
222
  const fork2 = await driver.post(
@@ -216,8 +224,9 @@ describe.skipIf(HTTP_SKIP)('agent-channel-dispatch (RFC 0082 §B): production ru
216
224
  { fromSeq: 0, mode: 'replay' },
217
225
  );
218
226
  if (forkDeclined(fork2.status, 'channel-dispatch replay fork 2')) {
219
- ctx.skip();
220
- return softSkip('blocked', 'precondition not met — `forkDeclined(fork2.status, \'channel-dispatch replay fork 2\')` returned early (seam, prior step, or fixture unavailable)');
227
+ // forkDeclined() already noted `blocked` with the status; ctx.skip()
228
+ // throws, so the row is `blocked` — nothing after it runs.
229
+ return ctx.skip();
221
230
  }
222
231
  expect(
223
232
  fork2.status,
@@ -50,7 +50,7 @@ describe('agent-live-invocation-bracket (RFC 0077 §E)', () => {
50
50
  started.length >= 1 && completed.length >= 1,
51
51
  req('openwop.it.agent-live-invocation-bracket.brackets-a-live-invocation-with-started-first-completed-last-matching-invocation', 'multi-agent-execution.md §"Live manifest dispatch"', 'a live invocation MUST emit agent.invocation.started + agent.invocation.completed'),
52
52
  ).toBe(true);
53
- if (started.length === 0 || completed.length === 0) return softSkip('blocked', 'precondition not met — `started.length === 0 || completed.length === 0` returned early (seam, prior step, or fixture unavailable)');
53
+ if (started.length === 0 || completed.length === 0) throw new Error('unreachable: the assertion above has already failed this leg');
54
54
 
55
55
  const start = started[0]!;
56
56
  const end = completed[completed.length - 1]!;
@@ -61,6 +61,9 @@ describe('agent-manifest-runtime (RFC 0070)', () => {
61
61
  // Empty is conformant; the cross-tenant no-disclosure 404 is covered by the
62
62
  // owner-triple isolation harness (RFC 0048/0059), not re-probed here. Nothing
63
63
  // to dispatch.
64
+ // partial-witness-ok: installScope, the served inventory and its array
65
+ // shape were observed; an empty tenant-scoped inventory is conformant,
66
+ // so the dispatch half has nothing to dispatch on this host.
64
67
  return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `agents.length === 0` returned early (RFC 0074 §A + Unresolved Q3 — tenant-scoped: GET /v1/agents is the authenticated principal\'s workspace set, which MAY be e…');
65
68
  }
66
69
 
@@ -168,7 +168,7 @@ describe('aiEnvelope.capBreached: behavioral cap enforcement (FINAL v1.1)', () =
168
168
  import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
169
169
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
170
170
  import { req } from '../lib/requirement-ids.js';
171
- import { softSkip } from '../lib/soft-skip.js';
171
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
172
172
 
173
173
  describe('aiEnvelope.capBreached: engine projection via event-log seam (capabilities.md §"cap.breached")', () => {
174
174
  it('breached outcome projects to cap.breached { kind: "envelopes" } event with causationId chain', async () => {
@@ -193,7 +193,7 @@ describe('aiEnvelope.capBreached: engine projection via event-log seam (capabili
193
193
  expect(r.body.status).toBe('breached');
194
194
 
195
195
  const events = await queryTestEvents(runId, { type: 'cap.breached' });
196
- if (!events.ok) return softSkip('blocked', 'precondition not met — `!events.ok` returned early (seam, prior step, or fixture unavailable)');
196
+ if (!events.ok) return blockedDespiteAssertions('the event-log seam is advertised but the cap.breached query failed — the projection is unobserved');
197
197
  expect(
198
198
  events.events.length,
199
199
  req('openwop.it.aiEnvelope.capBreached.breached-outcome-projects-to-cap-breached-kind-envelopes-event-with-causationid', 'capabilities.md §"Engine-enforced limits and the cap.breached event"', 'breached outcome MUST project to exactly one cap.breached event'),
@@ -143,7 +143,7 @@ describe('aiEnvelope.contractRefusal: behavioral accept-gate (FINAL v1.1)', () =
143
143
  // E.1 engine-projection via the test-only event-log seam.
144
144
  import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
145
145
  import { req } from '../lib/requirement-ids.js';
146
- import { softSkip } from '../lib/soft-skip.js';
146
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
147
147
 
148
148
  describe('aiEnvelope.contractRefusal: engine projection via event-log seam', () => {
149
149
  it('gated (fail-node) → node.failed { error.code: "envelope_contract_violation" }', async () => {
@@ -167,7 +167,7 @@ describe('aiEnvelope.contractRefusal: engine projection via event-log seam', ()
167
167
  if (r.status === 404) return softSkip('blocked', 'precondition not met — `r.status === 404` returned early (seam, prior step, or fixture unavailable)');
168
168
  expect(r.body.status).toBe('gated');
169
169
  const events = await queryTestEvents(runId, { type: 'node.failed' });
170
- if (!events.ok || events.events.length === 0) return softSkip('blocked', 'precondition not met — `!events.ok || events.events.length === 0` returned early (seam, prior step, or fixture unavailable)');
170
+ if (!events.ok || events.events.length === 0) return blockedDespiteAssertions('the gated outcome was accepted but no node.failed was read back from the event-log seam — the envelope_contract_violation projection is unobserved');
171
171
  const err = events.events[0]!.payload.error as { code?: string; details?: { refusedType?: string; acceptedTypes?: string[] } };
172
172
  expect(
173
173
  err.code,
@@ -191,7 +191,7 @@ import { queryTestSpans, isOtelSeamAvailable } from '../lib/otel-scrape.js';
191
191
  import { resetTestSeam } from '../lib/event-log-query.js';
192
192
  import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
193
193
  import { req } from '../lib/requirement-ids.js';
194
- import { softSkip } from '../lib/soft-skip.js';
194
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
195
195
 
196
196
  describe('aiEnvelope.schemaDrift: OTel drift attribute projection (E.2)', () => {
197
197
  it('below-floor + strictness:warn → OTel span MUST carry envelope_schema_version_drift attribute', async () => {
@@ -216,7 +216,7 @@ describe('aiEnvelope.schemaDrift: OTel drift attribute projection (E.2)', () =>
216
216
  expect(r.body.status).toBe('accepted');
217
217
 
218
218
  const spans = await queryTestSpans({ runId });
219
- if (!spans.ok) return softSkip('blocked', 'precondition not met — `!spans.ok` returned early (seam, prior step, or fixture unavailable)');
219
+ if (!spans.ok) return blockedDespiteAssertions('the OTel span seam is advertised but the span query failed — envelope_schema_version_drift is unobserved');
220
220
  expect(
221
221
  spans.data.some((s) => s.attributes.envelope_schema_version_drift === true),
222
222
  req('openwop.it.aiEnvelope.schemaDrift.below-floor-strictness-warn-otel-span-must-carry-envelope-schema-version-drift-a',
@@ -168,7 +168,7 @@ describe('aiEnvelope.universalKinds: behavioral accept via /v1/host/sample/envel
168
168
  import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
169
169
  import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
170
170
  import { req } from '../lib/requirement-ids.js';
171
- import { softSkip } from '../lib/soft-skip.js';
171
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
172
172
 
173
173
  describe('aiEnvelope.universalKinds: engine projection via event-log seam', () => {
174
174
  it('clarification.request MUST be lifted to interrupt.requested { kind: "clarification" } per interrupt.md', async () => {
@@ -188,7 +188,7 @@ describe('aiEnvelope.universalKinds: engine projection via event-log seam', () =
188
188
  if (r.status === 404) return softSkip('blocked', 'precondition not met — `r.status === 404` returned early (seam, prior step, or fixture unavailable)');
189
189
  expect(r.body.status).toBe('accepted');
190
190
  const events = await queryTestEvents(runId, { type: 'interrupt.requested' });
191
- if (!events.ok) return softSkip('blocked', 'precondition not met — `!events.ok` returned early (seam, prior step, or fixture unavailable)');
191
+ if (!events.ok) return blockedDespiteAssertions('the event-log seam is advertised but the interrupt.requested query failed — the clarification lift is unobserved');
192
192
  expect(
193
193
  events.events.length,
194
194
  req('openwop.it.aiEnvelope.universalKinds.clarification-request-must-be-lifted-to-interrupt-requested-kind-clarification-p', 'ai-envelope.md §"Universal kinds"', 'accepted clarification.request MUST project to interrupt.requested per interrupt.md'),
@@ -40,7 +40,7 @@ import { driver } from '../lib/driver.js';
40
40
  import { behaviorGate } from '../lib/behavior-gate.js';
41
41
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
42
42
  import { req } from '../lib/requirement-ids.js';
43
- import { softSkip } from '../lib/soft-skip.js';
43
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
44
44
 
45
45
  const SEAM = '/v1/host/sample/ai/call';
46
46
 
@@ -118,7 +118,7 @@ describe('aiproviders-selfhosted-honesty (RFC 0108 §A.2/§D)', () => {
118
118
  '[openwop-selfhosted-providers] §D disclosure check skipped — set OPENWOP_TEST_COMPAT_ENDPOINT ' +
119
119
  "to the host's configured compat endpoint to assert non-disclosure on the wire.",
120
120
  );
121
- return softSkip('blocked', 'precondition not met — `!endpoint` returned early (seam, prior step, or fixture unavailable)');
121
+ return blockedDespiteAssertions('§D non-disclosure is unobserved: OPENWOP_TEST_COMPAT_ENDPOINT is unset, so there is no configured endpoint to search the response for (§A.2 was observed)');
122
122
  }
123
123
 
124
124
  const serialized = JSON.stringify(res.json ?? {});
@@ -19,7 +19,7 @@ import { driver } from '../lib/driver.js';
19
19
  import { pollUntilTerminal } from '../lib/polling.js';
20
20
  import { isFixtureAdvertised } from '../lib/fixtures.js';
21
21
  import { req } from '../lib/requirement-ids.js';
22
- import { softSkip } from '../lib/soft-skip.js';
22
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
23
23
 
24
24
  const CANDIDATES = ['conformance-append-ordering', 'conformance-multi-node'] as const;
25
25
  const FIXTURE = CANDIDATES.find((id) => isFixtureAdvertised(id)) ?? null;
@@ -49,31 +49,36 @@ interface RunEvent {
49
49
  describe.skipIf(SKIP)('append-ordering: folded channel reflects event sequence', () => {
50
50
  it('append-reducer channels project entries in event-sequence order', async () => {
51
51
  const create = await driver.post('/v1/runs', { workflowId: FIXTURE! });
52
- expect(create.status).toBe(201);
53
- const runId = (create.json as { runId: string }).runId;
54
-
55
- await pollUntilTerminal(runId, { timeoutMs: 30_000 });
56
-
57
- const events = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events`);
58
- const list = (events.json as { events?: RunEvent[] }).events ?? [];
52
+ const runId = (create.json as { runId?: string } | undefined)?.runId;
59
53
 
60
54
  // Group channel.written events by channel.
61
55
  const byChannel = new Map<string, RunEvent[]>();
62
- for (const e of list) {
63
- if (e.type !== 'channel.written') continue;
64
- const ch = e.payload?.channel;
65
- if (typeof ch !== 'string') continue;
66
- const arr = byChannel.get(ch) ?? [];
67
- arr.push(e);
68
- byChannel.set(ch, arr);
56
+ if (create.status === 201 && typeof runId === 'string') {
57
+ await pollUntilTerminal(runId, { timeoutMs: 30_000 });
58
+ const events = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events`);
59
+ const list = (events.json as { events?: RunEvent[] }).events ?? [];
60
+ for (const e of list) {
61
+ if (e.type !== 'channel.written') continue;
62
+ const ch = e.payload?.channel;
63
+ if (typeof ch !== 'string') continue;
64
+ const arr = byChannel.get(ch) ?? [];
65
+ arr.push(e);
66
+ byChannel.set(ch, arr);
67
+ }
68
+ // The conformance-multi-node fallback is not required to write an append
69
+ // channel, so when it wrote none the ordering rule has nothing to bind on this
70
+ // host. Decided before any assertion so the row is `inapplicable`, not a
71
+ // partial-witness pass on the run-created assertion.
72
+ if (byChannel.size === 0 && FIXTURE !== 'conformance-append-ordering') {
73
+ return softSkip('inapplicable', 'the conformance-multi-node fallback fixture wrote no append channel and the host advertises no conformance-append-ordering fixture');
74
+ }
69
75
  }
76
+ expect(create.status).toBe(201);
70
77
 
71
78
  if (byChannel.size === 0) {
72
- // eslint-disable-next-line no-console
73
- console.warn(
74
- '[append-ordering] fixture emitted no channel.written events; skipping ordering assertions',
75
- );
76
- return softSkip('blocked', 'precondition not met — `byChannel.size === 0` returned early ([append-ordering] fixture emitted no channel.written events; skipping ordering assertions) (seam, prior step, or fixture unavailable)');
79
+ // The dedicated fixture exists to write an append channel, so a run of it with
80
+ // no channel.written leaves the ordering rule unobserved: blocked, not passed.
81
+ return blockedDespiteAssertions('conformance-append-ordering ran but emitted no channel.written event — append ordering is unobserved');
77
82
  }
78
83
 
79
84
  // For each channel, sequence MUST be strictly increasing within the run.
@@ -122,7 +127,7 @@ describe.skipIf(SKIP)('append-ordering: folded channel reflects event sequence',
122
127
 
123
128
  // Cross-check against the projected channel state on the run snapshot
124
129
  // (when surfaced) — projected array length MUST equal the number of writes.
125
- const snapshot = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
130
+ const snapshot = await driver.get(`/v1/runs/${encodeURIComponent(runId!)}`);
126
131
  const channels = (snapshot.json as { channels?: Record<string, unknown> }).channels ?? {};
127
132
  for (const [channel, writes] of byChannel) {
128
133
  const projected = (channels as Record<string, unknown>)[channel];
@@ -47,7 +47,9 @@ describe('artifact-auth: unauthenticated artifact requests are rejected', () =>
47
47
  } catch {
48
48
  // Some hosts return a bare 401 with no body — acceptable. Skip the
49
49
  // envelope-shape assertion in that case.
50
- return softSkip('blocked', 'precondition not met — an earlier step threw (Some hosts return a bare 401 with no body — acceptable. Skip the envelope-shape assertion in that case.) (seam, prior step, or fixture unavailable)');
50
+ // partial-witness-ok: the 401 refusal, the MUST, was asserted above; the
51
+ // `error: "unauthenticated"` envelope is a SHOULD and a bare 401 has no body.
52
+ return softSkip('inapplicable', 'the 401 carried no JSON body (acceptable), so the SHOULD envelope shape was not checked');
51
53
  }
52
54
  if (typeof body.error === 'string') {
53
55
  expect(body.error, req('openwop.it.artifact-auth.get-v1-runs-runid-artifacts-artifactid-without-authorization-returns-401',
@@ -37,7 +37,7 @@ import { driver } from '../lib/driver.js';
37
37
  import { behaviorGatePresent } from '../lib/behavior-gate.js';
38
38
  import { readArtifactTypesCap } from '../lib/artifactTypes.js';
39
39
  import { req } from '../lib/requirement-ids.js';
40
- import { softSkip } from '../lib/soft-skip.js';
40
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
41
41
  const CAPS = join(SCHEMAS_DIR, 'capabilities.schema.json');
42
42
  const EVENT_PAYLOADS = join(SCHEMAS_DIR, 'run-event-payloads.schema.json');
43
43
  const readDoc = (name: string): string => (V1_DIR ? readFileSync(join(V1_DIR, name), 'utf8') : '');
@@ -206,7 +206,7 @@ describe('artifact-type-registration-source: the advert agrees with the event (R
206
206
  const created = list.filter((e) => e['type'] === 'artifact.created');
207
207
  // Emission itself is RFC 0142's MUST, reported by its own leg. Reaching here without an
208
208
  // event means that leg is already red; don't restate its finding as a 0145 failure.
209
- if (created.length === 0) return softSkip('blocked', 'precondition not met — `created.length === 0` returned early (Emission itself is RFC 0142\'s MUST, reported by its own leg. Reaching here without an event means that leg is already red; don\'t restate its finding as a 0…');
209
+ if (created.length === 0) return blockedDespiteAssertions('the run emitted no artifact.created event, so registrationSource is unobserved (RFC 0142\'s emission leg reports the missing event)');
210
210
 
211
211
  const payload = (created[0]?.['payload'] ?? created[0]?.['data'] ?? {}) as Record<string, unknown>;
212
212
  expect(
@@ -0,0 +1,63 @@
1
+ /**
2
+ * RFC 0218 — a host's audit checkpoints are signed over their Merkle root
3
+ * (`spec/v1/auth-profiles.md` §"Audit-log integrity" 3).
4
+ *
5
+ * Gated on the `openwop-audit-log-integrity` profile. Every checkpoint
6
+ * `GET /v1/audit/verify` returns MUST verify under the advertised
7
+ * `checkpointPublicKey` as Ed25519 over its hex-decoded `merkleRoot`. The root
8
+ * itself cannot be recomputed from outside (the entries are not on the wire),
9
+ * so the black box witnesses the signature half only. The root half belongs to
10
+ * the vectors (`audit-checkpoint-vectors.test.ts`), the host-internal tamper
11
+ * test and the out-of-band verifier. A host with no checkpoint yet records
12
+ * `inapplicable` before any assertion. Sabotage: a host that signs the
13
+ * checkpoint's canonical JSON, or the 64 hex characters, fails the row.
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import { driver } from '../lib/driver.js';
18
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
19
+ import { req } from '../lib/requirement-ids.js';
20
+ import { softSkip } from '../lib/soft-skip.js';
21
+ import { checkpointPublicKey, verifyCheckpointSignature } from '../lib/audit-checkpoint.js';
22
+
23
+ const SPEC = 'RFC 0218 · spec/v1/auth-profiles.md §"Audit-log integrity" 3';
24
+ const SIGNED = 'openwop.requirement.0218.checkpoint-signature-over-root';
25
+
26
+ interface AuditIntegrityCaps { checkpointPublicKey?: string }
27
+ interface AuthCaps { profiles?: string[]; auditLogIntegrity?: AuditIntegrityCaps }
28
+
29
+ describe('RFC 0218 — a host\'s checkpoints are signed over their root', () => {
30
+ it('every checkpoint GET /v1/audit/verify returns verifies under the advertised key', async () => {
31
+ const disco = await driver.get('/.well-known/openwop');
32
+ const auth = capabilityFamily<AuthCaps>(disco.json, 'auth') ?? {};
33
+ if (!(Array.isArray(auth.profiles) && auth.profiles.includes('openwop-audit-log-integrity'))) {
34
+ return softSkip('inapplicable', 'the host does not claim openwop-audit-log-integrity');
35
+ }
36
+ const advertised = auth.auditLogIntegrity?.checkpointPublicKey;
37
+ if (typeof advertised !== 'string' || advertised.length === 0) {
38
+ // A claimed profile without its key is the profile-shape leg's failure
39
+ // (audit-log-integrity.test.ts); this row cannot be observed without it.
40
+ return softSkip('blocked', 'the host claims the profile but advertises no auditLogIntegrity.checkpointPublicKey');
41
+ }
42
+ let key;
43
+ try { key = checkpointPublicKey(advertised); } catch (e) {
44
+ return softSkip('blocked', `auditLogIntegrity.checkpointPublicKey is not an Ed25519 SPKI key: ${(e as Error).message}`);
45
+ }
46
+
47
+ const verify = await driver.get('/v1/audit/verify?fromSeq=0&toSeq=1000000');
48
+ if (verify.status !== 200) {
49
+ return softSkip('blocked', `GET /v1/audit/verify answered ${verify.status}; the verify leg of audit-log-integrity owns that contract`);
50
+ }
51
+ const checkpoints = ((verify.json as { checkpoints?: Array<{ atSequence?: number; merkleRoot?: string; signature?: string }> }).checkpoints) ?? [];
52
+ if (checkpoints.length === 0) {
53
+ return softSkip('inapplicable', 'the host has minted no checkpoint yet (its interval has not elapsed)');
54
+ }
55
+
56
+ for (const cp of checkpoints) {
57
+ expect(
58
+ verifyCheckpointSignature(String(cp.merkleRoot ?? ''), String(cp.signature ?? ''), key),
59
+ req(SIGNED, SPEC, `checkpoint at ${cp.atSequence}: signature MUST be Ed25519 over the 32 bytes merkleRoot hex-decodes to, under the advertised checkpointPublicKey`),
60
+ ).toBe(true);
61
+ }
62
+ });
63
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * RFC 0218 — the audit checkpoint preimage, server-free
3
+ * (`spec/v1/auth-profiles.md` §"Audit-log integrity" 3).
4
+ *
5
+ * `conformance/vectors/audit-checkpoint-v1.json` is normative. Every leg
6
+ * RECOMPUTES from the vector input through `lib/audit-checkpoint.ts` and compares
7
+ * with the committed value; none compares two constants from the file. The
8
+ * vectors were produced by an implementation independent of that lib and
9
+ * cross-checked in a second language. Sabotages, each of which turns a leg red:
10
+ * duplicating a last odd node (the 12-sequence checkpoint has 5 leaves); hashing
11
+ * the concatenated DECODED bytes instead of the hex text; ranging leaves over
12
+ * `0..atSequence` instead of `(P, atSequence]`; signing the checkpoint's
13
+ * canonical JSON or the hex text instead of the 32 root bytes; dropping the
14
+ * leaf-count check (the interior-nodes-as-leaves refusal is then accepted).
15
+ *
16
+ * Server-free and always-on; the vectors ship with the conformance package. The
17
+ * black-box half is `audit-checkpoint-signature.test.ts`.
18
+ */
19
+
20
+ import { describe, it, expect } from 'vitest';
21
+ import { readFileSync } from 'node:fs';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { dirname, resolve } from 'node:path';
24
+ import { req } from '../lib/requirement-ids.js';
25
+ import {
26
+ auditEntryHash,
27
+ checkpointMerkleRoot,
28
+ checkpointPublicKey,
29
+ verifyCheckpointSignature,
30
+ CheckpointPreimageError,
31
+ } from '../lib/audit-checkpoint.js';
32
+
33
+ interface VectorCheckpoint { atSequence: number; previousAtSequence: number; leafCount: number; merkleRoot: string; signature: string }
34
+ interface Refusal { id: string; why: string; leaves: string[]; expectedLeaves: number }
35
+ interface SigRefusal { id: string; why: string; merkleRoot: string; signature: string }
36
+
37
+ const VECTORS_PATH = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'vectors', 'audit-checkpoint-v1.json');
38
+ const doc = JSON.parse(readFileSync(VECTORS_PATH, 'utf8')) as {
39
+ publicKeySpkiBase64: string;
40
+ entries: Array<{ entry: { atSequence: number }; entryHash: string }>;
41
+ checkpoints: VectorCheckpoint[];
42
+ refusals: Refusal[];
43
+ signatureRefusals: SigRefusal[];
44
+ };
45
+
46
+ const SPEC = 'RFC 0218 · spec/v1/auth-profiles.md §"Audit-log integrity" 3';
47
+ const VECTORS = 'openwop.requirement.0218.checkpoint-preimage-vectors';
48
+
49
+ describe('RFC 0218 — the audit checkpoint preimage (vectors)', () => {
50
+ it('the vector set is present and non-trivial', () => {
51
+ // An empty or truncated file would make every leg below vacuous.
52
+ expect(doc.entries.length, req(VECTORS, SPEC, 'the vector file carries entries')).toBeGreaterThanOrEqual(12);
53
+ expect(doc.checkpoints.map((c) => c.leafCount), req(VECTORS, SPEC, 'the checkpoints cover 1, 2, 4 and 5 leaves (odd promotion at 5)')).toEqual([1, 2, 4, 5]);
54
+ });
55
+
56
+ it('every entry hash is the SHA-256 of the entry\'s JCS bytes, and chains as prevHash', () => {
57
+ let prev: string | null = null;
58
+ for (const { entry, entryHash } of doc.entries) {
59
+ expect(auditEntryHash(entry), req(VECTORS, SPEC, `entry ${entry.atSequence}: entryHash = SHA-256(JCS(entry))`)).toBe(entryHash);
60
+ expect((entry as { prevHash?: string | null }).prevHash ?? null, req(VECTORS, SPEC, `entry ${entry.atSequence}: prevHash is the previous entry's hash`)).toBe(prev);
61
+ prev = entryHash;
62
+ }
63
+ });
64
+
65
+ it('every root is recomputed over (P, atSequence], pairwise, odd node promoted', () => {
66
+ for (const c of doc.checkpoints) {
67
+ const leaves = doc.entries.filter((e) => e.entry.atSequence > c.previousAtSequence && e.entry.atSequence <= c.atSequence).map((e) => e.entryHash);
68
+ expect(checkpointMerkleRoot(leaves, c.atSequence - c.previousAtSequence), req(VECTORS, SPEC, `checkpoint at ${c.atSequence}: merkleRoot`)).toBe(c.merkleRoot);
69
+ }
70
+ });
71
+
72
+ it('every signature verifies as Ed25519 over the 32 root bytes', () => {
73
+ const key = checkpointPublicKey(doc.publicKeySpkiBase64);
74
+ for (const c of doc.checkpoints) {
75
+ expect(verifyCheckpointSignature(c.merkleRoot, c.signature, key), req(VECTORS, SPEC, `checkpoint at ${c.atSequence}: signature over the root bytes`)).toBe(true);
76
+ }
77
+ });
78
+
79
+ it('refuses a leaf list whose count is not atSequence − P, an uppercase leaf, and an empty range', () => {
80
+ for (const r of doc.refusals) {
81
+ let refused: unknown;
82
+ try { checkpointMerkleRoot(r.leaves, r.expectedLeaves); } catch (e) { refused = e; }
83
+ expect(refused instanceof CheckpointPreimageError, req(VECTORS, SPEC, `${r.id} MUST be refused — ${r.why}`)).toBe(true);
84
+ }
85
+ });
86
+
87
+ it('a signature over any other preimage does not verify', () => {
88
+ const key = checkpointPublicKey(doc.publicKeySpkiBase64);
89
+ for (const s of doc.signatureRefusals) {
90
+ expect(verifyCheckpointSignature(s.merkleRoot, s.signature, key), req(VECTORS, SPEC, `${s.id} MUST NOT verify — ${s.why}`)).toBe(false);
91
+ }
92
+ });
93
+ });
@@ -266,12 +266,13 @@ describe.skipIf(!RUN_BEHAVIOR)('auth-mtls: client cert behavior', () => {
266
266
  'auth-profiles.md §`openwop-auth-mtls`',
267
267
  'mtls.required: true MUST reject no-cert requests (TLS handshake failure is conformant)',
268
268
  )).toBe(true);
269
- return softSkip('blocked', 'precondition not met — `\'error\' in res` returned early (seam, prior step, or fixture unavailable)');
269
+ // The TLS-layer rejection IS the conformant refusal: the requirement was observed.
270
+ // (This branch used to end in a blocked note, a partial-witness pass at major 1.)
271
+ } else {
272
+ expect(res.status >= 400, req('openwop.it.auth-mtls.no-client-cert-against-mtls-required-true-non-2xx-or-tls-failure',
273
+ 'auth-profiles.md §`openwop-auth-mtls`',
274
+ 'mtls.required: true MUST reject no-cert requests at the auth layer (4xx) when not rejected at the TLS layer',
275
+ )).toBe(true);
270
276
  }
271
-
272
- expect(res.status >= 400, req('openwop.it.auth-mtls.no-client-cert-against-mtls-required-true-non-2xx-or-tls-failure',
273
- 'auth-profiles.md §`openwop-auth-mtls`',
274
- 'mtls.required: true MUST reject no-cert requests at the auth layer (4xx) when not rejected at the TLS layer',
275
- )).toBe(true);
276
277
  });
277
278
  });
@@ -39,7 +39,7 @@
39
39
  */
40
40
 
41
41
  import { describe, it, expect } from 'vitest';
42
- import { softSkip } from '../lib/soft-skip.js';
42
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
43
43
  import { driver } from '../lib/driver.js';
44
44
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
45
45
  import { req } from '../lib/requirement-ids.js';
@@ -150,7 +150,7 @@ describe('auth-subject-link-key-class: same-IdP trust root (RFC 0163 §B — beh
150
150
  expect(provSame.status, req('openwop.it.auth-subject-link-key-class.a-same-idp-link-forms-control-but-a-cross-idp-collision-must-not-link', 'auth-profiles.md §Subject linking', 'SCIM provisioning MUST succeed')).toBeLessThan(400);
151
151
 
152
152
  const sameIdp = await driver.post('/v1/host/sample/auth/saml/validate', { idpUrl: idpAUrl, variant: 'valid', nameId: sameId });
153
- if (sameIdp.status === 404) return softSkip('blocked', 'SAML validate seam unwired');
153
+ if (sameIdp.status === 404) return blockedDespiteAssertions('SAML validate seam answered 404 after SCIM provisioning succeeded — the same-IdP control and the cross-IdP MUST are unobserved');
154
154
  expect(
155
155
  (sameIdp.json as { authenticated?: boolean } | undefined)?.authenticated,
156
156
  req('openwop.it.auth-subject-link-key-class.a-same-idp-link-forms-control-but-a-cross-idp-collision-must-not-link', 'auth-profiles.md §Subject linking', 'RFC 0163 §B positive control: a SAML assertion from the SAME IdP trust root as the SCIM lane links and authenticates'),
@@ -172,7 +172,7 @@ describe('auth-subject-link-key-class: same-IdP trust root (RFC 0163 §B — beh
172
172
  expect(provCross.status, req('openwop.it.auth-subject-link-key-class.a-same-idp-link-forms-control-but-a-cross-idp-collision-must-not-link', 'auth-profiles.md §Subject linking', 'SCIM provisioning MUST succeed')).toBeLessThan(400);
173
173
 
174
174
  const crossIdp = await driver.post('/v1/host/sample/auth/saml/validate', { idpUrl: idpBUrl, variant: 'valid', nameId: collideId });
175
- if (crossIdp.status === 404) return softSkip('blocked', 'SAML validate seam unwired');
175
+ if (crossIdp.status === 404) return blockedDespiteAssertions('SAML validate seam answered 404 on the cross-IdP probe — RFC 0163 §B.1 is unobserved');
176
176
  expect(
177
177
  (crossIdp.json as { authenticated?: boolean } | undefined)?.authenticated === true,
178
178
  req('openwop.it.auth-subject-link-key-class.a-same-idp-link-forms-control-but-a-cross-idp-collision-must-not-link',