@openwop/openwop-conformance 1.106.1 → 1.124.0

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 (166) hide show
  1. package/README.md +3 -3
  2. package/coverage.md +6 -0
  3. package/dist/cli.js +89 -45
  4. package/dist/lib/certification-bundle-verify.js +274 -0
  5. package/dist/lib/profiles.js +102 -19
  6. package/dist/lib/requirement-ledger.js +182 -0
  7. package/dist/lib/requirement-registry.js +110 -0
  8. package/dist/lib/scenario-disposition.js +249 -0
  9. package/dist/lib/soft-skip.js +82 -0
  10. package/fixtures/conformance-a2a-task-roundtrip.json +1 -1
  11. package/fixtures.md +1 -1
  12. package/package.json +1 -1
  13. package/schemas/CORPUS-STAMP.json +2 -2
  14. package/schemas/README.md +1 -0
  15. package/schemas/capabilities.schema.json +2 -2
  16. package/schemas/compensation-policy.schema.json +64 -0
  17. package/schemas/run-snapshot.schema.json +5 -0
  18. package/schemas/workflow-chain-pack-manifest.schema.json +125 -0
  19. package/schemas/workflow-definition.schema.json +4 -0
  20. package/src/cli.ts +99 -45
  21. package/src/lib/a2a-fake-peer.ts +290 -110
  22. package/src/lib/certification-bundle-verify.ts +356 -0
  23. package/src/lib/error-envelope.ts +77 -0
  24. package/src/lib/llm-cache-key-recipe.ts +6 -5
  25. package/src/lib/mcp-fake-server.ts +235 -72
  26. package/src/lib/profiles.ts +132 -19
  27. package/src/lib/requirement-ledger.ts +98 -1
  28. package/src/lib/requirement-registry.ts +47 -4
  29. package/src/lib/scenario-disposition.ts +291 -0
  30. package/src/lib/soft-skip.ts +89 -0
  31. package/src/lib/workflow-chain-expansion.ts +189 -0
  32. package/src/scenarios/a2a-1-0-agent-card.test.ts +179 -0
  33. package/src/scenarios/a2a-1-0-task-roundtrip.test.ts +109 -0
  34. package/src/scenarios/a2a-card-runtime-consistency.test.ts +167 -0
  35. package/src/scenarios/a2a-peer-authority.test.ts +99 -0
  36. package/src/scenarios/a2a-task-roundtrip.test.ts +18 -6
  37. package/src/scenarios/agent-loop-iteration-monotonic.test.ts +3 -2
  38. package/src/scenarios/agent-loop-stateful-resume.test.ts +3 -2
  39. package/src/scenarios/agent-loop-version5-shape.test.ts +2 -1
  40. package/src/scenarios/agent-loop-workspace-snapshot.test.ts +4 -3
  41. package/src/scenarios/approval-gate-flow.test.ts +5 -4
  42. package/src/scenarios/artifact-type-store-without-render.test.ts +2 -1
  43. package/src/scenarios/auth-scim-profile.test.ts +5 -4
  44. package/src/scenarios/authorization-fail-closed.test.ts +4 -3
  45. package/src/scenarios/certification-bundle-non-vacuous.test.ts +249 -0
  46. package/src/scenarios/certification-bundle-redaction.test.ts +182 -0
  47. package/src/scenarios/certification-floor-enforcement.test.ts +30 -6
  48. package/src/scenarios/chain-compensation-expansion.test.ts +243 -0
  49. package/src/scenarios/commitment-fired.test.ts +3 -2
  50. package/src/scenarios/compensation-behavior.test.ts +63 -0
  51. package/src/scenarios/compensation-profile.test.ts +144 -1
  52. package/src/scenarios/conformance-advertised-seam-required.test.ts +118 -0
  53. package/src/scenarios/conformance-execution-witness.test.ts +104 -0
  54. package/src/scenarios/conversationCapabilityNegotiation.test.ts +3 -2
  55. package/src/scenarios/core-manifest-and-extension-registry.test.ts +26 -0
  56. package/src/scenarios/credential-payload-redaction.test.ts +4 -3
  57. package/src/scenarios/credentials-capability-shape.test.ts +4 -3
  58. package/src/scenarios/cross-engine-append-behavior.test.ts +5 -4
  59. package/src/scenarios/cross-engine-append-ordering.test.ts +3 -2
  60. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +5 -4
  61. package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
  62. package/src/scenarios/cross-workspace-isolation.test.ts +4 -3
  63. package/src/scenarios/data-residency-admission.test.ts +6 -6
  64. package/src/scenarios/deadletter-retry-exhaustion.test.ts +5 -4
  65. package/src/scenarios/distillation-index-roundtrip.test.ts +3 -2
  66. package/src/scenarios/distillation-secret-carryforward.test.ts +3 -2
  67. package/src/scenarios/distillation-shape.test.ts +2 -1
  68. package/src/scenarios/distillation-stable-archive.test.ts +4 -3
  69. package/src/scenarios/distillation-token-budget.test.ts +4 -3
  70. package/src/scenarios/envelope-recovery-applied.test.ts +3 -2
  71. package/src/scenarios/envelope-refusal-shape.test.ts +4 -3
  72. package/src/scenarios/error-envelope-canonical-shape.test.ts +64 -0
  73. package/src/scenarios/feedback-correction-redaction.test.ts +4 -3
  74. package/src/scenarios/feedback-cross-tenant-isolation.test.ts +4 -3
  75. package/src/scenarios/feedback-fork-not-copied.test.ts +6 -5
  76. package/src/scenarios/feedback-on-terminal-run.test.ts +4 -3
  77. package/src/scenarios/feedback-record-and-list.test.ts +4 -3
  78. package/src/scenarios/feedback-unsupported-501.test.ts +3 -2
  79. package/src/scenarios/fixtures-valid.test.ts +16 -18
  80. package/src/scenarios/fs-path-traversal.test.ts +3 -2
  81. package/src/scenarios/heartbeat-capability-shape.test.ts +2 -1
  82. package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +4 -3
  83. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +6 -5
  84. package/src/scenarios/heartbeat-runtime-bound.test.ts +4 -3
  85. package/src/scenarios/mcp-2026-07-28-discover.test.ts +207 -0
  86. package/src/scenarios/mcp-cache-tenant-scope.test.ts +66 -0
  87. package/src/scenarios/mcp-current-auth-boundary.test.ts +55 -0
  88. package/src/scenarios/mcp-extension-opacity.test.ts +79 -0
  89. package/src/scenarios/mcp-mrtr-roundtrip.test.ts +140 -0
  90. package/src/scenarios/mcp-stateless-request.test.ts +77 -0
  91. package/src/scenarios/mcp-tool-roundtrip.test.ts +4 -3
  92. package/src/scenarios/mcp-toolcall-redaction.test.ts +2 -1
  93. package/src/scenarios/memory-attribution-emits-on-write.test.ts +6 -5
  94. package/src/scenarios/memory-attribution-no-content.test.ts +4 -3
  95. package/src/scenarios/memory-attribution-replay-stable.test.ts +6 -5
  96. package/src/scenarios/memory-attribution-tenant-scoped.test.ts +4 -3
  97. package/src/scenarios/memory-compaction-event-emitted.test.ts +3 -2
  98. package/src/scenarios/memory-compaction-provenance-tag.test.ts +6 -5
  99. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +4 -3
  100. package/src/scenarios/memory-consolidation-idempotent.test.ts +4 -3
  101. package/src/scenarios/memory-injection-budget.test.ts +4 -3
  102. package/src/scenarios/metric-emission.test.ts +4 -3
  103. package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
  104. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +4 -3
  105. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +5 -4
  106. package/src/scenarios/multi-party-conversation-behavioral.test.ts +3 -2
  107. package/src/scenarios/multi-region-idempotency-behavior.test.ts +7 -6
  108. package/src/scenarios/multi-region-idempotency.test.ts +4 -3
  109. package/src/scenarios/normative-example-extraction.test.ts +242 -0
  110. package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +4 -3
  111. package/src/scenarios/oauth-capability-shape.test.ts +4 -3
  112. package/src/scenarios/oauth-connector-redaction.test.ts +4 -3
  113. package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +309 -0
  114. package/src/scenarios/org-position-no-authority-escalation.test.ts +2 -1
  115. package/src/scenarios/otel-emission-grpc.test.ts +4 -3
  116. package/src/scenarios/otel-emission.test.ts +4 -3
  117. package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +5 -4
  118. package/src/scenarios/otel-trace-propagation.test.ts +4 -3
  119. package/src/scenarios/pack-registry-isolation.test.ts +4 -3
  120. package/src/scenarios/pack-registry-publish.test.ts +65 -3
  121. package/src/scenarios/pack-registry.test.ts +22 -0
  122. package/src/scenarios/profile-discovery-core-alias.test.ts +137 -0
  123. package/src/scenarios/profileDerivation.test.ts +2 -1
  124. package/src/scenarios/provider-usage.test.ts +2 -2
  125. package/src/scenarios/rate-limit-envelope.test.ts +2 -1
  126. package/src/scenarios/registry-public.test.ts +6 -5
  127. package/src/scenarios/replay-divergence-at-refusal.test.ts +9 -8
  128. package/src/scenarios/replay-llm-cache-key-portable.test.ts +19 -13
  129. package/src/scenarios/replay-llm-cache-key.test.ts +120 -47
  130. package/src/scenarios/replay-observable-sequence-determinism.test.ts +5 -4
  131. package/src/scenarios/replay-side-effect-suppression.test.ts +20 -1
  132. package/src/scenarios/requirement-ledger.test.ts +29 -6
  133. package/src/scenarios/runner-ledger.test.ts +279 -0
  134. package/src/scenarios/runtime-requires-install-gate.test.ts +6 -5
  135. package/src/scenarios/sandbox-memory-cap.test.ts +3 -2
  136. package/src/scenarios/sandbox-mvp-behavior.test.ts +11 -10
  137. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
  138. package/src/scenarios/sandbox-timeout-cap.test.ts +3 -2
  139. package/src/scenarios/stream-text-fixture.test.ts +5 -4
  140. package/src/scenarios/subrun-approval-fail-closed.test.ts +3 -2
  141. package/src/scenarios/subrun-approval-gate.test.ts +4 -3
  142. package/src/scenarios/subrun-attestation-shape.test.ts +2 -1
  143. package/src/scenarios/subrun-checksum-stable.test.ts +4 -3
  144. package/src/scenarios/table-schema-enforcement.test.ts +2 -2
  145. package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +3 -2
  146. package/src/scenarios/tool-hooks-content-free.test.ts +3 -2
  147. package/src/scenarios/tool-hooks-rate-limit.test.ts +3 -2
  148. package/src/scenarios/tool-hooks-secret-redaction.test.ts +3 -2
  149. package/src/scenarios/tool-hooks-shape.test.ts +2 -1
  150. package/src/scenarios/trigger-bridge-delivery.test.ts +3 -2
  151. package/src/scenarios/version-fold.test.ts +5 -4
  152. package/src/scenarios/voice-streamref-tenant-bound.test.ts +2 -1
  153. package/src/scenarios/voice-transcription-streaming.test.ts +2 -1
  154. package/src/scenarios/voice-transcription-unadvertised.test.ts +2 -1
  155. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +5 -4
  156. package/src/scenarios/wasm-pack-invoke-completed.test.ts +3 -2
  157. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +3 -2
  158. package/src/scenarios/wasm-pack-load.test.ts +4 -3
  159. package/src/scenarios/wasm-pack-memory-cap.test.ts +4 -3
  160. package/src/scenarios/wasm-pack-replay-determinism.test.ts +3 -2
  161. package/src/scenarios/webhook-signed-delivery.test.ts +4 -3
  162. package/src/scenarios/workflow-primary-output-annotation.test.ts +13 -23
  163. package/src/scenarios/workload-identity-behavior.test.ts +7 -4
  164. package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +3 -2
  165. package/src/scenarios/workspace-cross-tenant-isolation.test.ts +3 -2
  166. package/src/setup.ts +95 -1
