@openwop/openwop-conformance 2.45.17 → 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 +8 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +757 -666
- package/scenario-majors.json +2 -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/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 +43 -7
- 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-provider-usage-advertisement.test.ts +1 -1
- package/src/scenarios/v2-run-snapshot-etag.test.ts +83 -2
- 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
|
@@ -9,14 +9,25 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The revalidation runs once per client spelling (`OpenWOP-Version: 2.0`,
|
|
11
11
|
* `2`, and `2` with a wildcard Accept): a host MUST treat them alike
|
|
12
|
-
* (versioning.md §1),
|
|
12
|
+
* (versioning.md §1), so a host that honours only the suite's own spelling
|
|
13
|
+
* cannot pass by it.
|
|
13
14
|
*
|
|
14
15
|
* Control: a non-matching `If-None-Match` MUST receive 200 with the body — a
|
|
15
16
|
* host that answers 304 to any conditional request fails here. `headers.md`
|
|
16
17
|
* scopes `If-None-Match` to the discovery document; runs.md applies it to the
|
|
17
18
|
* snapshot with a MUST (finding 5, filed).
|
|
18
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
|
+
*
|
|
19
29
|
* @see spec/v2/core/runs.md §Snapshot
|
|
30
|
+
* @see spec/v2/core/runs.md §Caching and encoding
|
|
20
31
|
* @see spec/v2/core/versioning.md §1.4
|
|
21
32
|
*/
|
|
22
33
|
|
|
@@ -27,11 +38,17 @@ import { v2Discovery } from '../lib/v2.js';
|
|
|
27
38
|
import { readErrorCode } from '../lib/error-envelope.js';
|
|
28
39
|
import { softSkip } from '../lib/soft-skip.js';
|
|
29
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';
|
|
30
42
|
|
|
31
43
|
const ID = 'openwop.requirement.0170.run-snapshot-etag';
|
|
32
44
|
const DOC = 'spec/v2/core/runs.md §Snapshot';
|
|
33
45
|
const NOOP = 'conformance-noop';
|
|
34
|
-
|
|
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). */
|
|
35
52
|
const SPELLINGS: ReadonlyArray<Readonly<Record<string, string>>> = [
|
|
36
53
|
{ 'OpenWOP-Version': '2.0' },
|
|
37
54
|
{ 'OpenWOP-Version': '2' },
|
|
@@ -92,4 +109,68 @@ describe('v2 run-snapshot-etag (runs.md §Snapshot)', () => {
|
|
|
92
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);
|
|
93
110
|
expect((miss.json as { runId?: unknown } | null)?.runId, req(ID, DOC, 'the 200 body is the snapshot')).toBe(runId);
|
|
94
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
|
+
});
|
|
95
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
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2 — the `media.*` payload shapes, statically (`spec/v2/core/host-services.md`
|
|
3
|
-
* §aiProviders: "A `media.*` envelope inlines base64 only up to
|
|
4
|
-
* `maxInlineMediaBytes` (default 256 KiB); above that the host MUST use a
|
|
5
|
-
* `url`"; RFC 0055 §C). The v1 twin is the static half of
|
|
6
|
-
* `media-url-inline-cap`. Its advertisement leg (`aiProviders.maxInlineMediaBytes`)
|
|
7
|
-
* is in `v2-ai-providers-advertisement`; its seam legs (the media store and the
|
|
8
|
-
* debug-bundle walk) are not ported.
|
|
9
|
-
*
|
|
10
|
-
* compile the three `media.{image,audio,file}` schemas compile;
|
|
11
|
-
* url a URL-reference payload validates;
|
|
12
|
-
* inline an inline-base64 payload validates;
|
|
13
|
-
* bytes a payload without `bytes` is rejected (`bytes` is what a cap is
|
|
14
|
-
* judged against);
|
|
15
|
-
* closed an unknown property is rejected.
|
|
16
|
-
*
|
|
17
|
-
* Host-free: every leg runs with no OPENWOP_BASE_URL.
|
|
18
|
-
*
|
|
19
|
-
* @see spec/v2/core/host-services.md §aiProviders
|
|
20
|
-
* @see schemas/v2/envelopes/media.image.schema.json
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { describe, it, expect } from 'vitest';
|
|
24
|
-
import { v2Validator } from '../lib/v2.js';
|
|
25
|
-
import { req } from '../lib/requirement-ids.js';
|
|
26
|
-
|
|
27
|
-
const DOC = 'host-services.md §aiProviders';
|
|
28
|
-
const MEDIA_KINDS = ['media.image', 'media.audio', 'media.file'] as const;
|
|
29
|
-
const ID_COMPILE = 'openwop.requirement.media.payload-schemas-compile';
|
|
30
|
-
const ID_URL = 'openwop.requirement.media.url-reference-accepted';
|
|
31
|
-
const ID_INLINE = 'openwop.requirement.media.inline-base64-accepted';
|
|
32
|
-
const ID_BYTES = 'openwop.requirement.media.bytes-required';
|
|
33
|
-
const ID_CLOSED = 'openwop.requirement.media.payload-closed';
|
|
34
|
-
|
|
35
|
-
describe('v2 media payload shapes (host-services.md §aiProviders)', () => {
|
|
36
|
-
for (const kind of MEDIA_KINDS) {
|
|
37
|
-
it(`envelopes/${kind}.schema.json compiles under Ajv 2020`, () => {
|
|
38
|
-
expect(() => v2Validator(`envelopes/${kind}`), req(ID_COMPILE, DOC, `schemas/v2/envelopes/${kind}.schema.json MUST compile`)).not.toThrow();
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
it('accepts a URL-reference image payload', () => {
|
|
43
|
-
const r = v2Validator('envelopes/media.image')({ url: 'https://host.example/runs/run_1/assets/img_9.png', bytes: 184320, mimeType: 'image/png' });
|
|
44
|
-
expect(r.ok, req(ID_URL, DOC, `a URL-reference media payload MUST validate (${r.errors})`)).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('accepts an inline-base64 audio payload', () => {
|
|
48
|
-
const r = v2Validator('envelopes/media.audio')({ base64: 'AAAA', bytes: 3, mimeType: 'audio/ogg', durationSeconds: 1.2 });
|
|
49
|
-
expect(r.ok, req(ID_INLINE, DOC, `an inline-base64 media payload MUST validate (${r.errors})`)).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('rejects a media payload missing bytes', () => {
|
|
53
|
-
expect(v2Validator('envelopes/media.file')({ url: 'https://host.example/runs/run_1/assets/report.pdf' }).ok, req(ID_BYTES, DOC, 'bytes is required on a media payload — it is what the inline cap is judged against')).toBe(false);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('rejects a media payload with an unknown property', () => {
|
|
57
|
-
expect(v2Validator('envelopes/media.image')({ bytes: 1, wat: true }).ok, req(ID_CLOSED, DOC, 'a media payload is closed: an unknown property MUST be rejected')).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2 — the `provider.usage` payload, statically (`spec/v2/core/events.md`
|
|
3
|
-
* §providerUsage; `schemas/v2/run-event-payloads.schema.json#/$defs/providerUsage`;
|
|
4
|
-
* RFC 0026; invariant `provider-usage-no-credential-leak`). The v1 twin is the
|
|
5
|
-
* schema half of `provider-usage`; its emit-seam legs (and the v1
|
|
6
|
-
* `provider_usage_credential_leak` code, which `spec/v2/errors.json` does not
|
|
7
|
-
* carry) are not ported.
|
|
8
|
-
*
|
|
9
|
-
* type `provider.usage` is a registered v2 event type;
|
|
10
|
-
* positive a payload with the four required fields, and one with every
|
|
11
|
-
* optional field, validates;
|
|
12
|
-
* required a payload missing provider, model, inputTokens or outputTokens
|
|
13
|
-
* is rejected;
|
|
14
|
-
* counts a fractional or negative token count is rejected;
|
|
15
|
-
* cost a negative `costEstimateUsd` is rejected; `currency` is an
|
|
16
|
-
* ISO 4217 code ("`providerUsage.currency` is the ISO 4217
|
|
17
|
-
* currency of `costEstimateUsd`");
|
|
18
|
-
* no-leak the payload is closed, so a credential ref, a hashed credential
|
|
19
|
-
* id or prompt/response text is rejected ("The payload MUST NOT
|
|
20
|
-
* carry credential refs, hashed credential identifiers, or prompt
|
|
21
|
-
* or response text").
|
|
22
|
-
*
|
|
23
|
-
* Host-free: every leg runs with no OPENWOP_BASE_URL.
|
|
24
|
-
*
|
|
25
|
-
* @see spec/v2/core/events.md §providerUsage
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { describe, it, expect } from 'vitest';
|
|
29
|
-
import { v2RefValidator } from '../lib/v2.js';
|
|
30
|
-
import { req } from '../lib/requirement-ids.js';
|
|
31
|
-
|
|
32
|
-
const DOC = 'events.md §providerUsage';
|
|
33
|
-
const ID_TYPE = 'openwop.requirement.provider-usage.event-type-registered';
|
|
34
|
-
const ID_POSITIVE = 'openwop.requirement.provider-usage.payload-well-formed-accepted';
|
|
35
|
-
const ID_REQUIRED = 'openwop.requirement.provider-usage.payload-required-fields';
|
|
36
|
-
const ID_COUNTS = 'openwop.requirement.provider-usage.payload-token-counts';
|
|
37
|
-
const ID_COST = 'openwop.requirement.provider-usage.payload-cost-and-currency';
|
|
38
|
-
const ID_NO_LEAK = 'openwop.requirement.provider-usage.payload-no-credential-or-content';
|
|
39
|
-
|
|
40
|
-
const MINIMAL = { provider: 'anthropic', model: 'claude-x', inputTokens: 120, outputTokens: 30 } as const;
|
|
41
|
-
const FULL = { ...MINIMAL, totalTokens: 150, costEstimateUsd: 0.0021, currency: 'EUR', cacheHit: false, cacheReadTokens: 0, cacheWriteTokens: 64 } as const;
|
|
42
|
-
|
|
43
|
-
function without(field: string): Record<string, unknown> {
|
|
44
|
-
const c: Record<string, unknown> = { ...MINIMAL };
|
|
45
|
-
delete c[field];
|
|
46
|
-
return c;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
describe('v2 provider.usage payload (events.md §providerUsage)', () => {
|
|
50
|
-
const validate = v2RefValidator('run-event-payloads.schema.json#/$defs/providerUsage');
|
|
51
|
-
|
|
52
|
-
it('provider.usage is a registered v2 run-event type', () => {
|
|
53
|
-
const r = v2RefValidator('run-event.schema.json#/properties/type')('provider.usage');
|
|
54
|
-
expect(r.ok, req(ID_TYPE, DOC, `provider.usage MUST be accepted by the v2 run-event type union (${r.errors})`)).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('accepts a minimal and a full payload', () => {
|
|
58
|
-
for (const p of [MINIMAL, FULL]) {
|
|
59
|
-
const r = validate(p);
|
|
60
|
-
expect(r.ok, req(ID_POSITIVE, DOC, `a well-formed provider.usage payload MUST validate (${r.errors})`)).toBe(true);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it.each(['provider', 'model', 'inputTokens', 'outputTokens'])('rejects a payload without %s', (field) => {
|
|
65
|
-
expect(validate(without(field)).ok, req(ID_REQUIRED, DOC, `a provider.usage payload without ${field} MUST be rejected`)).toBe(false);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it.each<[string, Record<string, unknown>]>([
|
|
69
|
-
['a fractional inputTokens', { ...MINIMAL, inputTokens: 1.5 }],
|
|
70
|
-
['a negative outputTokens', { ...MINIMAL, outputTokens: -1 }],
|
|
71
|
-
['a string inputTokens', { ...MINIMAL, inputTokens: '120' }],
|
|
72
|
-
])('rejects %s', (what, p) => {
|
|
73
|
-
expect(validate(p).ok, req(ID_COUNTS, DOC, `a provider.usage payload with ${what} MUST be rejected`)).toBe(false);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it.each<[string, Record<string, unknown>]>([
|
|
77
|
-
['a negative costEstimateUsd', { ...MINIMAL, costEstimateUsd: -0.01 }],
|
|
78
|
-
['a lower-case currency', { ...MINIMAL, costEstimateUsd: 0.01, currency: 'usd' }],
|
|
79
|
-
['a currency symbol', { ...MINIMAL, costEstimateUsd: 0.01, currency: '$' }],
|
|
80
|
-
])('rejects %s', (what, p) => {
|
|
81
|
-
expect(validate(p).ok, req(ID_COST, DOC, `a provider.usage payload with ${what} MUST be rejected`)).toBe(false);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it.each<[string, Record<string, unknown>]>([
|
|
85
|
-
['a credential ref', { ...MINIMAL, credentialRef: 'secret:tenant:byok-anthropic:v1' }],
|
|
86
|
-
['a hashed credential id', { ...MINIMAL, credentialHash: 'sha256:9f2c' }],
|
|
87
|
-
['prompt text', { ...MINIMAL, prompt: 'summarize the attached notes' }],
|
|
88
|
-
['response text', { ...MINIMAL, response: 'Here is a summary' }],
|
|
89
|
-
])('rejects a payload carrying %s', (what, p) => {
|
|
90
|
-
expect(validate(p).ok, req(ID_NO_LEAK, DOC, `the provider.usage payload MUST NOT carry ${what}`)).toBe(false);
|
|
91
|
-
});
|
|
92
|
-
});
|