@openwop/openwop-conformance 2.42.6 → 2.42.8

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 (129) hide show
  1. package/CHANGELOG.md +34 -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/fixtures/conformance-envelope-retry-attempted.json +2 -1
  7. package/package.json +2 -2
  8. package/requirements.json +581 -175
  9. package/scenario-majors.json +22 -3
  10. package/schemas/CORPUS-STAMP.json +70 -67
  11. package/src/lib/audit-checkpoint.ts +72 -0
  12. package/src/lib/ext-claims.ts +96 -0
  13. package/src/lib/mock-ai-budgets.ts +40 -0
  14. package/src/lib/soft-skip.ts +14 -0
  15. package/src/scenarios/a2a-peer-authority.test.ts +6 -13
  16. package/src/scenarios/a2a-task-roundtrip.test.ts +6 -34
  17. package/src/scenarios/a2a-version-negotiation.test.ts +6 -12
  18. package/src/scenarios/a2ui-surface-delta-transport.test.ts +23 -12
  19. package/src/scenarios/agent-channel-dispatch.test.ts +13 -4
  20. package/src/scenarios/agent-live-invocation-bracket.test.ts +1 -1
  21. package/src/scenarios/agent-manifest-runtime.test.ts +3 -0
  22. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -2
  23. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +2 -2
  24. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -2
  25. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -2
  26. package/src/scenarios/aiproviders-selfhosted-honesty.test.ts +2 -2
  27. package/src/scenarios/append-ordering.test.ts +26 -21
  28. package/src/scenarios/artifact-auth.test.ts +3 -1
  29. package/src/scenarios/artifact-type-registration-source.test.ts +2 -2
  30. package/src/scenarios/audit-checkpoint-signature.test.ts +63 -0
  31. package/src/scenarios/audit-checkpoint-vectors.test.ts +93 -0
  32. package/src/scenarios/audit-log-integrity.test.ts +7 -16
  33. package/src/scenarios/auth-mtls.test.ts +7 -6
  34. package/src/scenarios/auth-subject-link-key-class.test.ts +3 -3
  35. package/src/scenarios/auth-subject-link.test.ts +19 -18
  36. package/src/scenarios/blob-presign-expiry.test.ts +2 -2
  37. package/src/scenarios/byok-roundtrip.test.ts +4 -1
  38. package/src/scenarios/compensation-behavior.test.ts +10 -5
  39. package/src/scenarios/conformance-advertised-seam-required.test.ts +6 -0
  40. package/src/scenarios/connection-pack-apihosts.test.ts +2 -2
  41. package/src/scenarios/connection-provider-resolution.test.ts +2 -2
  42. package/src/scenarios/context-budget-transcript-bound.test.ts +7 -3
  43. package/src/scenarios/context-summarization-replay.test.ts +2 -0
  44. package/src/scenarios/cost-attribution.test.ts +3 -1
  45. package/src/scenarios/debugBundle.test.ts +12 -4
  46. package/src/scenarios/discovery.test.ts +6 -2
  47. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +2 -2
  48. package/src/scenarios/dispatch-input-mapping.test.ts +2 -2
  49. package/src/scenarios/dispatch-output-mapping.test.ts +15 -2
  50. package/src/scenarios/distillation-token-budget.test.ts +2 -2
  51. package/src/scenarios/durability-poison-exhaustion.test.ts +2 -2
  52. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +51 -15
  53. package/src/scenarios/envelope-nl-to-format-engaged.test.ts +2 -2
  54. package/src/scenarios/envelope-recovery-applied.test.ts +2 -2
  55. package/src/scenarios/envelope-refusal-shape.test.ts +2 -2
  56. package/src/scenarios/envelope-retry-attempted.test.ts +2 -2
  57. package/src/scenarios/envelope-retry-exhausted.test.ts +2 -2
  58. package/src/scenarios/envelope-tier-one-subset-static.test.ts +1 -1
  59. package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +2 -2
  60. package/src/scenarios/eventOrdering.test.ts +17 -10
  61. package/src/scenarios/form-content-instantiation.test.ts +2 -2
  62. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +2 -2
  63. package/src/scenarios/highConcurrency.test.ts +9 -9
  64. package/src/scenarios/idempotency-concurrent-claim.test.ts +5 -5
  65. package/src/scenarios/idempotency-key-determinism.test.ts +8 -8
  66. package/src/scenarios/idempotency.test.ts +6 -6
  67. package/src/scenarios/idempotencyRetry.test.ts +16 -11
  68. package/src/scenarios/interrupt-token-matrix.test.ts +24 -11
  69. package/src/scenarios/mcp-cache-tenant-scope.test.ts +3 -3
  70. package/src/scenarios/mcp-current-auth-boundary.test.ts +9 -6
  71. package/src/scenarios/mcp-extension-opacity.test.ts +6 -5
  72. package/src/scenarios/mcp-mrtr-roundtrip.test.ts +9 -8
  73. package/src/scenarios/mcp-version-negotiation.test.ts +5 -10
  74. package/src/scenarios/media-url-inline-cap.test.ts +3 -2
  75. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -0
  76. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -2
  77. package/src/scenarios/memory-consolidation-idempotent.test.ts +2 -2
  78. package/src/scenarios/memory-injection-budget.test.ts +2 -2
  79. package/src/scenarios/multi-party-conversation-behavioral.test.ts +2 -2
  80. package/src/scenarios/multi-region-idempotency-behavior.test.ts +9 -9
  81. package/src/scenarios/multi-region-idempotency.test.ts +2 -2
  82. package/src/scenarios/pack-registry-isolation.test.ts +13 -12
  83. package/src/scenarios/pack-registry.test.ts +3 -2
  84. package/src/scenarios/pause-resume.test.ts +30 -11
  85. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +43 -34
  86. package/src/scenarios/provider-usage.test.ts +2 -2
  87. package/src/scenarios/redaction.test.ts +7 -5
  88. package/src/scenarios/redactionAdversarial.test.ts +5 -4
  89. package/src/scenarios/registry-public.test.ts +5 -1
  90. package/src/scenarios/replay-divergence-at-refusal.test.ts +10 -7
  91. package/src/scenarios/replay-fanout-suppression.test.ts +7 -4
  92. package/src/scenarios/replay-fork-arbitrary.test.ts +2 -2
  93. package/src/scenarios/replay-side-effect-suppression.test.ts +2 -2
  94. package/src/scenarios/replayDeterminism.test.ts +9 -4
  95. package/src/scenarios/runner-ledger.test.ts +4 -0
  96. package/src/scenarios/runtime-capabilities.test.ts +2 -0
  97. package/src/scenarios/speech-synthesis-roundtrip.test.ts +1 -1
  98. package/src/scenarios/streamReconnect.test.ts +2 -2
  99. package/src/scenarios/subrun-approval-gate.test.ts +2 -2
  100. package/src/scenarios/subworkflow-input-mapping.test.ts +2 -2
  101. package/src/scenarios/tool-catalog-projection.test.ts +10 -7
  102. package/src/scenarios/tool-session-lifecycle.test.ts +1 -1
  103. package/src/scenarios/trigger-ingestion.test.ts +2 -2
  104. package/src/scenarios/v2-a2ui-v09-surface.test.ts +15 -0
  105. package/src/scenarios/v2-client-version-header.test.ts +124 -0
  106. package/src/scenarios/v2-effect-identity-business-key.test.ts +12 -0
  107. package/src/scenarios/v2-ext-family-claims.test.ts +62 -0
  108. package/src/scenarios/v2-ext-rest-transport.test.ts +151 -0
  109. package/src/scenarios/v2-id-grammar.test.ts +17 -5
  110. package/src/scenarios/v2-idempotency-in-flight.test.ts +3 -0
  111. package/src/scenarios/v2-legacy-profiles-absent.test.ts +4 -6
  112. package/src/scenarios/v2-mcp-tasks.test.ts +1 -1
  113. package/src/scenarios/v2-min-client-version.test.ts +20 -14
  114. package/src/scenarios/v2-preferred-version-default.test.ts +19 -1
  115. package/src/scenarios/v2-refresh-sla.test.ts +6 -5
  116. package/src/scenarios/v2-sse-last-event-id-cursor.test.ts +3 -3
  117. package/src/scenarios/v2-sse-last-event-id.test.ts +2 -0
  118. package/src/scenarios/v2-version-header-honored.test.ts +3 -0
  119. package/src/scenarios/v2-webhook-durable-delivery.test.ts +2 -0
  120. package/src/scenarios/v2-webhook-generated-secret.test.ts +76 -0
  121. package/src/scenarios/v2-webhook-secret-rotation.test.ts +8 -1
  122. package/src/scenarios/verifier-gating.test.ts +2 -2
  123. package/src/scenarios/version-fold.test.ts +2 -2
  124. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +7 -6
  125. package/src/scenarios/webhook-signed-delivery.test.ts +2 -0
  126. package/src/scenarios/workflow-chain-host-expansion.test.ts +10 -1
  127. package/src/scenarios/workflow-variable-format.test.ts +2 -2
  128. package/src/scenarios/workload-identity-chain-bounds.test.ts +6 -3
  129. package/vectors/audit-checkpoint-v1.json +260 -0
