@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
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import { describe, it, expect } from 'vitest';
|
|
34
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
34
35
|
import { driver } from '../lib/driver.js';
|
|
35
36
|
|
|
36
37
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
@@ -63,7 +64,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
|
|
|
63
64
|
]);
|
|
64
65
|
if (probe.status === 404) {
|
|
65
66
|
ctx.skip(); // host doesn't expose the simulator seam
|
|
66
|
-
return;
|
|
67
|
+
return softSkip('blocked', 'precondition not met — `probe.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
67
68
|
}
|
|
68
69
|
expect(
|
|
69
70
|
probe.status,
|
|
@@ -89,7 +90,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
|
|
|
89
90
|
]);
|
|
90
91
|
if (probe.status === 404) {
|
|
91
92
|
ctx.skip();
|
|
92
|
-
return;
|
|
93
|
+
return softSkip('blocked', 'precondition not met — `probe.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
93
94
|
}
|
|
94
95
|
expect(probe.status).toBe(200);
|
|
95
96
|
expect(
|
|
@@ -115,7 +116,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
|
|
|
115
116
|
]);
|
|
116
117
|
if (probe.status === 404) {
|
|
117
118
|
ctx.skip();
|
|
118
|
-
return;
|
|
119
|
+
return softSkip('blocked', 'precondition not met — `probe.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
119
120
|
}
|
|
120
121
|
expect(probe.status).toBe(200);
|
|
121
122
|
const redirects = probe.body.cacheRedirects ?? [];
|
|
@@ -144,7 +145,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
|
|
|
144
145
|
]);
|
|
145
146
|
if (probe.status === 404) {
|
|
146
147
|
ctx.skip();
|
|
147
|
-
return;
|
|
148
|
+
return softSkip('blocked', 'precondition not met — `probe.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
148
149
|
}
|
|
149
150
|
expect(probe.status).toBe(200);
|
|
150
151
|
expect(
|
|
@@ -165,7 +166,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
|
|
|
165
166
|
const p1 = await simulatePartition(claims);
|
|
166
167
|
if (p1.status === 404) {
|
|
167
168
|
ctx.skip();
|
|
168
|
-
return;
|
|
169
|
+
return softSkip('blocked', 'precondition not met — `p1.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
169
170
|
}
|
|
170
171
|
expect(p1.status).toBe(200);
|
|
171
172
|
const p2 = await simulatePartition([claims[2]!, claims[0]!, claims[1]!]);
|
|
@@ -190,7 +191,7 @@ describe.skipIf(HTTP_SKIP)('multi-region-idempotency-behavior: convergence rule
|
|
|
190
191
|
]);
|
|
191
192
|
if (probe.status === 404) {
|
|
192
193
|
ctx.skip();
|
|
193
|
-
return;
|
|
194
|
+
return softSkip('blocked', 'precondition not met — `probe.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
194
195
|
}
|
|
195
196
|
expect(
|
|
196
197
|
probe.status,
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { describe, it, expect } from 'vitest';
|
|
32
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
32
33
|
import { driver } from '../lib/driver.js';
|
|
33
34
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
34
35
|
|
|
@@ -58,7 +59,7 @@ describe('multi-region-idempotency: capability shape', () => {
|
|
|
58
59
|
console.warn(
|
|
59
60
|
'[multi-region-idempotency] capabilities.idempotency.crossRegion not advertised; skipping',
|
|
60
61
|
);
|
|
61
|
-
return;
|
|
62
|
+
return softSkip('blocked', 'precondition not met — `!idem || idem.crossRegion === undefined` returned early (seam, prior step, or fixture unavailable)');
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
expect(ALLOWED.has(idem.crossRegion), driver.describe(
|
|
@@ -82,7 +83,7 @@ describe('multi-region-idempotency: capability shape', () => {
|
|
|
82
83
|
|
|
83
84
|
if (crossRegion !== 'reconciled-records' && crossRegion !== 'fenced-effects') {
|
|
84
85
|
// Single-region hosts have no conflicts to count — skip.
|
|
85
|
-
return;
|
|
86
|
+
return softSkip('blocked', 'precondition not met — `crossRegion !== \'reconciled-records\' && crossRegion !== \'fenced-effects\'` returned early (seam, prior step, or fixture unavailable)');
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
const advertised = new Set(observability?.metrics?.names ?? []);
|
|
@@ -117,7 +118,7 @@ describe('multi-region-idempotency: granular multiRegion advertisement shape (RF
|
|
|
117
118
|
'idempotency',
|
|
118
119
|
);
|
|
119
120
|
const mr = idem?.multiRegion;
|
|
120
|
-
if (mr === undefined) return
|
|
121
|
+
if (mr === undefined) return softSkip('inapplicable', 'host doesn\'t advertise the granular block — soft-skip');
|
|
121
122
|
|
|
122
123
|
expect(
|
|
123
124
|
typeof mr.supported,
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0149 §D — normative examples are extracted and validated against the
|
|
3
|
+
* same schemas conformance uses.
|
|
4
|
+
*
|
|
5
|
+
* "Normative examples MUST be extracted into fixtures and validated against
|
|
6
|
+
* the same schemas used by conformance" (§D). Until now a fenced example was
|
|
7
|
+
* prose to every validator in the corpus: `capability-example-root-layout`
|
|
8
|
+
* catches one wrong SHAPE (the `capabilities` wrapper), but an example could
|
|
9
|
+
* carry a misspelled field, a wrong enum value, or a stale required property
|
|
10
|
+
* and every gate stayed green — a green corpus teaching a non-conformant
|
|
11
|
+
* document.
|
|
12
|
+
*
|
|
13
|
+
* Mechanism — a declaration, not a heuristic:
|
|
14
|
+
*
|
|
15
|
+
* <!-- normative-example: <name>.schema.json -->
|
|
16
|
+
* ```json
|
|
17
|
+
* { ... }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The HTML comment on the line immediately before a ```json / ```jsonc fence
|
|
21
|
+
* declares that the fenced body is a WHOLE instance of the named schema. The
|
|
22
|
+
* scenario extracts each declared example at test time and validates it with
|
|
23
|
+
* Ajv against `schemas/<name>` — the identical schema files and registration
|
|
24
|
+
* (`fixtures-valid.test.ts` enumerates the directory the same way) that every
|
|
25
|
+
* other conformance leg uses. Extraction is in-process rather than committed
|
|
26
|
+
* under `fixtures/`: a committed copy of a prose example is a second source
|
|
27
|
+
* of truth that drifts, which is the defect §D exists to remove.
|
|
28
|
+
*
|
|
29
|
+
* Non-vacuity guards (RFC 0148 §A — an extractor that matches nothing must
|
|
30
|
+
* not pass):
|
|
31
|
+
* - the corpus MUST contain declared examples (floor asserted);
|
|
32
|
+
* - every fenced JSON block in spec/v1 that parses and validates as a whole
|
|
33
|
+
* instance of some non-permissive schema MUST be declared — an example
|
|
34
|
+
* that IS a schema instance is a normative example, and the marker is how
|
|
35
|
+
* it says so; a new whole-document example added without the marker fails
|
|
36
|
+
* here rather than silently escaping validation;
|
|
37
|
+
* - every declared example MUST parse as strict JSON (jsonc comments and
|
|
38
|
+
* ellipses are prose, not instances) and MUST validate;
|
|
39
|
+
* - every discovery-shaped example (root `protocolVersion` +
|
|
40
|
+
* `supportedEnvelopes`) MUST be declared against `capabilities.schema.json`
|
|
41
|
+
* ("all normative discovery examples ... validate" — RFC 0149 acceptance);
|
|
42
|
+
* - §E: no declared discovery example carries credential- or tenant-shaped
|
|
43
|
+
* keys.
|
|
44
|
+
*
|
|
45
|
+
* Server-free; always-on in the corpus layout (spec prose is not shipped in the
|
|
46
|
+
* published package, so `V1_DIR === null` skips the describe there).
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import { describe, it, expect } from 'vitest';
|
|
50
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
51
|
+
import { join } from 'node:path';
|
|
52
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
53
|
+
import addFormats from 'ajv-formats';
|
|
54
|
+
import type { ValidateFunction } from 'ajv';
|
|
55
|
+
import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
|
|
56
|
+
|
|
57
|
+
export const HOST_CALLBACK_NOT_REQUIRED = 'server-free: extracts fenced prose examples and validates them against the schema files on disk; no host is contacted';
|
|
58
|
+
|
|
59
|
+
const MARKER = /^<!--\s*normative-example:\s*([a-z0-9-]+\.schema\.json)\s*-->\s*$/;
|
|
60
|
+
|
|
61
|
+
interface Fenced {
|
|
62
|
+
readonly file: string;
|
|
63
|
+
/** 1-based line of the ``` opener. */
|
|
64
|
+
readonly line: number;
|
|
65
|
+
readonly info: string;
|
|
66
|
+
readonly body: string;
|
|
67
|
+
/** Schema declared by a marker on the preceding line, if any. */
|
|
68
|
+
readonly declared: string | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fencedBlocks(dir: string): Fenced[] {
|
|
72
|
+
const out: Fenced[] = [];
|
|
73
|
+
for (const name of readdirSync(dir).filter((f) => f.endsWith('.md')).sort()) {
|
|
74
|
+
const lines = readFileSync(join(dir, name), 'utf8').split('\n');
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const open = /^```(json|jsonc)(\s.*)?$/.exec(lines[i]!.trim());
|
|
77
|
+
if (open === null) continue;
|
|
78
|
+
const body: string[] = [];
|
|
79
|
+
let j = i + 1;
|
|
80
|
+
while (j < lines.length && lines[j]!.trim() !== '```') {
|
|
81
|
+
body.push(lines[j]!);
|
|
82
|
+
j++;
|
|
83
|
+
}
|
|
84
|
+
const prev = i > 0 ? (lines[i - 1] ?? '') : '';
|
|
85
|
+
const m = MARKER.exec(prev.trim());
|
|
86
|
+
out.push({ file: name, line: i + 1, info: open[1] ?? 'json', body: body.join('\n'), declared: m?.[1] ?? null });
|
|
87
|
+
i = j;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Stray markers: a marker line NOT immediately followed by a fence opener. */
|
|
94
|
+
function strayMarkers(dir: string): string[] {
|
|
95
|
+
const out: string[] = [];
|
|
96
|
+
for (const name of readdirSync(dir).filter((f) => f.endsWith('.md')).sort()) {
|
|
97
|
+
const lines = readFileSync(join(dir, name), 'utf8').split('\n');
|
|
98
|
+
for (let i = 0; i < lines.length; i++) {
|
|
99
|
+
if (!MARKER.test(lines[i]!.trim())) continue;
|
|
100
|
+
const next = (lines[i + 1] ?? '').trim();
|
|
101
|
+
if (!/^```(json|jsonc)(\s|$)/.test(next)) out.push(`spec/v1/${name}:${i + 1}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseStrict(body: string): unknown | undefined {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(body) as unknown;
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isDiscoveryShaped(v: unknown): boolean {
|
|
116
|
+
return (
|
|
117
|
+
v !== null &&
|
|
118
|
+
typeof v === 'object' &&
|
|
119
|
+
!Array.isArray(v) &&
|
|
120
|
+
typeof (v as Record<string, unknown>)['protocolVersion'] === 'string' &&
|
|
121
|
+
Array.isArray((v as Record<string, unknown>)['supportedEnvelopes'])
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Register every schema in the directory (S14 pattern) and hand back validators by file name. */
|
|
126
|
+
function validators(): { byFile: Map<string, ValidateFunction>; permissive: Set<string> } {
|
|
127
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
128
|
+
addFormats(ajv);
|
|
129
|
+
const files = readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json')).sort();
|
|
130
|
+
const docs = new Map<string, { $id?: string }>();
|
|
131
|
+
for (const f of files) {
|
|
132
|
+
const doc = JSON.parse(readFileSync(join(SCHEMAS_DIR, f), 'utf8')) as { $id?: string };
|
|
133
|
+
docs.set(f, doc);
|
|
134
|
+
ajv.addSchema(doc);
|
|
135
|
+
}
|
|
136
|
+
const byFile = new Map<string, ValidateFunction>();
|
|
137
|
+
for (const [f, doc] of docs) {
|
|
138
|
+
const v = doc.$id === undefined ? undefined : ajv.getSchema(doc.$id);
|
|
139
|
+
if (v !== undefined) byFile.set(f, v);
|
|
140
|
+
}
|
|
141
|
+
// A schema that accepts `{}` AND `{x:1}` constrains nothing at the root; a
|
|
142
|
+
// whole-document match against it says nothing about the example, so it is
|
|
143
|
+
// excluded from the "must be declared" auto-detection (still usable as an
|
|
144
|
+
// explicit declaration).
|
|
145
|
+
const permissive = new Set([...byFile.entries()].filter(([, v]) => v({}) === true && v({ x: 1 }) === true).map(([f]) => f));
|
|
146
|
+
return { byFile, permissive };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const CREDENTIAL_KEY = /^(apiKey|api_key|token|accessToken|refreshToken|secret|clientSecret|password|authorization|bearer|tenantId|tenant_id|organizationId)$/i;
|
|
150
|
+
function credentialShapedKeys(v: unknown, path = '$'): string[] {
|
|
151
|
+
const out: string[] = [];
|
|
152
|
+
if (Array.isArray(v)) {
|
|
153
|
+
v.forEach((x, i) => out.push(...credentialShapedKeys(x, `${path}[${i}]`)));
|
|
154
|
+
} else if (v !== null && typeof v === 'object') {
|
|
155
|
+
for (const [k, x] of Object.entries(v as Record<string, unknown>)) {
|
|
156
|
+
if (CREDENTIAL_KEY.test(k)) out.push(`${path}.${k}`);
|
|
157
|
+
out.push(...credentialShapedKeys(x, `${path}.${k}`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
describe.skipIf(V1_DIR === null)('RFC 0149 §D — normative examples are extracted and validated against the conformance schemas', () => {
|
|
164
|
+
// `describe.skipIf` still RUNS this factory in the published layout (vitest
|
|
165
|
+
// collects skipped suites), so nothing here may touch the prose directory
|
|
166
|
+
// when it is absent — the guards below keep collection green in the tarball.
|
|
167
|
+
const v1Dir = V1_DIR ?? '';
|
|
168
|
+
const blocks = V1_DIR === null ? [] : fencedBlocks(v1Dir);
|
|
169
|
+
const declared = blocks.filter((b) => b.declared !== null);
|
|
170
|
+
const { byFile, permissive } = V1_DIR === null ? { byFile: new Map<string, ValidateFunction>(), permissive: new Set<string>() } : validators();
|
|
171
|
+
|
|
172
|
+
it('the extractor finds fenced examples and the corpus declares normative ones (non-vacuity floor)', () => {
|
|
173
|
+
expect(blocks.length, 'spec/v1 MUST contain fenced json/jsonc examples').toBeGreaterThan(100);
|
|
174
|
+
expect(declared.length, 'spec/v1 MUST declare normative examples with <!-- normative-example: … -->').toBeGreaterThanOrEqual(20);
|
|
175
|
+
expect(byFile.size, 'schemas/ MUST register').toBeGreaterThan(50);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('every marker names a schema that exists and sits immediately above a json/jsonc fence', () => {
|
|
179
|
+
const unknown = declared.filter((b) => !byFile.has(b.declared as string)).map((b) => `spec/v1/${b.file}:${b.line} → ${String(b.declared)}`);
|
|
180
|
+
expect(unknown, 'normative-example markers naming a schema that does not exist in schemas/').toEqual([]);
|
|
181
|
+
expect(strayMarkers(v1Dir), 'normative-example markers not directly followed by a ```json fence').toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('every declared example parses as strict JSON — jsonc comments and ellipses are prose, not instances', () => {
|
|
185
|
+
const unparsable = declared.filter((b) => parseStrict(b.body) === undefined).map((b) => `spec/v1/${b.file}:${b.line} (${b.info}) declared ${String(b.declared)}`);
|
|
186
|
+
expect(unparsable, 'declared normative examples MUST be strict JSON').toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('every declared example VALIDATES against its declared schema — the same schema file conformance uses', () => {
|
|
190
|
+
const failures: string[] = [];
|
|
191
|
+
for (const b of declared) {
|
|
192
|
+
const doc = parseStrict(b.body);
|
|
193
|
+
if (doc === undefined) continue; // reported by the leg above
|
|
194
|
+
const v = byFile.get(b.declared as string);
|
|
195
|
+
if (v === undefined) continue; // reported by the leg above
|
|
196
|
+
if (v(doc) !== true) {
|
|
197
|
+
const errs = (v.errors ?? []).slice(0, 3).map((e) => `${e.instancePath || '/'} ${e.message ?? ''}`).join('; ');
|
|
198
|
+
failures.push(`spec/v1/${b.file}:${b.line} ✗ ${String(b.declared)}: ${errs}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
expect(failures, 'RFC 0149 §D: a normative example that does not validate teaches a non-conformant document').toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('every fenced JSON block that IS a whole instance of a (non-permissive) schema is declared — no undeclared normative example', () => {
|
|
205
|
+
const undeclared: string[] = [];
|
|
206
|
+
for (const b of blocks) {
|
|
207
|
+
if (b.declared !== null) continue;
|
|
208
|
+
const doc = parseStrict(b.body);
|
|
209
|
+
if (doc === undefined) continue;
|
|
210
|
+
const hits = [...byFile.entries()].filter(([f, v]) => !permissive.has(f) && v(doc) === true).map(([f]) => f);
|
|
211
|
+
if (hits.length > 0) undeclared.push(`spec/v1/${b.file}:${b.line} validates as ${hits.join(' | ')} — add <!-- normative-example: <one of these> --> above the fence`);
|
|
212
|
+
}
|
|
213
|
+
expect(undeclared, 'whole-document examples MUST be declared so they are validated on every run').toEqual([]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('every discovery-shaped example is declared against capabilities.schema.json and validates (RFC 0149: "all normative discovery examples ... validate")', () => {
|
|
217
|
+
const discovery = blocks.filter((b) => isDiscoveryShaped(parseStrict(b.body)));
|
|
218
|
+
expect(discovery.length, 'spec/v1 MUST carry at least one whole discovery example').toBeGreaterThan(0);
|
|
219
|
+
const wrong = discovery.filter((b) => b.declared !== 'capabilities.schema.json').map((b) => `spec/v1/${b.file}:${b.line} declared ${String(b.declared)}`);
|
|
220
|
+
expect(wrong, 'discovery-shaped examples MUST be declared normative-example: capabilities.schema.json').toEqual([]);
|
|
221
|
+
const v = byFile.get('capabilities.schema.json');
|
|
222
|
+
expect(v).toBeDefined();
|
|
223
|
+
for (const b of discovery) expect((v as ValidateFunction)(parseStrict(b.body)), `spec/v1/${b.file}:${b.line}`).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('§E: no declared discovery example carries credential- or tenant-shaped keys', () => {
|
|
227
|
+
const offenders: string[] = [];
|
|
228
|
+
for (const b of declared.filter((x) => x.declared === 'capabilities.schema.json')) {
|
|
229
|
+
for (const p of credentialShapedKeys(parseStrict(b.body))) offenders.push(`spec/v1/${b.file}:${b.line} ${p}`);
|
|
230
|
+
}
|
|
231
|
+
expect(offenders, 'RFC 0149 §E: no discovery example may contain credentials or tenant data').toEqual([]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('sabotage: a declared example with a wrong enum value is caught by the same validator path', () => {
|
|
235
|
+
// Guard against a validator that compiles to `true` (strict:false + a
|
|
236
|
+
// schema that failed to register would do that silently).
|
|
237
|
+
const v = byFile.get('error-envelope.schema.json');
|
|
238
|
+
expect(v).toBeDefined();
|
|
239
|
+
expect((v as ValidateFunction)({ error: 'not_found', message: 'gone', retriable: 'yes' })).toBe(false);
|
|
240
|
+
expect((v as ValidateFunction)({ error: 'not_found', message: 'gone' })).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { describe, it, expect } from 'vitest';
|
|
38
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
38
39
|
import { driver } from '../lib/driver.js';
|
|
39
40
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
40
41
|
|
|
@@ -74,8 +75,8 @@ async function readOAuth(): Promise<DiscoveryOAuth | null> {
|
|
|
74
75
|
describe('oauth-authorization-code-roundtrip: the grant dance (RFC 0047 §C)', () => {
|
|
75
76
|
it('acquires a token via authorization_code and returns a reference, never the token', async () => {
|
|
76
77
|
const oauth = await readOAuth();
|
|
77
|
-
if (!oauth?.supported) return
|
|
78
|
-
if (!Array.isArray(oauth.grants) || !oauth.grants.includes('authorization_code')) return
|
|
78
|
+
if (!oauth?.supported) return softSkip('inapplicable', 'capability-gated');
|
|
79
|
+
if (!Array.isArray(oauth.grants) || !oauth.grants.includes('authorization_code')) return softSkip('inapplicable', 'grant-gated (!Array.isArray(oauth.grants) || !oauth.grants.includes(\'authorization_code\'))');
|
|
79
80
|
|
|
80
81
|
// Seam contract: the host performs the full authorization-code roundtrip
|
|
81
82
|
// against the synthetic provider's authUrl/tokenUrl, persists the acquired
|
|
@@ -95,7 +96,7 @@ describe('oauth-authorization-code-roundtrip: the grant dance (RFC 0047 §C)', (
|
|
|
95
96
|
refreshTokenCanary: SYNTHETIC.refreshTokenCanary,
|
|
96
97
|
});
|
|
97
98
|
// A host that hasn't wired the seam soft-skips (Tier-2, host-pending).
|
|
98
|
-
if (res.status === 404) return;
|
|
99
|
+
if (res.status === 404) return softSkip('blocked', 'precondition not met — `res.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
99
100
|
|
|
100
101
|
expect(
|
|
101
102
|
res.status,
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { describe, it, expect } from 'vitest';
|
|
20
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
20
21
|
import { driver } from '../lib/driver.js';
|
|
21
22
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
22
23
|
|
|
@@ -54,7 +55,7 @@ async function readOAuth(): Promise<DiscoveryOAuth | null> {
|
|
|
54
55
|
describe('oauth-capability-shape: advertisement shape (RFC 0047 §A)', () => {
|
|
55
56
|
it('capabilities.oauth is either absent or well-formed', async () => {
|
|
56
57
|
const oauth = await readOAuth();
|
|
57
|
-
if (oauth === null) return
|
|
58
|
+
if (oauth === null) return softSkip('inapplicable', 'host doesn\'t advertise host.oauth at all');
|
|
58
59
|
expect(
|
|
59
60
|
typeof oauth.supported,
|
|
60
61
|
driver.describe(
|
|
@@ -66,7 +67,7 @@ describe('oauth-capability-shape: advertisement shape (RFC 0047 §A)', () => {
|
|
|
66
67
|
|
|
67
68
|
it('grants is a subset of the canonical grant set when supported', async () => {
|
|
68
69
|
const oauth = await readOAuth();
|
|
69
|
-
if (!oauth?.supported || oauth.grants === undefined) return;
|
|
70
|
+
if (!oauth?.supported || oauth.grants === undefined) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!oauth?.supported || oauth.grants === undefined` returned early');
|
|
70
71
|
expect(
|
|
71
72
|
Array.isArray(oauth.grants),
|
|
72
73
|
driver.describe('RFC 0047 §A', 'capabilities.oauth.grants MUST be an array'),
|
|
@@ -84,7 +85,7 @@ describe('oauth-capability-shape: advertisement shape (RFC 0047 §A)', () => {
|
|
|
84
85
|
|
|
85
86
|
it('every advertised provider has a non-empty id when supported', async () => {
|
|
86
87
|
const oauth = await readOAuth();
|
|
87
|
-
if (!oauth?.supported || oauth.providers === undefined) return;
|
|
88
|
+
if (!oauth?.supported || oauth.providers === undefined) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!oauth?.supported || oauth.providers === undefined` returned early');
|
|
88
89
|
for (const provider of oauth.providers) {
|
|
89
90
|
expect(
|
|
90
91
|
typeof provider.id === 'string' && provider.id.length > 0,
|
|
@@ -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
|
|
|
@@ -49,7 +50,7 @@ async function readOAuth(): Promise<DiscoveryOAuth | null> {
|
|
|
49
50
|
describe('oauth-connector-redaction: advertisement shape (RFC 0047 §A)', () => {
|
|
50
51
|
it('capabilities.oauth.supported is a boolean when advertised', async () => {
|
|
51
52
|
const oauth = await readOAuth();
|
|
52
|
-
if (oauth === null) return;
|
|
53
|
+
if (oauth === null) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `oauth === null` returned early');
|
|
53
54
|
expect(
|
|
54
55
|
typeof oauth.supported,
|
|
55
56
|
driver.describe(
|
|
@@ -63,14 +64,14 @@ describe('oauth-connector-redaction: advertisement shape (RFC 0047 §A)', () =>
|
|
|
63
64
|
describe('oauth-connector-redaction: token material MUST NOT cross the wire (RFC 0047 §C.2)', () => {
|
|
64
65
|
it('canary token is absent from every observable run surface', async () => {
|
|
65
66
|
const oauth = await readOAuth();
|
|
66
|
-
if (!oauth?.supported) return
|
|
67
|
+
if (!oauth?.supported) return softSkip('inapplicable', 'capability-gated');
|
|
67
68
|
|
|
68
69
|
// Seam contract: a synthetic provider issues a token whose value is
|
|
69
70
|
// TOKEN_CANARY, a connector node runs, and the run's observable surfaces
|
|
70
71
|
// (events incl. connector.authorized + snapshot + debug bundle) are returned.
|
|
71
72
|
const res = await driver.post('/v1/host/sample/oauth/connector-echo', { canary: TOKEN_CANARY });
|
|
72
73
|
// 404 from a host that hasn't wired the test seam is a soft-skip.
|
|
73
|
-
if (res.status === 404) return;
|
|
74
|
+
if (res.status === 404) return softSkip('blocked', 'precondition not met — `res.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
74
75
|
|
|
75
76
|
expect(
|
|
76
77
|
res.status,
|