@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.
- package/README.md +3 -3
- package/coverage.md +6 -0
- package/dist/cli.js +89 -45
- package/dist/lib/certification-bundle-verify.js +274 -0
- package/dist/lib/profiles.js +102 -19
- package/dist/lib/requirement-ledger.js +182 -0
- package/dist/lib/requirement-registry.js +110 -0
- package/dist/lib/scenario-disposition.js +249 -0
- package/dist/lib/soft-skip.js +82 -0
- package/fixtures/conformance-a2a-task-roundtrip.json +1 -1
- package/fixtures.md +1 -1
- package/package.json +1 -1
- package/schemas/CORPUS-STAMP.json +2 -2
- package/schemas/README.md +1 -0
- package/schemas/capabilities.schema.json +2 -2
- package/schemas/compensation-policy.schema.json +64 -0
- package/schemas/run-snapshot.schema.json +5 -0
- package/schemas/workflow-chain-pack-manifest.schema.json +125 -0
- package/schemas/workflow-definition.schema.json +4 -0
- package/src/cli.ts +99 -45
- package/src/lib/a2a-fake-peer.ts +290 -110
- package/src/lib/certification-bundle-verify.ts +356 -0
- package/src/lib/error-envelope.ts +77 -0
- package/src/lib/llm-cache-key-recipe.ts +6 -5
- package/src/lib/mcp-fake-server.ts +235 -72
- package/src/lib/profiles.ts +132 -19
- package/src/lib/requirement-ledger.ts +98 -1
- package/src/lib/requirement-registry.ts +47 -4
- package/src/lib/scenario-disposition.ts +291 -0
- package/src/lib/soft-skip.ts +89 -0
- package/src/lib/workflow-chain-expansion.ts +189 -0
- package/src/scenarios/a2a-1-0-agent-card.test.ts +179 -0
- package/src/scenarios/a2a-1-0-task-roundtrip.test.ts +109 -0
- package/src/scenarios/a2a-card-runtime-consistency.test.ts +167 -0
- package/src/scenarios/a2a-peer-authority.test.ts +99 -0
- package/src/scenarios/a2a-task-roundtrip.test.ts +18 -6
- package/src/scenarios/agent-loop-iteration-monotonic.test.ts +3 -2
- package/src/scenarios/agent-loop-stateful-resume.test.ts +3 -2
- package/src/scenarios/agent-loop-version5-shape.test.ts +2 -1
- package/src/scenarios/agent-loop-workspace-snapshot.test.ts +4 -3
- package/src/scenarios/approval-gate-flow.test.ts +5 -4
- package/src/scenarios/artifact-type-store-without-render.test.ts +2 -1
- package/src/scenarios/auth-scim-profile.test.ts +5 -4
- package/src/scenarios/authorization-fail-closed.test.ts +4 -3
- package/src/scenarios/certification-bundle-non-vacuous.test.ts +249 -0
- package/src/scenarios/certification-bundle-redaction.test.ts +182 -0
- package/src/scenarios/certification-floor-enforcement.test.ts +30 -6
- package/src/scenarios/chain-compensation-expansion.test.ts +243 -0
- package/src/scenarios/commitment-fired.test.ts +3 -2
- package/src/scenarios/compensation-behavior.test.ts +63 -0
- package/src/scenarios/compensation-profile.test.ts +144 -1
- package/src/scenarios/conformance-advertised-seam-required.test.ts +118 -0
- package/src/scenarios/conformance-execution-witness.test.ts +104 -0
- package/src/scenarios/conversationCapabilityNegotiation.test.ts +3 -2
- package/src/scenarios/core-manifest-and-extension-registry.test.ts +26 -0
- package/src/scenarios/credential-payload-redaction.test.ts +4 -3
- package/src/scenarios/credentials-capability-shape.test.ts +4 -3
- package/src/scenarios/cross-engine-append-behavior.test.ts +5 -4
- package/src/scenarios/cross-engine-append-ordering.test.ts +3 -2
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +5 -4
- package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
- package/src/scenarios/cross-workspace-isolation.test.ts +4 -3
- package/src/scenarios/data-residency-admission.test.ts +6 -6
- package/src/scenarios/deadletter-retry-exhaustion.test.ts +5 -4
- package/src/scenarios/distillation-index-roundtrip.test.ts +3 -2
- package/src/scenarios/distillation-secret-carryforward.test.ts +3 -2
- package/src/scenarios/distillation-shape.test.ts +2 -1
- package/src/scenarios/distillation-stable-archive.test.ts +4 -3
- package/src/scenarios/distillation-token-budget.test.ts +4 -3
- package/src/scenarios/envelope-recovery-applied.test.ts +3 -2
- package/src/scenarios/envelope-refusal-shape.test.ts +4 -3
- package/src/scenarios/error-envelope-canonical-shape.test.ts +64 -0
- package/src/scenarios/feedback-correction-redaction.test.ts +4 -3
- package/src/scenarios/feedback-cross-tenant-isolation.test.ts +4 -3
- package/src/scenarios/feedback-fork-not-copied.test.ts +6 -5
- package/src/scenarios/feedback-on-terminal-run.test.ts +4 -3
- package/src/scenarios/feedback-record-and-list.test.ts +4 -3
- package/src/scenarios/feedback-unsupported-501.test.ts +3 -2
- package/src/scenarios/fixtures-valid.test.ts +16 -18
- package/src/scenarios/fs-path-traversal.test.ts +3 -2
- package/src/scenarios/heartbeat-capability-shape.test.ts +2 -1
- package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +4 -3
- package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +6 -5
- package/src/scenarios/heartbeat-runtime-bound.test.ts +4 -3
- package/src/scenarios/mcp-2026-07-28-discover.test.ts +207 -0
- package/src/scenarios/mcp-cache-tenant-scope.test.ts +66 -0
- package/src/scenarios/mcp-current-auth-boundary.test.ts +55 -0
- package/src/scenarios/mcp-extension-opacity.test.ts +79 -0
- package/src/scenarios/mcp-mrtr-roundtrip.test.ts +140 -0
- package/src/scenarios/mcp-stateless-request.test.ts +77 -0
- package/src/scenarios/mcp-tool-roundtrip.test.ts +4 -3
- package/src/scenarios/mcp-toolcall-redaction.test.ts +2 -1
- package/src/scenarios/memory-attribution-emits-on-write.test.ts +6 -5
- package/src/scenarios/memory-attribution-no-content.test.ts +4 -3
- package/src/scenarios/memory-attribution-replay-stable.test.ts +6 -5
- package/src/scenarios/memory-attribution-tenant-scoped.test.ts +4 -3
- package/src/scenarios/memory-compaction-event-emitted.test.ts +3 -2
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +6 -5
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +4 -3
- package/src/scenarios/memory-consolidation-idempotent.test.ts +4 -3
- package/src/scenarios/memory-injection-budget.test.ts +4 -3
- package/src/scenarios/metric-emission.test.ts +4 -3
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +4 -3
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +5 -4
- package/src/scenarios/multi-party-conversation-behavioral.test.ts +3 -2
- package/src/scenarios/multi-region-idempotency-behavior.test.ts +7 -6
- package/src/scenarios/multi-region-idempotency.test.ts +4 -3
- package/src/scenarios/normative-example-extraction.test.ts +242 -0
- package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +4 -3
- package/src/scenarios/oauth-capability-shape.test.ts +4 -3
- package/src/scenarios/oauth-connector-redaction.test.ts +4 -3
- package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +309 -0
- package/src/scenarios/org-position-no-authority-escalation.test.ts +2 -1
- package/src/scenarios/otel-emission-grpc.test.ts +4 -3
- package/src/scenarios/otel-emission.test.ts +4 -3
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +5 -4
- package/src/scenarios/otel-trace-propagation.test.ts +4 -3
- package/src/scenarios/pack-registry-isolation.test.ts +4 -3
- package/src/scenarios/pack-registry-publish.test.ts +65 -3
- package/src/scenarios/pack-registry.test.ts +22 -0
- package/src/scenarios/profile-discovery-core-alias.test.ts +137 -0
- package/src/scenarios/profileDerivation.test.ts +2 -1
- package/src/scenarios/provider-usage.test.ts +2 -2
- package/src/scenarios/rate-limit-envelope.test.ts +2 -1
- package/src/scenarios/registry-public.test.ts +6 -5
- package/src/scenarios/replay-divergence-at-refusal.test.ts +9 -8
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +19 -13
- package/src/scenarios/replay-llm-cache-key.test.ts +120 -47
- package/src/scenarios/replay-observable-sequence-determinism.test.ts +5 -4
- package/src/scenarios/replay-side-effect-suppression.test.ts +20 -1
- package/src/scenarios/requirement-ledger.test.ts +29 -6
- package/src/scenarios/runner-ledger.test.ts +279 -0
- package/src/scenarios/runtime-requires-install-gate.test.ts +6 -5
- package/src/scenarios/sandbox-memory-cap.test.ts +3 -2
- package/src/scenarios/sandbox-mvp-behavior.test.ts +11 -10
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
- package/src/scenarios/sandbox-timeout-cap.test.ts +3 -2
- package/src/scenarios/stream-text-fixture.test.ts +5 -4
- package/src/scenarios/subrun-approval-fail-closed.test.ts +3 -2
- package/src/scenarios/subrun-approval-gate.test.ts +4 -3
- package/src/scenarios/subrun-attestation-shape.test.ts +2 -1
- package/src/scenarios/subrun-checksum-stable.test.ts +4 -3
- package/src/scenarios/table-schema-enforcement.test.ts +2 -2
- package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +3 -2
- package/src/scenarios/tool-hooks-content-free.test.ts +3 -2
- package/src/scenarios/tool-hooks-rate-limit.test.ts +3 -2
- package/src/scenarios/tool-hooks-secret-redaction.test.ts +3 -2
- package/src/scenarios/tool-hooks-shape.test.ts +2 -1
- package/src/scenarios/trigger-bridge-delivery.test.ts +3 -2
- package/src/scenarios/version-fold.test.ts +5 -4
- package/src/scenarios/voice-streamref-tenant-bound.test.ts +2 -1
- package/src/scenarios/voice-transcription-streaming.test.ts +2 -1
- package/src/scenarios/voice-transcription-unadvertised.test.ts +2 -1
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +5 -4
- package/src/scenarios/wasm-pack-invoke-completed.test.ts +3 -2
- package/src/scenarios/wasm-pack-invoke-suspended.test.ts +3 -2
- package/src/scenarios/wasm-pack-load.test.ts +4 -3
- package/src/scenarios/wasm-pack-memory-cap.test.ts +4 -3
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +3 -2
- package/src/scenarios/webhook-signed-delivery.test.ts +4 -3
- package/src/scenarios/workflow-primary-output-annotation.test.ts +13 -23
- package/src/scenarios/workload-identity-behavior.test.ts +7 -4
- package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +3 -2
- package/src/scenarios/workspace-cross-tenant-isolation.test.ts +3 -2
- 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
|
-
|
|
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`
|
|
126
|
-
//
|
|
127
|
-
//
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
43
|
-
if (sb.memoryLimitBytes === undefined) return
|
|
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
|
|
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
|
|
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,
|