@@ -46,7 +46,8 @@
46
46
  * diagnostic log. Indicates the host doesn't recognize `?workspaceId=`
47
47
  * on this endpoint (e.g., host-only template library with no
48
48
  * workspace dimension).
49
- * - 5xx — PASS (refused; envelope shape unconstrained).
49
+ * - 5xx — `blocked`: a server error is not a readable refusal (a crash
50
+ * also answers 5xx), so enforcement was not observed.
50
51
  *
51
52
  * Why a random workspaceId is sufficient: the assertion is negative-space.
52
53
  * A host that correctly enforces membership MUST refuse for ANY workspace
@@ -118,27 +119,17 @@ describe.skipIf(HTTP_SKIP)(
118
119
  `/v1/prompts?workspaceId=${encodeURIComponent(nonMemberWorkspaceId)}`,
119
120
  );
120
121
 
121
- // 4xx — refused. Acceptable shape for the membership-required failure
122
- // (and any other refusal mode the host chooses: 401, 404 for
123
- // existence-disclosure avoidance, etc).
124
- if (res.status >= 400 && res.status < 500) {
125
- // Canonical envelope on 403 per rest-endpoints.md §"Common error codes".
126
- if (res.status === 403) {
127
- const body = res.json as { error?: unknown } | null;
128
- expect(
129
- body?.error,
130
- req('openwop.it.prompt-read-workspace-membership-enforced.get-v1-prompts-workspaceid-non-member-must-refuse-or-return-empty-templates-neve',
131
- 'spec/v1/rest-endpoints.md §Common error codes — workspace_membership_required',
132
- `403 refusal of a workspace-scoped read MUST carry error: "workspace_membership_required"; got error: ${JSON.stringify(body?.error)}`,
133
- ),
134
- ).toBe('workspace_membership_required');
135
- }
136
- return softSkip('blocked', 'precondition not met — `res.status >= 400 && res.status < 500` returned early (4xx — refused. Acceptable shape for the membership-required failure (and any other refusal mode the host chooses: 401, 404 for existence-d…');
137
- }
122
+ const ID = 'openwop.it.prompt-read-workspace-membership-enforced.get-v1-prompts-workspaceid-non-member-must-refuse-or-return-empty-templates-neve';
123
+
124
+ // 5xx — not a refusal the suite can read as enforcement (a crash also
125
+ // answers 5xx). Checked first so it returns before any assertion.
126
+ if (res.status >= 500) return softSkip('blocked', `GET /v1/prompts?workspaceId=<non-member> answered ${res.status} — a server error is neither a refusal nor an empty result, so membership enforcement was not observed`);
138
127
 
