@openwop/openwop-conformance 1.106.0 → 1.123.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 (154) 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/llm-cache-key-recipe.ts +6 -5
  24. package/src/lib/mcp-fake-server.ts +235 -72
  25. package/src/lib/profiles.ts +132 -19
  26. package/src/lib/requirement-ledger.ts +98 -1
  27. package/src/lib/requirement-registry.ts +47 -4
  28. package/src/lib/scenario-disposition.ts +291 -0
  29. package/src/lib/soft-skip.ts +89 -0
  30. package/src/lib/workflow-chain-expansion.ts +189 -0
  31. package/src/scenarios/a2a-1-0-agent-card.test.ts +179 -0
  32. package/src/scenarios/a2a-1-0-task-roundtrip.test.ts +109 -0
  33. package/src/scenarios/a2a-card-runtime-consistency.test.ts +167 -0
  34. package/src/scenarios/a2a-peer-authority.test.ts +99 -0
  35. package/src/scenarios/a2a-task-roundtrip.test.ts +18 -6
  36. package/src/scenarios/agent-loop-iteration-monotonic.test.ts +3 -2
  37. package/src/scenarios/agent-loop-stateful-resume.test.ts +3 -2
  38. package/src/scenarios/agent-loop-version5-shape.test.ts +2 -1
  39. package/src/scenarios/agent-loop-workspace-snapshot.test.ts +4 -3
  40. package/src/scenarios/approval-gate-flow.test.ts +5 -4
  41. package/src/scenarios/artifact-type-store-without-render.test.ts +2 -1
  42. package/src/scenarios/auth-scim-profile.test.ts +5 -4
  43. package/src/scenarios/authorization-fail-closed.test.ts +4 -3
  44. package/src/scenarios/certification-bundle-non-vacuous.test.ts +249 -0
  45. package/src/scenarios/certification-bundle-redaction.test.ts +182 -0
  46. package/src/scenarios/certification-floor-enforcement.test.ts +30 -6
  47. package/src/scenarios/chain-compensation-expansion.test.ts +243 -0
  48. package/src/scenarios/commitment-fired.test.ts +3 -2
  49. package/src/scenarios/compensation-behavior.test.ts +63 -0
  50. package/src/scenarios/compensation-profile.test.ts +144 -1
  51. package/src/scenarios/conformance-advertised-seam-required.test.ts +118 -0
  52. package/src/scenarios/conformance-execution-witness.test.ts +104 -0
  53. package/src/scenarios/core-manifest-and-extension-registry.test.ts +26 -0
  54. package/src/scenarios/credential-payload-redaction.test.ts +4 -3
  55. package/src/scenarios/credentials-capability-shape.test.ts +4 -3
  56. package/src/scenarios/cross-engine-append-behavior.test.ts +5 -4
  57. package/src/scenarios/cross-engine-append-ordering.test.ts +3 -2
  58. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +5 -4
  59. package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
  60. package/src/scenarios/cross-workspace-isolation.test.ts +4 -3
  61. package/src/scenarios/deadletter-retry-exhaustion.test.ts +5 -4
  62. package/src/scenarios/distillation-index-roundtrip.test.ts +3 -2
  63. package/src/scenarios/distillation-secret-carryforward.test.ts +3 -2
  64. package/src/scenarios/distillation-shape.test.ts +2 -1
  65. package/src/scenarios/distillation-stable-archive.test.ts +4 -3
  66. package/src/scenarios/distillation-token-budget.test.ts +4 -3
  67. package/src/scenarios/feedback-correction-redaction.test.ts +4 -3
  68. package/src/scenarios/feedback-cross-tenant-isolation.test.ts +4 -3
  69. package/src/scenarios/feedback-fork-not-copied.test.ts +6 -5
  70. package/src/scenarios/feedback-on-terminal-run.test.ts +4 -3
  71. package/src/scenarios/feedback-record-and-list.test.ts +4 -3
  72. package/src/scenarios/feedback-unsupported-501.test.ts +3 -2
  73. package/src/scenarios/fixtures-valid.test.ts +16 -18
  74. package/src/scenarios/heartbeat-capability-shape.test.ts +2 -1
  75. package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +4 -3
  76. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +6 -5
  77. package/src/scenarios/heartbeat-runtime-bound.test.ts +4 -3
  78. package/src/scenarios/mcp-2026-07-28-discover.test.ts +207 -0
  79. package/src/scenarios/mcp-cache-tenant-scope.test.ts +66 -0
  80. package/src/scenarios/mcp-current-auth-boundary.test.ts +55 -0
  81. package/src/scenarios/mcp-extension-opacity.test.ts +79 -0
  82. package/src/scenarios/mcp-mrtr-roundtrip.test.ts +140 -0
  83. package/src/scenarios/mcp-stateless-request.test.ts +77 -0
  84. package/src/scenarios/mcp-tool-roundtrip.test.ts +4 -3
  85. package/src/scenarios/mcp-toolcall-redaction.test.ts +2 -1
  86. package/src/scenarios/memory-attribution-emits-on-write.test.ts +6 -5
  87. package/src/scenarios/memory-attribution-no-content.test.ts +4 -3
  88. package/src/scenarios/memory-attribution-replay-stable.test.ts +6 -5
  89. package/src/scenarios/memory-attribution-tenant-scoped.test.ts +4 -3
  90. package/src/scenarios/memory-compaction-event-emitted.test.ts +3 -2
  91. package/src/scenarios/memory-compaction-provenance-tag.test.ts +6 -5
  92. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +4 -3
  93. package/src/scenarios/memory-consolidation-idempotent.test.ts +4 -3
  94. package/src/scenarios/memory-injection-budget.test.ts +4 -3
  95. package/src/scenarios/metric-emission.test.ts +4 -3
  96. package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
  97. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +4 -3
  98. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +5 -4
  99. package/src/scenarios/multi-party-conversation-behavioral.test.ts +3 -2
  100. package/src/scenarios/multi-region-idempotency-behavior.test.ts +7 -6
  101. package/src/scenarios/multi-region-idempotency.test.ts +4 -3
  102. package/src/scenarios/normative-example-extraction.test.ts +242 -0
  103. package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +4 -3
  104. package/src/scenarios/oauth-capability-shape.test.ts +4 -3
  105. package/src/scenarios/oauth-connector-redaction.test.ts +4 -3
  106. package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +309 -0
  107. package/src/scenarios/org-position-no-authority-escalation.test.ts +2 -1
  108. package/src/scenarios/otel-emission-grpc.test.ts +4 -3
  109. package/src/scenarios/otel-emission.test.ts +4 -3
  110. package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +5 -4
  111. package/src/scenarios/otel-trace-propagation.test.ts +4 -3
  112. package/src/scenarios/pack-registry-isolation.test.ts +4 -3
  113. package/src/scenarios/pack-registry-publish.test.ts +65 -3
  114. package/src/scenarios/pack-registry.test.ts +22 -0
  115. package/src/scenarios/profile-discovery-core-alias.test.ts +137 -0
  116. package/src/scenarios/profileDerivation.test.ts +2 -1
  117. package/src/scenarios/rate-limit-envelope.test.ts +2 -1
  118. package/src/scenarios/registry-public.test.ts +6 -5
  119. package/src/scenarios/replay-divergence-at-refusal.test.ts +9 -8
  120. package/src/scenarios/replay-llm-cache-key-portable.test.ts +19 -13
  121. package/src/scenarios/replay-llm-cache-key.test.ts +120 -47
  122. package/src/scenarios/replay-observable-sequence-determinism.test.ts +5 -4
  123. package/src/scenarios/replay-side-effect-suppression.test.ts +20 -1
  124. package/src/scenarios/requirement-ledger.test.ts +29 -6
  125. package/src/scenarios/runner-ledger.test.ts +279 -0
  126. package/src/scenarios/runtime-requires-install-gate.test.ts +6 -5
  127. package/src/scenarios/sandbox-memory-cap.test.ts +3 -2
  128. package/src/scenarios/sandbox-mvp-behavior.test.ts +11 -10
  129. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
  130. package/src/scenarios/sandbox-timeout-cap.test.ts +3 -2
  131. package/src/scenarios/stream-text-fixture.test.ts +5 -4
  132. package/src/scenarios/subrun-approval-fail-closed.test.ts +3 -2
  133. package/src/scenarios/subrun-approval-gate.test.ts +4 -3
  134. package/src/scenarios/subrun-attestation-shape.test.ts +2 -1
  135. package/src/scenarios/subrun-checksum-stable.test.ts +4 -3
  136. package/src/scenarios/test-seam-unauthenticated.test.ts +61 -11
  137. package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +3 -2
  138. package/src/scenarios/tool-hooks-content-free.test.ts +3 -2
  139. package/src/scenarios/tool-hooks-rate-limit.test.ts +3 -2
  140. package/src/scenarios/tool-hooks-secret-redaction.test.ts +3 -2
  141. package/src/scenarios/tool-hooks-shape.test.ts +2 -1
  142. package/src/scenarios/trigger-bridge-delivery.test.ts +3 -2
  143. package/src/scenarios/version-fold.test.ts +5 -4
  144. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +5 -4
  145. package/src/scenarios/wasm-pack-invoke-completed.test.ts +3 -2
  146. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +3 -2
  147. package/src/scenarios/wasm-pack-load.test.ts +4 -3
  148. package/src/scenarios/wasm-pack-memory-cap.test.ts +4 -3
  149. package/src/scenarios/wasm-pack-replay-determinism.test.ts +3 -2
  150. package/src/scenarios/webhook-signed-delivery.test.ts +4 -3
  151. package/src/scenarios/workflow-primary-output-annotation.test.ts +13 -23
  152. package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +3 -2
  153. package/src/scenarios/workspace-cross-tenant-isolation.test.ts +3 -2
  154. package/src/setup.ts +95 -1