@@ -31,7 +31,7 @@ import { describe, it, expect } from 'vitest';
31
31
  import { readFileSync, readdirSync } from 'node:fs';
32
32
  import { join, resolve as pathResolve } from 'node:path';
33
33
  import Ajv2020 from 'ajv/dist/2020.js';
34
- import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
34
+ import { FIXTURES_DIR, SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
35
35
 
36
36
  /**
37
37
  * Schemas ship inside the package; RFC prose does not. The old form derived
@@ -154,6 +154,149 @@ describe('RFC 0151 §B — node compensation declaration', () => {
154
154
  });
155
155
  });
156
156
 
157
+ describe('RFC 0151 §B — the workflow-level compensation policy (`settings.compensation`)', () => {
158
+ // `compensation-policy.schema.json` was the last `Affects` artifact of the whole
159
+ // RFC 0147 program that did not exist. It says WHEN an unwind starts and HOW it
160
+ // runs; the node-level declaration only says WHAT the inverse action is.
161
+ const POLICY_SCHEMA = 'compensation-policy.schema.json';
162
+
163
+ function ajvAll() {
164
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
165
+ for (const file of readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json'))) {
166
+ ajv.addSchema(schema(file), file);
167
+ }
168
+ return ajv;
169
+ }
170
+ const policyValidator = () => {
171
+ const ajv = ajvAll();
172
+ return ajv.getSchema(POLICY_SCHEMA) ?? ajv.compile(schema(POLICY_SCHEMA));
173
+ };
174
+ const workflowValidator = () => {
175
+ const ajv = ajvAll();
176
+ return ajv.getSchema('workflow-definition.schema.json') ?? ajv.compile(schema('workflow-definition.schema.json'));
177
+ };
178
+
179
+ const minimalPolicy = { triggers: ['node-failure'] };
180
+ const fullPolicy = {
181
+ profileVersion: '1',
182
+ orderingModel: 'reverse-completion',
183
+ triggers: ['node-failure', 'run-cancel', 'cap-breach', 'operator-request'],
184
+ retry: { maxAttempts: 3, backoffMs: 500 },
185
+ timeoutMs: 30_000,
186
+ exhaustedDisposition: 'manual-intervention',
187
+ approvalScope: 'all',
188
+ onParentCancel: 'pause',
189
+ };
190
+
191
+ it('the schema exists, is closed, and requires triggers', () => {
192
+ const p = schema(POLICY_SCHEMA) as { additionalProperties?: boolean; required?: string[]; $id?: string };
193
+ expect(p.$id).toBe('https://openwop.dev/spec/v1/compensation-policy.schema.json');
194
+ expect(p.additionalProperties, 'closed — a host and an author must not be able to disagree about a key').toBe(false);
195
+ expect(p.required, 'a policy that names no trigger is not a policy').toEqual(['triggers']);
196
+ });
197
+
198
+ it('a minimal and a full policy validate', () => {
199
+ const validate = policyValidator();
200
+ expect(validate(minimalPolicy), JSON.stringify(validate.errors)).toBe(true);
201
+ expect(validate(fullPolicy), JSON.stringify(validate.errors)).toBe(true);
202
+ });
203
+
204
+ it('closed vocabularies: an unknown key, trigger, ordering model, or disposition is rejected', () => {
205
+ const validate = policyValidator();
206
+ for (const bad of [
207
+ { ...minimalPolicy, rollback: true },
208
+ { triggers: [] },
209
+ { triggers: ['on-error'] },
210
+ { triggers: ['node-failure', 'node-failure'] },
211
+ { ...minimalPolicy, orderingModel: 'forward' },
212
+ { ...minimalPolicy, exhaustedDisposition: 'ignore' },
213
+ { ...minimalPolicy, approvalScope: 'none' },
214
+ { ...minimalPolicy, onParentCancel: 'abandon' },
215
+ { ...minimalPolicy, profileVersion: '0' },
216
+ { ...minimalPolicy, retry: { maxAttempts: 0 } },
217
+ ]) {
218
+ expect(validate(bad), `MUST be rejected: ${JSON.stringify(bad)}`).toBe(false);
219
+ }
220
+ });
221
+
222
+ it('there is no `none` approval scope — a policy can only escalate approval, never strip it', () => {
223
+ const p = schema(POLICY_SCHEMA) as { properties: { approvalScope: { enum: string[] } } };
224
+ expect(p.properties.approvalScope.enum).toEqual(['declared', 'all']);
225
+ });
226
+
227
+ it('attaches to WorkflowDefinition as `settings.compensation` and validates through the workflow schema', () => {
228
+ const wf = schema('workflow-definition.schema.json') as {
229
+ $defs: { WorkflowSettings: { properties: Record<string, { $ref?: string }> } };
230
+ };
231
+ expect(wf.$defs.WorkflowSettings.properties['compensation']?.$ref).toBe(POLICY_SCHEMA);
232
+ const validate = workflowValidator();
233
+ // A real, valid workflow fixture — so the leg proves the $ref is enforced
234
+ // through the workflow schema rather than that a hand-built object happens
235
+ // to satisfy WorkflowDefinition's required set.
236
+ const base = JSON.parse(
237
+ readFileSync(join(FIXTURES_DIR, 'conformance-subworkflow-child.json'), 'utf8'),
238
+ ) as { settings?: Record<string, unknown> };
239
+ expect(validate(base), `fixture must be valid on its own: ${JSON.stringify(validate.errors)}`).toBe(true);
240
+ const ok = validate({ ...base, settings: { ...(base.settings ?? {}), compensation: fullPolicy } });
241
+ expect(ok, JSON.stringify(validate.errors)).toBe(true);
242
+ const bad = validate({ ...base, settings: { ...(base.settings ?? {}), compensation: { triggers: [] } } });
243
+ expect(bad, 'the $ref must actually be enforced through the workflow schema').toBe(false);
244
+ });
245
+ });
246
+
247
+ describe('RFC 0151 §D — the run rollup `compensationStatus` (RunSnapshot)', () => {
248
+ // Resolves RFC 0151 UQ3: `RunSnapshot` is the sole owner. Debug bundles and the
249
+ // AsyncAPI `run.snapshot` reuse the snapshot by $ref, so one property covers all
250
+ // three surfaces — and one enum keeps them from drifting apart.
251
+ const RUN_SNAPSHOT_SCHEMA = 'run-snapshot.schema.json';
252
+ const STATUSES = ['none', 'pending', 'running', 'completed', 'partial', 'failed', 'manual'] as const;
253
+
254
+ function snapshotValidator() {
255
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
256
+ for (const file of readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json'))) {
257
+ ajv.addSchema(schema(file), file);
258
+ }
259
+ return ajv.getSchema(RUN_SNAPSHOT_SCHEMA) ?? ajv.compile(schema(RUN_SNAPSHOT_SCHEMA));
260
+ }
261
+
262
+ it('is declared on RunSnapshot as a closed enum of the seven §D values', () => {
263
+ const snap = schema(RUN_SNAPSHOT_SCHEMA) as {
264
+ properties: Record<string, { type?: string; enum?: string[] }>;
265
+ required: string[];
266
+ };
267
+ const field = snap.properties['compensationStatus'];
268
+ expect(field, 'RunSnapshot MUST declare `compensationStatus` (RFC 0151 §D, UQ3)').toBeDefined();
269
+ expect(field?.enum, 'the value set is closed and exactly the seven §D values').toEqual([...STATUSES]);
270
+ expect(
271
+ snap.required.includes('compensationStatus'),
272
+ 'OPTIONAL on the schema — presence is governed by the capability gate in prose, not by `required`, ' +
273
+ 'so a host that does not advertise `compensation` still validates',
274
+ ).toBe(false);
275
+ });
276
+
277
+ it('every §D value validates and a foreign value is rejected', () => {
278
+ const validate = snapshotValidator();
279
+ for (const value of STATUSES) {
280
+ const ok = validate({ runId: 'r1', workflowId: 'w1', status: 'failed', compensationStatus: value });
281
+ expect(ok, `compensationStatus=${value} MUST validate: ${JSON.stringify(validate.errors)}`).toBe(true);
282
+ }
283
+ for (const bad of ['compensating', 'COMPLETED', 'done', 'skipped', 'paused', 1, null, true]) {
284
+ const ok = validate({ runId: 'r1', workflowId: 'w1', status: 'failed', compensationStatus: bad });
285
+ expect(ok, `compensationStatus=${JSON.stringify(bad)} MUST be rejected — the fold is closed`).toBe(false);
286
+ }
287
+ });
288
+
289
+ it('a snapshot without the field still validates — the gate lives in prose', () => {
290
+ const validate = snapshotValidator();
291
+ expect(validate({ runId: 'r1', workflowId: 'w1', status: 'completed' })).toBe(true);
292
+ });
293
+
294
+ it('the forward `status` enum gained no `compensating` value — §D forbids reinterpreting it', () => {
295
+ const snap = schema(RUN_SNAPSHOT_SCHEMA) as { properties: { status: { enum: string[] } } };
296
+ expect(snap.properties.status.enum).not.toContain('compensating');
297
+ });
298
+ });
299
+
157
300
  describe.skipIf(RFCS_DIR === null)('RFC 0151 — what this file does NOT establish', () => {
158
301
  it('records that behavioral conformance is absent, per RFC 0147 §A.5', () => {
159
302
  // Not decoration. RFC 0147 §A.5 forbids `Accepted` on shape-only evidence
@@ -0,0 +1,118 @@
1
+ /**
2
+ * RFC 0148 §B / RFC 0147 program scenario — `conformance-advertised-seam-required`:
3
+ * under `OPENWOP_REQUIRE_BEHAVIOR=true` an advertised capability whose
4
+ * observation seam is missing FAILS; it never soft-skips into a pass.
5
+ *
6
+ * Three mechanisms carry the rule and each is pinned here with the env
7
+ * toggled in-process:
8
+ * - `behaviorGate(profile, advertised=false)` — a profile the host does not
9
+ * advertise and did not opt out of: default mode records `inapplicable`
10
+ * and returns false; strict mode THROWS (advertise or opt out explicitly);
11
+ * - `behaviorGatePresent(profile, null)` — a seam helper returned nothing:
12
+ * the same rule, because an advertised capability with no seam is a claim
13
+ * the suite cannot check;
14
+ * - `seamAbsent(reason)` — an advertised capability whose seam answered
15
+ * 404/403 mid-scenario: default mode notes `blocked` with the reason;
16
+ * strict mode THROWS (RFC 0148 §B; RFC 0139 G14 flip). A 403 is not a
17
+ * pass.
18
+ * `OPENWOP_OPTED_OUT_PROFILES` is the only sanctioned way to keep strict mode
19
+ * green for a profile a host genuinely does not implement — and it records
20
+ * `skipped` with the reason, never a pass.
21
+ *
22
+ * Server-free, always-on; MUST NOT capability-skip (RFC 0148 §Conformance).
23
+ */
24
+
25
+ import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
26
+ import { behaviorGate, behaviorGatePresent } from '../lib/behavior-gate.js';
27
+ import { seamAbsent, softSkipDisposition, resetSoftSkips } from '../lib/soft-skip.js';
28
+ import { dispositionOf, resetLedger, suspendSinkForFixtures } from '../lib/requirement-ledger.js';
29
+ import { __resetEnvCacheForTests } from '../lib/env.js';
30
+
31
+ export const HOST_CALLBACK_NOT_REQUIRED = 'server-free: toggles OPENWOP_REQUIRE_BEHAVIOR in-process and pins the advertised-missing-seam rule';
32
+
33
+ const PROFILE = 'openwop-conformance-advertised-seam-probe';
34
+ const ENV_KEYS = ['OPENWOP_REQUIRE_BEHAVIOR', 'OPENWOP_OPTED_OUT_PROFILES', 'OPENWOP_BASE_URL', 'OPENWOP_API_KEY'] as const;
35
+ let saved: Record<string, string | undefined> = {};
36
+ let restoreSink: (() => void) | undefined;
37
+
38
+ beforeAll(() => {
39
+ // this file records fixture profile ids into the ledger — keep them out of a live sink
40
+ restoreSink = suspendSinkForFixtures();
41
+ });
42
+ afterAll(() => restoreSink?.());
43
+
44
+ beforeEach(() => {
45
+ resetLedger();
46
+ resetSoftSkips();
47
+ saved = {};
48
+ for (const k of ENV_KEYS) {
49
+ saved[k] = process.env[k];
50
+ delete process.env[k];
51
+ }
52
+ // `loadEnv()` requires a base URL and an api key even on paths that reach no
53
+ // host, and it caches — stub both and drop the cache per test.
54
+ process.env['OPENWOP_BASE_URL'] = 'https://conformance.invalid';
55
+ process.env['OPENWOP_API_KEY'] = 'dummy-not-used-no-request-is-made';
56
+ __resetEnvCacheForTests();
57
+ });
58
+
59
+ afterEach(() => {
60
+ for (const k of ENV_KEYS) {
61
+ if (saved[k] === undefined) delete process.env[k];
62
+ else process.env[k] = saved[k];
63
+ }
64
+ __resetEnvCacheForTests();
65
+ resetLedger();
66
+ resetSoftSkips();
67
+ });
68
+
69
+ describe('RFC 0148 §B — conformance-advertised-seam-required', () => {
70
+ it('default mode: an unadvertised profile records inapplicable and gates false — never a pass', () => {
71
+ delete process.env['OPENWOP_REQUIRE_BEHAVIOR'];
72
+ __resetEnvCacheForTests();
73
+ delete process.env['OPENWOP_OPTED_OUT_PROFILES'];
74
+ expect(behaviorGate(PROFILE, false)).toBe(false);
75
+ expect(dispositionOf(`openwop.profile.${PROFILE}`)).toBe('inapplicable');
76
+ });
77
+
78
+ it('strict mode: an unadvertised, un-opted-out profile FAILS (advertise or opt out explicitly)', () => {
79
+ process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
80
+ __resetEnvCacheForTests();
81
+ delete process.env['OPENWOP_OPTED_OUT_PROFILES'];
82
+ expect(() => behaviorGate(PROFILE, false)).toThrow(/OPENWOP_REQUIRE_BEHAVIOR=true/);
83
+ });
84
+
85
+ it('strict mode: an advertised capability whose seam helper returned nothing FAILS (behaviorGatePresent)', () => {
86
+ process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
87
+ __resetEnvCacheForTests();
88
+ delete process.env['OPENWOP_OPTED_OUT_PROFILES'];
89
+ expect(() => behaviorGatePresent(PROFILE, null)).toThrow();
90
+ // and with a value present it narrows and passes through
91
+ delete process.env['OPENWOP_REQUIRE_BEHAVIOR'];
92
+ __resetEnvCacheForTests();
93
+ expect(behaviorGatePresent(PROFILE, { ok: true })).toBe(true);
94
+ });
95
+
96
+ it('strict mode: seamAbsent (advertised, seam answered 404/403 mid-scenario) FAILS; default mode notes blocked with the reason', () => {
97
+ process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
98
+ __resetEnvCacheForTests();
99
+ expect(() => seamAbsent('host advertises X but /v1/host/sample/x answered 404')).toThrow(/RFC 0148 §B/);
100
+ delete process.env['OPENWOP_REQUIRE_BEHAVIOR'];
101
+ __resetEnvCacheForTests();
102
+ resetSoftSkips();
103
+ expect(seamAbsent('host advertises X but /v1/host/sample/x answered 403')).toBeUndefined();
104
+ const noted = softSkipDisposition('conformance-advertised-seam-required.test.ts');
105
+ expect(noted?.kind).toBe('blocked');
106
+ expect(noted?.reason).toContain('answered 403');
107
+ });
108
+
109
+ it('the sanctioned escape is an explicit opt-out, which records skipped with the reason — still never a pass', () => {
110
+ process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
111
+ __resetEnvCacheForTests();
112
+ process.env['OPENWOP_OPTED_OUT_PROFILES'] = PROFILE;
113
+ __resetEnvCacheForTests();
114
+ expect(behaviorGate(PROFILE, false)).toBe(false);
115
+ expect(dispositionOf(`openwop.profile.${PROFILE}`)).toBe('skipped');
116
+ });
117
+ });
118
+
@@ -0,0 +1,104 @@
1
+ /**
2
+ * RFC 0148 §A / RFC 0147 program scenario — `conformance-execution-witness`:
3
+ * an early return cannot become a pass.
4
+ *
5
+ * The runner's file-level record (`resolveFileRecord`, applied by `setup.ts`
6
+ * `afterAll`) is the mechanism that turned every "test returned before its
7
+ * first expect → vitest pass" into a disposition RFC 0148 §A can act on:
8
+ *
9
+ * - a failed test ⇒ executed-fail
10
+ * - a witnessed pass ⇒ executed-pass (assertionCount > 0)
11
+ * - a zero-assertion pass with a noted reason (softSkip / seamAbsent /
12
+ * behaviorGate) ⇒ inapplicable | skipped | blocked, with that reason
13
+ * - a zero-assertion pass with NO reason ⇒ blocked + the marker detail,
14
+ * which certification still treats as an UNCLASSIFIED return
15
+ *
16
+ * and the certification layer refuses a claim carrying an unwitnessed row.
17
+ * These legs pin the rule at each layer — runner record, emitter derivation,
18
+ * consumer verifier — with sabotage vectors, so the guarantee is a property
19
+ * the suite defends rather than a property it happens to have.
20
+ *
21
+ * Server-free, always-on; MUST NOT capability-skip (RFC 0148 §Conformance).
22
+ */
23
+
24
+ import { describe, it, expect } from 'vitest';
25
+ import { resolveFileRecord, deriveRequirementDispositions } from '../lib/scenario-disposition.js';
26
+ import { UNCLASSIFIED_RETURN_DETAIL } from '../lib/soft-skip.js';
27
+ import { PROFILE_FLOOR_SCENARIOS } from '../lib/profiles.js';
28
+ import { requirementIdForScenario, requirementsFor } from '../lib/requirement-registry.js';
29
+ import { verifyBundleV2, type BundleV2Requirement } from '../lib/certification-bundle-verify.js';
30
+
31
+ export const HOST_CALLBACK_NOT_REQUIRED = 'server-free: pins the runner/emitter/verifier rule that an early return is never a pass';
32
+
33
+ const HEX = 'c'.repeat(64);
34
+
35
+ describe('RFC 0148 §A — conformance-execution-witness: the runner record', () => {
36
+ it('a witnessed pass is executed-pass; a failed test is executed-fail regardless of count', () => {
37
+ expect(resolveFileRecord(['pass', 'pass'], undefined, 7, null).disposition).toBe('executed-pass');
38
+ expect(resolveFileRecord(['pass', 'fail'], undefined, 7, null).disposition).toBe('executed-fail');
39
+ expect(resolveFileRecord(['fail'], undefined, 0, null).disposition).toBe('executed-fail');
40
+ });
41
+
42
+ it('a zero-assertion pass with NO reason is BLOCKED with the marker — never a pass', () => {
43
+ const r = resolveFileRecord(['pass', 'pass'], undefined, 0, null);
44
+ expect(r.disposition).toBe('blocked');
45
+ expect(r.detail).toBe(UNCLASSIFIED_RETURN_DETAIL);
46
+ });
47
+
48
+ it('a zero-assertion pass takes the noted reason (softSkip / seamAbsent) — inapplicable, skipped, or blocked with the text', () => {
49
+ expect(resolveFileRecord(['pass'], undefined, 0, { kind: 'inapplicable', reason: 'host does not advertise X' })).toEqual({ disposition: 'inapplicable', detail: 'host does not advertise X' });
50
+ expect(resolveFileRecord(['pass'], undefined, 0, { kind: 'blocked', reason: 'seam answered 404' })).toEqual({ disposition: 'blocked', detail: 'seam answered 404' });
51
+ expect(resolveFileRecord(['pass'], undefined, 0, { kind: 'skipped', reason: 'operator opt-out' })).toEqual({ disposition: 'skipped', detail: 'operator opt-out' });
52
+ });
53
+
54
+ it('a behaviorGate reason resolves a zero-assertion pass to inapplicable/skipped; a note outranks nothing but never a witnessed pass', () => {
55
+ expect(resolveFileRecord(['pass'], 'inapplicable', 0, null).disposition).toBe('inapplicable');
56
+ expect(resolveFileRecord(['pass'], 'skipped', 0, null).disposition).toBe('skipped');
57
+ // real assertions ran: the note is irrelevant, the file passed
58
+ expect(resolveFileRecord(['pass'], undefined, 3, { kind: 'blocked', reason: 'irrelevant' }).disposition).toBe('executed-pass');
59
+ });
60
+ });
61
+
62
+ describe('RFC 0148 §A — conformance-execution-witness: emitter and verifier refuse an unwitnessed floor row', () => {
63
+ const profile = 'openwop-core-standard';
64
+ const files = PROFILE_FLOOR_SCENARIOS[profile]!.required;
65
+ const ids = requirementsFor(profile, {}) ?? [];
66
+
67
+ it('the emitter derivation marks a marker-blocked floor row unclassified and rejects the claim', () => {
68
+ const rep = new Map<string, 'passed' | 'failed' | 'skipped'>(files.map((f) => [f, 'passed' as const]));
69
+ const ledger = files.map((f) => ({ requirementId: requirementIdForScenario(f), disposition: 'executed-pass' as const, assertionCount: 3 }));
70
+ // sabotage: one floor file returned early with no reason
71
+ ledger[0] = { requirementId: requirementIdForScenario(files[0]!), disposition: 'blocked' as unknown as 'executed-pass', assertionCount: 0, ...( { detail: UNCLASSIFIED_RETURN_DETAIL } as object) } as typeof ledger[number];
72
+ // add the prefix witness so only the sabotaged row is unclassified
73
+ ledger.push({ requirementId: 'openwop.scenario.interrupt-alpha', disposition: 'executed-pass' as const, assertionCount: 2 });
74
+ rep.set('interrupt-alpha.test.ts', 'passed');
75
+ const d = deriveRequirementDispositions(rep, ledger, [profile], {});
76
+ const v = d.verdicts.find((x) => x.profile === profile)!;
77
+ expect(v.unclassified).toEqual([requirementIdForScenario(files[0]!)]);
78
+ expect(d.rejectUnclassified).toBe(true);
79
+ // and the same file with a witnessed pass certifies
80
+ ledger[0] = { requirementId: requirementIdForScenario(files[0]!), disposition: 'executed-pass' as const, assertionCount: 3 };
81
+ const d2 = deriveRequirementDispositions(rep, ledger, [profile], {});
82
+ expect(d2.verdicts.find((x) => x.profile === profile)!.certifiable).toBe(true);
83
+ expect(ids.length).toBeGreaterThan(0);
84
+ });
85
+
86
+ it('the consumer verifier rejects a bundle whose required row is a vacuous pass or the runner marker, and accepts the witnessed twin', () => {
87
+ const doc = { protocolVersion: '1.0', supportedEnvelopes: ['clarification.request'], supportedTransports: ['rest'], schemaVersions: { workflow: '1.0' }, limits: { clarificationRounds: 1, schemaRounds: 1, envelopesPerTurn: 1 } };
88
+ const rows: BundleV2Requirement[] = files.map((f) => ({ requirementId: requirementIdForScenario(f), scenarioId: f, disposition: 'executed-pass', assertionCount: 3 }));
89
+ rows.push({ requirementId: 'openwop.scenario.interrupt-alpha', scenarioId: 'interrupt-alpha.test.ts', disposition: 'executed-pass', assertionCount: 2 });
90
+ const bundle = (rs: BundleV2Requirement[]) => ({
91
+ bundleVersion: '2', suite: { package: '@openwop/openwop-conformance', version: '1.123.0' }, host: { name: 'v', version: '0' },
92
+ discovery: { url: 'https://example.invalid/.well-known/openwop', sha256: HEX, document: doc }, claimedProfiles: [profile],
93
+ results: { totals: { executedPass: rs.filter((r) => r.disposition === 'executed-pass').length, executedFail: 0, skipped: 0, inapplicable: 0, blocked: rs.filter((r) => r.disposition === 'blocked').length }, requirements: rs },
94
+ scenarioManifestSha256: HEX, targetConfigurationSha256: HEX,
95
+ });
96
+ expect(verifyBundleV2(bundle(rows)).certified).toBe(true);
97
+ const vacuous = rows.map((r, i) => (i === 0 ? { ...r, assertionCount: 0 } : r));
98
+ expect(verifyBundleV2(bundle(vacuous)).evidenceValid).toBe(false);
99
+ const marker = rows.map((r, i) => (i === 0 ? { requirementId: r.requirementId, scenarioId: r.scenarioId, disposition: 'blocked', detail: UNCLASSIFIED_RETURN_DETAIL, assertionCount: 0 } : r));
100
+ const v = verifyBundleV2(bundle(marker));
101
+ expect(v.evidenceValid).toBe(false);
102
+ expect(v.profiles[0]?.rejections[0]?.kind).toBe('unwitnessed-requirement');
103
+ });
104
+ });
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { describe, it, expect } from 'vitest';
20
+ import { readErrorCode } from '../lib/error-envelope.js';
20
21
  import { driver } from '../lib/driver.js';
21
22
  import { isFixtureAdvertised } from '../lib/fixtures.js';
22
23
  import { isConversationPrimitiveSupported } from '../lib/multi-agent-capabilities.js';
@@ -33,8 +34,8 @@ describe.skipIf(SKIP)('conversationCapabilityNegotiation: refusal contract', ()
33
34
  // or at run-create (400). What MUST NOT happen is a successful
34
35
  // 201 followed by silent fallback.
35
36
  expect([400, 404, 422]).toContain(create.status);
36
- const body = create.json as { error?: { code?: string }; code?: string };
37
- const code = body.error?.code ?? body.code ?? '';
37
+ const body = create.json as { code?: string };
38
+ const code = readErrorCode(create.json) ?? body.code ?? '';
38
39
  expect(typeof code).toBe('string');
39
40
  });
