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