@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
@@ -41,6 +41,7 @@
41
41
  */
42
42
 
43
43
  import { describe, it, expect } from 'vitest';
44
+ import { softSkip } from '../lib/soft-skip.js';
44
45
  import { driver } from '../lib/driver.js';
45
46
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
46
47
 
@@ -162,10 +163,10 @@ describe.skipIf(HTTP_SKIP)(
162
163
  'replay-observable-sequence-determinism: prefix byte-equivalence (RFC 0041 §C)',
163
164
  () => {
164
165
  it('original and replay event-log prefixes MUST be byte-equivalent (modulo per-event clock + ULID entropy)', async (ctx) => {
165
- if (!(await gateOnPhase4(ctx))) return;
166
+ if (!(await gateOnPhase4(ctx))) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await gateOnPhase4(ctx))` returned early');
166
167
 
167
168
  const sourceRunId = await startFixtureRun(ctx);
168
- if (sourceRunId === null) return;
169
+ if (sourceRunId === null) return softSkip('blocked', 'precondition not met — `sourceRunId === null` returned early (seam, prior step, or fixture unavailable)');
169
170
  const sourceTerminal = await pollUntilTerminal(sourceRunId);
170
171
  expect(sourceTerminal.status).toBe('completed');
171
172
  const sourceEvents = await readEvents(sourceRunId);
@@ -196,10 +197,10 @@ describe.skipIf(HTTP_SKIP)(
196
197
  'replay-observable-sequence-determinism: observable-result caching (RFC 0041 §C)',
197
198
  () => {
198
199
  it('replay of a nondeterministic tool node reproduces the ORIGINAL observable result, NOT a fresh call', async (ctx) => {
199
- if (!(await gateOnPhase4(ctx))) return;
200
+ if (!(await gateOnPhase4(ctx))) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await gateOnPhase4(ctx))` returned early');
200
201
 
201
202
  const sourceRunId = await startFixtureRun(ctx);
202
- if (sourceRunId === null) return;
203
+ if (sourceRunId === null) return softSkip('blocked', 'precondition not met — `sourceRunId === null` returned early (seam, prior step, or fixture unavailable)');
203
204
  expect((await pollUntilTerminal(sourceRunId)).status).toBe('completed');
204
205
  const sourceEvents = await readEvents(sourceRunId);
205
206
 
@@ -45,6 +45,8 @@
45
45
  */
46
46
 
47
47
  import { describe, it, expect } from 'vitest';
48
+ import { recordRequirement } from '../lib/requirement-ledger.js';
49
+ import { requirementIdForScenario } from '../lib/requirement-registry.js';
48
50
  import { driver } from '../lib/driver.js';
49
51
  import { pollUntilTerminal } from '../lib/polling.js';
50
52
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -111,7 +113,24 @@ describe.skipIf(SKIP_NO_FIXTURE)('replay-side-effect-suppression: a replay does
111
113
  const replay = await fetchReplay();
112
114
 
113
115
  if (replay?.sideEffectSuppression !== 'recorded-outcome') {
114
- ctx.skip(); // host makes no such guarantee RFC 0140 imposes nothing
116
+ // `replay.md` §"Side-effect suppression" (corrected 2026-08-08): caveat 1
117
+ // a replay MUST NOT call the external system twice — is UNCONDITIONAL and
118
+ // `none` is not permission to re-fire; it only means no probeable mechanism
119
+ // is declared. So this is not `inapplicable`: the obligation applies and is
120
+ // unwitnessable here. Record the floor row `blocked` (RFC 0148 §A) so a
121
+ // host advertising `none` cannot certify `openwop-replay-fork` on the
122
+ // strength of determinism alone (H20 — re-advertising `recorded-outcome`
123
+ // is what makes this leg run).
124
+ try {
125
+ recordRequirement(
126
+ requirementIdForScenario('replay-side-effect-suppression.test.ts'),
127
+ 'blocked',
128
+ `host advertises replay.sideEffectSuppression ${JSON.stringify(replay?.sideEffectSuppression ?? 'none')} — caveat 1 (a replay MUST NOT re-fire external effects) is unconditional but no declared mechanism is probeable; unwitnessed, not inapplicable`,
129
+ );
130
+ } catch {
131
+ /* already recorded */
132
+ }
133
+ ctx.skip();
115
134
  return;
116
135
  }
117
136
  if (!replay.modes?.includes('replay')) {
@@ -21,7 +21,7 @@
21
21
  * Server-free and always-on.
22
22
  */
23
23
 
24
- import { describe, it, expect, beforeEach } from 'vitest';
24
+ import { describe, it, expect, beforeEach, beforeAll, afterAll } from 'vitest';
25
25
  import {
26
26
  DISPOSITIONS,
27
27
  CERTIFIABLE,
@@ -30,10 +30,19 @@ import {
30
30
  entryOf,
31
31
  snapshot,
32
32
  resetLedger,
33
+ suspendSinkForFixtures,
33
34
  verifyProfileRequirements,
34
35
  } from '../lib/requirement-ledger.js';
35
36
  import { allRequirements, requirementsFor } from '../lib/requirement-registry.js';
36
37
 
38
+ // This file records REAL requirement ids as fixtures; keep them out of a live
39
+ // --certify ledger sink (they would out-vote genuine dispositions).
40
+ let restoreSink: (() => void) | undefined;
41
+ beforeAll(() => {
42
+ restoreSink = suspendSinkForFixtures();
43
+ });
44
+ afterAll(() => restoreSink?.());
45
+
37
46
  describe('RFC 0148 §A — requirement execution ledger', () => {
38
47
  beforeEach(() => resetLedger());
39
48
 
@@ -121,15 +130,29 @@ describe('RFC 0148 §A — the registry binds to the certification floor', () =>
121
130
  expect(allRequirements()).toContain('openwop.floor.any.interrupt-');
122
131
  });
123
132
 
124
- it('an unwritten floor returns null, not an empty list', () => {
125
- // `openwop-replay-fork` is deliberately unspecified `profiles.md` gives it
126
- // a discovery-conditional floor a flat list cannot express (gap G7). Null
127
- // forces the caller to decide; an empty array would silently certify.
133
+ it('an unwritten floor returns null, not an empty list — and so does a conditional floor without its document (G7)', () => {
134
+ // `openwop-replay-fork` has a discovery-conditional floor (RFC 0148 §C G7,
135
+ // transcribed 2026-08-16): "the conformance scenarios pass on whichever mode
136
+ // the host advertises". Without the document it is UNEVALUABLE — null forces
137
+ // the caller to decide; an empty array would silently certify.
128
138
  expect(
129
139
  requirementsFor('openwop-replay-fork'),
130
- 'RFC 0148 §C: unspecified MUST be distinguishable from empty-by-design',
140
+ 'RFC 0148 §C: unevaluable MUST be distinguishable from empty-by-design',
131
141
  ).toBeNull();
132
142
  expect(requirementsFor('openwop-does-not-exist')).toBeNull();
143
+ // With the document, the advertised branch is required
144
+ const doc = (modes: string[]) => ({ replay: { supported: true, modes } });
145
+ expect([...(requirementsFor('openwop-replay-fork', doc(['replay'])) ?? [])].sort()).toEqual(['openwop.floor.replay-side-effect-suppression', 'openwop.floor.replayDeterminism']);
146
+ expect(requirementsFor('openwop-replay-fork', doc(['branch']))).toEqual(['openwop.floor.replay-fork']);
147
+ expect([...(requirementsFor('openwop-replay-fork', doc(['replay', 'branch'])) ?? [])].sort()).toEqual(['openwop.floor.replay-fork', 'openwop.floor.replay-side-effect-suppression', 'openwop.floor.replayDeterminism']);
148
+ expect(requirementsFor('openwop-replay-fork', doc([]))).toEqual([]);
149
+ // and every branch is a registered requirement
150
+ expect(allRequirements()).toContain('openwop.floor.replayDeterminism');
151
+ expect(allRequirements()).toContain('openwop.floor.replay-fork');
152
+ // the six formerly-unprovable profiles now have floors
153
+ for (const p of ['openwop-interrupts', 'openwop-secrets', 'openwop-provider-policy', 'openwop-memory', 'openwop-trigger-bridge']) {
154
+ expect((requirementsFor(p) ?? []).length, `${p} floor MUST be non-empty`).toBeGreaterThan(0);
155
+ }
133
156
  });
134
157
 
135
158
  it('a discovery-only profile returns an empty list, which does not certify', () => {
@@ -0,0 +1,279 @@
1
+ /**
2
+ * RFC 0148 §A acceptance item 2 (S6) — "Runner records requirement-level
3
+ * dispositions and rejects unclassified returns."
4
+ *
5
+ * The runner is a separate process reading a vitest JSON report; the §A ledger
6
+ * lives in the workers. S6 connects them with a JSONL file sink
7
+ * (`OPENWOP_LEDGER_PATH`), a file-level recording hook in `setup.ts` (every
8
+ * scenario file records ONE disposition + its assertion count when it finishes),
9
+ * and a pure derivation (`scenario-disposition.ts`) the runner uses to build
10
+ * bundle v2 rows and to REJECT certification when a claimed profile's floor has
11
+ * an unclassified requirement.
12
+ *
13
+ * Pinned here, server-free:
14
+ * - the sink writes JSONL and the reader merges duplicates, resolving a
15
+ * conflict to the least certifiable disposition;
16
+ * - `fileDisposition` folds per-test states honestly (fail > pass > gate
17
+ * reason > blocked);
18
+ * - `deriveRequirementDispositions`: ledger rows win over report inference;
19
+ * a file with no entry is `blocked`/unclassified; prefix requirements derive
20
+ * from matching files; a claimed profile with an unclassified floor
21
+ * requirement flags rejection; a VACUOUS pass (executed-pass with
22
+ * assertionCount 0) on a claimed floor is unclassified — a witness of nothing;
23
+ * a discoveryOnly profile certifies with an empty floor;
24
+ * - and the live proof: this very run, when `OPENWOP_LEDGER_PATH` is set,
25
+ * records the files that ran before this one (checked when the env is set).
26
+ *
27
+ * @see conformance/src/lib/scenario-disposition.ts, requirement-ledger.ts, setup.ts, cli.ts
28
+ * @see RFCS/0148-non-vacuous-conformance-certification.md §A/§C
29
+ */
30
+
31
+ import { describe, it, expect } from 'vitest';
32
+ import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
33
+ import { join } from 'node:path';
34
+ import { tmpdir } from 'node:os';
35
+ import { readLedgerFile, recordRequirement, resetLedger, snapshot } from '../lib/requirement-ledger.js';
36
+ import { fileDisposition, deriveRequirementDispositions, requirementIdForFile, floorScenarioFiles } from '../lib/scenario-disposition.js';
37
+ import { softSkip, softSkipDisposition, resetSoftSkips, UNCLASSIFIED_RETURN_DETAIL } from '../lib/soft-skip.js';
38
+ import { PROFILE_FLOOR_SCENARIOS } from '../lib/profiles.js';
39
+ import { requirementsFor, requirementIdForScenario, requirementIdForPrefix } from '../lib/requirement-registry.js';
40
+
41
+ describe('RFC 0148 §A (S6) — ledger file sink and reader', () => {
42
+ it('recordRequirement appends JSONL when OPENWOP_LEDGER_PATH is set; the reader merges and ranks conflicts least-certifiable-first', () => {
43
+ const dir = mkdtempSync(join(tmpdir(), 'owp-ledger-'));
44
+ const path = join(dir, 'l.jsonl');
45
+ const prev = process.env['OPENWOP_LEDGER_PATH'];
46
+ process.env['OPENWOP_LEDGER_PATH'] = path;
47
+ resetLedger();
48
+ try {
49
+ recordRequirement('t.a', 'executed-pass', undefined, { assertionCount: 3 });
50
+ recordRequirement('t.b', 'inapplicable', 'not advertised');
51
+ // simulate a second worker writing a conflicting line for t.a
52
+ // (the in-memory map would throw; the FILE can carry both)
53
+ const { appendFileSync } = require('node:fs') as typeof import('node:fs');
54
+ appendFileSync(path, JSON.stringify({ requirementId: 't.a', disposition: 'blocked', detail: 'other worker' }) + '\n');
55
+ appendFileSync(path, 'not json\n');
56
+ const lines = readFileSync(path, 'utf8').trim().split('\n');
57
+ expect(lines.length).toBe(4);
58
+ const merged = readLedgerFile(path);
59
+ expect(merged.map((e) => e.requirementId)).toEqual(['t.a', 't.b']);
60
+ expect(merged.find((e) => e.requirementId === 't.a')?.disposition, 'conflict resolves to the least certifiable (blocked over executed-pass)').toBe('blocked');
61
+ expect(merged.find((e) => e.requirementId === 't.b')?.disposition).toBe('inapplicable');
62
+ expect(readLedgerFile(join(dir, 'missing.jsonl'))).toEqual([]);
63
+ } finally {
64
+ if (prev === undefined) delete process.env['OPENWOP_LEDGER_PATH'];
65
+ else process.env['OPENWOP_LEDGER_PATH'] = prev;
66
+ resetLedger();
67
+ rmSync(dir, { recursive: true, force: true });
68
+ }
69
+ });
70
+ });
71
+
72
+ describe('RFC 0148 §A (S6) — fileDisposition folds a file honestly', () => {
73
+ it('fail beats pass; pass beats a gate reason; a gate reason beats blocked; nothing is blocked (unclassified)', () => {
74
+ expect(fileDisposition(['pass', 'fail', 'skip'], undefined).disposition).toBe('executed-fail');
75
+ expect(fileDisposition(['pass', 'skip'], 'inapplicable').disposition).toBe('executed-pass');
76
+ // real assertions + a gate reason → still an executed pass
77
+ expect(fileDisposition(['pass', 'skip'], 'inapplicable', 4).disposition).toBe('executed-pass');
78
+ // every "pass" was a behaviorGate early-return (zero assertions) → the gate's reason wins
79
+ expect(fileDisposition(['pass', 'pass'], 'inapplicable', 0).disposition).toBe('inapplicable');
80
+ expect(fileDisposition(['pass'], 'skipped', 0).disposition).toBe('skipped');
81
+ // zero assertions with NO gate reason stays a (vacuous) executed-pass — certification rejects it
82
+ expect(fileDisposition(['pass'], undefined, 0).disposition).toBe('executed-pass');
83
+ expect(fileDisposition(['skip', 'skip'], 'inapplicable').disposition).toBe('inapplicable');
84
+ expect(fileDisposition(['skip'], 'skipped').disposition).toBe('skipped');
85
+ const b = fileDisposition(['skip'], undefined);
86
+ expect(b.disposition).toBe('blocked');
87
+ expect(b.detail).toMatch(/unclassified/);
88
+ expect(fileDisposition([], undefined).disposition).toBe('blocked');
89
+ });
90
+
91
+ it('a floor file records under its §A floor id; any other file under openwop.scenario.*', () => {
92
+ const floor = [...floorScenarioFiles()];
93
+ expect(floor.length).toBeGreaterThan(0);
94
+ expect(requirementIdForFile(floor[0]!)).toBe(requirementIdForScenario(floor[0]!));
95
+ expect(requirementIdForFile('some-other.test.ts')).toBe('openwop.scenario.some-other');
96
+ });
97
+ });
98
+
99
+ describe('RFC 0148 §A (S6) — the runner derivation', () => {
100
+ // A profile with a real floor of file requirements + a prefix requirement.
101
+ const profile = Object.entries(PROFILE_FLOOR_SCENARIOS).find(([, f]) => f.required.length > 0 && (f.requiredAnyPrefix?.length ?? 0) > 0)?.[0]
102
+ ?? Object.entries(PROFILE_FLOOR_SCENARIOS).find(([, f]) => f.required.length > 0)![0];
103
+ const floor = PROFILE_FLOOR_SCENARIOS[profile]!;
104
+ const files = floor.required;
105
+ const prefix = floor.requiredAnyPrefix?.[0];
106
+
107
+ function reportAllPassed(extra: Record<string, 'passed' | 'failed' | 'skipped'> = {}) {
108
+ const m = new Map<string, 'passed' | 'failed' | 'skipped'>();
109
+ for (const f of files) m.set(f, 'passed');
110
+ if (prefix) m.set(`${prefix}alpha.test.ts`, 'passed');
111
+ for (const [k, v] of Object.entries(extra)) m.set(k, v);
112
+ return m;
113
+ }
114
+ function ledgerAllPassed(count = 5) {
115
+ const l = files.map((f) => ({ requirementId: requirementIdForScenario(f), disposition: 'executed-pass' as const, assertionCount: count }));
116
+ if (prefix) l.push({ requirementId: `openwop.scenario.${prefix}alpha`, disposition: 'executed-pass' as const, assertionCount: count });
117
+ return l;
118
+ }
119
+
120
+ it('ledger rows win, prefix requirements derive from matching files, and a fully recorded floor certifies', () => {
121
+ const d = deriveRequirementDispositions(reportAllPassed(), ledgerAllPassed(), [profile]);
122
+ expect(d.ledgerPresent).toBe(true);
123
+ expect(d.rejectUnclassified).toBe(false);
124
+ const v = d.verdicts.find((x) => x.profile === profile)!;
125
+ expect(v.certifiable, JSON.stringify(v)).toBe(true);
126
+ expect(v.unclassified).toEqual([]);
127
+ for (const f of files) {
128
+ const row = d.requirements.find((r) => r.requirementId === requirementIdForScenario(f))!;
129
+ expect(row.disposition).toBe('executed-pass');
130
+ expect(row.assertionCount).toBe(5);
131
+ }
132
+ if (prefix) {
133
+ const p = d.requirements.find((r) => r.requirementId === requirementIdForPrefix(prefix))!;
134
+ expect(p.disposition).toBe('executed-pass');
135
+ expect(p.scenarioId).toBe(`${prefix}*`);
136
+ }
137
+ expect(d.totals.executedPass).toBe(d.requirements.length);
138
+ });
139
+
140
+ it('a floor file that vitest passed but that recorded NOTHING is unclassified when a ledger exists — silence is not a witness', () => {
141
+ const l = ledgerAllPassed().filter((e) => e.requirementId !== requirementIdForScenario(files[0]!));
142
+ const d = deriveRequirementDispositions(reportAllPassed(), l, [profile]);
143
+ const v = d.verdicts.find((x) => x.profile === profile)!;
144
+ const row = d.requirements.find((r) => r.requirementId === requirementIdForScenario(files[0]!))!;
145
+ expect(row.disposition).toBe('executed-pass'); // vitest did say passed — the row is honest about the source
146
+ expect(row.detail).toMatch(/report-derived/);
147
+ expect(v.unclassified, 'no ledger entry ⇒ unclassified for a claimed floor').toContain(requirementIdForScenario(files[0]!));
148
+ expect(v.certifiable).toBe(false);
149
+ expect(d.rejectUnclassified).toBe(true);
150
+ });
151
+
152
+ it('a VACUOUS pass (executed-pass with assertionCount 0) on a claimed floor is unclassified and REJECTS certification', () => {
153
+ const l = ledgerAllPassed(5).map((e) => (e.requirementId === requirementIdForScenario(files[0]!) ? { ...e, assertionCount: 0 } : e));
154
+ const d = deriveRequirementDispositions(reportAllPassed(), l, [profile]);
155
+ const v = d.verdicts.find((x) => x.profile === profile)!;
156
+ expect(v.unclassified).toContain(requirementIdForScenario(files[0]!));
157
+ expect(v.certifiable).toBe(false);
158
+ expect(d.rejectUnclassified).toBe(true);
159
+ });
160
+
161
+ it('a skipped file with no ledger entry is blocked (unclassified) — and it never reads as skipped/inapplicable', () => {
162
+ const rep = reportAllPassed({ [files[0]!]: 'skipped' });
163
+ const l = ledgerAllPassed().filter((e) => e.requirementId !== requirementIdForScenario(files[0]!));
164
+ const d = deriveRequirementDispositions(rep, l, [profile]);
165
+ const row = d.requirements.find((r) => r.requirementId === requirementIdForScenario(files[0]!))!;
166
+ expect(row.disposition).toBe('blocked');
167
+ expect(row.detail).toMatch(/unclassified/);
168
+ expect(d.verdicts[0]!.unclassified).toContain(requirementIdForScenario(files[0]!));
169
+ expect(d.rejectUnclassified).toBe(true);
170
+ expect(d.totals.blocked).toBeGreaterThan(0);
171
+ });
172
+
173
+ it('a recorded inapplicable/skipped is certifiable and NOT unclassified; a recorded executed-fail blocks but is classified', () => {
174
+ const l = ledgerAllPassed().map((e) => (e.requirementId === requirementIdForScenario(files[0]!) ? { requirementId: e.requirementId, disposition: 'inapplicable' as const, detail: 'not advertised' } : e));
175
+ const d = deriveRequirementDispositions(reportAllPassed({ [files[0]!]: 'skipped' }), l, [profile]);
176
+ expect(d.verdicts[0]!.unclassified).toEqual([]);
177
+ expect(d.verdicts[0]!.certifiable).toBe(true);
178
+ const l2 = ledgerAllPassed().map((e) => (e.requirementId === requirementIdForScenario(files[0]!) ? { requirementId: e.requirementId, disposition: 'executed-fail' as const, detail: 'assertion failed', assertionCount: 2 } : e));
179
+ const d2 = deriveRequirementDispositions(reportAllPassed({ [files[0]!]: 'failed' }), l2, [profile]);
180
+ expect(d2.verdicts[0]!.unclassified).toEqual([]);
181
+ expect(d2.verdicts[0]!.blocking).toContain(requirementIdForScenario(files[0]!));
182
+ expect(d2.verdicts[0]!.certifiable).toBe(false);
183
+ expect(d2.rejectUnclassified).toBe(false);
184
+ });
185
+
186
+ it('a discoveryOnly profile certifies with an empty floor; an unknown profile does not', () => {
187
+ const d = deriveRequirementDispositions(new Map(), [], ['openwop-discovery-core', 'openwop-nope']);
188
+ expect(d.verdicts.find((v) => v.profile === 'openwop-discovery-core')?.certifiable).toBe(true);
189
+ expect(d.verdicts.find((v) => v.profile === 'openwop-nope')?.certifiable).toBe(false);
190
+ expect(requirementsFor('openwop-discovery-core')).toEqual([]);
191
+ });
192
+
193
+ it('a RUNTIME-DERIVED profile (openwop-node-packs) is HELD only when every floor row is a witnessed pass; otherwise it is not held — not rejected, not blocked', () => {
194
+ const np = 'openwop-node-packs';
195
+ const f = PROFILE_FLOOR_SCENARIOS[np]!;
196
+ expect(f.runtimeDerived, 'profiles.md: node-packs is "derivable from which scenarios pass"').toBe(true);
197
+ const ids = f.required.map((x) => requirementIdForScenario(x));
198
+ const rep = new Map<string, 'passed' | 'failed' | 'skipped'>(f.required.map((x) => [x, 'passed' as const]));
199
+ // (a) held: every row a witnessed pass
200
+ const held = deriveRequirementDispositions(rep, ids.map((id) => ({ requirementId: id, disposition: 'executed-pass' as const, assertionCount: 4 })), [np]);
201
+ const vh = held.verdicts.find((v) => v.profile === np)!;
202
+ expect(vh.runtimeDerived).toBe(true);
203
+ expect(vh.held).toBe(true);
204
+ expect(vh.certifiable).toBe(true);
205
+ // (b) not held: registry absent → the scenario recorded inapplicable-with-reason; publish recorded inapplicable
206
+ const notHeld = deriveRequirementDispositions(rep, ids.map((id) => ({ requirementId: id, disposition: 'inapplicable' as const, detail: 'host ships no pack registry (probe 404)' })), [np]);
207
+ const vn = notHeld.verdicts.find((v) => v.profile === np)!;
208
+ expect(vn.held).toBe(false);
209
+ expect(vn.certifiable).toBe(false);
210
+ expect(vn.unclassified, 'a profile the host does not hold is not a rejected claim').toEqual([]);
211
+ expect([...vn.blocking].sort()).toEqual([...ids].sort());
212
+ expect(notHeld.rejectUnclassified).toBe(false);
213
+ // (c) a vacuous pass on a runtime-derived floor is still not a rejection — the emitter drops the claim
214
+ const vac = deriveRequirementDispositions(rep, ids.map((id) => ({ requirementId: id, disposition: 'executed-pass' as const, assertionCount: 0 })), [np]);
215
+ const vv = vac.verdicts.find((v) => v.profile === np)!;
216
+ expect(vv.held).toBe(false);
217
+ expect(vv.unclassified).toEqual([]);
218
+ expect(vac.rejectUnclassified).toBe(false);
219
+ // (d) and a floor-defined NON-runtime-derived profile keeps the strict rule
220
+ const strictProfile = Object.entries(PROFILE_FLOOR_SCENARIOS).find(([, x]) => x.required.length > 0 && !x.runtimeDerived)![0];
221
+ const sIds = PROFILE_FLOOR_SCENARIOS[strictProfile]!.required.map((x) => requirementIdForScenario(x));
222
+ const sRep = new Map<string, 'passed' | 'failed' | 'skipped'>(PROFILE_FLOOR_SCENARIOS[strictProfile]!.required.map((x) => [x, 'passed' as const]));
223
+ const strict = deriveRequirementDispositions(sRep, sIds.map((id) => ({ requirementId: id, disposition: 'executed-pass' as const, assertionCount: 0 })), [strictProfile]);
224
+ expect(strict.verdicts[0]?.runtimeDerived).toBe(false);
225
+ expect(strict.rejectUnclassified).toBe(true);
226
+ });
227
+
228
+ it('the runner\'s §A resolution of a silent zero-assertion file (blocked + marker) is STILL unclassified for a claimed floor', () => {
229
+ const l = ledgerAllPassed().map((e) => (e.requirementId === requirementIdForScenario(files[0]!) ? { requirementId: e.requirementId, disposition: 'blocked' as const, detail: UNCLASSIFIED_RETURN_DETAIL, assertionCount: 0 } : e));
230
+ const d = deriveRequirementDispositions(reportAllPassed(), l, [profile]);
231
+ const v = d.verdicts.find((x) => x.profile === profile)!;
232
+ expect(v.unclassified).toContain(requirementIdForScenario(files[0]!));
233
+ expect(d.rejectUnclassified).toBe(true);
234
+ // whereas a blocked row WITH a scenario-authored reason is classified (blocking, not unclassified)
235
+ const l2 = ledgerAllPassed().map((e) => (e.requirementId === requirementIdForScenario(files[0]!) ? { requirementId: e.requirementId, disposition: 'blocked' as const, detail: 'seam /v1/host/sample/x not mounted', assertionCount: 0 } : e));
236
+ const d2 = deriveRequirementDispositions(reportAllPassed(), l2, [profile]);
237
+ expect(d2.verdicts.find((x) => x.profile === profile)!.unclassified).toEqual([]);
238
+ expect(d2.rejectUnclassified).toBe(false);
239
+ });
240
+
241
+ it('with no ledger at all, every skipped file is blocked and the runner says so (the pre-S6 honest reading)', () => {
242
+ const d = deriveRequirementDispositions(reportAllPassed({ [files[0]!]: 'skipped' }), [], [profile]);
243
+ expect(d.ledgerPresent).toBe(false);
244
+ const row = d.requirements.find((r) => r.requirementId === requirementIdForScenario(files[0]!))!;
245
+ expect(row.disposition).toBe('blocked');
246
+ expect(row.detail).toMatch(/without a ledger/);
247
+ });
248
+ });
249
+
250
+ describe('RFC 0148 §A — softSkip notes the reason for an early return, per file', () => {
251
+ it('notes are keyed to the current test file, worst-first when mixed, and read back with joined reasons', () => {
252
+ resetSoftSkips();
253
+ expect(softSkip('inapplicable', 'host does not advertise X')).toBeUndefined();
254
+ softSkip('inapplicable', 'host does not advertise X'); // de-duplicated
255
+ let d = softSkipDisposition('runner-ledger.test.ts');
256
+ expect(d).toEqual({ kind: 'inapplicable', reason: 'host does not advertise X' });
257
+ softSkip('blocked', 'seam not mounted');
258
+ d = softSkipDisposition('runner-ledger.test.ts');
259
+ expect(d?.kind).toBe('blocked');
260
+ expect(d?.reason).toContain('[blocked] seam not mounted');
261
+ expect(d?.reason).toContain('[inapplicable] host does not advertise X');
262
+ expect(softSkipDisposition('some-other-file.test.ts')).toBeNull();
263
+ resetSoftSkips();
264
+ expect(softSkipDisposition('runner-ledger.test.ts')).toBeNull();
265
+ });
266
+ });
267
+
268
+ describe('RFC 0148 §A (S6) — the live sink, when this run was given one', () => {
269
+ it('files that finished before this one appear in the ledger file with an assertion count', () => {
270
+ const path = process.env['OPENWOP_LEDGER_PATH'];
271
+ if (!path || !existsSync(path)) return; // not a --certify run; nothing to inspect
272
+ const entries = readLedgerFile(path);
273
+ // At least the in-memory ledger of THIS worker has file-level entries for
274
+ // earlier files (setup.ts afterAll), and each carries assertionCount.
275
+ const fileEntries = snapshot().filter((e) => e.requirementId.startsWith('openwop.scenario.') || e.requirementId.startsWith('openwop.floor.'));
276
+ for (const e of fileEntries) expect(typeof e.assertionCount).toBe('number');
277
+ expect(entries.length).toBeGreaterThanOrEqual(0);
278
+ });
279
+ });
@@ -28,6 +28,7 @@
28
28
  */
29
29
 
30
30
  import { describe, it, expect } from 'vitest';
31
+ import { softSkip } from '../lib/soft-skip.js';
31
32
  import { driver } from '../lib/driver.js';
32
33
  import { installGate } from '../lib/runtimeRequires.js';
33
34
 
@@ -44,7 +45,7 @@ function manifest(requires: string[]) {
44
45
  describe('runtime-requires install gate (RFC 0076 §A)', () => {
45
46
  it('install-grant: requires ⊆ grant-set ⇒ install succeeds', async () => {
46
47
  const res = await installGate({ manifest: manifest(['net.dns']), grantSet: ['net.dns', 'net.outbound'] });
47
- if (res === null) return; // seam absent — soft-skip
48
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
48
49
  expect(
49
50
  res.status,
50
51
  driver.describe('registry-operations.md §"Runtime-requirement install gate"', 'a pack whose runtime.requires are all grantable MUST install (no refusal)'),
@@ -57,7 +58,7 @@ describe('runtime-requires install gate (RFC 0076 §A)', () => {
57
58
 
58
59
  it('install-refuse: an ungrantable primitive ⇒ pack_runtime_requirement_unmet', async () => {
59
60
  const res = await installGate({ manifest: manifest(['net.dns']), grantSet: [] });
60
- if (res === null) return; // seam absent — soft-skip
61
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
61
62
  expect(
62
63
  res.status,
63
64
  driver.describe('registry-operations.md §"Runtime-requirement install gate"', 'a pack requiring an ungranted primitive MUST be refused at install (not at first invocation)'),
@@ -78,11 +79,11 @@ describe('runtime-requires install gate (RFC 0076 §A)', () => {
78
79
 
79
80
  it('non-sandbox projection: a non-gating host installs and projects requires[] (§A SHOULD)', async () => {
80
81
  const res = await installGate({ manifest: manifest(['net.dns', 'net.outbound']), gating: false });
81
- if (res === null) return; // seam absent — soft-skip
82
+ if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
82
83
  // A non-gating host installs unconditionally; the SHOULD is the projection.
83
84
  // If the host gates anyway (returns 400) the projection SHOULD does not apply — tolerate either install shape.
84
- if (res.status !== 200) return;
85
- if (res.body.requiresProjected === undefined) return; // SHOULD, not MUST — a non-projecting host is conformant
85
+ if (res.status !== 200) return softSkip('blocked', 'precondition not met — `res.status !== 200` returned early (seam, prior step, or fixture unavailable)');
86
+ if (res.body.requiresProjected === undefined) return softSkip('inapplicable', 'SHOULD, not MUST — a non-projecting host is conformant');
86
87
  const projected = res.body.requiresProjected as unknown;
87
88
  expect(
88
89
  Array.isArray(projected) && ['net.dns', 'net.outbound'].every((t) => (projected as unknown[]).includes(t)),
@@ -14,6 +14,7 @@
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 { capabilityFamily } from '../lib/discovery-capabilities.js';
19
20
 
@@ -39,8 +40,8 @@ async function readSandbox(): Promise<{ supported: boolean; memoryLimitBytes?: n
39
40
  describe.skipIf(HTTP_SKIP)('sandbox-memory-cap: capability shape + behavioral (RFC 0035 §B)', () => {
40
41
  it('memoryLimitBytes MUST be integer ≥ 1 MiB when present (per schema)', async () => {
41
42
  const sb = await readSandbox();
42
- if (!sb) return; // soft-skip
43
- if (sb.memoryLimitBytes === undefined) return; // optional field
43
+ if (!sb) return softSkip('blocked', 'soft-skip (!sb)');
44
+ if (sb.memoryLimitBytes === undefined) return softSkip('inapplicable', 'optional field (sb.memoryLimitBytes === undefined)');
44
45
 
45
46
  expect(
46
47
  Number.isInteger(sb.memoryLimitBytes) && sb.memoryLimitBytes >= 1048576,
@@ -36,6 +36,7 @@
36
36
  */
37
37
 
38
38
  import { describe, it, expect } from 'vitest';
39
+ import { softSkip } from '../lib/soft-skip.js';
39
40
  import { driver } from '../lib/driver.js';
40
41
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
41
42
 
@@ -82,7 +83,7 @@ describe.skipIf(HTTP_SKIP)('sandbox-mvp-behavior: RFC 0035 §B failure-mode inva
82
83
  it('host-fs-escape — fs access from sandboxed code fails closed', async (ctx) => {
83
84
  if (!(await isSandboxAdvertised())) {
84
85
  ctx.skip();
85
- return;
86
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isSandboxAdvertised())` returned early');
86
87
  }
87
88
  const probe = await invoke('misbehave.fs-escape-read');
88
89
  expect(probe.status).toBe(200);
@@ -105,7 +106,7 @@ describe.skipIf(HTTP_SKIP)('sandbox-mvp-behavior: RFC 0035 §B failure-mode inva
105
106
  it('host-env-leak — process.env access from sandboxed code fails closed', async (ctx) => {
106
107
  if (!(await isSandboxAdvertised())) {
107
108
  ctx.skip();
108
- return;
109
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isSandboxAdvertised())` returned early');
109
110
  }
110
111
  const probe = await invoke('misbehave.env-leak');
111
112
  expect(probe.status).toBe(200);
@@ -122,7 +123,7 @@ describe.skipIf(HTTP_SKIP)('sandbox-mvp-behavior: RFC 0035 §B failure-mode inva
122
123
  it('network-escape — http/net access from sandboxed code fails closed', async (ctx) => {
123
124
  if (!(await isSandboxAdvertised())) {
124
125
  ctx.skip();
125
- return;
126
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isSandboxAdvertised())` returned early');
126
127
  }
127
128
  const probe = await invoke('misbehave.network-escape');
128
129
  expect(probe.status).toBe(200);
@@ -139,7 +140,7 @@ describe.skipIf(HTTP_SKIP)('sandbox-mvp-behavior: RFC 0035 §B failure-mode inva
139
140
  it('host-process-escape — child_process access from sandboxed code fails closed', async (ctx) => {
140
141
  if (!(await isSandboxAdvertised())) {
141
142
  ctx.skip();
142
- return;
143
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isSandboxAdvertised())` returned early');
143
144
  }
144
145
  const probe = await invoke('misbehave.process-escape');
145
146
  expect(probe.status).toBe(200);
@@ -153,7 +154,7 @@ describe.skipIf(HTTP_SKIP)('sandbox-mvp-behavior: RFC 0035 §B failure-mode inva
153
154
  it('sandbox-timeout — runaway loop terminated by wallClockLimitMs', async (ctx) => {
154
155
  if (!(await isSandboxAdvertised())) {
155
156
  ctx.skip();
156
- return;
157
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isSandboxAdvertised())` returned early');
157
158
  }
158
159
  const start = Date.now();
159
160
  const probe = await invoke('misbehave.timeout');
@@ -179,7 +180,7 @@ describe.skipIf(HTTP_SKIP)('sandbox-mvp-behavior: RFC 0035 §B failure-mode inva
179
180
  it('cross-pack-mutation — fresh vm context per invocation, no state leaks', async (ctx) => {
180
181
  if (!(await isSandboxAdvertised())) {
181
182
  ctx.skip();
182
- return;
183
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isSandboxAdvertised())` returned early');
183
184
  }
184
185
  const r1 = await invoke('misbehave.cross-pack-mutate');
185
186
  const r2 = await invoke('misbehave.cross-pack-mutate');
@@ -205,7 +206,7 @@ describe.skipIf(HTTP_SKIP)('sandbox-mvp-behavior: RFC 0035 §B failure-mode inva
205
206
  it('capability-gate-respected — host call NOT in allowedHostCalls fails with sandbox_capability_denied', async (ctx) => {
206
207
  if (!(await isSandboxAdvertised())) {
207
208
  ctx.skip();
208
- return;
209
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isSandboxAdvertised())` returned early');
209
210
  }
210
211
  const probe = await invoke('misbehave.capability-gate-violation', {}, []);
211
212
  expect(probe.status).toBe(200);
@@ -228,7 +229,7 @@ describe.skipIf(HTTP_SKIP)('sandbox-mvp-behavior: RFC 0035 §B failure-mode inva
228
229
  it('memory-exceeded — runaway allocation fails with sandbox_memory_exceeded', async (ctx) => {
229
230
  if (!(await isSandboxAdvertised())) {
230
231
  ctx.skip();
231
- return;
232
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isSandboxAdvertised())` returned early');
232
233
  }
233
234
  const probe = await invoke('misbehave.memory-bomb');
234
235
  expect(probe.status).toBe(200);
@@ -255,7 +256,7 @@ describe.skipIf(HTTP_SKIP)('sandbox-mvp-behavior: RFC 0035 §B failure-mode inva
255
256
  it('well-behaved.host-fetch — allowedHostCalls=[fetch] permits the host call', async (ctx) => {
256
257
  if (!(await isSandboxAdvertised())) {
257
258
  ctx.skip();
258
- return;
259
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isSandboxAdvertised())` returned early');
259
260
  }
260
261
  const probe = await invoke('well-behaved.host-fetch', {}, ['fetch']);
261
262
  expect(probe.status).toBe(200);
@@ -271,7 +272,7 @@ describe.skipIf(HTTP_SKIP)('sandbox-mvp-behavior: RFC 0035 §B failure-mode inva
271
272
  it('well-behaved.echo — sandboxed code returns args round-trip when no escape attempt', async (ctx) => {
272
273
  if (!(await isSandboxAdvertised())) {
273
274
  ctx.skip();
274
- return;
275
+ return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isSandboxAdvertised())` returned early');
275
276
  }
276
277
  const probe = await invoke('well-behaved.echo', { input: 'hello-sandbox' });
277
278
  expect(probe.status).toBe(200);
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  import { describe, it, expect } from 'vitest';
28
+ import { softSkip } from '../lib/soft-skip.js';
28
29
  import { driver } from '../lib/driver.js';
29
30
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
30
31
 
@@ -55,7 +56,7 @@ async function readSandboxCaps(): Promise<SandboxCaps | null> {
55
56
  describe.skipIf(HTTP_SKIP)('sandbox-no-host-fs-escape: capability shape (RFC 0035 §A)', () => {
56
57
  it('capabilities.sandbox (when present) conforms to RFC 0035 §A', async () => {
57
58
  const sb = await readSandboxCaps();
58
- if (sb === null) return; // host omits the block — soft-skip cleanly
59
+ if (sb === null) return softSkip('inapplicable', 'host omits the block — soft-skip cleanly (sb === null)');
59
60
 
60
61
  expect(typeof sb.supported, 'capabilities.sandbox.supported MUST be boolean when present').toBe('boolean');
61
62
 
@@ -14,6 +14,7 @@
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 { capabilityFamily } from '../lib/discovery-capabilities.js';
19
20
 
@@ -39,8 +40,8 @@ async function readSandbox(): Promise<{ supported: boolean; wallClockLimitMs?: n
39
40
  describe.skipIf(HTTP_SKIP)('sandbox-timeout-cap: capability shape + behavioral (RFC 0035 §B)', () => {
40
41
  it('wallClockLimitMs MUST be integer ≥ 100 ms when present (per schema)', async () => {
41
42
  const sb = await readSandbox();
42
- if (!sb) return;
43
- if (sb.wallClockLimitMs === undefined) return; // optional
43
+ if (!sb) return softSkip('blocked', 'precondition not met — `!sb` returned early (seam, prior step, or fixture unavailable)');
44
+ if (sb.wallClockLimitMs === undefined) return softSkip('inapplicable', 'optional (sb.wallClockLimitMs === undefined)');
44
45
 
45
46
  expect(
46
47
  Number.isInteger(sb.wallClockLimitMs) && sb.wallClockLimitMs >= 100,