@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
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import { describe, it, expect } from 'vitest';
24
+ import { softSkip } from '../lib/soft-skip.js';
24
25
  import { driver } from '../lib/driver.js';
25
26
 
26
27
  const ISOLATION_CODES: ReadonlySet<string> = new Set(['run_forbidden', 'not_found']);
@@ -35,9 +36,9 @@ describe('cross-workspace-isolation: run-ownership echo shape (RFC 0048 §C)', (
35
36
  it('owner, when present on a run snapshot, carries a non-empty tenant', async () => {
36
37
  // Best-effort: probe a sample run if the host exposes one; otherwise skip.
37
38
  const res = await driver.get('/v1/host/sample/identity/owned-run');
38
- if (res.status === 404) return; // no sample-run seam — soft-skip
39
+ if (res.status === 404) return softSkip('blocked', 'no sample-run seam — soft-skip');
39
40
  const owner = (res.json as { owner?: OwnerTriple } | undefined)?.owner;
40
- if (owner === undefined) return; // single-tenant host — owner omitted
41
+ if (owner === undefined) return softSkip('blocked', 'single-tenant host — owner omitted (owner === undefined)');
41
42
  expect(
42
43
  typeof owner.tenant === 'string' && owner.tenant.length > 0,
43
44
  driver.describe('RFC 0048 §C', 'RunSnapshot.owner MUST carry a non-empty tenant when present'),
@@ -50,7 +51,7 @@ describe('cross-workspace-isolation: a principal MUST NOT read another workspace
50
51
  // Seam contract: a principal scoped to workspace A requests a run owned
51
52
  // by workspace B. The host MUST refuse rather than return B's run.
52
53
  const res = await driver.post('/v1/host/sample/identity/cross-workspace-read', {});
53
- if (res.status === 404) return; // seam unwired — soft-skip
54
+ if (res.status === 404) return softSkip('blocked', 'seam unwired — soft-skip');
54
55
 
55
56
  expect(
56
57
  res.status,
@@ -36,6 +36,7 @@
36
36
  */
37
37
 
38
38
  import { describe, it, expect } from 'vitest';
39
+ import { readErrorCode } from '../lib/error-envelope.js';
39
40
  import { readFileSync } from 'node:fs';
40
41
  import { join } from 'node:path';
41
42
  import Ajv2020 from 'ajv/dist/2020.js';
@@ -96,12 +97,11 @@ describe.skipIf(HTTP_SKIP)('data-residency: admission control (capability-gated,
96
97
  // A host may legitimately reject the probe workflow for reasons unrelated to
97
98
  // residency (e.g. unknown workflowId) — but it MUST NOT reject an ADVERTISED
98
99
  // region with residency_unavailable. That specific pairing is the violation.
99
- const okBody = ok.json as { error?: { code?: string } } | undefined;
100
100
  expect(
101
- okBody?.error?.code !== 'residency_unavailable',
101
+ readErrorCode(ok.json) !== 'residency_unavailable',
102
102
  driver.describe(
103
103
  'capabilities.md §dataResidency / RFC 0129 §3',
104
- `an ADVERTISED region ("${advertised}") MUST NOT be rejected with residency_unavailable (accept iff advertised) — got ${ok.status} ${JSON.stringify(okBody?.error)}`,
104
+ `an ADVERTISED region ("${advertised}") MUST NOT be rejected with residency_unavailable (accept iff advertised) — got ${ok.status} ${JSON.stringify(ok.json).slice(0, 200)}`,
105
105
  ),
106
106
  ).toBe(true);
107
107
 
@@ -114,7 +114,7 @@ describe.skipIf(HTTP_SKIP)('data-residency: admission control (capability-gated,
114
114
  inputs: {},
115
115
  residency: { region: bogus },
116
116
  });
117
- const rejBody = rej.json as { error?: { code?: string }; runId?: string } | undefined;
117
+ const rejBody = rej.json as { runId?: string } | undefined;
118
118
 
119
119
  expect(
120
120
  rej.status === 400 || rej.status === 404 || rej.status === 422,
@@ -124,10 +124,10 @@ describe.skipIf(HTTP_SKIP)('data-residency: admission control (capability-gated,
124
124
  ),
125
125
  ).toBe(true);
126
126
  expect(
127
- rejBody?.error?.code === 'residency_unavailable',
127
+ readErrorCode(rej.json) === 'residency_unavailable',
128
128
  driver.describe(
129
129
  'RFC 0129 §3',
130
- `an unadvertised region MUST be rejected with error code "residency_unavailable" (MUST NOT silently accept-and-ignore) — got ${JSON.stringify(rejBody?.error)}`,
130
+ `an unadvertised region MUST be rejected with error code "residency_unavailable" (MUST NOT silently accept-and-ignore) — got ${JSON.stringify(rej.json).slice(0, 200)}`,
131
131
  ),
132
132
  ).toBe(true);
133
133
  expect(
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import { describe, it, expect } from 'vitest';
25
+ import { softSkip } from '../lib/soft-skip.js';
25
26
  import { driver } from '../lib/driver.js';
26
27
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
27
28
 
@@ -36,9 +37,9 @@ async function deadLetterSupported(): Promise<boolean> {
36
37
 
37
38
  describe('deadletter-retry-exhaustion: retry exhaustion → dead-lettered + fork-eligible (RFC 0053 §C)', () => {
38
39
  it('a retry-exhausted run emits run.dead_lettered with attempts', async () => {
39
- if (!(await deadLetterSupported())) return; // capability-gated
40
+ if (!(await deadLetterSupported())) return softSkip('inapplicable', 'capability-gated');
40
41
  const res = await driver.post('/v1/host/sample/deadletter/exhaust', { scenario: 'exhaust-retries' });
41
- if (res.status === 404) return; // seam unwired — soft-skip
42
+ if (res.status === 404) return softSkip('blocked', 'seam unwired — soft-skip');
42
43
  const body = res.json as { event?: { type?: string; payload?: { attempts?: number; runId?: string } } } | undefined;
43
44
  expect(
44
45
  body?.event?.type,
@@ -51,9 +52,9 @@ describe('deadletter-retry-exhaustion: retry exhaustion → dead-lettered + fork
51
52
  });
52
53
 
53
54
  it('the dead-lettered run is fork-eligible (RFC 0011)', async () => {
54
- if (!(await deadLetterSupported())) return; // capability-gated
55
+ if (!(await deadLetterSupported())) return softSkip('inapplicable', 'capability-gated');
55
56
  const res = await driver.post('/v1/host/sample/deadletter/exhaust', { scenario: 'fork-after-dead-letter' });
56
- if (res.status === 404) return; // seam unwired — soft-skip
57
+ if (res.status === 404) return softSkip('blocked', 'seam unwired — soft-skip');
57
58
  const body = res.json as { forkEligible?: boolean } | undefined;
58
59
  expect(
59
60
  body?.forkEligible,
@@ -14,15 +14,16 @@
14
14
  */
15
15
 
16
16
  import { describe, it, expect } from 'vitest';
17
+ import { softSkip } from '../lib/soft-skip.js';
17
18
  import { driver } from '../lib/driver.js';
18
19
  import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
19
20
 
20
21
  describe('distillation-index-roundtrip (RFC 0062 §B)', () => {
21
22
  it('an indexEmitted run updates a retrievable memory-index manifest', async () => {
22
23
  const cap = await readDistillationCap();
23
- if (cap?.supported !== true || cap?.indexEmitted !== true) return;
24
+ if (cap?.supported !== true || cap?.indexEmitted !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true || cap?.indexEmitted !== true` returned early');
24
25
  const res = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 8000, indexEmitted: true });
25
- if (res === null) return; // seam absent — soft-skip
26
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
26
27
  expect(
27
28
  res.body.indexUpdated === true || res.body.event?.distillation?.indexUpdated === true,
28
29
  driver.describe('RFC 0062 §B', 'an indexEmitted distillation MUST report updating the memory index'),
@@ -12,6 +12,7 @@
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 { readDistillationCap, invokeDistill } from '../lib/distillation.js';
17
18
 
@@ -19,14 +20,14 @@ const CANARY = 'sk-canary-rfc0062-do-not-leak-qrs456';
19
20
 
20
21
  describe('distillation-secret-carryforward (RFC 0062 §B)', () => {
21
22
  it('a redacted secret in source memory never appears in the distilled output', async () => {
22
- if ((await readDistillationCap())?.supported !== true) return;
23
+ if ((await readDistillationCap())?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `(await readDistillationCap())?.supported !== true` returned early');
23
24
  const res = await invokeDistill({
24
25
  memoryRef: 'conformance-distill',
25
26
  tokenBudget: 8000,
26
27
  includeSecretCanary: true,
27
28
  sources: [{ content: `notes with embedded secret ${CANARY}` }],
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
  JSON.stringify(res.body).includes(CANARY),
32
33
  driver.describe('RFC 0062 §B', 'SR-1 carry-forward: a redacted secret MUST NOT re-appear in the archive or memory.compacted event'),
@@ -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 { readDistillationCap } from '../lib/distillation.js';
17
18
 
18
19
  describe('distillation-shape: advertisement (RFC 0062 §A)', () => {
19
20
  it('capabilities.memory.distillation is absent or a well-formed object', async () => {
20
21
  const cap = await readDistillationCap();
21
- if (cap === null) return; // not advertised — valid
22
+ if (cap === null) return softSkip('inapplicable', 'not advertised — valid');
22
23
  expect(
23
24
  typeof cap.supported,
24
25
  driver.describe('capabilities.schema.json §memory.distillation', 'distillation.supported MUST be a boolean when the block is present'),
@@ -10,21 +10,22 @@
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 { readDistillationCap, invokeDistill } from '../lib/distillation.js';
15
16
 
16
17
  describe('distillation-stable-archive (RFC 0062 §B)', () => {
17
18
  it('identical sources + budget produce an identical archive checksum', async () => {
18
- if ((await readDistillationCap())?.supported !== true) return;
19
+ if ((await readDistillationCap())?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `(await readDistillationCap())?.supported !== true` returned early');
19
20
  const req = {
20
21
  memoryRef: 'conformance-distill',
21
22
  tokenBudget: 8000,
22
23
  sources: ['s1', 's2', 's3'],
23
24
  };
24
25
  const a = await invokeDistill(req);
25
- if (a === null) return; // seam absent — soft-skip
26
+ if (a === null) return softSkip('blocked', 'seam absent — soft-skip');
26
27
  const b = await invokeDistill(req);
27
- if (b === null) return;
28
+ if (b === null) return softSkip('blocked', 'precondition not met — `b === null` returned early (seam, prior step, or fixture unavailable)');
28
29
  expect(
29
30
  typeof a.body.archiveChecksum === 'string' && (a.body.archiveChecksum as string).length > 0,
30
31
  driver.describe('RFC 0062 §B', 'a distillation run MUST produce a non-empty archive checksum'),
@@ -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 { readDistillationCap, invokeDistill } from '../lib/distillation.js';
16
17
 
17
18
  describe('distillation-token-budget (RFC 0062 §B)', () => {
18
19
  it('within budget tokensUsed ≤ tokenBudget; an un-meetable budget fails atomically', async () => {
19
- if ((await readDistillationCap())?.supported !== true) return;
20
+ if ((await readDistillationCap())?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `(await readDistillationCap())?.supported !== true` returned early');
20
21
 
21
22
  const ok = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 8000 });
22
- if (ok === null) return; // seam absent — soft-skip
23
+ if (ok === null) return softSkip('blocked', 'seam absent — soft-skip');
23
24
  const dist = ok.body.event?.distillation ?? {};
24
25
  expect(
25
26
  typeof dist.tokenBudget === 'number' && typeof dist.tokensUsed === 'number',
@@ -32,7 +33,7 @@ describe('distillation-token-budget (RFC 0062 §B)', () => {
32
33
 
33
34
  // A budget too small to distill the corpus MUST fail closed, no partial archive.
34
35
  const tooSmall = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 1 });
35
- if (tooSmall === null) return;
36
+ if (tooSmall === null) return softSkip('blocked', 'precondition not met — `tooSmall === null` returned early (seam, prior step, or fixture unavailable)');
36
37
  expect(
37
38
  tooSmall.status >= 400 && tooSmall.body.error === 'token_budget_exceeded',
38
39
  driver.describe('RFC 0062 §B', 'an un-meetable budget MUST fail with token_budget_exceeded'),
@@ -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
 
22
23
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -83,7 +84,7 @@ describe.skipIf(HTTP_SKIP)('envelope-recovery-applied: SECURITY invariant envelo
83
84
  'envelope.recovery.applied payload MUST NOT carry pre-recovery output substrings; only the canonical {nodeId, path, byteOffset?} keys per RFC 0032 §B.6 + §G — the recovered content rides on downstream RunEventDoc, not on the recovery event',
84
85
  ),
85
86
  ).toBe(400);
86
- expect(r.body.error?.code).toBe('envelope_recovery_content_leak');
87
+ expect(readErrorCode(r.body)).toBe('envelope_recovery_content_leak');
87
88
  });
88
89
 
89
90
  it('rejects payloads carrying any extra field outside {nodeId, path, byteOffset}', async () => {
@@ -104,7 +105,7 @@ describe.skipIf(HTTP_SKIP)('envelope-recovery-applied: SECURITY invariant envelo
104
105
  'envelope.recovery.applied has additionalProperties: false on the payload — any extra field MUST be rejected to prevent regression carriers for pre-recovery output (defense-in-depth on top of envelope-recovery-no-content-leak)',
105
106
  ),
106
107
  ).toBe(400);
107
- expect(r.body.error?.code).toBe('envelope_recovery_content_leak');
108
+ expect(readErrorCode(r.body)).toBe('envelope_recovery_content_leak');
108
109
  });
109
110
  });
110
111
 
@@ -27,6 +27,7 @@
27
27
  */
28
28
 
29
29
  import { describe, it, expect } from 'vitest';
30
+ import { readErrorCode } from '../lib/error-envelope.js';
30
31
  import { driver } from '../lib/driver.js';
31
32
 
32
33
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -115,7 +116,7 @@ describe.skipIf(HTTP_SKIP)('envelope-refusal-shape: seam emission (RFC 0032 §B.
115
116
  'envelope.refusal.refusalText MUST be passed through the host BYOK redaction harness; seam refuses payloads carrying secret-canary-* substrings (defense-in-depth CI gate per RFC 0032 §B.3 + §G)',
116
117
  ),
117
118
  ).toBe(400);
118
- expect(r.body.error?.code).toBe('envelope_reliability_credential_leak');
119
+ expect(readErrorCode(r.body)).toBe('envelope_reliability_credential_leak');
119
120
  });
120
121
 
121
122
  it('rejects payloads with a top-level `credentialRef` field', async () => {
@@ -131,7 +132,7 @@ describe.skipIf(HTTP_SKIP)('envelope-refusal-shape: seam emission (RFC 0032 §B.
131
132
  });
132
133
  if (r.status === 404) return;
133
134
  expect(r.status).toBe(400);
134
- expect(r.body.error?.code).toBe('envelope_reliability_credential_leak');
135
+ expect(readErrorCode(r.body)).toBe('envelope_reliability_credential_leak');
135
136
  });
136
137
 
137
138
  it('rejects payloads missing required `provider` field', async () => {
@@ -146,7 +147,7 @@ describe.skipIf(HTTP_SKIP)('envelope-refusal-shape: seam emission (RFC 0032 §B.
146
147
  });
147
148
  if (r.status === 404) return;
148
149
  expect(r.status).toBe(400);
149
- expect(r.body.error?.code).toBe('invalid_argument');
150
+ expect(readErrorCode(r.body)).toBe('invalid_argument');
150
151
  });
151
152
  });
152
153
 
@@ -0,0 +1,64 @@
1
+ /**
2
+ * S22 (2026-08-16) — the HTTP error envelope is FLAT, and the suite reads it
3
+ * through one helper.
4
+ *
5
+ * `rest-endpoints.md` §"Error response shape" and `schemas/error-envelope.schema.json`
6
+ * lock `{ error: <string code>, message, details? }` with `additionalProperties:
7
+ * false`. A nested `{ error: { code, retriable } }` shape had crept into three
8
+ * code-list entries, four seam contracts and ~15 scenarios; this scenario pins
9
+ * the decision at the three places it lives — the schema, the helper that every
10
+ * HTTP-envelope leg now reads through, and the prose — so it cannot re-open
11
+ * quietly. Server-free, always-on.
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest';
15
+ import { readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import Ajv2020 from 'ajv/dist/2020.js';
18
+ import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
19
+ import { readErrorCode, readRetriable, isCanonicalErrorEnvelope, isLegacyNestedEnvelope } from '../lib/error-envelope.js';
20
+
21
+ export const HOST_CALLBACK_NOT_REQUIRED = 'server-free: pins the flat error-envelope decision against the schema, the helper and the prose';
22
+
23
+ describe('S22 — the canonical HTTP error envelope is flat', () => {
24
+ const schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'error-envelope.schema.json'), 'utf8')) as Record<string, unknown>;
25
+ const validate = new Ajv2020({ allErrors: true, strict: false }).compile(schema);
26
+
27
+ it('the schema says `error` is a string, requires `message`, and forbids other top-level keys', () => {
28
+ expect(validate({ error: 'runner_unavailable', message: 'no runner', details: { retriable: true } })).toBe(true);
29
+ expect(validate({ error: { code: 'runner_unavailable', retriable: true } })).toBe(false);
30
+ expect(validate({ error: 'x', message: 'y', retriable: true })).toBe(false); // top-level retriable is illegal
31
+ expect(validate({ error: 'x' })).toBe(false); // message required
32
+ });
33
+
34
+ it('readErrorCode / readRetriable read the canonical shape, and tolerate the legacy nested shape only as legacy', () => {
35
+ const flat = { error: 'interop_version_unsupported', message: 'peer offers 0.3', details: { retriable: false, protocol: 'a2a' } };
36
+ expect(readErrorCode(flat)).toBe('interop_version_unsupported');
37
+ expect(readRetriable(flat)).toBe(false);
38
+ expect(isCanonicalErrorEnvelope(flat)).toBe(true);
39
+ expect(isLegacyNestedEnvelope(flat)).toBe(false);
40
+ const nested = { error: { code: 'runner_unavailable', retriable: true } };
41
+ expect(readErrorCode(nested)).toBe('runner_unavailable');
42
+ expect(readRetriable(nested)).toBe(true);
43
+ expect(isCanonicalErrorEnvelope(nested)).toBe(false);
44
+ expect(isLegacyNestedEnvelope(nested)).toBe(true);
45
+ expect(readErrorCode({ message: 'no code' })).toBeUndefined();
46
+ expect(readErrorCode(null)).toBeUndefined();
47
+ expect(readRetriable({ error: 'x', message: 'y' })).toBeUndefined();
48
+ });
49
+
50
+ it.skipIf(V1_DIR === null)('rest-endpoints.md states the precedence, the details.retriable convention, and carries no nested code-list entry', () => {
51
+ const prose = readFileSync(join(V1_DIR as string, 'rest-endpoints.md'), 'utf8');
52
+ expect(prose).toContain('flat** shape above');
53
+ expect(prose).toContain('### `details.retriable` convention');
54
+ // the three formerly-nested code-list entries are flat now
55
+ for (const code of ['runner_unavailable', 'residency_unavailable', 'interop_version_unsupported']) {
56
+ const line = prose.split('\n').find((l) => l.startsWith('- `' + code + '`')) ?? '';
57
+ expect(line, code + ' code-list entry MUST exist').not.toBe('');
58
+ expect(line, code + ' MUST NOT be described with the nested envelope').not.toMatch(/\{\s*"error"\s*:\s*\{/);
59
+ }
60
+ // the seam catalog too
61
+ const seams = readFileSync(join(V1_DIR as string, 'host-sample-test-seams.md'), 'utf8');
62
+ expect(seams).not.toMatch(/"error":\s*\{\s*"code"/);
63
+ });
64
+ });
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { describe, it, expect } from 'vitest';
12
+ import { softSkip } from '../lib/soft-skip.js';
12
13
  import { driver } from '../lib/driver.js';
13
14
  import { readFeedbackCap, seedRun } from '../lib/feedback.js';
14
15
 
@@ -17,14 +18,14 @@ const CANARY = 'sk-canary-rfc0056-do-not-leak-abc123';
17
18
  describe('feedback-correction-redaction (RFC 0056 §E)', () => {
18
19
  it('secret-shaped material in correction/note is redacted in the annotation list', async () => {
19
20
  const cap = await readFeedbackCap();
20
- if (cap?.supported !== true) return;
21
+ if (cap?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true` returned early');
21
22
  const runId = await seedRun('feedback-redact');
22
- if (!runId) return;
23
+ if (!runId) return softSkip('blocked', 'precondition not met — `!runId` returned early (seam, prior step, or fixture unavailable)');
23
24
  const post = await driver.post(`/v1/runs/${runId}/annotations`, {
24
25
  signal: { kind: 'correction', correction: `please use ${CANARY}` },
25
26
  note: CANARY,
26
27
  });
27
- if (post.status === 501 || post.status === 404) return;
28
+ if (post.status === 501 || post.status === 404) return softSkip('blocked', 'precondition not met — `post.status === 501 || post.status === 404` returned early (seam, prior step, or fixture unavailable)');
28
29
  expect(post.status).toBe(201);
29
30
  const list = await driver.get(`/v1/runs/${runId}/annotations`);
30
31
  expect(
@@ -13,17 +13,18 @@
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 { readFeedbackCap, seedRun } from '../lib/feedback.js';
18
19
 
19
20
  describe('feedback-cross-tenant-isolation (RFC 0056 §E)', () => {
20
21
  it('a run\'s annotation list contains only that run\'s annotations', async () => {
21
22
  const cap = await readFeedbackCap();
22
- if (cap?.supported !== true) return;
23
+ if (cap?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true` returned early');
23
24
  const runId = await seedRun('feedback-cti');
24
- if (!runId) return;
25
+ if (!runId) return softSkip('blocked', 'precondition not met — `!runId` returned early (seam, prior step, or fixture unavailable)');
25
26
  const post = await driver.post(`/v1/runs/${runId}/annotations`, { signal: { kind: 'label', label: 'cti-probe' } });
26
- if (post.status === 501 || post.status === 404) return;
27
+ if (post.status === 501 || post.status === 404) return softSkip('blocked', 'precondition not met — `post.status === 501 || post.status === 404` returned early (seam, prior step, or fixture unavailable)');
27
28
  expect(post.status).toBe(201);
28
29
  const list = await driver.get(`/v1/runs/${runId}/annotations`);
29
30
  const ann = (list.json as { annotations?: Array<{ target?: { runId?: string } }> } | undefined)?.annotations ?? [];
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { describe, it, expect } from 'vitest';
11
+ import { softSkip } from '../lib/soft-skip.js';
11
12
  import { driver } from '../lib/driver.js';
12
13
  import { pollUntilTerminal } from '../lib/polling.js';
13
14
  import { readFeedbackCap, seedRun } from '../lib/feedback.js';
@@ -15,11 +16,11 @@ import { readFeedbackCap, seedRun } from '../lib/feedback.js';
15
16
  describe('feedback-fork-not-copied (RFC 0056 §D)', () => {
16
17
  it('a fork of an annotated run starts with zero annotations', async () => {
17
18
  const cap = await readFeedbackCap();
18
- if (cap?.supported !== true) return;
19
+ if (cap?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true` returned early');
19
20
  const runId = await seedRun('feedback-fork');
20
- if (!runId) return;
21
+ if (!runId) return softSkip('blocked', 'precondition not met — `!runId` returned early (seam, prior step, or fixture unavailable)');
21
22
  const post = await driver.post(`/v1/runs/${runId}/annotations`, { signal: { kind: 'flag' } });
22
- if (post.status === 501 || post.status === 404) return;
23
+ if (post.status === 501 || post.status === 404) return softSkip('blocked', 'precondition not met — `post.status === 501 || post.status === 404` returned early (seam, prior step, or fixture unavailable)');
23
24
  expect(post.status).toBe(201);
24
25
  try {
25
26
  await pollUntilTerminal(runId, { timeoutMs: 10_000 });
@@ -27,9 +28,9 @@ describe('feedback-fork-not-copied (RFC 0056 §D)', () => {
27
28
  return;
28
29
  }
29
30
  const fork = await driver.post(`/v1/runs/${runId}:fork`, { fromSeq: 0, mode: 'branch' });
30
- if (fork.status !== 200 && fork.status !== 201) return; // fork unsupported — soft-skip
31
+ if (fork.status !== 200 && fork.status !== 201) return softSkip('inapplicable', 'fork unsupported — soft-skip (fork.status !== 200 && fork.status !== 201)');
31
32
  const forkId = (fork.json as { runId?: string } | undefined)?.runId;
32
- if (!forkId) return;
33
+ if (!forkId) return softSkip('blocked', 'precondition not met — `!forkId` returned early (seam, prior step, or fixture unavailable)');
33
34
  const list = await driver.get(`/v1/runs/${forkId}/annotations`);
34
35
  const ann = (list.json as { annotations?: unknown[] } | undefined)?.annotations ?? [];
35
36
  expect(
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { describe, it, expect } from 'vitest';
10
+ import { softSkip } from '../lib/soft-skip.js';
10
11
  import { driver } from '../lib/driver.js';
11
12
  import { pollUntilTerminal } from '../lib/polling.js';
12
13
  import { readFeedbackCap, seedRun } from '../lib/feedback.js';
@@ -14,16 +15,16 @@ import { readFeedbackCap, seedRun } from '../lib/feedback.js';
14
15
  describe('feedback-on-terminal-run (RFC 0056 §C)', () => {
15
16
  it('annotating a terminal run is accepted', async () => {
16
17
  const cap = await readFeedbackCap();
17
- if (cap?.supported !== true) return;
18
+ if (cap?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true` returned early');
18
19
  const runId = await seedRun('feedback-terminal');
19
- if (!runId) return;
20
+ if (!runId) return softSkip('blocked', 'precondition not met — `!runId` returned early (seam, prior step, or fixture unavailable)');
20
21
  try {
21
22
  await pollUntilTerminal(runId, { timeoutMs: 10_000 });
22
23
  } catch {
23
24
  return; // run didn't reach terminal in time — soft-skip
24
25
  }
25
26
  const post = await driver.post(`/v1/runs/${runId}/annotations`, { signal: { kind: 'flag' }, note: 'post-hoc review' });
26
- if (post.status === 501 || post.status === 404) return;
27
+ if (post.status === 501 || post.status === 404) return softSkip('blocked', 'precondition not met — `post.status === 501 || post.status === 404` returned early (seam, prior step, or fixture unavailable)');
27
28
  expect(
28
29
  post.status,
29
30
  driver.describe('RFC 0056 §C', 'a host MUST accept an annotation on a terminal run'),
@@ -7,17 +7,18 @@
7
7
  */
8
8
 
9
9
  import { describe, it, expect } from 'vitest';
10
+ import { softSkip } from '../lib/soft-skip.js';
10
11
  import { driver } from '../lib/driver.js';
11
12
  import { readFeedbackCap, seedRun } from '../lib/feedback.js';
12
13
 
13
14
  describe('feedback-record-and-list (RFC 0056 §C)', () => {
14
15
  it('POST an annotation then GET returns it', async () => {
15
16
  const cap = await readFeedbackCap();
16
- if (cap?.supported !== true) return;
17
+ if (cap?.supported !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.supported !== true` returned early');
17
18
  const runId = await seedRun('feedback-rl');
18
- if (!runId) return;
19
+ if (!runId) return softSkip('blocked', 'precondition not met — `!runId` returned early (seam, prior step, or fixture unavailable)');
19
20
  const post = await driver.post(`/v1/runs/${runId}/annotations`, { signal: { kind: 'rating', rating: 5 } });
20
- if (post.status === 501 || post.status === 404) return;
21
+ if (post.status === 501 || post.status === 404) return softSkip('blocked', 'precondition not met — `post.status === 501 || post.status === 404` returned early (seam, prior step, or fixture unavailable)');
21
22
  expect(
22
23
  post.status,
23
24
  driver.describe('RFC 0056 §C', 'POST annotation returns 201 with the persisted annotation'),
@@ -11,17 +11,18 @@
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 { readFeedbackCap } from '../lib/feedback.js';
16
17
 
17
18
  describe('feedback-unsupported-501 (RFC 0056 §C)', () => {
18
19
  it('POST annotations returns 501 capability_not_provided when feedback is unadvertised', async () => {
19
20
  const cap = await readFeedbackCap();
20
- if (cap?.supported === true) return; // host supports feedback — 501 N/A
21
+ if (cap?.supported === true) return softSkip('inapplicable', 'host supports feedback — 501 N/A (cap?.supported === true)');
21
22
  const res = await driver.post('/v1/runs/probe-run-rfc0056/annotations', {
22
23
  signal: { kind: 'flag' },
23
24
  });
24
- if (res.status === 404 || res.status === 405) return; // route absent — host predates RFC 0056
25
+ if (res.status === 404 || res.status === 405) return softSkip('blocked', 'route absent — host predates RFC 0056');
25
26
  expect(
26
27
  res.status,
27
28
  driver.describe('rest-endpoints.md / RFC 0056 §C', 'unadvertised feedback MUST return 501, not 404'),
@@ -24,24 +24,22 @@ const PROMPT_TEMPLATE_SCHEMA_PATH = join(SCHEMAS_DIR, 'prompt-template.schema.js
24
24
  describe('fixtures: workflow-definition schema validity', () => {
25
25
  const ajv = new Ajv2020({ allErrors: true, strict: false });
26
26
  addFormats(ajv);
27
- // Pre-load peer schemas that workflow-definition cross-`$ref`s:
28
- // - agent-ref.schema.json — `WorkflowNode.agent` (Phase 1 multi-agent)
29
- // - prompt-ref.schema.json — `WorkflowDefinition.defaults.promptRefs.*`
30
- // (RFC 0029 §B resolution-chain layer 3)
31
- // - prompt-kind.schema.json — transitively referenced by prompt-ref's
32
- // object form when validating PromptRef variants
33
- // Register each under both the canonical $id and the relative file
34
- // name so Ajv resolves either way the host schema spelled the ref.
35
- const agentRefSchema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'agent-ref.schema.json'), 'utf8'));
36
- const promptRefSchema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'prompt-ref.schema.json'), 'utf8'));
37
- const promptKindSchema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'prompt-kind.schema.json'), 'utf8'));
38
- ajv.addSchema(agentRefSchema, 'agent-ref.schema.json');
39
- ajv.addSchema(promptRefSchema, 'prompt-ref.schema.json');
40
- ajv.addSchema(promptRefSchema, './prompt-ref.schema.json');
41
- ajv.addSchema(promptKindSchema, 'prompt-kind.schema.json');
42
- ajv.addSchema(promptKindSchema, './prompt-kind.schema.json');
43
- const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
44
- const validate = ajv.compile(schema);
27
+ // Pre-load EVERY schema in the corpus (S14, 2026-08-16). This used to be a
28
+ // fixed list of the peer schemas workflow-definition cross-`$ref`s (agent-ref,
29
+ // prompt-ref, prompt-kind) — and when `compensation-policy.schema.json` became
30
+ // the fourth cross-file `$ref` (#1009), `ajv.compile` threw at describe time
31
+ // here and in every downstream runner that resolves the sibling corpus with a
32
+ // pinned suite. A fixed list is a claim about the schema graph that nothing
33
+ // keeps true; enumerating the directory is not. Each schema is registered
34
+ // under both its bare file name and `./<name>` so Ajv resolves either spelling
35
+ // of a relative ref (canonical `$id`s resolve on their own).
36
+ for (const file of readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json'))) {
37
+ const s = JSON.parse(readFileSync(join(SCHEMAS_DIR, file), 'utf8')) as { $id?: string };
38
+ ajv.addSchema(s, file);
39
+ ajv.addSchema(s, `./${file}`);
40
+ }
41
+ // The root schema is already registered under its $id above; compile via getSchema.
42
+ const validate = ajv.getSchema('workflow-definition.schema.json') ?? ajv.compile(JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')));
45
43
 
46
44
  const files = readdirSync(FIXTURES_DIR)
47
45
  .filter((f) => f.endsWith('.json')) // top-level only — sub-dirs hold non-workflow fixtures
@@ -26,6 +26,7 @@
26
26
  */
27
27
 
28
28
  import { describe, it, expect } from 'vitest';
29
+ import { readErrorCode } from '../lib/error-envelope.js';
29
30
  import { driver } from '../lib/driver.js';
30
31
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
31
32
 
@@ -97,7 +98,7 @@ describe('fs-path-traversal: MUST-NOT escape sandboxRoot (RFC 0014 §C)', () =>
97
98
  'absolute paths outside sandboxRoot MUST be rejected with a 4xx envelope',
98
99
  ),
99
100
  ).toBeGreaterThanOrEqual(400);
100
- const code = (res.json as { error?: { code?: string } } | undefined)?.error?.code;
101
+ const code = readErrorCode(res.json);
101
102
  expect(
102
103
  code !== undefined && PATH_REJECTION_CODES.has(code),
103
104
  driver.describe(
@@ -113,7 +114,7 @@ describe('fs-path-traversal: MUST-NOT escape sandboxRoot (RFC 0014 §C)', () =>
113
114
  const res = await driver.post('/v1/host/sample/fs/read', { path: '../../etc/passwd' });
114
115
  if (res.status === 404) return;
115
116
  expect(res.status).toBeGreaterThanOrEqual(400);
116
- const code = (res.json as { error?: { code?: string } } | undefined)?.error?.code;
117
+ const code = readErrorCode(res.json);
117
118
  expect(
118
119
  code !== undefined && PATH_REJECTION_CODES.has(code),
119
120
  driver.describe(
@@ -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 { readHeartbeatCap } from '../lib/heartbeat.js';
16
17
 
17
18
  describe('heartbeat-capability-shape: advertisement (RFC 0060 §A)', () => {
18
19
  it('capabilities.heartbeat is absent or a well-formed object', async () => {
19
20
  const cap = await readHeartbeatCap();
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 §heartbeat', 'heartbeat.supported MUST be a boolean when the block is present'),
@@ -10,16 +10,17 @@
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 { readHeartbeatCap, heartbeatSupported, tickHeartbeat } from '../lib/heartbeat.js';
15
16
 
16
17
  describe('heartbeat-fires-once-per-tick (RFC 0060 §B.1)', () => {
17
18
  it('one tick emits exactly one heartbeat.evaluated', async () => {
18
- if (!heartbeatSupported(await readHeartbeatCap())) return;
19
+ if (!heartbeatSupported(await readHeartbeatCap())) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!heartbeatSupported(await readHeartbeatCap())` returned early');
19
20
  const res = await tickHeartbeat({ heartbeatId: 'conformance-hb', observedState: { n: 0 } });
20
- if (res === null) return; // seam absent — soft-skip
21
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
21
22
  const evaluated = (res.json as { evaluated?: unknown[] } | undefined)?.evaluated;
22
- if (!Array.isArray(evaluated)) return; // host doesn't surface per-tick events on the seam
23
+ if (!Array.isArray(evaluated)) return softSkip('blocked', 'host doesn\'t surface per-tick events on the seam');
23
24
  expect(
24
25
  evaluated.length,
25
26
  driver.describe('RFC 0060 §B.1', 'a single tick MUST emit exactly one heartbeat.evaluated'),