@openwop/openwop-conformance 2.45.16 → 2.45.18
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/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +808 -673
- package/scenario-majors.json +5 -23
- package/schemas/CORPUS-STAMP.json +30 -30
- package/src/lib/envelope-schema-static.ts +3 -2
- package/src/lib/if-none-match-witness.ts +139 -0
- package/src/lib/prompt-library-witness.ts +11 -2
- package/src/scenarios/otel-emission-grpc.test.ts +1 -1
- package/src/scenarios/v2-artifact-types-advertisement.test.ts +1 -1
- package/src/scenarios/v2-discovery-etag.test.ts +122 -0
- package/src/scenarios/v2-envelope-tier-one-subset-static.test.ts +7 -20
- package/src/scenarios/v2-portability-advertisement.test.ts +1 -1
- package/src/scenarios/v2-prompt-list-and-fetch.test.ts +4 -2
- package/src/scenarios/v2-provider-usage-advertisement.test.ts +1 -1
- package/src/scenarios/v2-run-snapshot-etag.test.ts +116 -22
- package/src/scenarios/v2-self-hosted-runner-advertisement.test.ts +1 -1
- package/src/scenarios/v2-ai-envelope-shape-static.test.ts +0 -93
- package/src/scenarios/v2-artifact-created-static.test.ts +0 -92
- package/src/scenarios/v2-envelope-variant-discriminator-static.test.ts +0 -57
- package/src/scenarios/v2-export-bundle-static.test.ts +0 -108
- package/src/scenarios/v2-media-inline-cap-static.test.ts +0 -59
- package/src/scenarios/v2-provider-usage-payload-static.test.ts +0 -92
- package/src/scenarios/v2-self-hosted-runner-frames-static.test.ts +0 -121
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
* The v1 twin is `envelope-tier-one-subset-static`; the walker lives in
|
|
5
5
|
* `lib/envelope-schema-static.ts`.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* properties;
|
|
7
|
+
* The always-on load-bearing walk over the universal kinds reads only the
|
|
8
|
+
* corpus, so it lives in `src/coherence/v2-envelope-tier-one-universal-static`
|
|
9
|
+
* (moved in 2.45.18; `conformance.md` §Two products). This file keeps the one
|
|
10
|
+
* leg that reads host output:
|
|
11
|
+
*
|
|
13
12
|
* strict LIVE: a host advertising `tierOneSubsetCompliance: "strict"`
|
|
14
13
|
* has, for every kind it lists in `supportedEnvelopes.kinds`
|
|
15
14
|
* that resolves to a corpus schema, a schema meeting the whole
|
|
@@ -23,7 +22,7 @@
|
|
|
23
22
|
* host MUST accept an envelope restricted to the subset from any advertised
|
|
24
23
|
* provider — and does not distinguish strict, warn and off.
|
|
25
24
|
*
|
|
26
|
-
* Dispositions
|
|
25
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `envelopes` not
|
|
27
26
|
* advertised, or `tierOneSubsetCompliance` other than "strict" ⇒
|
|
28
27
|
* `inapplicable`; no advertised kind resolving to a corpus schema ⇒
|
|
29
28
|
* `inapplicable`.
|
|
@@ -36,22 +35,10 @@ import { describe, it, expect } from 'vitest';
|
|
|
36
35
|
import { familyAdvertised, v2Discovery } from '../lib/v2.js';
|
|
37
36
|
import { softSkip } from '../lib/soft-skip.js';
|
|
38
37
|
import { req } from '../lib/requirement-ids.js';
|
|
39
|
-
import { loadV2EnvelopeSchema, tierOneViolations,
|
|
38
|
+
import { loadV2EnvelopeSchema, tierOneViolations, type SchemaViolation } from '../lib/envelope-schema-static.js';
|
|
40
39
|
|
|
41
|
-
const ID_LOAD_BEARING = 'openwop.requirement.envelopes.tier-one-load-bearing-universal-kinds';
|
|
42
40
|
const ID_STRICT = 'openwop.requirement.envelopes.tier-one-strict-advertised-kinds';
|
|
43
41
|
|
|
44
|
-
describe('v2 Tier-1 subset: universal kinds meet the load-bearing rules (always on)', () => {
|
|
45
|
-
for (const kind of UNIVERSAL_KINDS) {
|
|
46
|
-
it(`${kind} uses no keyword that fails across Tier-1 vendors`, () => {
|
|
47
|
-
const schema = loadV2EnvelopeSchema(kind);
|
|
48
|
-
expect(schema !== null, req(ID_LOAD_BEARING, 'RFC 0030 §B', `schemas/v2/envelopes/${kind}.schema.json MUST exist`)).toBe(true);
|
|
49
|
-
const violations = schema === null ? [] : tierOneViolations(schema, 'load-bearing');
|
|
50
|
-
expect(violations, req(ID_LOAD_BEARING, 'RFC 0030 §B', `${kind} load-bearing Tier-1 violations: ${JSON.stringify(violations)}`)).toEqual([]);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
42
|
describe('v2 Tier-1 subset: a strict host\'s advertised kinds meet the whole intersection', () => {
|
|
56
43
|
it('a host advertising tierOneSubsetCompliance strict has advertised kinds whose corpus schemas satisfy the Tier-1 intersection', async () => {
|
|
57
44
|
let doc: Record<string, unknown> | null;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Not here: import refusal of credential material, dry-run no-write and
|
|
12
12
|
* idempotence: v2 defines no import route ("a host serves them on routes of its
|
|
13
|
-
* own"). The bundle shape is `v2-export-bundle-static`.
|
|
13
|
+
* own"). The bundle shape is the corpus gate `coherence/v2-export-bundle-static`.
|
|
14
14
|
*
|
|
15
15
|
* Dispositions: discovery unreadable ⇒ `blocked`; `portability` absent, or the
|
|
16
16
|
* facet a leg reads absent ⇒ `inapplicable`.
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* validates against the closed v2 PromptTemplate;
|
|
9
9
|
* filters `?kind=system` and `?source=host` narrow;
|
|
10
10
|
* fetch a listed template is fetched by id;
|
|
11
|
-
* etag an ETag, when sent (a SHOULD), revalidates to `304`;
|
|
11
|
+
* etag advisory: an ETag, when sent (a SHOULD), revalidates to `304`;
|
|
12
|
+
* a non-304 is recorded `inapplicable`, never failed (v2 binds
|
|
13
|
+
* 304 only on discovery and the run snapshot, headers.md);
|
|
12
14
|
* unknown an unknown id is `404` with the v2 error envelope;
|
|
13
15
|
* gate off NEW at v2, unaided: with `prompts.endpointsSupported` not
|
|
14
16
|
* advertised (the record absent, or the facet not `true`),
|
|
@@ -77,7 +79,7 @@ describe('v2 prompt library: read surface (host-services.md §prompts)', () => {
|
|
|
77
79
|
for (const x of out.findings) expect(x.ok, req(ID_FETCH, x.doc, x.message)).toBe(true);
|
|
78
80
|
});
|
|
79
81
|
|
|
80
|
-
it('an ETag, when sent, revalidates to 304', async () => {
|
|
82
|
+
it('advisory: an ETag, when sent, revalidates to 304', async () => {
|
|
81
83
|
const r = await ready();
|
|
82
84
|
if (!r.ok) return r.skip();
|
|
83
85
|
const out = await etagLeg(PROFILE, r.a);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* currency an ISO 4217 code, `^[A-Z]{3}$` (absent means USD).
|
|
9
9
|
*
|
|
10
10
|
* Not here: exactly-one-per-invocation emission needs a mock-provider run; the
|
|
11
|
-
* payload shape is `v2-provider-usage-payload-static`.
|
|
11
|
+
* payload shape is the corpus gate `coherence/v2-provider-usage-payload-static`.
|
|
12
12
|
*
|
|
13
13
|
* Dispositions: discovery unreadable ⇒ `blocked`; `providerUsage` absent, or the
|
|
14
14
|
* facet a leg reads absent ⇒ `inapplicable`.
|
|
@@ -7,12 +7,27 @@
|
|
|
7
7
|
* a host that sends no ETag records `inapplicable`. Every response carries
|
|
8
8
|
* `OpenWOP-Version` (versioning.md §1.4), the 304 included.
|
|
9
9
|
*
|
|
10
|
+
* The revalidation runs once per client spelling (`OpenWOP-Version: 2.0`,
|
|
11
|
+
* `2`, and `2` with a wildcard Accept): a host MUST treat them alike
|
|
12
|
+
* (versioning.md §1), so a host that honours only the suite's own spelling
|
|
13
|
+
* cannot pass by it.
|
|
14
|
+
*
|
|
10
15
|
* Control: a non-matching `If-None-Match` MUST receive 200 with the body — a
|
|
11
16
|
* host that answers 304 to any conditional request fails here. `headers.md`
|
|
12
17
|
* scopes `If-None-Match` to the discovery document; runs.md applies it to the
|
|
13
18
|
* snapshot with a MUST (finding 5, filed).
|
|
14
19
|
*
|
|
20
|
+
* RFC 0235 (`runs.md` §Caching and encoding) adds four legs, shared with
|
|
21
|
+
* `v2-discovery-etag` through `lib/if-none-match-witness.ts`: the RFC 9110
|
|
22
|
+
* match (weak, list, `*`, and two negatives), a 304 carrying the ETag, a
|
|
23
|
+
* request `Cache-Control: no-cache` not suppressing evaluation, and — only on
|
|
24
|
+
* this surface — evaluation only where the answer would be 2xx: `*` or a
|
|
25
|
+
* minted tag on a run id that does not exist gets the unconditional status,
|
|
26
|
+
* never a 304 that would disclose the run. Each of them is `inapplicable` when
|
|
27
|
+
* the snapshot carries no ETag, like the base leg.
|
|
28
|
+
*
|
|
15
29
|
* @see spec/v2/core/runs.md §Snapshot
|
|
30
|
+
* @see spec/v2/core/runs.md §Caching and encoding
|
|
16
31
|
* @see spec/v2/core/versioning.md §1.4
|
|
17
32
|
*/
|
|
18
33
|
|
|
@@ -23,10 +38,23 @@ import { v2Discovery } from '../lib/v2.js';
|
|
|
23
38
|
import { readErrorCode } from '../lib/error-envelope.js';
|
|
24
39
|
import { softSkip } from '../lib/soft-skip.js';
|
|
25
40
|
import { req } from '../lib/requirement-ids.js';
|
|
41
|
+
import { headersLeg, judgeNot2xx, matchLeg, noCacheLeg, type Get, type LegOutcome } from '../lib/if-none-match-witness.js';
|
|
26
42
|
|
|
27
43
|
const ID = 'openwop.requirement.0170.run-snapshot-etag';
|
|
28
44
|
const DOC = 'spec/v2/core/runs.md §Snapshot';
|
|
29
45
|
const NOOP = 'conformance-noop';
|
|
46
|
+
const MATCH_DOC = 'spec/v2/core/runs.md §Caching and encoding';
|
|
47
|
+
const ID_MATCH = 'openwop.requirement.0235.if-none-match.rfc9110-match';
|
|
48
|
+
const ID_304_ETAG = 'openwop.requirement.0235.if-none-match.304-carries-etag';
|
|
49
|
+
const ID_NO_CACHE = 'openwop.requirement.0235.if-none-match.no-cache-ignored';
|
|
50
|
+
const ID_ONLY_2XX = 'openwop.requirement.0235.if-none-match.only-on-2xx';
|
|
51
|
+
/** The client spellings a host MUST treat alike (versioning.md §1). */
|
|
52
|
+
const SPELLINGS: ReadonlyArray<Readonly<Record<string, string>>> = [
|
|
53
|
+
{ 'OpenWOP-Version': '2.0' },
|
|
54
|
+
{ 'OpenWOP-Version': '2' },
|
|
55
|
+
{ 'OpenWOP-Version': '2', Accept: '*/*' },
|
|
56
|
+
];
|
|
57
|
+
const label = (h: Readonly<Record<string, string>>): string => Object.entries(h).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
30
58
|
|
|
31
59
|
async function discovery(): Promise<Record<string, unknown> | null> { try { return await v2Discovery(); } catch { return null; } }
|
|
32
60
|
async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> { try { return await fn(); } catch { return null; } }
|
|
@@ -51,32 +79,98 @@ describe('v2 run-snapshot-etag (runs.md §Snapshot)', () => {
|
|
|
51
79
|
// happened to finish inside the sleep. It also let a host with a CONSTANT tag
|
|
52
80
|
// pass for the wrong reason, which the control below still catches.
|
|
53
81
|
await pollUntilTerminal(runId, { timeoutMs: scaledTimeoutMs(30_000) });
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
for (const spelling of SPELLINGS) {
|
|
83
|
+
let etag: string | null = null;
|
|
84
|
+
let hit: OpenWOPResponse | null = null;
|
|
85
|
+
// A terminal run should be still, but "should" is not the claim under test:
|
|
86
|
+
// if the tag moves again, take the NEW tag and retry. The violation is a 200
|
|
87
|
+
// whose ETag EQUALS the If-None-Match that was sent — never a 200 per se.
|
|
88
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
89
|
+
const first = await http(() => driver.get(path, { headers: { ...spelling } }));
|
|
90
|
+
if (first === null || first.status !== 200) return softSkip('blocked', `GET /runs/{runId} answered ${first?.status ?? 'no response'}`);
|
|
91
|
+
etag = first.headers.get('etag');
|
|
92
|
+
if (!etag) return softSkip('inapplicable', 'the snapshot carries no ETag — runs.md §Snapshot makes the ETag a SHOULD; the 304 rule applies only when it is present');
|
|
93
|
+
const sent = etag;
|
|
94
|
+
hit = await http(() => driver.get(path, { headers: { ...spelling, 'If-None-Match': sent } }));
|
|
95
|
+
if (hit === null) return softSkip('blocked', 'conditional GET unreachable (fetch failed)');
|
|
96
|
+
if (hit.status !== 200 || hit.headers.get('etag') === sent) break; // 304, or the real violation: decided below
|
|
97
|
+
await new Promise((r) => setTimeout(r, 500)); // 200 with a DIFFERENT tag: the representation moved; the tag sent was honestly stale
|
|
98
|
+
}
|
|
99
|
+
if (hit === null || etag === null) return softSkip('blocked', 'no conditional GET was made');
|
|
100
|
+
if (hit.status === 200 && hit.headers.get('etag') !== etag) {
|
|
101
|
+
return softSkip('blocked', `the snapshot of a TERMINAL run kept changing across four reads (last tag sent ${etag}, answered 200 with ${String(hit.headers.get('etag'))}) — the 304 rule cannot be witnessed against a representation that never holds still`);
|
|
102
|
+
}
|
|
103
|
+
expect(hit.status, req(ID, DOC, `under ${label(spelling)}, a request whose If-None-Match matches the CURRENT ETag MUST receive 304 — got ${hit.status} while the response still carried the same tag ${etag}`)).toBe(304);
|
|
104
|
+
expect(hit.text.length, req(ID, DOC, `under ${label(spelling)}, the 304 MUST carry no body (got ${hit.text.length} byte(s))`)).toBe(0);
|
|
105
|
+
expect(hit.headers.get('openwop-version'), req(ID, 'spec/v2/core/versioning.md §1.4', `under ${label(spelling)}, every response carries OpenWOP-Version, the 304 included`)).not.toBeNull();
|
|
69
106
|
}
|
|
70
|
-
if (hit === null || etag === null) return softSkip('blocked', 'no conditional GET was made');
|
|
71
|
-
if (hit.status === 200 && hit.headers.get('etag') !== etag) {
|
|
72
|
-
return softSkip('blocked', `the snapshot of a TERMINAL run kept changing across four reads (last tag sent ${etag}, answered 200 with ${String(hit.headers.get('etag'))}) — the 304 rule cannot be witnessed against a representation that never holds still`);
|
|
73
|
-
}
|
|
74
|
-
expect(hit.status, req(ID, DOC, `a request whose If-None-Match matches the CURRENT ETag MUST receive 304 — got ${hit.status} while the response still carried the same tag ${etag}`)).toBe(304);
|
|
75
|
-
expect(hit.text.length, req(ID, DOC, `the 304 MUST carry no body (got ${hit.text.length} byte(s))`)).toBe(0);
|
|
76
|
-
expect(hit.headers.get('openwop-version'), req(ID, 'spec/v2/core/versioning.md §1.4', 'every response carries OpenWOP-Version, the 304 included')).not.toBeNull();
|
|
77
107
|
const miss = await http(() => driver.get(path, { headers: { 'If-None-Match': '"openwop-conformance-no-such-tag"' } }));
|
|
78
108
|
if (miss === null) return softSkip('blocked', 'conditional GET unreachable (fetch failed)');
|
|
79
109
|
expect(miss.status, req(ID, DOC, `a non-matching If-None-Match MUST receive 200 with the body — got ${miss.status} (a host answering 304 to any conditional request is not honouring the tag)`)).toBe(200);
|
|
80
110
|
expect((miss.json as { runId?: unknown } | null)?.runId, req(ID, DOC, 'the 200 body is the snapshot')).toBe(runId);
|
|
81
111
|
});
|
|
112
|
+
|
|
113
|
+
/** A settled run whose snapshot carries an ETag, or the reason there is none. */
|
|
114
|
+
let settled: Promise<{ runId: string; tag: string } | { skip: 'blocked' | 'inapplicable'; reason: string }> | null = null;
|
|
115
|
+
function settledRun(): Promise<{ runId: string; tag: string } | { skip: 'blocked' | 'inapplicable'; reason: string }> {
|
|
116
|
+
settled ??= (async () => {
|
|
117
|
+
if (!(await discovery())) return { skip: 'blocked' as const, reason: 'v2 discovery unreachable' };
|
|
118
|
+
const created = await http(() => driver.post('/runs', { workflowId: NOOP }));
|
|
119
|
+
const runId = (created?.json as { runId?: unknown } | null)?.runId;
|
|
120
|
+
if (created === null || created.status !== 201 || typeof runId !== 'string') return { skip: 'blocked' as const, reason: `POST /runs answered ${created?.status ?? 'no response'} — create refused` };
|
|
121
|
+
await pollUntilTerminal(runId, { timeoutMs: scaledTimeoutMs(30_000) });
|
|
122
|
+
const ok = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}`));
|
|
123
|
+
if (ok === null || ok.status !== 200) return { skip: 'blocked' as const, reason: `GET /runs/{runId} answered ${ok?.status ?? 'no response'}` };
|
|
124
|
+
const tag = ok.headers.get('etag');
|
|
125
|
+
if (!tag) return { skip: 'inapplicable' as const, reason: 'the snapshot carries no ETag — runs.md makes it a SHOULD; the If-None-Match rules apply only when it is present' };
|
|
126
|
+
return { runId, tag };
|
|
127
|
+
})();
|
|
128
|
+
return settled;
|
|
129
|
+
}
|
|
130
|
+
const runGet = (runId: string): Get => (headers) => http(() => driver.get(`/runs/${encodeURIComponent(runId)}`, { headers: { ...headers } }));
|
|
131
|
+
async function leg(fn: (get: Get) => Promise<LegOutcome>): Promise<LegOutcome | { kind: 'skip'; skip: 'blocked' | 'inapplicable'; reason: string }> {
|
|
132
|
+
const run = await settledRun();
|
|
133
|
+
if ('skip' in run) return { kind: 'skip', ...run };
|
|
134
|
+
return fn(runGet(run.runId));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
it('If-None-Match is evaluated as RFC 9110 defines it: a weak tag, a list holding the tag and `*` match; other tags do not', async () => {
|
|
138
|
+
const out = await leg(matchLeg);
|
|
139
|
+
if (out.kind === 'skip') return softSkip(out.skip, out.reason);
|
|
140
|
+
if (out.kind === 'unreadable') return softSkip('blocked', `run snapshot: ${out.reason}`);
|
|
141
|
+
expect(out.findings.join('; '), req(ID_MATCH, MATCH_DOC, 'If-None-Match is `*` or a list of entity tags compared weakly (RFC 9110 §13.1.2)')).toBe('');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('a 304 carries the ETag and the Vary of the 200', async () => {
|
|
145
|
+
const out = await leg(headersLeg);
|
|
146
|
+
if (out.kind === 'skip') return softSkip(out.skip, out.reason);
|
|
147
|
+
if (out.kind === 'unreadable') return softSkip('blocked', `run snapshot: ${out.reason}`);
|
|
148
|
+
expect(out.findings.join('; '), req(ID_304_ETAG, MATCH_DOC, 'on a match the host MUST answer 304 carrying the ETag and the Vary the 200 would carry')).toBe('');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('a request Cache-Control: no-cache does not suppress the evaluation', async () => {
|
|
152
|
+
const out = await leg(noCacheLeg);
|
|
153
|
+
if (out.kind === 'skip') return softSkip(out.skip, out.reason);
|
|
154
|
+
if (out.kind === 'unreadable') return softSkip('blocked', `run snapshot: ${out.reason}`);
|
|
155
|
+
expect(out.findings.join('; '), req(ID_NO_CACHE, MATCH_DOC, 'a request Cache-Control: no-cache MUST NOT suppress the evaluation')).toBe('');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('a conditional request on a run that does not exist gets the unconditional status, never 304', async () => {
|
|
159
|
+
const run = await settledRun();
|
|
160
|
+
if ('skip' in run) return softSkip(run.skip, run.reason);
|
|
161
|
+
// Same grammar and tenant as a real id, last character changed: an id this host never minted.
|
|
162
|
+
const last = run.runId.slice(-1);
|
|
163
|
+
const absent = run.runId.slice(0, -1) + (last === 'a' ? 'b' : 'a');
|
|
164
|
+
const get = runGet(absent);
|
|
165
|
+
const plain = await get({});
|
|
166
|
+
if (plain === null) return softSkip('blocked', 'GET /runs/{runId} on an absent id could not be made');
|
|
167
|
+
if (plain.status >= 200 && plain.status < 300) return softSkip('blocked', `an id differing from a real run in its last character answered ${plain.status}; no absent run to probe`);
|
|
168
|
+
const findings: string[] = [];
|
|
169
|
+
for (const sent of ['*', run.tag]) {
|
|
170
|
+
const c = await get({ 'If-None-Match': sent });
|
|
171
|
+
if (c === null) return softSkip('blocked', 'the conditional GET on an absent id could not be made');
|
|
172
|
+
findings.push(...judgeNot2xx(plain.status, c.status, sent));
|
|
173
|
+
}
|
|
174
|
+
expect(findings.join('; '), req(ID_ONLY_2XX, MATCH_DOC, 'If-None-Match is evaluated only when the unconditional response would be 2xx; a run the caller cannot read stays 404')).toBe('');
|
|
175
|
+
});
|
|
82
176
|
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* dispatchKinds `dispatchKinds` lists `model`, `tool` or both, each once.
|
|
9
9
|
*
|
|
10
10
|
* Not here: registration, subject isolation, at-most-once and liveness need a
|
|
11
|
-
* runner seam; the frame shapes are `v2-self-hosted-runner-frames-static`.
|
|
11
|
+
* runner seam; the frame shapes are the corpus gate `coherence/v2-self-hosted-runner-frames-static`.
|
|
12
12
|
*
|
|
13
13
|
* Dispositions: discovery unreadable ⇒ `blocked`; `selfHostedRunner` absent, or the
|
|
14
14
|
* facet a leg reads absent ⇒ `inapplicable`.
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2 — the AI envelope wire shape, statically (`spec/v2/core/events.md`
|
|
3
|
-
* §"AI envelopes: E1–E5"; `schemas/v2/ai-envelope.schema.json`). The v1 twin is
|
|
4
|
-
* the static half of `ai-envelope-shape`; its live halves (the
|
|
5
|
-
* `envelope/accept` seam legs) are seam-only and not ported.
|
|
6
|
-
*
|
|
7
|
-
* compile `ai-envelope.schema.json` and each universal-kind payload
|
|
8
|
-
* schema compile under Ajv 2020 with the v2 tree registered;
|
|
9
|
-
* positive a well-formed envelope validates — with `correlationId` and
|
|
10
|
-
* `meta.source`, which v2 makes REQUIRED (migration C4.16);
|
|
11
|
-
* meta an envelope without `meta` is rejected;
|
|
12
|
-
* closed an unknown top-level property is rejected;
|
|
13
|
-
* correlation NEW at v2: an envelope without `correlationId` is rejected
|
|
14
|
-
* ("`correlationId` and `meta.source` are REQUIRED on every
|
|
15
|
-
* envelope … nothing is synthesized");
|
|
16
|
-
* source NEW at v2: an envelope whose `meta` has no `source` is rejected.
|
|
17
|
-
*
|
|
18
|
-
* The `<org>.` namespacing rule for kinds is not witnessed here: the schema's
|
|
19
|
-
* `type` is an open string, so the rule binds the engine's acceptance, not the
|
|
20
|
-
* wire shape.
|
|
21
|
-
*
|
|
22
|
-
* Host-free: every leg runs with no OPENWOP_BASE_URL.
|
|
23
|
-
*
|
|
24
|
-
* @see spec/v2/core/events.md §"AI envelopes: E1–E5"
|
|
25
|
-
* @see spec/v1/migrations.json openwop.migration.C4.16
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { describe, it, expect } from 'vitest';
|
|
29
|
-
import { v2Validator } from '../lib/v2.js';
|
|
30
|
-
import { req } from '../lib/requirement-ids.js';
|
|
31
|
-
import { UNIVERSAL_KINDS } from '../lib/envelope-schema-static.js';
|
|
32
|
-
|
|
33
|
-
const DOC = 'events.md §AI envelopes: E1–E5';
|
|
34
|
-
const ID_COMPILE = 'openwop.requirement.ai-envelope.schemas-compile';
|
|
35
|
-
const ID_POSITIVE = 'openwop.requirement.ai-envelope.well-formed-accepted';
|
|
36
|
-
const ID_META = 'openwop.requirement.ai-envelope.meta-required';
|
|
37
|
-
const ID_CLOSED = 'openwop.requirement.ai-envelope.top-level-closed';
|
|
38
|
-
const ID_CORRELATION = 'openwop.requirement.ai-envelope.correlation-id-required';
|
|
39
|
-
const ID_SOURCE = 'openwop.requirement.ai-envelope.meta-source-required';
|
|
40
|
-
|
|
41
|
-
const WELL_FORMED = {
|
|
42
|
-
type: 'clarification.request',
|
|
43
|
-
schemaVersion: 1,
|
|
44
|
-
envelopeId: 'env-v2-positive-1',
|
|
45
|
-
correlationId: 'run-1:node-2:turn-0:abc123',
|
|
46
|
-
payload: { questions: [{ id: 'q1', question: 'Which provider?' }] },
|
|
47
|
-
meta: { source: 'ai-generation', ts: '2026-10-04T10:00:00Z', contentTrust: 'trusted' },
|
|
48
|
-
} as const;
|
|
49
|
-
|
|
50
|
-
/** The well-formed envelope with one top-level field removed. */
|
|
51
|
-
function without(field: string): Record<string, unknown> {
|
|
52
|
-
const e: Record<string, unknown> = { ...WELL_FORMED };
|
|
53
|
-
delete e[field];
|
|
54
|
-
return e;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
describe('v2 AI envelope shape: schemas compile', () => {
|
|
58
|
-
it('ai-envelope.schema.json compiles under Ajv 2020', () => {
|
|
59
|
-
expect(() => v2Validator('ai-envelope'), req(ID_COMPILE, DOC, 'schemas/v2/ai-envelope.schema.json MUST compile')).not.toThrow();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
for (const kind of UNIVERSAL_KINDS) {
|
|
63
|
-
it(`envelopes/${kind}.schema.json compiles under Ajv 2020`, () => {
|
|
64
|
-
expect(() => v2Validator(`envelopes/${kind}`), req(ID_COMPILE, DOC, `schemas/v2/envelopes/${kind}.schema.json MUST compile`)).not.toThrow();
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('v2 AI envelope shape: round-trip', () => {
|
|
70
|
-
const validate = v2Validator('ai-envelope');
|
|
71
|
-
|
|
72
|
-
it('accepts a well-formed envelope carrying correlationId and meta.source', () => {
|
|
73
|
-
const r = validate(WELL_FORMED);
|
|
74
|
-
expect(r.ok, req(ID_POSITIVE, DOC, `a well-formed envelope MUST validate (${r.errors})`)).toBe(true);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('rejects an envelope missing meta', () => {
|
|
78
|
-
expect(validate(without('meta')).ok, req(ID_META, DOC, 'an envelope without meta MUST be rejected')).toBe(false);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('rejects an envelope with an unknown top-level property', () => {
|
|
82
|
-
expect(validate({ ...WELL_FORMED, unknownTopLevel: 'x' }).ok, req(ID_CLOSED, 'schemas/v2/ai-envelope.schema.json', 'the envelope is closed: an unknown top-level property MUST be rejected')).toBe(false);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('rejects an envelope missing correlationId', () => {
|
|
86
|
-
expect(validate(without('correlationId')).ok, req(ID_CORRELATION, DOC, 'correlationId is REQUIRED on every envelope at v2 — an envelope without it MUST be rejected, never synthesized')).toBe(false);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('rejects an envelope whose meta carries no source', () => {
|
|
90
|
-
const { source: _source, ...meta } = WELL_FORMED.meta;
|
|
91
|
-
expect(validate({ ...WELL_FORMED, meta }).ok, req(ID_SOURCE, DOC, 'meta.source is REQUIRED on every envelope at v2 — an envelope without it MUST be rejected')).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2 — the `artifact.created` payload, statically
|
|
3
|
-
* (`spec/v2/core/artifact-type-packs.md` §Registration;
|
|
4
|
-
* `schemas/v2/run-event-payloads.schema.json#/$defs/artifactCreated`; RFCs
|
|
5
|
-
* 0071, 0075, 0138, 0145, 0185). The v1 twins are the schema halves of
|
|
6
|
-
* `artifact-type-store-emission` and `artifact-type-registration-source`;
|
|
7
|
-
* their fixture-run legs are not ported here.
|
|
8
|
-
*
|
|
9
|
-
* type `artifact.created` is a registered v2 event type;
|
|
10
|
-
* required `artifactId` and `artifactType` are REQUIRED (the RFC 0138
|
|
11
|
-
* replay correction reads the type from the event);
|
|
12
|
-
* provenance `registrationSource` is `pack` or `host` ("pack-registered …
|
|
13
|
-
* `registrationSource: "pack"`, host-registered … `"host"`"),
|
|
14
|
-
* and the discovery per-type seat carries the same vocabulary,
|
|
15
|
-
* so the two surfaces cannot disagree on a value;
|
|
16
|
-
* registered `registered` is a boolean;
|
|
17
|
-
* closed an unknown key is rejected; a vendor key under the RFC 0185
|
|
18
|
-
* hatch (`x-`, `vendor.`, `openwop-`) is carried.
|
|
19
|
-
*
|
|
20
|
-
* Host-free: every leg runs with no OPENWOP_BASE_URL.
|
|
21
|
-
*
|
|
22
|
-
* @see spec/v2/core/artifact-type-packs.md §Registration
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { describe, it, expect } from 'vitest';
|
|
26
|
-
import { readFileSync } from 'node:fs';
|
|
27
|
-
import { join } from 'node:path';
|
|
28
|
-
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
29
|
-
import { v2RefValidator } from '../lib/v2.js';
|
|
30
|
-
import { req } from '../lib/requirement-ids.js';
|
|
31
|
-
|
|
32
|
-
const DOC = 'artifact-type-packs.md §Registration';
|
|
33
|
-
const ID_TYPE = 'openwop.requirement.artifact-types.created-event-type-registered';
|
|
34
|
-
const ID_REQUIRED = 'openwop.requirement.artifact-types.created-requires-artifact-type';
|
|
35
|
-
const ID_PROVENANCE = 'openwop.requirement.artifact-types.created-registration-source-enum';
|
|
36
|
-
const ID_REGISTERED = 'openwop.requirement.artifact-types.created-registered-boolean';
|
|
37
|
-
const ID_CLOSED = 'openwop.requirement.artifact-types.created-payload-closed';
|
|
38
|
-
|
|
39
|
-
const GOOD = { artifactId: 'art_1', artifactType: 'vendor.acme.prd', nodeId: 'n1', registered: true, registrationSource: 'host' } as const;
|
|
40
|
-
|
|
41
|
-
const isRecord = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
42
|
-
function enumAt(root: unknown, path: readonly string[]): unknown {
|
|
43
|
-
let cur: unknown = root;
|
|
44
|
-
for (const k of path) cur = isRecord(cur) ? cur[k] : undefined;
|
|
45
|
-
return isRecord(cur) ? cur['enum'] : undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
describe('v2 artifact.created payload (artifact-type-packs.md §Registration)', () => {
|
|
49
|
-
const validate = v2RefValidator('run-event-payloads.schema.json#/$defs/artifactCreated');
|
|
50
|
-
|
|
51
|
-
it('artifact.created is a registered v2 run-event type', () => {
|
|
52
|
-
const r = v2RefValidator('run-event.schema.json#/properties/type')('artifact.created');
|
|
53
|
-
expect(r.ok, req(ID_TYPE, DOC, `artifact.created MUST be accepted by the v2 run-event type union (${r.errors})`)).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('accepts a well-formed payload, with either provenance or none', () => {
|
|
57
|
-
const { registrationSource: _src, ...noSource } = GOOD;
|
|
58
|
-
for (const p of [GOOD, { ...GOOD, registrationSource: 'pack' }, noSource]) {
|
|
59
|
-
const r = validate(p);
|
|
60
|
-
expect(r.ok, req(ID_PROVENANCE, DOC, `artifact.created ${JSON.stringify(p)} MUST validate (${r.errors})`)).toBe(true);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it.each(['artifactId', 'artifactType'])('rejects a payload without %s', (field) => {
|
|
65
|
-
const p: Record<string, unknown> = { ...GOOD };
|
|
66
|
-
delete p[field];
|
|
67
|
-
expect(validate(p).ok, req(ID_REQUIRED, DOC, `artifact.created without ${field} MUST be rejected`)).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('rejects a registrationSource outside pack | host', () => {
|
|
71
|
-
expect(validate({ ...GOOD, registrationSource: 'vendor' }).ok, req(ID_PROVENANCE, DOC, 'registrationSource MUST be pack or host')).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('the discovery per-type registrationSource carries the same vocabulary as the event', () => {
|
|
75
|
-
const payloads: unknown = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'run-event-payloads.schema.json'), 'utf8'));
|
|
76
|
-
const caps: unknown = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'capabilities.schema.json'), 'utf8'));
|
|
77
|
-
const event = enumAt(payloads, ['$defs', 'artifactCreated', 'properties', 'registrationSource']);
|
|
78
|
-
const advert = enumAt(caps, ['properties', 'artifactTypes', 'properties', 'types', 'additionalProperties', 'properties', 'registrationSource']);
|
|
79
|
-
expect(event, req(ID_PROVENANCE, DOC, 'artifact.created.registrationSource MUST be the pack | host enum')).toEqual(['pack', 'host']);
|
|
80
|
-
expect(advert, req(ID_PROVENANCE, 'capabilities.schema.json §artifactTypes.types', 'the advertised per-type registrationSource MUST use the vocabulary artifact.created carries')).toEqual(event);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('rejects a non-boolean registered', () => {
|
|
84
|
-
expect(validate({ ...GOOD, registered: 'true' }).ok, req(ID_REGISTERED, DOC, 'registered MUST be a boolean')).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('rejects an unknown key, and carries a vendor key under the RFC 0185 hatch', () => {
|
|
88
|
-
expect(validate({ ...GOOD, payload: { secret: 1 } }).ok, req(ID_CLOSED, DOC, 'an unknown key on artifact.created MUST be rejected')).toBe(false);
|
|
89
|
-
const r = validate({ ...GOOD, 'x-acme-trace': 'abc', 'vendor.acme.note': 'n' });
|
|
90
|
-
expect(r.ok, req(ID_CLOSED, 'run-event-payloads.schema.json §artifactCreated (RFC 0185)', `a vendor-prefixed key MUST be carried (${r.errors})`)).toBe(true);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2 — variant payload discrimination, statically (RFC 0031 §A). The v1 twin
|
|
3
|
-
* is `envelope-variant-discriminator-static`; the walker lives in
|
|
4
|
-
* `lib/envelope-schema-static.ts`.
|
|
5
|
-
*
|
|
6
|
-
* For every payload schema in `schemas/v2/envelopes/`:
|
|
7
|
-
* no oneOf `oneOf` appears at no depth (Gemini drops it silently, so a
|
|
8
|
-
* model gets a looser schema than declared);
|
|
9
|
-
* discriminated in a variant union — an `anyOf` where two or more branches
|
|
10
|
-
* can be objects — every object branch declares, in `required`,
|
|
11
|
-
* a property of `type: string` with a one-value `enum`; a `$ref`
|
|
12
|
-
* branch is accepted unresolved, as at v1. A type union with at
|
|
13
|
-
* most one object branch (`string | binding`, as
|
|
14
|
-
* `ui.a2ui-surface` payload v2's `dyn*` defs declare) is
|
|
15
|
-
* selected by JSON type and is not judged: a scalar branch has
|
|
16
|
-
* no property to discriminate on. This scoping is new at v2 —
|
|
17
|
-
* the v1 walker flagged every non-`$ref` branch, which no v1
|
|
18
|
-
* schema exercised.
|
|
19
|
-
*
|
|
20
|
-
* No v2 core document states this rule: `spec/v2/core/` carries no text on
|
|
21
|
-
* variant payload discrimination (the nearest is `ext/a2uiSurface/README.md`,
|
|
22
|
-
* whose components are "discriminated by a single-string-enum `component`").
|
|
23
|
-
* The requirement is cited to RFC 0031 §A, its only normative home.
|
|
24
|
-
*
|
|
25
|
-
* Host-free: every leg runs with no OPENWOP_BASE_URL.
|
|
26
|
-
*
|
|
27
|
-
* @see RFCS/0031-envelope-variants-and-model-capabilities.md §A
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { describe, it, expect } from 'vitest';
|
|
31
|
-
import { req } from '../lib/requirement-ids.js';
|
|
32
|
-
import { anyOfDiscriminatorViolations, listV2EnvelopeKinds, loadV2EnvelopeSchema, oneOfViolations, UNIVERSAL_KINDS } from '../lib/envelope-schema-static.js';
|
|
33
|
-
|
|
34
|
-
const DOC = 'RFC 0031 §A';
|
|
35
|
-
const ID_PRESENT = 'openwop.requirement.envelopes.payload-schemas-present';
|
|
36
|
-
const ID_NO_ONEOF = 'openwop.requirement.envelopes.variant-no-oneof';
|
|
37
|
-
const ID_DISCRIMINATED = 'openwop.requirement.envelopes.variant-anyof-discriminated';
|
|
38
|
-
|
|
39
|
-
const KINDS = listV2EnvelopeKinds();
|
|
40
|
-
|
|
41
|
-
describe('v2 envelope variant discrimination (RFC 0031 §A)', () => {
|
|
42
|
-
it('schemas/v2/envelopes carries a payload schema for each universal kind', () => {
|
|
43
|
-
for (const k of UNIVERSAL_KINDS) expect(KINDS.includes(k), req(ID_PRESENT, DOC, `schemas/v2/envelopes/${k}.schema.json MUST exist`)).toBe(true);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
for (const kind of KINDS) {
|
|
47
|
-
it(`${kind} contains no oneOf at any depth`, () => {
|
|
48
|
-
const v = oneOfViolations(loadV2EnvelopeSchema(kind));
|
|
49
|
-
expect(v, req(ID_NO_ONEOF, DOC, `${kind} MUST NOT use oneOf — use anyOf with a single-string-enum discriminator: ${JSON.stringify(v)}`)).toEqual([]);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it(`${kind} discriminates every anyOf branch by a single-string-enum property`, () => {
|
|
53
|
-
const v = anyOfDiscriminatorViolations(loadV2EnvelopeSchema(kind));
|
|
54
|
-
expect(v, req(ID_DISCRIMINATED, DOC, `${kind} anyOf branches without a discriminator: ${JSON.stringify(v)}`)).toEqual([]);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2 — the export bundle, the `import.applied` payload and the `portability`
|
|
3
|
-
* record's `import ⇒ dryRun` rule, statically (`spec/v2/core/portability.md`;
|
|
4
|
-
* RFC 0098; invariant `export-bundle-no-credential-material`). The v1 twin is
|
|
5
|
-
* the server-free half of `export-bundle-portability`; its import-seam leg is
|
|
6
|
-
* not ported (v2 defines no import route).
|
|
7
|
-
*
|
|
8
|
-
* version a bundle carries `bundleVersion: "2"`. This FLIPS the v1 leg:
|
|
9
|
-
* at v1 `"1"` was good and `"2"` rejected; at v2 `"1"` is
|
|
10
|
-
* rejected;
|
|
11
|
-
* shape an unknown item kind and an item without `ref` are rejected;
|
|
12
|
-
* no-cred no credential-named field is admitted at the bundle root or
|
|
13
|
-
* under `source` ("A bundle MUST NOT contain credential values");
|
|
14
|
-
* applied `import.applied` is a registered v2 event type, and its payload
|
|
15
|
-
* is counts and references only, never item payloads;
|
|
16
|
-
* dryRun the capabilities seat refuses `import: true` without
|
|
17
|
-
* `dryRun: true` ("A host advertising `import` MUST advertise
|
|
18
|
-
* `dryRun: true`"). At v2 that `if/then` sits in the record's
|
|
19
|
-
* `allOf`, so it is witnessed by validation, not by reading
|
|
20
|
-
* `portability.if`.
|
|
21
|
-
*
|
|
22
|
-
* Host-free: every leg runs with no OPENWOP_BASE_URL.
|
|
23
|
-
*
|
|
24
|
-
* @see spec/v2/core/portability.md
|
|
25
|
-
* @see schemas/v2/export-bundle.schema.json
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { describe, it, expect } from 'vitest';
|
|
29
|
-
import { v2RefValidator, v2Validator } from '../lib/v2.js';
|
|
30
|
-
import { req } from '../lib/requirement-ids.js';
|
|
31
|
-
|
|
32
|
-
const DOC = 'portability.md §The export bundle';
|
|
33
|
-
const ID_VERSION = 'openwop.requirement.portability.bundle-version-2';
|
|
34
|
-
const ID_SHAPE = 'openwop.requirement.portability.bundle-item-shape';
|
|
35
|
-
const ID_NO_CRED = 'openwop.requirement.portability.bundle-no-credential-field';
|
|
36
|
-
const ID_APPLIED = 'openwop.requirement.portability.import-applied-content-free';
|
|
37
|
-
const ID_DRY_RUN = 'openwop.requirement.portability.import-dry-run-schema-rule';
|
|
38
|
-
|
|
39
|
-
const CRED_NAMES = ['clientSecret', 'client_secret', 'apiKey', 'api_key', 'accessToken', 'refreshToken', 'password', 'privateKey'] as const;
|
|
40
|
-
|
|
41
|
-
const GOOD = {
|
|
42
|
-
bundleVersion: '2',
|
|
43
|
-
source: { origin: 'https://host-a.example', exportedAt: '2026-10-04T00:00:00Z' },
|
|
44
|
-
items: [
|
|
45
|
-
{ kind: 'prompt-template', ref: 'tpl-1', payload: { templateId: 'welcome', version: '1.0.0' } },
|
|
46
|
-
{ kind: 'connection-ref', ref: 'conn-1', dependsOn: ['tpl-1'], payload: { provider: 'slack', credentialRef: 'cred:abc' } },
|
|
47
|
-
],
|
|
48
|
-
} as const;
|
|
49
|
-
|
|
50
|
-
describe('v2 export bundle (portability.md §The export bundle)', () => {
|
|
51
|
-
const validate = v2Validator('export-bundle');
|
|
52
|
-
|
|
53
|
-
it('accepts a conforming bundle at bundleVersion "2"', () => {
|
|
54
|
-
const r = validate(GOOD);
|
|
55
|
-
expect(r.ok, req(ID_VERSION, DOC, `a conforming v2 bundle MUST validate (${r.errors})`)).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('rejects the v1 bundleVersion "1" and a missing bundleVersion', () => {
|
|
59
|
-
expect(validate({ ...GOOD, bundleVersion: '1' }).ok, req(ID_VERSION, DOC, 'a v2 bundle MUST carry bundleVersion "2"; "1" MUST be rejected')).toBe(false);
|
|
60
|
-
const { bundleVersion: _gone, ...rest } = GOOD;
|
|
61
|
-
expect(validate(rest).ok, req(ID_VERSION, DOC, 'a bundle without bundleVersion MUST be rejected')).toBe(false);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('rejects an unknown item kind and an item without ref', () => {
|
|
65
|
-
expect(validate({ ...GOOD, items: [{ kind: 'workflow', ref: 'x', payload: {} }] }).ok, req(ID_SHAPE, DOC, 'an unknown item kind MUST be rejected')).toBe(false);
|
|
66
|
-
expect(validate({ ...GOOD, items: [{ kind: 'agent', payload: {} }] }).ok, req(ID_SHAPE, DOC, 'an item without a ref MUST be rejected')).toBe(false);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it.each(CRED_NAMES)('admits no %s field at the bundle root or under source', (name) => {
|
|
70
|
-
expect(validate({ ...GOOD, [name]: 'xxx' }).ok, req(ID_NO_CRED, DOC, `a "${name}" field at the bundle root MUST NOT validate`)).toBe(false);
|
|
71
|
-
expect(validate({ ...GOOD, source: { ...GOOD.source, [name]: 'xxx' } }).ok, req(ID_NO_CRED, DOC, `a "${name}" field under source MUST NOT validate`)).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('v2 import.applied (portability.md §The import.applied event)', () => {
|
|
76
|
-
const applied = v2RefValidator('run-event-payloads.schema.json#/$defs/importApplied');
|
|
77
|
-
const ADOC = 'portability.md §The import.applied event';
|
|
78
|
-
|
|
79
|
-
it('import.applied is a registered v2 run-event type', () => {
|
|
80
|
-
const r = v2RefValidator('run-event.schema.json#/properties/type')('import.applied');
|
|
81
|
-
expect(r.ok, req(ID_APPLIED, ADOC, `import.applied MUST be accepted by the v2 run-event type union (${r.errors})`)).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('a counts-and-refs payload validates; one carrying item payloads does not', () => {
|
|
85
|
-
const r = applied({ bundleOrigin: 'https://host-a.example', counts: { created: 2, skipped: 1 }, secretsToRebind: ['anthropic'] });
|
|
86
|
-
expect(r.ok, req(ID_APPLIED, ADOC, `a content-free import.applied MUST validate (${r.errors})`)).toBe(true);
|
|
87
|
-
expect(applied({ bundleOrigin: 'h', counts: { created: 1 }, items: [{ payload: {} }] }).ok, req(ID_APPLIED, ADOC, 'import.applied MUST NOT carry item payloads')).toBe(false);
|
|
88
|
-
expect(applied({ bundleOrigin: 'h', counts: { created: 1, leaked: 1 } }).ok, req(ID_APPLIED, ADOC, 'import.applied counts MUST be the four outcome tallies only')).toBe(false);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('v2 portability record: import ⇒ dryRun (portability.md §The portability record)', () => {
|
|
93
|
-
const record = v2RefValidator('capabilities.schema.json#/properties/portability');
|
|
94
|
-
const PDOC = 'portability.md §The portability record';
|
|
95
|
-
const base = { status: 'experimental', since: '2.0', until: '2099-01-01', witness: 'witnessable-gated', export: true } as const;
|
|
96
|
-
|
|
97
|
-
it('import: true with dryRun: true validates; import: false needs no dryRun', () => {
|
|
98
|
-
for (const r of [{ ...base, import: true, dryRun: true }, { ...base, import: false }, { ...base }]) {
|
|
99
|
-
const v = record(r);
|
|
100
|
-
expect(v.ok, req(ID_DRY_RUN, PDOC, `a portability record ${JSON.stringify(r)} MUST validate (${v.errors})`)).toBe(true);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('import: true without dryRun, or with dryRun: false, is rejected', () => {
|
|
105
|
-
expect(record({ ...base, import: true }).ok, req(ID_DRY_RUN, PDOC, 'import: true without dryRun MUST be rejected')).toBe(false);
|
|
106
|
-
expect(record({ ...base, import: true, dryRun: false }).ok, req(ID_DRY_RUN, PDOC, 'import: true with dryRun: false MUST be rejected')).toBe(false);
|
|
107
|
-
});
|
|
108
|
-
});
|