139
- // 5xx — refused (infrastructure failure is acceptable; envelope shape
140
- // unconstrained).
141
- if (res.status >= 500) return softSkip('blocked', 'precondition not met — `res.status >= 500` returned early (5xx — refused (infrastructure failure is acceptable; envelope shape unconstrained).) (seam, prior step, or fixture unavailable)');
128
+ // Other status codes (1xx, 3xx) — not a clear signal either way.
129
+ if (res.status < 200 || (res.status >= 300 && res.status < 400)) {
130
+ softSkip('skipped', `GET /v1/prompts?workspaceId=… answered ${res.status} — neither a refusal nor a list`);
131
+ return ctx.skip();
132
+ }
142
133
 
143
134
  // 2xx — must inspect the response body. The failure mode this
144
135
  // invariant guards against is a 200 response that LEAKS templates
@@ -152,35 +143,53 @@ describe.skipIf(HTTP_SKIP)(
152
143
  ) {
153
144
  // Host doesn't recognize `?workspaceId=` on this endpoint
154
145
  // (response shape doesn't include the documented `templates[]`
155
- // field). Soft-skip: this scenario probes hosts that expose
146
+ // field). Skip: this scenario probes hosts that expose
156
147
  // workspace-scoped reads, and a host without that surface is
157
- // simply out of scope.
158
- ctx.skip();
159
- return softSkip('blocked', 'precondition not met — `body === null || typeof body !== \'object\' || !(\'templates\' in body)` returned early (seam, prior step, or fixture unavailable)');
148
+ // simply out of scope. The note is written BEFORE ctx.skip(),
149
+ // which throws.
150
+ softSkip('skipped', 'GET /v1/prompts?workspaceId=… answered 2xx without a templates field — the host does not expose workspace-scoped prompt reads');
151
+ return ctx.skip();
160
152
  }
161
153
  const templates = body.templates;
162
154
  if (!Array.isArray(templates)) {
163
155
  // Same: unrecognized shape, skip.
164
- ctx.skip();
165
- return softSkip('blocked', 'precondition not met — `!Array.isArray(templates)` returned early (seam, prior step, or fixture unavailable)');
156
+ softSkip('skipped', 'GET /v1/prompts?workspaceId=… answered 2xx with a non-array templates field — the host does not expose workspace-scoped prompt reads');
157
+ return ctx.skip();
166
158
  }
167
159
 
168
160
  // A random non-member workspaceId can never legitimately contain
169
161
  // templates the caller is authorized to see. Any non-empty result
170
- // is a cross-tenant data leak.
162
+ // is a cross-tenant data leak. An empty result IS the requirement
163
+ // observed, so the leg ends as a plain pass.
171
164
  expect(
172
165
  templates.length,
173
- req('openwop.it.prompt-read-workspace-membership-enforced.get-v1-prompts-workspaceid-non-member-must-refuse-or-return-empty-templates-neve',
166
+ req(ID,
174
167
  'spec/v1/prompts.md §Workspace membership on workspace-scoped reads and writes',
175
168
  `GET /v1/prompts?workspaceId=<random-non-member> MUST NOT return any templates; got ${templates.length} templates which is a cross-tenant data leak (the random workspaceId is freshly generated per probe and cannot legitimately contain authorized content)`,
176
169
  ),
177
170
  ).toBe(0);
178
- return softSkip('blocked', 'precondition not met — `res.status >= 200 && res.status < 300` returned early (2xx — must inspect the response body. The failure mode this invariant guards against is a 200 response that LEAKS templates from a workspa…');
171
+ } else {
172
+ // 4xx — refused. Acceptable shape for the membership-required failure
173
+ // (and any other refusal mode the host chooses: 401, 404 for
174
+ // existence-disclosure avoidance, etc). The refusal IS the requirement
175
+ // observed; until 2026-09-27 this branch ended in a blocked soft-skip,
176
+ // recording a refused read as blocked (401/404) or a partial witness (403).
177
+ expect(
178
+ res.status,
179
+ req(ID, 'spec/v1/prompts.md §Workspace membership on workspace-scoped reads and writes', 'a workspace-scoped read for a non-member workspace MUST be refused (4xx) or return no templates'),
180
+ ).toBeGreaterThanOrEqual(400);
181
+ // Canonical envelope on 403 per rest-endpoints.md §"Common error codes".
182
+ if (res.status === 403) {
183
+ const body = res.json as { error?: unknown } | null;
184
+ expect(
185
+ body?.error,
186
+ req(ID,
187
+ 'spec/v1/rest-endpoints.md §Common error codes — workspace_membership_required',
188
+ `403 refusal of a workspace-scoped read MUST carry error: "workspace_membership_required"; got error: ${JSON.stringify(body?.error)}`,
189
+ ),
190
+ ).toBe('workspace_membership_required');
191
+ }
179
192
  }
180
-
181
- // Other status codes (1xx, 3xx) — soft-skip with note. Not a clear
182
- // signal either way.
183
- ctx.skip();
184
193
  });
185
194
  },
186
195
  );
@@ -29,7 +29,7 @@ import { SCHEMAS_DIR } from '../lib/paths.js';
29
29
  import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
30
30
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
31
31
  import { req } from '../lib/requirement-ids.js';
32
- import { softSkip } from '../lib/soft-skip.js';
32
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
33
33
 