40
41
  });
@@ -182,6 +182,32 @@ describe.skipIf(V1_DIR === null)('RFC 0155 §C — extension registry', () => {
182
182
  ).toEqual([]);
183
183
  });
184
184
 
185
+ it('the coverage block is present and accounts for every capability family (RFC 0155 §C, acceptance item 3)', () => {
186
+ // "Unlisted means uncovered" was a sentence; this makes it a checked list.
187
+ // The block is DERIVED by scripts/generate-extension-registry-coverage.mjs
188
+ // (--check runs in openwop:check); here we assert the invariant it encodes
189
+ // so a tarball consumer sees it too: every top-level capability family is
190
+ // either a core predicate field, covered by a record's capabilityPath, or
191
+ // listed as uncovered — and nothing is in two buckets.
192
+ const reg = registry as unknown as {
193
+ coverage?: { familiesTotal: number; coreFields: string[]; covered: string[]; uncovered: string[] };
194
+ extensions: Extension[];
195
+ };
196
+ expect(reg.coverage, 'RFC 0155 §C: the registry MUST carry a derived `coverage` block').toBeDefined();
197
+ const cov = reg.coverage as NonNullable<typeof reg.coverage>;
198
+ const families = Object.keys((caps().properties as Record<string, unknown>) ?? {}).sort();
199
+ expect(cov.familiesTotal).toBe(families.length);
200
+ const all = [...cov.coreFields, ...cov.covered, ...cov.uncovered].sort();
201
+ expect(all, 'core + covered + uncovered MUST partition the family set exactly').toEqual(families);
202
+ expect(new Set(all).size, 'no family may sit in two buckets').toBe(all.length);
203
+ const reached = new Set(reg.extensions.map((e) => e.capabilityPath.split('.')[0]));
204
+ for (const f of cov.covered) expect(reached.has(f), `${f} listed as covered MUST be reached by a record`).toBe(true);
205
+ for (const f of cov.uncovered) expect(reached.has(f), `${f} listed as uncovered MUST NOT be reached by a record`).toBe(false);
206
+ // The honest number, asserted so it cannot silently shrink by deletion of the
207
+ // uncovered list rather than by adding records.
208
+ expect(cov.uncovered.length + cov.covered.length + cov.coreFields.length).toBe(families.length);
209
+ });
210
+
185
211
  it('every record names the RFC that owns it', () => {
186
212
  // Vendor extensions may not use an `openwop-*` id without an accepted RFC
187
213
  // (§F). The owning RFC is what makes that checkable.
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  import { describe, it, expect } from 'vitest';
28
+ import { softSkip } from '../lib/soft-skip.js';
28
29
  import { driver } from '../lib/driver.js';
29
30
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
30
31
 
@@ -50,7 +51,7 @@ async function readCredentials(): Promise<DiscoveryCredentials | null> {
50
51
  describe('credential-payload-redaction: advertisement shape (RFC 0046 §A)', () => {
51
52
  it('capabilities.credentials.supported is a boolean when advertised', async () => {
52
53
  const cred = await readCredentials();
53
- if (cred === null) return;
54
+ if (cred === null) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cred === null` returned early');
54
55
  expect(
55
56
  typeof cred.supported,
56
57
  driver.describe(
@@ -64,13 +65,13 @@ describe('credential-payload-redaction: advertisement shape (RFC 0046 §A)', ()
64
65
  describe('credential-payload-redaction: resolved material MUST NOT cross the wire (RFC 0046 §C.1)', () => {
65
66
  it('canary plaintext is absent from every observable run surface', async () => {
66
67
  const cred = await readCredentials();
67
- if (!cred?.supported) return; // capability-gated
68
+ if (!cred?.supported) return softSkip('inapplicable', 'capability-gated');
68
69
 
69
70
  // Seam contract: resolve a seeded credential whose plaintext is CANARY,
70
71
  // run an echo node, and return the run's observable surfaces.
71
72
  const res = await driver.post('/v1/host/sample/credentials/echo', { canary: CANARY });
72
73
  // 404 from a host that hasn't wired the test seam is a soft-skip.
73
- if (res.status === 404) return;
74
+ if (res.status === 404) return softSkip('blocked', 'precondition not met — `res.status === 404` returned early (seam, prior step, or fixture unavailable)');
74
75
 
75
76
  expect(
76
77
  res.status,
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import { describe, it, expect } from 'vitest';
23
+ import { softSkip } from '../lib/soft-skip.js';
23
24
  import { driver } from '../lib/driver.js';
24
25
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
25
26
 
@@ -49,7 +50,7 @@ async function readCredentials(): Promise<DiscoveryCredentials | null> {
49
50
  describe('credentials-capability-shape: advertisement shape (RFC 0046 §A)', () => {
50
51
  it('capabilities.credentials is either absent or well-formed', async () => {
51
52
  const cred = await readCredentials();
52
- if (cred === null) return; // host doesn't advertise host.credentials at all
53
+ if (cred === null) return softSkip('inapplicable', 'host doesn\'t advertise host.credentials at all');
53
54
  expect(
54
55
  typeof cred.supported,
55
56
  driver.describe(
@@ -61,7 +62,7 @@ describe('credentials-capability-shape: advertisement shape (RFC 0046 §A)', ()
61
62
 
62
63
  it('scopes is a subset of {user, workspace, tenant} when supported', async () => {
63
64
  const cred = await readCredentials();
64
- if (!cred?.supported || cred.scopes === undefined) return;
65
+ if (!cred?.supported || cred.scopes === undefined) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!cred?.supported || cred.scopes === undefined` returned early');
65
66
  expect(
66
67
  Array.isArray(cred.scopes),
67
68
  driver.describe('RFC 0046 §A', 'capabilities.credentials.scopes MUST be an array'),
@@ -79,7 +80,7 @@ describe('credentials-capability-shape: advertisement shape (RFC 0046 §A)', ()
79
80
 
80
81
  it('rotation is one of {none, two-key-overlap} when present', async () => {
81
82
  const cred = await readCredentials();
82
- if (!cred?.supported || cred.rotation === undefined) return;
83
+ if (!cred?.supported || cred.rotation === undefined) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!cred?.supported || cred.rotation === undefined` returned early');
83
84
  expect(
84
85
  VALID_ROTATION.has(cred.rotation),
85
86
  driver.describe(
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  import { describe, it, expect } from 'vitest';
32
+ import { softSkip } from '../lib/soft-skip.js';
32
33
  import { driver } from '../lib/driver.js';
33
34
 
34
35
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -73,7 +74,7 @@ describe.skipIf(HTTP_SKIP)('cross-engine-append-behavior: §B cross-engine order
73
74
  const resetStatus = await resetLog();
74
75
  if (resetStatus === 404) {
75
76
  ctx.skip(); // host doesn't expose the cross-engine harness seam
76
- return;
77
+ return softSkip('blocked', 'precondition not met — `resetStatus === 404` returned early (seam, prior step, or fixture unavailable)');
77
78
  }
78
79
  expect(resetStatus).toBe(200);
79
80
 
@@ -125,7 +126,7 @@ describe.skipIf(HTTP_SKIP)('cross-engine-append-behavior: §B cross-engine order
125
126
  const resetStatus = await resetLog();
126
127
  if (resetStatus === 404) {
127
128
  ctx.skip();
128
- return;
129
+ return softSkip('blocked', 'precondition not met — `resetStatus === 404` returned early (seam, prior step, or fixture unavailable)');
129
130
  }
130
131
  expect(resetStatus).toBe(200);
131
132
 
@@ -153,7 +154,7 @@ describe.skipIf(HTTP_SKIP)('cross-engine-append-behavior: §B cross-engine order
153
154
  const resetStatus = await resetLog();
154
155
  if (resetStatus === 404) {
155
156
  ctx.skip();
156
- return;
157
+ return softSkip('blocked', 'precondition not met — `resetStatus === 404` returned early (seam, prior step, or fixture unavailable)');
157
158
  }
158
159
  expect(resetStatus).toBe(200);
159
160
 
@@ -180,7 +181,7 @@ describe.skipIf(HTTP_SKIP)('cross-engine-append-behavior: §B cross-engine order
180
181
  const resetStatus = await resetLog();
181
182
  if (resetStatus === 404) {
182
183
  ctx.skip();
183
- return;
184
+ return softSkip('blocked', 'precondition not met — `resetStatus === 404` returned early (seam, prior step, or fixture unavailable)');
184
185
  }
185
186
  expect(resetStatus).toBe(200);
186
187
 
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  import { describe, it, expect } from 'vitest';
28
+ import { softSkip } from '../lib/soft-skip.js';
28
29
  import { driver } from '../lib/driver.js';
29
30
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
30
31
 
@@ -55,9 +56,9 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
55
56
  describe.skipIf(HTTP_SKIP)('cross-engine-append-ordering: advertisement shape (RFC 0036 §B)', () => {
56
57
  it('capabilities.eventLog.crossEngineOrdering (when present) conforms to RFC 0036 §B', async () => {
57
58
  const d = await readDiscovery();
58
- if (d === null) return;
59
+ if (d === null) return softSkip('blocked', 'precondition not met — `d === null` returned early (seam, prior step, or fixture unavailable)');
59
60
  const ceo = capabilityFamily<{ crossEngineOrdering?: { supported?: unknown; orderingModel?: unknown } }>(d, 'eventLog')?.crossEngineOrdering;
60
- if (ceo === undefined) return; // host doesn't advertise — soft-skip
61
+ if (ceo === undefined) return softSkip('inapplicable', 'host doesn\'t advertise — soft-skip');
61
62
 
62
63
  expect(
63
64
  typeof ceo.supported,
@@ -34,6 +34,7 @@
34
34
  */
35
35
 
36
36
  import { describe, it, expect } from 'vitest';
37
+ import { softSkip } from '../lib/soft-skip.js';
37
38
  import { driver } from '../lib/driver.js';
38
39
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
39
40
 
@@ -69,7 +70,7 @@ describe.skipIf(HTTP_SKIP)('cross-host-ancestry-endpoint: behavioral (RFC 0040
69
70
  const chc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossHostCausation;
70
71
  if (chc?.ancestryEndpointSupported !== true) {
71
72
  ctx.skip();
72
- return;
73
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `chc?.ancestryEndpointSupported !== true` returned early');
73
74
  }
74
75
 
75
76
  // Create a fresh top-level run via the host's conformance-dispatch-loop
@@ -78,7 +79,7 @@ describe.skipIf(HTTP_SKIP)('cross-host-ancestry-endpoint: behavioral (RFC 0040
78
79
  const create = await driver.post('/v1/runs', { workflowId: 'conformance-dispatch-loop' });
79
80
  if (create.status !== 201) {
80
81
  ctx.skip();
81
- return;
82
+ return softSkip('blocked', 'precondition not met — `create.status !== 201` returned early (seam, prior step, or fixture unavailable)');
82
83
  }
83
84
  const runId = (create.json as { runId: string }).runId;
84
85
 
@@ -116,11 +117,11 @@ describe.skipIf(HTTP_SKIP)('cross-host-ancestry-endpoint: behavioral (RFC 0040
116
117
  const chc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossHostCausation;
117
118
  if (chc?.supported !== true) {
118
119
  ctx.skip();
119
- return;
120
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `chc?.supported !== true` returned early');
120
121
  }
121
122
  if (chc.ancestryEndpointSupported === true) {
122
123
  ctx.skip(); // covered by the test above
123
- return;
124
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `chc.ancestryEndpointSupported === true` returned early');
124
125
  }
125
126
 
126
127
  // Use any runId — even a synthetic non-existent one. The endpoint should
@@ -26,6 +26,7 @@
26
26
  */
27
27
 
28
28
  import { describe, it, expect } from 'vitest';
29
+ import { softSkip } from '../lib/soft-skip.js';
29
30
  import { driver } from '../lib/driver.js';
30
31
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
31
32
 
@@ -62,12 +63,12 @@ describe.skipIf(HTTP_SKIP)('cross-host-causation-shape: advertisement shape (RFC
62
63
  const d = await readDiscovery();
63
64
  if (d === null) {
64
65
  ctx.skip();
65
- return;
66
+ return softSkip('blocked', 'precondition not met — `d === null` returned early (seam, prior step, or fixture unavailable)');
66
67
  }
67
68
  const chc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossHostCausation;
68
69
  if (chc === undefined) {
69
70
  ctx.skip(); // host doesn't advertise — soft-skip
70
- return;
71
+ return softSkip('blocked', 'precondition not met — `chc === undefined` returned early (seam, prior step, or fixture unavailable)');
71
72
  }
72
73
 
73
74
  expect(