@@ -10,23 +10,24 @@
10
10
  */
11
11
 
12
12
  import { describe, it, expect } from 'vitest';
13
+ import { softSkip } from '../lib/soft-skip.js';
13
14
  import { driver } from '../lib/driver.js';
14
15
  import { readSubRunAttestationCap, invokeSubRunAttest } from '../lib/subRunAttestation.js';
15
16
 
16
17
  describe('subrun-approval-gate (RFC 0063 §C)', () => {
17
18
  it('accept merges the child outputs; reject does not', async () => {
18
- if ((await readSubRunAttestationCap()) !== true) return;
19
+ if ((await readSubRunAttestationCap()) !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `(await readSubRunAttestationCap()) !== true` returned early');
19
20
  const base = { childOutputs: { artifact: 'x' }, outputAttestation: { requireApproval: true } };
20
21
 
21
22
  const accepted = await invokeSubRunAttest({ ...base, approvalAction: 'accept' });
22
- if (accepted === null) return; // seam absent — soft-skip
23
+ if (accepted === null) return softSkip('blocked', 'seam absent — soft-skip');
23
24
  expect(
24
25
  accepted.merged,
25
26
  driver.describe('RFC 0063 §C', 'an `accept` approval MUST merge the child outputs'),
26
27
  ).toBe(true);
27
28
 
28
29
  const rejected = await invokeSubRunAttest({ ...base, approvalAction: 'reject' });
29
- if (rejected === null) return;
30
+ if (rejected === null) return softSkip('blocked', 'precondition not met — `rejected === null` returned early (seam, prior step, or fixture unavailable)');
30
31
  expect(
31
32
  rejected.merged,
32
33
  driver.describe('RFC 0063 §C', 'a `reject` approval MUST NOT merge the child outputs'),
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
+ import { softSkip } from '../lib/soft-skip.js';
14
15
  import { driver } from '../lib/driver.js';
15
16
  import { readSubRunAttestationCap } from '../lib/subRunAttestation.js';
16
17
 
@@ -18,7 +19,7 @@ describe('subrun-attestation-shape: advertisement (RFC 0063 §A)', () => {
18
19
  it('capabilities.agents.subRunAttestation is absent or a boolean', async () => {
19
20
  const cap = await readSubRunAttestationCap();
20
21
  // null = unadvertised (no agents block OR flag omitted) — valid.
21
- if (cap === null) return;
22
+ if (cap === null) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap === null` returned early');
22
23
  expect(
23
24
  typeof cap,
24
25
  driver.describe(
@@ -11,21 +11,22 @@
11
11
  */
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
+ import { softSkip } from '../lib/soft-skip.js';
14
15
  import { driver } from '../lib/driver.js';
15
16
  import { readSubRunAttestationCap, invokeSubRunAttest } from '../lib/subRunAttestation.js';
16
17
 
17
18
  describe('subrun-checksum-stable (RFC 0063 §B)', () => {
18
19
  it('identical child outputs produce an identical sha256 attestation checksum', async () => {
19
- if ((await readSubRunAttestationCap()) !== true) return;
20
+ if ((await readSubRunAttestationCap()) !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `(await readSubRunAttestationCap()) !== true` returned early');
20
21
  const childOutputs = { report: 'done', score: 0.9, tags: ['a', 'b'] };
21
22
  const a = await invokeSubRunAttest({ childOutputs, outputAttestation: { checksum: true } });
22
- if (a === null) return; // seam absent — soft-skip
23
+ if (a === null) return softSkip('blocked', 'seam absent — soft-skip');
23
24
  // Key-reordered but value-identical: JCS canonicalization MUST yield the same hash.
24
25
  const b = await invokeSubRunAttest({
25
26
  childOutputs: { tags: ['a', 'b'], score: 0.9, report: 'done' },
26
27
  outputAttestation: { checksum: true },
27
28
  });
28
- if (b === null) return;
29
+ if (b === null) return softSkip('blocked', 'precondition not met — `b === null` returned early (seam, prior step, or fixture unavailable)');
29
30
  const att = a.attestation ?? {};
30
31
  expect(
31
32
  typeof att.checksum === 'string' && (att.checksum as string).length > 0,
@@ -67,11 +67,41 @@ const SEAM_PROBES: readonly string[] = [
67
67
  /** 200 is the only unambiguous failure: the seam answered a stranger. */
68
68
  const ACCEPTABLE = new Set([401, 403, 404, 405, 501]);
69
69
 
70
+ /**
71
+ * A `200` can arrive from two very different places, and the difference decides
72
+ * what you should go fix.
73
+ *
74
+ * Every seam in `host-sample-test-seams.md` answers with a JSON body — the doc
75
+ * specifies each one's response shape as an object (`{ released, event }`,
76
+ * `{ ok: true, packId }`, `{ entries: … }`). **No seam answers with an HTML
77
+ * document.** So a `200 text/html` is not a seam that talked to a stranger; it
78
+ * is a static-hosting catch-all — an SPA rewrite in front of the API — matching
79
+ * a path the backend never saw.
80
+ *
81
+ * This was found by probing `app.openwop.dev` (Firebase Hosting, `**` rewrite
82
+ * to the SPA shell) instead of the backend's own origin. Every probe returned
83
+ * `200 text/html` with the app's `index.html`, which under the previous message
84
+ * read as *"an open control surface on a public origin"* — alarming, and wrong.
85
+ * The same paths on `…-backend-….run.app` return `404 application/json`.
86
+ *
87
+ * **Both cases still fail.** Nothing here relaxes the assertion, because the
88
+ * cheap way to silence a false alarm is to stop failing on `200`, and that would
89
+ * delete the check. What changes is the diagnosis: an HTML catch-all means the
90
+ * suite is pointed at the CDN rather than the API origin, which is its own
91
+ * defect — a run against the wrong origin cannot witness anything about the
92
+ * host, and every other leg in the suite is equally blind at that base URL.
93
+ */
94
+ function classify200(contentType: string | null): 'seam' | 'catchall' {
95
+ return (contentType ?? '').toLowerCase().includes('text/html') ? 'catchall' : 'seam';
96
+ }
97
+
70
98
  describe('test-seam-unauthenticated: an enabled seam still authenticates', () => {
71
99
  it('no /v1/host/sample/* seam answers an unauthenticated request with 200', async () => {
72
100
  const env = loadEnv();
73
101
  const answered: string[] = [];
74
102
  let probed = 0;
103
+ let seamAnswers = 0;
104
+ let catchAllAnswers = 0;
75
105
 
76
106
  for (const path of SEAM_PROBES) {
77
107
  // node:fetch directly with NO Authorization header — the driver's
@@ -83,8 +113,18 @@ describe('test-seam-unauthenticated: an enabled seam still authenticates', () =>
83
113
  continue; // connection-level refusal is a stronger answer than 404
84
114
  }
85
115
  probed += 1;
86
- if (res.status === 200) answered.push(`${path} -> 200`);
87
- else if (!ACCEPTABLE.has(res.status)) {
116
+ if (res.status === 200) {
117
+ const contentType = res.headers.get('content-type');
118
+ const kind = classify200(contentType);
119
+ if (kind === 'seam') seamAnswers += 1;
120
+ else catchAllAnswers += 1;
121
+ answered.push(
122
+ `${path} -> 200 (${contentType ?? 'no content-type'}) — ` +
123
+ (kind === 'seam'
124
+ ? 'SEAM ANSWERED A CREDENTIAL-LESS CALLER'
125
+ : 'HTML body: a static-hosting catch-all, not the API origin'),
126
+ );
127
+ } else if (!ACCEPTABLE.has(res.status)) {
88
128
  // Not a pass and not the known failure — record it rather than
89
129
  // silently tolerating a status nobody reasoned about.
90
130
  answered.push(`${path} -> ${res.status} (unexpected; expected one of ${[...ACCEPTABLE].join('/')})`);
@@ -100,17 +140,27 @@ describe('test-seam-unauthenticated: an enabled seam still authenticates', () =>
100
140
  'not looking, which is the failure mode it exists to prevent',
101
141
  ).toBeGreaterThan(0);
102
142
 
143
+ // The diagnosis is chosen from what the answers actually were. An HTML
144
+ // catch-all and an open seam are both failures and they are not the same
145
+ // bug, so the message must not name the wrong one — a check that reports a
146
+ // security finding for a misrouted base URL trains its reader to distrust it.
147
+ const diagnosis =
148
+ seamAnswers === 0 && catchAllAnswers > 0
149
+ ? 'WRONG ORIGIN, not an open seam. Every 200 above carried an HTML body, and no seam in ' +
150
+ 'host-sample-test-seams.md answers with HTML — so these are a static-hosting rewrite ' +
151
+ '(an SPA `**` catch-all) matching paths the backend never received. Point ' +
152
+ 'OPENWOP_BASE_URL at the API origin itself. This is still a failure: at this base URL ' +
153
+ 'no leg in the suite is witnessing the host, so a green run here would mean nothing.'
154
+ : 'An ENABLED seam MUST require an authenticated, NON-ANONYMOUS principal. A host that ' +
155
+ 'mints an anonymous identity for credential-less callers MUST NOT treat it as ' +
156
+ 'satisfying that. The env-gate governs whether a seam EXISTS; it does not govern who ' +
157
+ 'may call it. A seam answering a credential-less request with 200 is an open control ' +
158
+ 'surface on a public origin — and staging keys such as `nodeId` are not secrets, they ' +
159
+ 'ship inside chain packs.';
160
+
103
161
  expect(
104
162
  answered,
105
- driver.describe(
106
- 'host-sample-test-seams.md §"Production safety (normative)"',
107
- 'An ENABLED seam MUST require an authenticated, NON-ANONYMOUS principal. A host that mints ' +
108
- 'an anonymous identity for credential-less callers MUST NOT treat it as satisfying that. ' +
109
- 'The env-gate governs whether a seam EXISTS; it does not govern who may call it. A seam ' +
110
- 'answering a credential-less request with 200 is an open control surface on a public ' +
111
- 'origin — and staging keys such as `nodeId` are not secrets, they ship inside chain ' +
112
- 'packs.\n ' + answered.join('\n '),
113
- ),
163
+ driver.describe('host-sample-test-seams.md §"Production safety (normative)"', `${diagnosis}\n ${answered.join('\n ')}`),
114
164
  ).toEqual([]);
115
165
  });
116
166
  });
@@ -12,13 +12,14 @@
12
12
  */
13
13
 
14
14
  import { describe, it, expect } from 'vitest';
15
+ import { softSkip } from '../lib/soft-skip.js';
15
16
  import { driver } from '../lib/driver.js';
16
17
  import { readToolHooksCap, invokeToolHook } from '../lib/toolHooks.js';
17
18
 
18
19
  describe('tool-hooks-authorization-fail-closed (RFC 0064 §C)', () => {
19
20
  it('a principal lacking a tool scope is denied and the tool is not invoked', async () => {
20
21
  const cap = await readToolHooksCap();
21
- if (cap?.perToolAuthorization !== true) return;
22
+ if (cap?.perToolAuthorization !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.perToolAuthorization !== true` returned early');
22
23
  // A principal with no scopes against a tool requiring one MUST be denied.
23
24
  const res = await invokeToolHook({
24
25
  principal: 'conformance-unprivileged',
@@ -26,7 +27,7 @@ describe('tool-hooks-authorization-fail-closed (RFC 0064 §C)', () => {
26
27
  requiredScopes: ['db:write'],
27
28
  args: {},
28
29
  });
29
- if (res === null) return; // seam absent — soft-skip
30
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
30
31
  expect(
31
32
  (res.toolReturned ?? {}).status,
32
33
  driver.describe('RFC 0064 §C', 'a missing/unevaluable tool scope MUST fail closed → status:"forbidden"'),
@@ -11,15 +11,16 @@
11
11
  */
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
+ import { softSkip } from '../lib/soft-skip.js';
14
15
  import { driver } from '../lib/driver.js';
15
16
  import { readToolHooksCap, invokeToolHook } from '../lib/toolHooks.js';
16
17
 
17
18
  describe('tool-hooks-content-free (RFC 0064 §B)', () => {
18
19
  it('toolCalled carries argsHash; toolReturned carries status + durationMs', async () => {
19
20
  const cap = await readToolHooksCap();
20
- if (cap?.prePostEvents !== true) return;
21
+ if (cap?.prePostEvents !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.prePostEvents !== true` returned early');
21
22
  const res = await invokeToolHook({ principal: 'core.system', toolName: 'web.search', args: { q: 'openwop' } });
22
- if (res === null) return; // seam absent — soft-skip
23
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
23
24
  const called = res.toolCalled ?? {};
24
25
  const returned = res.toolReturned ?? {};
25
26
  expect(
@@ -10,20 +10,21 @@
10
10
  */
11
11
 
12
12
  import { describe, it, expect } from 'vitest';
13
+ import { softSkip } from '../lib/soft-skip.js';
13
14
  import { driver } from '../lib/driver.js';
14
15
  import { readToolHooksCap, invokeToolHook } from '../lib/toolHooks.js';
15
16
 
16
17
  describe('tool-hooks-rate-limit (RFC 0064 §D)', () => {
17
18
  it('an exhausted (principal, tool) bucket yields status:"rate_limited"', async () => {
18
19
  const cap = await readToolHooksCap();
19
- if (cap?.perToolRateLimit !== true) return;
20
+ if (cap?.perToolRateLimit !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.perToolRateLimit !== true` returned early');
20
21
  const res = await invokeToolHook({
21
22
  principal: 'core.system',
22
23
  toolName: 'web.search',
23
24
  args: { q: 'x' },
24
25
  simulateRateLimitExhausted: true,
25
26
  });
26
- if (res === null) return; // seam absent — soft-skip
27
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
27
28
  expect(
28
29
  (res.toolReturned ?? {}).status,
29
30
  driver.describe('RFC 0064 §D', 'an exhausted token bucket MUST yield status:"rate_limited" without invoking the tool'),
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
+ import { softSkip } from '../lib/soft-skip.js';
14
15
  import { driver } from '../lib/driver.js';
15
16
  import { readToolHooksCap, invokeToolHook } from '../lib/toolHooks.js';
16
17
 
@@ -19,13 +20,13 @@ const CANARY = 'sk-canary-rfc0064-do-not-leak-xyz789';
19
20
  describe('tool-hooks-secret-redaction (RFC 0064 §E)', () => {
20
21
  it('a secret-shaped tool arg never appears in the emitted events', async () => {
21
22
  const cap = await readToolHooksCap();
22
- if (cap?.prePostEvents !== true) return;
23
+ if (cap?.prePostEvents !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.prePostEvents !== true` returned early');
23
24
  const res = await invokeToolHook({
24
25
  principal: 'core.system',
25
26
  toolName: 'web.search',
26
27
  args: { apiKey: CANARY, q: 'openwop' },
27
28
  });
28
- if (res === null) return; // seam absent — soft-skip
29
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
29
30
  expect(
30
31
  JSON.stringify(res).includes(CANARY),
31
32
  driver.describe('RFC 0064 §B', 'a resolved secret MUST be redacted before hashing; the raw value MUST NOT appear in argsHash or any emitted field (SR-1)'),
@@ -11,13 +11,14 @@
11
11
  */
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
+ import { softSkip } from '../lib/soft-skip.js';
14
15
  import { driver } from '../lib/driver.js';
15
16
  import { readToolHooksCap } from '../lib/toolHooks.js';
16
17
 
17
18
  describe('tool-hooks-shape: advertisement (RFC 0064 §A)', () => {
18
19
  it('capabilities.toolHooks is absent or a well-formed object', async () => {
19
20
  const cap = await readToolHooksCap();
20
- if (cap === null) return; // not advertised — valid
21
+ if (cap === null) return softSkip('inapplicable', 'not advertised — valid');
21
22
  expect(
22
23
  typeof cap.supported,
23
24
  driver.describe('capabilities.schema.json §toolHooks', 'toolHooks.supported MUST be a boolean when the block is present'),
@@ -28,6 +28,7 @@
28
28
  */
29
29
 
30
30
  import { describe, it, expect } from 'vitest';
31
+ import { seamAbsent } from '../lib/soft-skip.js';
31
32
  import { driver } from '../lib/driver.js';
32
33
  import { behaviorGate } from '../lib/behavior-gate.js';
33
34
  import {
@@ -52,11 +53,11 @@ function expectContentFree(payload: Record<string, unknown>, where: string): voi
52
53
  describe('trigger-bridge-delivery (RFC 0083 §C)', () => {
53
54
  it('de-dups by dedupKey, retries to dead-letter, and links delivery→run causation', async () => {
54
55
  if (!behaviorGate('openwop-trigger-bridge', await isTriggerBridgeProfileAdvertised())) return;
55
- if (!(await isEventLogSeamAvailable())) return; // event-log seam absent — soft-skip
56
+ if (!(await isEventLogSeamAvailable())) return seamAbsent('host advertises openwop-trigger-bridge but the event-log seam is absent');
56
57
 
57
58
  // ---- Leg 1: dedup → effectively-once (§C-1) ---------------------------
58
59
  const dedup = await driveDelivery({ scenario: 'dedup', dedupKey: 'conformance-dedup-key', source: 'queue' });
59
- if (dedup === null) return; // delivery seam unwired — soft-skip the whole behavioral suite
60
+ if (dedup === null) return seamAbsent('host advertises openwop-trigger-bridge but the delivery seam is unwired');
60
61
 
61
62
  // The profile is derived AND the seam is wired — missing evidence is a
62
63
  // FAILURE, not a soft-skip. A repeated dedupKey MUST be effectively-once:
@@ -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, type OpenWOPResponse } from '../lib/driver.js';
38
39
  import { pollUntilTerminal } from '../lib/polling.js';
39
40
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -84,7 +85,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('version-fold: forced engine versions fold-best
84
85
  '[version-fold] host does not advertise Capabilities.testing.forceEngineVersionRange; ' +
85
86
  'skipping (the X-Force-Engine-Version seam is test-keys-only and optional)',
86
87
  );
87
- return;
88
+ return softSkip('blocked', 'precondition not met — `seam === null` returned early (seam, prior step, or fixture unavailable)');
88
89
  }
89
90
 
90
91
  const candidates = [seam.range.min, ...(seam.current === null ? [] : [seam.current]), seam.range.max];
@@ -104,7 +105,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('version-fold: forced engine versions fold-best
104
105
  if (create.status === 403 && errCode(create.json) === 'force_engine_version_forbidden') {
105
106
  // eslint-disable-next-line no-console
106
107
  console.warn('[version-fold] API key is production-scoped (403 force_engine_version_forbidden); skipping');
107
- return;
108
+ return softSkip('blocked', '[version-fold] API key is production-scoped (403 force_engine_version_forbidden); skipping (create.status === 403 && errCode(create.json) === \'force_engine_version_forbidden\')');
108
109
  }
109
110
  expect(
110
111
  create.status,
@@ -165,7 +166,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('version-fold: forced engine versions fold-best
165
166
  if (seam === null) {
166
167
  // eslint-disable-next-line no-console
167
168
  console.warn('[version-fold] forceEngineVersionRange not advertised; skipping negative leg');
168
- return;
169
+ return softSkip('inapplicable', '[version-fold] forceEngineVersionRange not advertised; skipping negative leg');
169
170
  }
170
171
 
171
172
  const outOfRange = seam.range.max + 1;
@@ -173,7 +174,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('version-fold: forced engine versions fold-best
173
174
  if (create.status === 403 && errCode(create.json) === 'force_engine_version_forbidden') {
174
175
  // eslint-disable-next-line no-console
175
176
  console.warn('[version-fold] API key is production-scoped; skipping negative leg');
176
- return;
177
+ return softSkip('blocked', '[version-fold] API key is production-scoped; skipping negative leg (create.status === 403 && errCode(create.json) === \'force_engine_version_forbidden\')');
177
178
  }
178
179
  expect(
179
180
  create.status,
@@ -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
 
@@ -37,7 +38,7 @@ describe('wasm-pack-abi-version-rejection: host advertises supported ABI version
37
38
  const wasm =
38
39
  capabilityFamily<{ wasm?: Record<string, unknown> }>(disco.json, 'nodePackRuntimes')?.wasm;
39
40
 
40
- if (!wasm?.supported) return;
41
+ if (!wasm?.supported) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm?.supported` returned early');
41
42
 
42
43
  expect(Array.isArray(wasm.abiVersions), driver.describe(
43
44
  'RFCS/0008-wasm-abi.md §H',
@@ -63,7 +64,7 @@ describe('wasm-pack-abi-version-rejection: positive path via misbehaving pack',
63
64
  const wasm =
64
65
  capabilityFamily<{ wasm?: Record<string, unknown> }>(disco.json, 'nodePackRuntimes')?.wasm;
65
66
 
66
- if (!wasm?.supported) return;
67
+ if (!wasm?.supported) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm?.supported` returned early');
67
68
 
68
69
  if (!Array.isArray(wasm.loadedPacks)) {
69
70
  // eslint-disable-next-line no-console
@@ -71,7 +72,7 @@ describe('wasm-pack-abi-version-rejection: positive path via misbehaving pack',
71
72
  '[wasm-pack-abi-version-rejection] host does not advertise capabilities.nodePackRuntimes.wasm.loadedPacks[]; skipping positive path. ' +
72
73
  'Hosts MAY advertise loadedPacks[] to surface ABI rejection observably (RFC 0008 §H + Track 7).',
73
74
  );
74
- return;
75
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!Array.isArray(wasm.loadedPacks)` returned early');
75
76
  }
76
77
 
77
78
  // Soft-skip when the well-behaved reference pack isn't loaded. If
@@ -84,7 +85,7 @@ describe('wasm-pack-abi-version-rejection: positive path via misbehaving pack',
84
85
  `[wasm-pack-abi-version-rejection] reference pack ${WELL_BEHAVED_PACK_NAME} not in loadedPacks[]; ` +
85
86
  'skipping positive path (build examples/packs/rust-hello first or run against a host that ships it).',
86
87
  );
87
- return;
88
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm.loadedPacks.includes(WELL_BEHAVED_PACK_NAME)` returned early');
88
89
  }
89
90
 
90
91
  // Core assertion: ABI 999 pack MUST NOT be in loadedPacks[]
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { describe, it, expect } from 'vitest';
16
+ import { softSkip } from '../lib/soft-skip.js';
16
17
  import { driver } from '../lib/driver.js';
17
18
  import { pollUntilTerminal } from '../lib/polling.js';
18
19
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -29,11 +30,11 @@ async function isWasmSupported(): Promise<boolean> {
29
30
 
30
31
  describe('wasm-pack-invoke-completed: round-trip output', () => {
31
32
  it('greet node returns Hello, <name>! and run reaches completed', async () => {
32
- if (!isFixtureAdvertised(FIXTURE)) return;
33
+ if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isFixtureAdvertised(FIXTURE)` returned early');
33
34
  if (!(await isWasmSupported())) {
34
35
  // eslint-disable-next-line no-console
35
36
  console.warn('[wasm-pack-invoke-completed] WASM not advertised; skipping');
36
- return;
37
+ return softSkip('inapplicable', '[wasm-pack-invoke-completed] WASM not advertised; skipping');
37
38
  }
38
39
 
39
40
  const create = await driver.post('/v1/runs', {
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { describe, it, expect } from 'vitest';
20
+ import { softSkip } from '../lib/soft-skip.js';
20
21
  import { driver } from '../lib/driver.js';
21
22
  import { pollUntilTerminal } from '../lib/polling.js';
22
23
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -33,8 +34,8 @@ async function isWasmSupported(): Promise<boolean> {
33
34
 
34
35
  describe('wasm-pack-invoke-suspended: suspend → resume round-trip', () => {
35
36
  it('host either suspends the run or explicitly reports wasm_suspend_not_implemented', async () => {
36
- if (!isFixtureAdvertised(FIXTURE)) return;
37
- if (!(await isWasmSupported())) return;
37
+ if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isFixtureAdvertised(FIXTURE)` returned early');
38
+ if (!(await isWasmSupported())) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isWasmSupported())` returned early');
38
39
 
39
40
  // The reference rust-hello pack does NOT itself suspend (it always
40
41
  // returns `completed`), so against that pack this scenario can only
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { describe, it, expect } from 'vitest';
16
+ import { softSkip } from '../lib/soft-skip.js';
16
17
  import { driver } from '../lib/driver.js';
17
18
  import { isFixtureAdvertised } from '../lib/fixtures.js';
18
19
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
@@ -39,7 +40,7 @@ describe('wasm-pack-load: discovery surfaces WASM runtime support', () => {
39
40
  if (!wasm?.supported) {
40
41
  // eslint-disable-next-line no-console
41
42
  console.warn('[wasm-pack-load] host does not advertise WASM support; skipping');
42
- return;
43
+ return softSkip('inapplicable', '[wasm-pack-load] host does not advertise WASM support; skipping');
43
44
  }
44
45
  expect(Array.isArray(wasm.abiVersions), driver.describe(
45
46
  'RFCS/0008-wasm-abi.md §H',
@@ -55,11 +56,11 @@ describe('wasm-pack-load: discovery surfaces WASM runtime support', () => {
55
56
  describe('wasm-pack-load: loaded pack typeIds are dispatchable', () => {
56
57
  it('host accepts a workflow whose node typeId is provided by a loaded WASM pack', async () => {
57
58
  const wasm = await getWasmCaps();
58
- if (!wasm?.supported) return;
59
+ if (!wasm?.supported) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm?.supported` returned early');
59
60
  if (!isFixtureAdvertised(FIXTURE)) {
60
61
  // eslint-disable-next-line no-console
61
62
  console.warn(`[wasm-pack-load] fixture ${FIXTURE} not advertised; skipping`);
62
- return;
63
+ return softSkip('inapplicable', '[wasm-pack-load] fixture … not advertised; skipping');
63
64
  }
64
65
  // Creating a run against the fixture proves the host knows about the
65
66
  // WASM-provided typeId. A host that loaded the pack accepts the
@@ -23,6 +23,7 @@
23
23
  */
24
24
 
25
25
  import { describe, it, expect } from 'vitest';
26
+ import { softSkip } from '../lib/soft-skip.js';
26
27
  import { driver } from '../lib/driver.js';
27
28
  import { pollUntilTerminal } from '../lib/polling.js';
28
29
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -36,7 +37,7 @@ describe('wasm-pack-memory-cap: host advertises maxMemoryBytes', () => {
36
37
  const wasm =
37
38
  capabilityFamily<{ wasm?: Record<string, unknown> }>(disco.json, 'nodePackRuntimes')?.wasm;
38
39
 
39
- if (!wasm?.supported) return;
40
+ if (!wasm?.supported) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm?.supported` returned early');
40
41
 
41
42
  // The cap is REQUIRED to enforce §K. Hosts MUST surface the configured
42
43
  // ceiling so clients can size their packs accordingly.
@@ -59,12 +60,12 @@ describe('wasm-pack-memory-cap: positive path via misbehaving pack', () => {
59
60
  `[wasm-pack-memory-cap] fixture ${CAP_BREACH_FIXTURE} not advertised; skipping positive path. ` +
60
61
  'Build the misbehaving pack at examples/packs/rust-misbehaving-memory/ and ensure the host serves the fixture.',
61
62
  );
62
- return;
63
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isFixtureAdvertised(CAP_BREACH_FIXTURE)` returned early');
63
64
  }
64
65
  const disco = await driver.get('/.well-known/openwop');
65
66
  const wasm =
66
67
  capabilityFamily<{ wasm?: Record<string, unknown> }>(disco.json, 'nodePackRuntimes')?.wasm;
67
- if (!wasm?.supported) return;
68
+ if (!wasm?.supported) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm?.supported` returned early');
68
69
 
69
70
  const create = await driver.post('/v1/runs', {
70
71
  workflowId: CAP_BREACH_FIXTURE,
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
+ import { softSkip } from '../lib/soft-skip.js';
14
15
  import { driver } from '../lib/driver.js';
15
16
  import { pollUntilTerminal } from '../lib/polling.js';
16
17
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -33,8 +34,8 @@ function extractGreeting(events: Array<{ type: string; data?: unknown }>): strin
33
34
 
34
35
  describe('wasm-pack-replay-determinism: same inputs → same output', () => {
35
36
  it('two independent runs with same inputs produce same WASM output', async () => {
36
- if (!isFixtureAdvertised(FIXTURE)) return;
37
- if (!(await isWasmSupported())) return;
37
+ if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isFixtureAdvertised(FIXTURE)` returned early');
38
+ if (!(await isWasmSupported())) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isWasmSupported())` returned early');
38
39
 
39
40
  const inputs = { name: 'determinism-probe' };
40
41
 
@@ -24,6 +24,7 @@
24
24
  */
25
25
 
26
26
  import { afterEach, describe, expect, it } from 'vitest';
27
+ import { softSkip } from '../lib/soft-skip.js';
27
28
  import { createHmac } from 'node:crypto';
28
29
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
29
30
  import { driver } from '../lib/driver.js';
@@ -78,12 +79,12 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
78
79
  if (!(await isWebhookSupported())) {
79
80
  // eslint-disable-next-line no-console
80
81
  console.warn('[webhook-signed-delivery] host does not advertise webhook support; skipping');
81
- return;
82
+ return softSkip('inapplicable', '[webhook-signed-delivery] host does not advertise webhook support; skipping');
82
83
  }
83
84
  if (!isFixtureAdvertised('conformance-noop')) {
84
85
  // eslint-disable-next-line no-console
85
86
  console.warn('[webhook-signed-delivery] conformance-noop not advertised; skipping');
86
- return;
87
+ return softSkip('inapplicable', '[webhook-signed-delivery] conformance-noop not advertised; skipping');
87
88
  }
88
89
 
89
90
  const receiver = await startReceiver();
@@ -114,7 +115,7 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
114
115
  '[webhook-signed-delivery] host SSRF guard rejected the loopback receiver; ' +
115
116
  'set OPENWOP_WEBHOOK_ALLOW_PRIVATE=true on the host (or equivalent) to run',
116
117
  );
117
- return;
118
+ return softSkip('blocked', 'precondition not met — `body.error === \'webhook_url_rejected\'` returned early (seam, prior step, or fixture unavailable)');
118
119
  }
119
120
  }
120
121
 
@@ -20,35 +20,25 @@
20
20
  import { describe, it, expect } from 'vitest';
21
21
  import Ajv2020 from 'ajv/dist/2020.js';
22
22
  import addFormats from 'ajv-formats';
23
- import { readFileSync } from 'node:fs';
23
+ import { readFileSync, readdirSync } from 'node:fs';
24
24
  import { join } from 'node:path';
25
25
  import { SCHEMAS_DIR } from '../lib/paths.js';
26
26
 
27
27
  function compileWorkflowDefinition(): ReturnType<Ajv2020['compile']> {
28
28
  const ajv = new Ajv2020({ strict: false, allErrors: true });
29
29
  addFormats(ajv);
30
- // Register cross-file `$ref` targets — same pattern as
31
- // `fixtures-valid.test.ts`. Without these, Ajv throws
32
- // `missingRef` when compiling `workflow-definition.schema.json`
33
- // because it references agent-ref + prompt-ref by URL.
34
- const agentRefSchema = JSON.parse(
35
- readFileSync(join(SCHEMAS_DIR, 'agent-ref.schema.json'), 'utf8'),
36
- ) as Record<string, unknown>;
37
- const promptRefSchema = JSON.parse(
38
- readFileSync(join(SCHEMAS_DIR, 'prompt-ref.schema.json'), 'utf8'),
39
- ) as Record<string, unknown>;
40
- const promptKindSchema = JSON.parse(
41
- readFileSync(join(SCHEMAS_DIR, 'prompt-kind.schema.json'), 'utf8'),
42
- ) as Record<string, unknown>;
43
- ajv.addSchema(agentRefSchema, 'agent-ref.schema.json');
44
- ajv.addSchema(promptRefSchema, 'prompt-ref.schema.json');
45
- ajv.addSchema(promptRefSchema, './prompt-ref.schema.json');
46
- ajv.addSchema(promptKindSchema, 'prompt-kind.schema.json');
47
- ajv.addSchema(promptKindSchema, './prompt-kind.schema.json');
48
- const schema = JSON.parse(
49
- readFileSync(join(SCHEMAS_DIR, 'workflow-definition.schema.json'), 'utf8'),
50
- ) as Record<string, unknown>;
51
- return ajv.compile(schema);
30
+ // Register EVERY schema in the corpus (S14, 2026-08-16) — a fixed ref list is
31
+ // a claim about the schema graph that nothing keeps true; the fourth cross-file
32
+ // `$ref` (#1009) broke the previous list at describe time.
33
+ for (const file of readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json'))) {
34
+ const s = JSON.parse(readFileSync(join(SCHEMAS_DIR, file), 'utf8')) as Record<string, unknown>;
35
+ ajv.addSchema(s, file);
36
+ ajv.addSchema(s, `./${file}`);
37
+ }
38
+ return (
39
+ ajv.getSchema('workflow-definition.schema.json') ??
40
+ ajv.compile(JSON.parse(readFileSync(join(SCHEMAS_DIR, 'workflow-definition.schema.json'), 'utf8')) as Record<string, unknown>)
41
+ );
52
42
  }
53
43
 
54
44
  /** Build the minimal-required shape of a WorkflowDefinition. Tests
@@ -24,6 +24,7 @@
24
24
  * @see SECURITY/invariants.yaml workspace-cross-tenant-isolation
25
25
  */
26
26
  import { describe, it, expect } from 'vitest';
27
+ import { softSkip } from '../lib/soft-skip.js';
27
28
  import { randomUUID } from 'node:crypto';
28
29
  import { driver } from '../lib/driver.js';
29
30
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
@@ -42,11 +43,11 @@ const asTenantB = { headers: { Authorization: `Bearer ${tenantBKey ?? ''}` } };
42
43
 
43
44
  describe('workspace-cross-tenant-isolation (black-box): a §C file MUST NOT leak across owners (RFC 0059 §E WCT-1)', () => {
44
45
  it('a file written by owner A is unreadable + un-enumerable by a second-tenant credential', async () => {
45
- if (!(await workspaceSupported())) return; // capability not advertised — skip
46
+ if (!(await workspaceSupported())) return softSkip('inapplicable', 'capability not advertised — skip');
46
47
  if (!tenantBKey) {
47
48
  // eslint-disable-next-line no-console
48
49
  console.warn('[workspace-cross-tenant-isolation-blackbox] OPENWOP_TEST_TENANT_B_API_KEY not supplied; skipping the production-path cross-tenant assertion');
49
- return;
50
+ return softSkip('blocked', '[workspace-cross-tenant-isolation-blackbox] OPENWOP_TEST_TENANT_B_API_KEY not supplied; skipping the production-path cross-tenant assertion');
50
51
  }
51
52
 
52
53
  const path = `wct-blackbox-${randomUUID()}.md`;