34
34
  interface DiscoveryDoc {
35
35
  capabilities?: {
@@ -145,7 +145,7 @@ describe('provider-usage: event presence via emit-seam + event-log query (RFC 00
145
145
  expect(emit.status).toBe(200);
146
146
 
147
147
  const events = await queryTestEvents(runId, { type: 'provider.usage' });
148
- if (!events.ok) return softSkip('blocked', 'precondition not met — `!events.ok` returned early (seam, prior step, or fixture unavailable)');
148
+ if (!events.ok) return blockedDespiteAssertions('the event-log seam is advertised but the provider.usage query failed — the projection is unobserved');
149
149
  expect(
150
150
  events.events.length,
151
151
  req('openwop.it.provider-usage.emit-seam-projects-exactly-one-provider-usage-event-with-required-fields-populat', 'RFC 0026 §B', 'emit-seam MUST project exactly one provider.usage event'),
@@ -66,15 +66,17 @@ const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
66
66
  describe('redaction: /.well-known/openwop secrets+aiProviders shape contract', () => {
67
67
  it('secrets is well-formed regardless of supported value', async () => {
68
68
  const res = await driver.get('/.well-known/openwop', { authenticated: false });
69
- expect(res.status).toBe(200);
70
-
71
69
  const body = res.json as { secrets?: unknown } | undefined;
72
70
  const secrets = body?.secrets;
73
71
 
74
- if (secrets === undefined) {
75
- // Optional v1 field — hosts MAY omit. Spec-allowed; nothing to assert.
76
- return softSkip('blocked', 'precondition not met — `secrets === undefined` returned early (Optional v1 field — hosts MAY omit. Spec-allowed; nothing to assert.) (seam, prior step, or fixture unavailable)');
72
+ if (res.status === 200 && secrets === undefined) {
73
+ // Optional v1 field — hosts MAY omit. Spec-allowed; nothing to assert,
74
+ // so the leg returns before any assertion and records `inapplicable`
75
+ // (it was a `blocked` note after the status assertion until 2026-09-27,
76
+ // i.e. a partial-witness pass).
77
+ return softSkip('inapplicable', '`secrets` is omitted from discovery — an OPTIONAL v1 field, so there is no shape to check');
77
78
  }
79
+ expect(res.status).toBe(200);
78
80
 
79
81
  // Per capabilities.schema.json: `secrets.supported` is REQUIRED
80
82
  // when secrets is present.
@@ -48,7 +48,7 @@ import { CANARIES, CANARY_MARKER, getCanary } from '../lib/canaries.js';
48
48
  import { pollUntilTerminal } from '../lib/polling.js';
49
49
  import { isFixtureAdvertised } from '../lib/fixtures.js';
50
50
  import { req } from '../lib/requirement-ids.js';
51
- import { softSkip } from '../lib/soft-skip.js';
51
+ import { blockedDespiteAssertions } from '../lib/soft-skip.js';
52
52
 
53
53
  const NOOP_WORKFLOW_ID = 'conformance-noop';
54
54
  const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
@@ -77,8 +77,9 @@ describe.skipIf(SKIP_NO_NOOP)('redaction-adversarial: workflow input canaries',
77
77
  `/v1/runs/${encodeURIComponent(runId)}/events/poll`,
78
78
  );
79
79
  if (events.status !== 200) {
80
- // Host doesn't expose polling; SSE is the alternative. Skip-equivalent.
81
- return softSkip('blocked', 'precondition not met — `events.status !== 200` returned early (Host doesn\'t expose polling; SSE is the alternative. Skip-equivalent.) (seam, prior step, or fixture unavailable)');
80
+ // events/poll is a core endpoint (rest-endpoints.md); without it the
81
+ // canary check below never runs, so the leg is blocked, not passed.
82
+ return blockedDespiteAssertions('GET /v1/runs/{runId}/events/poll did not answer 200 — the canary non-disclosure is unobserved');
82
83
  }
83
84
 
84
85
  const eventsText = events.text;
@@ -139,7 +140,7 @@ describe.skipIf(SKIP_NO_NOOP)('redaction-adversarial: bearer-shaped strings roun
139
140
  const events = await driver.get(
140
141
  `/v1/runs/${encodeURIComponent(runId)}/events/poll`,
141
142
  );
142
- if (events.status !== 200) return softSkip('blocked', 'precondition not met — `events.status !== 200` returned early (seam, prior step, or fixture unavailable)');
143
+ if (events.status !== 200) return blockedDespiteAssertions('GET /v1/runs/{runId}/events/poll did not answer 200 — the canary non-disclosure is unobserved');
143
144
 
144
145
  expect(events.text.includes(canary), req('openwop.it.redactionAdversarial.anthropic-shaped-canary-embedded-in-non-credential-input-must-not-appear-verbati',
145
146
  'capabilities.md §"Secrets" + NFR-7',
@@ -215,7 +215,11 @@ describe('registry-public: tarball + signature + Ed25519 verify roundtrip', () =
215
215
  // `method=ed25519` — the simpler path. Extending to `manual` would
216
216
  // require the tarball extractor from registry/scripts/verify-
217
217
  // signatures.mjs which is intentionally out of scope here.
218
- if (method !== 'ed25519') return softSkip('blocked', 'precondition not met — `method !== \'ed25519\'` returned early (seam, prior step, or fixture unavailable)');
218
+ // partial-witness-ok: tarball, signature, public key and SRI were asserted
219
+ // above. `manual` is a conforming signing method whose pack.json-in-tarball
220
+ // verification this scenario does not implement, a suite limitation, not a
221
+ // host shortfall.
222
+ if (method !== 'ed25519') return softSkip('inapplicable', `${PACK_NAME} is signed method=manual; this scenario verifies only method=ed25519 signatures`);
219
223
 
220
224
  const verified = cryptoVerify(null, tarball.bytes, publicKey, sig.bytes);
221
225
  expect(verified, req('openwop.it.registry-public.tarball-sig-public-key-all-retrievable-sri-matches-ed25519-verifies-for', 'spec/v1/registry-operations.md', `Ed25519 signature over ${PACK_NAME}@${PACK_VERSION}.tgz MUST verify against ${manifest.signing!.keyId}`))
@@ -45,7 +45,7 @@
45
45
  */
46
46
 
47
47
  import { describe, it, expect } from 'vitest';
48
- import { softSkip } from '../lib/soft-skip.js';
48
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
49
49
  import { driver } from '../lib/driver.js';
50
50
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
51
51
  import { req } from '../lib/requirement-ids.js';
@@ -246,9 +246,12 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: behavioral (RFC 0041
246
246
  workflowId: 'conformance-phase4-replay-divergence',
247
247
  });
248
248
  if (createRes.status === 404 || createRes.status === 422) {
249
- softSkip('blocked', 'precondition not met — `createRes.status === 404 || createRes.status === 422` returned early (seam, prior step, or fixture unavailable)');
250
- ctx.skip(); // fixture not advertised
251
- return softSkip('blocked', 'precondition not met — `createRes.status === 404 || createRes.status === 422` returned early (seam, prior step, or fixture unavailable)');
249
+ // The host advertises refusalDivergenceEmission and its mock seam took the
250
+ // program (asserted above), but the fixture run would not start: the
251
+ // requirement was never observed. ctx.skip() throws, so this note is the
252
+ // row's disposition (`blocked`), exactly as before 2026-09-27.
253
+ blockedDespiteAssertions('the mock-AI seam accepted the program but POST /v1/runs of conformance-phase4-replay-divergence answered 404/422 — the fixture is not seeded, so no divergence was driven');
254
+ return ctx.skip();
252
255
  }
253
256
  expect(createRes.status).toBe(201);
254
257
  const sourceRunId = (createRes.json as { runId: string }).runId;
@@ -336,9 +339,9 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: behavioral (RFC 0041
336
339
  workflowId: 'conformance-phase4-replay-divergence',
337
340
  });
338
341
  if (createRes.status === 404 || createRes.status === 422) {
339
- softSkip('blocked', 'precondition not met — `createRes.status === 404 || createRes.status === 422` returned early (seam, prior step, or fixture unavailable)');
340
- ctx.skip();
341
- return softSkip('blocked', 'precondition not met — `createRes.status === 404 || createRes.status === 422` returned early (seam, prior step, or fixture unavailable)');
342
+ // As in the first case: ctx.skip() throws, so the note is the disposition.
343
+ blockedDespiteAssertions('the mock-AI seam accepted the program but POST /v1/runs of conformance-phase4-replay-divergence answered 404/422 — the fixture is not seeded, so no divergence was driven');
344
+ return ctx.skip();
342
345
  }
343
346
  expect(createRes.status).toBe(201);
344
347
  const sourceRunId = (createRes.json as { runId: string }).runId;
@@ -283,8 +283,8 @@ describe('replay-fanout-suppression: a replay fork MUST NOT fan out re-emitted e
283
283
  'host does not advertise `replay.supported: true`, so a replay fork cannot occur and this MUST NOT '
284
284
  + 'has nothing to constrain on this host',
285
285
  );
286
- ctx.skip();
287
- return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `replayCap?.supported !== true` returned early');
286
+ // recordRequirement above carries the disposition; ctx.skip() throws.
287
+ return ctx.skip();
288
288
  }
289
289
 
290
290
  // ── LEG 2 — THE MUST NOT. A replay fork re-emits; it must not deliver. ───
@@ -308,8 +308,8 @@ describe('replay-fanout-suppression: a replay fork MUST NOT fan out re-emitted e
308
308
  `replay fork returned ${replay.status} — the re-emission this requirement is stated over never happened, `
309
309
  + 'so the absence of deliveries below would prove nothing',
310
310
  );
311
- ctx.skip();
312
- return softSkip('blocked', 'precondition not met — `forkDeclined(replay.status, \'fanout-suppression replay fork\')` returned early (seam, prior step, or fixture unavailable)');
311
+ // recordRequirement above carries the disposition; ctx.skip() throws.
312
+ return ctx.skip();
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;
@@ -373,6 +373,9 @@ describe('replay-fanout-suppression: a replay fork MUST NOT fan out re-emitted e
373
373
  });
374
374
  if (branch.status === 501 || branch.status === 400) {
375
375
  // branch not offered on this range — leg 2 still stands on its own.
376
+ // partial-witness-ok: the MUST NOT (leg 2) was observed and recorded
377
+ // executed-pass above; leg 3 only pins the out-of-scope boundary, on a
378
+ // fork mode the host may not offer.
376
379
  return softSkip('blocked', 'precondition not met — `branch.status === 501 || branch.status === 400` returned early (branch not offered on this range — leg 2 still stands on its own.) (seam, prior step, or fixture unavailable)');
377
380
  }
378
381
  expect(branch.status, req('openwop.it.replay-fanout-suppression.delivers-for-a-live-run-suppresses-for-a-replay-fork-and-delivers-again-for-a-br', 'replay.md §"Host-initiated fan-out is an external effect"', 'branch fork should be accepted')).toBe(201);
@@ -30,7 +30,7 @@
30
30
 
31
31
  import { describe, it, expect } from 'vitest';
32
32
  import { driver } from '../lib/driver.js';
33
- import { softSkip } from '../lib/soft-skip.js';
33
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
34
34
  import { forkDeclined } from '../lib/fork-availability.js';
35
35
  import { pollUntilTerminal } from '../lib/polling.js';
36
36
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -246,7 +246,7 @@ describe.skipIf(SKIP_NO_MULTI)(
246
246
  );
247
247
  if (forkDeclined(fork2.status, 'arbitrary-event fork 2')) {
248
248
  ctx.skip();
249
- return softSkip('blocked', 'precondition not met — `forkDeclined(fork2.status, \'arbitrary-event fork 2\')` returned early (seam, prior step, or fixture unavailable)');
249
+ return blockedDespiteAssertions('arbitrary-event fork 2 was declined — fork determinism is unobserved');
250
250
  }
251
251
  expect(fork2.status).toBe(201);
252
252
  const fork2Id = (fork2.json as { runId: string }).runId;
@@ -55,7 +55,7 @@ import { readFileSync } from 'node:fs';
55
55
  import { join } from 'node:path';
56
56
  import { V1_DIR } from '../lib/paths.js';
57
57
  import { req } from '../lib/requirement-ids.js';
58
- import { softSkip } from '../lib/soft-skip.js';
58
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
59
59
 
60
60
  const WORKFLOW_ID = 'conformance-replay-side-effect';
61
61
  const EFFECT_NODE = 'effect';
@@ -178,7 +178,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('replay-side-effect-suppression: a replay does
178
178
  });
179
179
  if (forkDeclined(fork.status, 'side-effect-suppression replay fork')) {
180
180
  ctx.skip();
181
- return softSkip('blocked', 'precondition not met — `forkDeclined(fork.status, \'side-effect-suppression replay fork\')` returned early (seam, prior step, or fixture unavailable)');
181
+ return blockedDespiteAssertions('the replay fork was declined — side-effect suppression is unobserved');
182
182
  }
183
183
  expect(fork.status, req('openwop.it.replay-side-effect-suppression.fails-a-side-effecting-node-closed-with-replay-source-missing-when-the-source-ne', 'replay.md §"Side-effect suppression in replay" requirement 1', 'fork should be accepted')).toBe(201);
184
184
  const forkRunId = (fork.json as { runId: string }).runId;
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { describe, it, expect } from 'vitest';
20
20
  import { driver } from '../lib/driver.js';
21
- import { softSkip } from '../lib/soft-skip.js';
21
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
22
22
  import { forkDeclined } from '../lib/fork-availability.js';
23
23
  import { pollUntilTerminal } from '../lib/polling.js';
24
24
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -121,7 +121,10 @@ describe.skipIf(SKIP_NO_NOOP)('replay-determinism: same fromSeq + same workflow
121
121
  mode: 'replay',
122
122
  fromSeq: 0,
123
123
  });
124
- if (forkDeclined(fork2.status, 'determinism fork 2')) return softSkip('blocked', 'precondition not met — `forkDeclined(fork2.status, \'determinism fork 2\')` returned early (seam, prior step, or fixture unavailable)');
124
+ // Fork 1 of the same run was accepted (asserted above), so a decline here
125
+ // leaves the determinism comparison unobserved on a host that can fork:
126
+ // `blocked`, not the partial-witness pass a plain softSkip records.
127
+ if (forkDeclined(fork2.status, 'determinism fork 2')) return blockedDespiteAssertions(`the second replay fork of ${originalRunId} was declined (${fork2.status}) after the first was accepted — there is no second event list to compare`);
125
128
  expect(fork2.status).toBe(201);
126
129
  const fork2Id = (fork2.json as { runId: string }).runId;
127
130
  await pollUntilTerminal(fork2Id, { timeoutMs: 10_000 });
@@ -129,11 +132,13 @@ describe.skipIf(SKIP_NO_NOOP)('replay-determinism: same fromSeq + same workflow
129
132
  // Phase 4: fetch both fork event streams.
130
133
  const fork1Events = await driver.get(`/v1/runs/${encodeURIComponent(fork1Id)}/events/poll`);
131
134
  const fork2Events = await driver.get(`/v1/runs/${encodeURIComponent(fork2Id)}/events/poll`);
132
- if (fork1Events.status !== 200 || fork2Events.status !== 200) return softSkip('blocked', 'precondition not met — `fork1Events.status !== 200 || fork2Events.status !== 200` returned early (seam, prior step, or fixture unavailable)');
135
+ // rest-endpoints.md §"Required endpoints": /events/poll is a MUST, so a
136
+ // conforming host always makes the two event lists readable.
137
+ if (fork1Events.status !== 200 || fork2Events.status !== 200) return blockedDespiteAssertions(`GET /events/poll for the forks answered ${fork1Events.status} / ${fork2Events.status} — the event lists were never compared`);
133
138
 
134
139
  const fork1Body = fork1Events.json as { events?: RawEvent[] };
135
140
  const fork2Body = fork2Events.json as { events?: RawEvent[] };
136
- if (!fork1Body.events || !fork2Body.events) return softSkip('blocked', 'precondition not met — `!fork1Body.events || !fork2Body.events` returned early (seam, prior step, or fixture unavailable)');
141
+ if (!fork1Body.events || !fork2Body.events) return blockedDespiteAssertions('a fork /events/poll body carried no events[] — the event lists were never compared');
137
142
 
138
143
  // Phase 5: assert structural identity (modulo timestamps + IDs).
139
144
  expect(fork1Body.events.length, req('openwop.it.replayDeterminism.two-replay-forks-of-the-same-point-produce-structurally-identical-event-lists',
@@ -251,10 +251,14 @@ describe('RFC 0148 §A (S6) — the runner derivation', () => {
251
251
  describe('RFC 0148 §A — softSkip notes the reason for an early return, per file', () => {
252
252
  it('notes are keyed to the current test file, worst-first when mixed, and read back with joined reasons', () => {
253
253
  resetSoftSkips();
254
+ // partial-witness-ok: not a skip — this leg unit-tests softSkip itself, and
255
+ // resetSoftSkips() below clears every note it writes before the leg ends.
254
256
  expect(softSkip('inapplicable', 'host does not advertise X'), req('openwop.it.runner-ledger.notes-are-keyed-to-the-current-test-file-worst-first-when-mixed-and-read-back-wi', 'RFC 0148 §A', 'notes are keyed to the current test file, worst-first when mixed, and read back with joined reasons')).toBeUndefined();
257
+ // partial-witness-ok: not a skip — the subject under test (de-duplication).
255
258
  softSkip('inapplicable', 'host does not advertise X'); // de-duplicated
256
259
  let d = softSkipDisposition('runner-ledger.test.ts');
257
260
  expect(d).toEqual({ kind: 'inapplicable', reason: 'host does not advertise X' });
261
+ // partial-witness-ok: not a skip — the subject under test (worst-first fold).
258
262
  softSkip('blocked', 'seam not mounted');
259
263
  d = softSkipDisposition('runner-ledger.test.ts');
260
264
  expect(d?.kind).toBe('blocked');
@@ -52,6 +52,8 @@ describe('runtime-capabilities: /.well-known/openwop forward-compat shape', () =
52
52
  // of v1 hosts will omit it. Assertion passes trivially; don't
53
53
  // force a value on a host that doesn't advertise any.
54
54
  expect(caps).toBeUndefined();
55
+ // partial-witness-ok: discovery answered 200 and runtimeCapabilities is OPTIONAL; when
56
+ // absent, the IF-present MUSTs below bind nothing.
55
57
  return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `caps === undefined` returned early');
56
58
  }
57
59
 
@@ -58,7 +58,7 @@ describe('speech-synthesis-roundtrip (RFC 0105 §A)', () => {
58
58
  audio !== undefined,
59
59
  req('openwop.it.speech-synthesis-roundtrip.synthesizes-an-audio-asset-with-exactly-one-of-url-base64-and-echoes-the-voiceid', 'RFC 0105 §A', 'the response MUST carry an `audio` object'),
60
60
  ).toBe(true);
61
- if (!audio) return softSkip('blocked', 'precondition not met — `!audio` returned early (seam, prior step, or fixture unavailable)');
61
+ if (!audio) throw new Error('unreachable: the assertion above has already failed this leg');
62
62
 
63
63
  const hasUrl = typeof audio.url === 'string' && (audio.url as string).length > 0;
64
64
  const hasBase64 = typeof audio.base64 === 'string' && (audio.base64 as string).length > 0;
@@ -22,7 +22,7 @@ import { subscribe, type SseEvent } from '../lib/sse.js';
22
22
  import { pollUntilTerminal } from '../lib/polling.js';
23
23
  import { isFixtureAdvertised } from '../lib/fixtures.js';
24
24
  import { req } from '../lib/requirement-ids.js';
25
- import { softSkip } from '../lib/soft-skip.js';
25
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
26
26
 
27
27
  const WORKFLOW_ID = 'conformance-cancellable';
28
28
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
@@ -84,7 +84,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('stream-reconnect: Last-Event-ID resume per spe
84
84
  // because the run already completed and the server closed before we
85
85
  // got events. Skip the rest of this scenario; the host is fast enough
86
86
  // that the reconnect path doesn't apply.
87
- return softSkip('blocked', 'precondition not met — `lastSeen < 0` returned early (First connection emitted no events with a parseable sequence — e.g. because the run already completed and the server closed before we got events. Skip the rest of …');
87
+ return blockedDespiteAssertions('the first SSE connection yielded no event with a parseable sequence within 1s — the Last-Event-ID resume is unobserved');
88
88
  }
89
89
 
90
90
  // Phase 3: reconnect with Last-Event-ID set to the last seq we saw.
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { describe, it, expect } from 'vitest';
13
- import { softSkip } from '../lib/soft-skip.js';
13
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
14
14
  import { readSubRunAttestationCap, invokeSubRunAttest } from '../lib/subRunAttestation.js';
15
15
  import { req } from '../lib/requirement-ids.js';
16
16
 
@@ -27,7 +27,7 @@ describe('subrun-approval-gate (RFC 0063 §C)', () => {
27
27
  ).toBe(true);
28
28
 
29
29
  const rejected = await invokeSubRunAttest({ ...base, approvalAction: 'reject' });
30
- if (rejected === null) return softSkip('blocked', 'precondition not met — `rejected === null` returned early (seam, prior step, or fixture unavailable)');
30
+ if (rejected === null) return blockedDespiteAssertions('the sub-run attestation seam answered accept but not reject — the no-merge-on-reject rule is unobserved');
31
31
  expect(
32
32
  rejected.merged,
33
33
  req('openwop.it.subrun-approval-gate.accept-merges-the-child-outputs-reject-does-not', 'RFC 0063 §C', 'a `reject` approval MUST NOT merge the child outputs'),
@@ -24,7 +24,7 @@ import { pollUntilTerminal, pollUntilStatus } from '../lib/polling.js';
24
24
  import { isFixtureAdvertised } from '../lib/fixtures.js';
25
25
  import { setHostCapability, resetHostCapabilities, isToggleAvailable } from '../lib/host-toggle.js';
26
26
  import { req } from '../lib/requirement-ids.js';
27
- import { softSkip } from '../lib/soft-skip.js';
27
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
28
28
 
29
29
  const PARENT = 'conformance-subworkflow-input-mapping';
30
30
  const CHILD = 'conformance-subworkflow-input-mapping-child';
@@ -102,7 +102,7 @@ describe.skipIf(SKIP)('subworkflow-input-mapping: parent → child variable seed
102
102
  const subwfCompleted = events.find(
103
103
  (e) => e.type === 'node.completed' && e.nodeId === 'subwf-call',
104
104
  );
105
- if (!subwfCompleted) return softSkip('blocked', 'precondition not met — `!subwfCompleted` returned early (seam, prior step, or fixture unavailable)');
105
+ if (!subwfCompleted) return blockedDespiteAssertions('the advertised fixture\'s parent run has no node.completed for subwf-call — the unset-variable projection is unobserved');
106
106
  const childRunId = subwfCompleted.payload?.outputs?.childRunId;
107
107
 
108
108
  const childRes = await driver.get(`/v1/runs/${encodeURIComponent(childRunId!)}`);
@@ -84,13 +84,6 @@ describe('tool-catalog-projection (RFC 0078 §B/§F)', () => {
84
84
  if (!(await toolCatalogGate('openwop-tool-catalog'))) return;
85
85
  const validate = descriptorValidator();
86
86
 
87
- // ---- Leg 3: auth-gated (unauthenticated list MUST be 401) -------------
88
- const unauth = await driver.get(toolsPath(), { authenticated: false });
89
- expect(
90
- unauth.status === 401,
91
- 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 require authentication (401 unauthenticated)`),
92
- ).toBe(true);
93
-
94
87
  // ---- Leg 1: the list (§B) -------------------------------------------
95
88
  // unfailable-leg audit wave 2, 2026-09-27: a host that ADVERTISES the
96
89
  // catalog but 404s the list (or answers 500 / a non-list 200, which the
@@ -99,7 +92,17 @@ describe('tool-catalog-projection (RFC 0078 §B/§F)', () => {
99
92
  // fails under OPENWOP_REQUIRE_BEHAVIOR=true), and any other non-200 or
100
93
  // non-list body FAILS.
101
94
  const read = await listTools();
95
+ // The unserved check runs before any assertion: after the 401 assertion below,
96
+ // seamAbsent's `blocked` note recorded a partial-witness pass at major 1.
102
97
  if (read.unserved) return seamAbsent(`host advertises toolCatalog but ${toolsPath()} answered ${read.status}`);
98
+
99
+ // ---- Leg 3: auth-gated (unauthenticated list MUST be 401) -------------
100
+ const unauth = await driver.get(toolsPath(), { authenticated: false });
101
+ expect(
102
+ unauth.status === 401,
103
+ 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 require authentication (401 unauthenticated)`),
104
+ ).toBe(true);
105
+
103
106
  expect(
104
107
  read.status,
105
108
  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`),
@@ -50,7 +50,7 @@ describe('tool-session-lifecycle (RFC 0078 §D)', () => {
50
50
  opened.length >= 1 && closed.length >= 1,
51
51
  req('openwop.it.tool-session-lifecycle.brackets-the-call-events-with-tool-session-opened-first-closed-last-one-sessioni', 'tool-catalog.md §D', 'a tool session MUST emit tool.session.opened + tool.session.closed'),
52
52
  ).toBe(true);
53
- if (opened.length === 0 || closed.length === 0) return softSkip('blocked', 'precondition not met — `opened.length === 0 || closed.length === 0` returned early (seam, prior step, or fixture unavailable)');
53
+ if (opened.length === 0 || closed.length === 0) throw new Error('unreachable: the assertion above has already failed this leg');
54
54
 
55
55
  const open = opened[0]!;
56
56
  const close = closed[closed.length - 1]!;
@@ -48,7 +48,7 @@ import { driver } from '../lib/driver.js';
48
48
  import { behaviorGate } from '../lib/behavior-gate.js';
49
49
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
50
50
  import { req } from '../lib/requirement-ids.js';
51
- import { softSkip } from '../lib/soft-skip.js';
51
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
52
52
 
53
53
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
54
54
 
@@ -213,7 +213,7 @@ describe.skipIf(HTTP_SKIP)('trigger-ingestion: behavioral ingestion + SSRF (capa
213
213
  verification: { mode: 'none' },
214
214
  webhook: { method: 'POST', headers: { Authorization: 'Bearer canary' }, body: { x: 1 } },
215
215
  });
216
- if (del.status === 404 || del.status === 403) return softSkip('blocked', 'precondition not met — `del.status === 404 || del.status === 403` returned early (seam, prior step, or fixture unavailable)');
216
+ if (del.status === 404 || del.status === 403) return blockedDespiteAssertions('the ingest seam answered the SSRF probe but not the webhook delivery — ingestion content redaction is unobserved');
217
217
  const delBody = del.json as
218
218
  | { deliveryEvent?: Record<string, unknown>; triggerEvent?: { webhook?: { headers?: Record<string, string> } } }
219
219
  | undefined;
@@ -306,3 +306,18 @@ describe('RFC 0209 §C.9 — no render before root (declared unwitnessable here;
306
306
  return softSkip('inapplicable', 'unwitnessable by this suite (RFC 0209 §C.9 Falsifiability verdict): a render-side guarantee, and a server-oriented suite cannot observe a renderer. The fold guard the server CAN observe is openwop.requirement.0209.fold-guarded, above. The render half is witnessed as a reference-app client probe (tier: reference-impl, the a2ui-surface-no-code-exec precedent): openwop/openwop-app frontend/react/src/chat/a2ui/__tests__/a2ui-v09-render-needs-root.test.tsx (openwop-app#4121, ADR 0749). Removing the `if (!state.renderable)` guard in frontend/react/src/chat/a2ui/v09/A2uiV09Surface.tsx turns 2 of its 4 cases red. This row records that the suite reached the requirement, and it is not a pass.');
307
307
  });
308
308
  });
309
+
310
+ describe('RFC 0220 §C — the a2uiSurface family witness (seam-gated)', () => {
311
+ it('a version-2 surface is admitted and a version-1 body under schema version 2 is refused, in the same run', async () => {
312
+ const g = await gate((f) => f >= 2, 'a floor of at least 2');
313
+ if (!g.ok) return softSkip(g.kind, g.reason);
314
+ const { runId } = g.ctx;
315
+ const R = (why: string) => req('openwop.family.a2uiSurface', 'spec/v2/ext/a2uiSurface/README.md §Conformance', why);
316
+ try {
317
+ const good = await emit(runId, envelope(2, v2(sid('fam-good'), (POSITIVE['messages'] as Json[]))));
318
+ expect(good.status, R(`the positive version-2 surface MUST be admitted (201), got ${code(good)}`)).toBe(201);
319
+ const mixed = await emit(runId, envelope(2, V1_SURFACE));
320
+ expect(code(mixed), R('a version-1 body under schema version 2 MUST be refused with envelope_invalid (control: a host that admits everything fails here)')).toBe('422 envelope_invalid');
321
+ } finally { await done(runId); }
322
+ });
323
+ });