@openwop/openwop-conformance 2.45.15 → 2.45.17
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 +33 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +1335 -14
- package/scenario-majors.json +104 -2
- package/schemas/CORPUS-STAMP.json +13 -13
- package/src/lib/artifact-schema-url-witness.ts +83 -0
- package/src/lib/dispatch-witness.ts +212 -0
- package/src/lib/family-advert-witness.ts +204 -5
- package/src/lib/fixture-run-observer.ts +97 -0
- package/src/lib/prompt-library-witness.ts +11 -2
- package/src/lib/provider-usage-witness.ts +63 -0
- package/src/lib/run-graph-double.ts +144 -0
- package/src/lib/subworkflow-witness.ts +192 -0
- package/src/scenarios/v2-agent-runtime-advertisement.test.ts +51 -0
- package/src/scenarios/v2-ai-envelope-advertisement.test.ts +43 -0
- package/src/scenarios/v2-artifact-created-static.test.ts +92 -0
- package/src/scenarios/v2-artifact-type-schema-url.test.ts +44 -0
- package/src/scenarios/v2-artifact-types-advertisement.test.ts +64 -0
- package/src/scenarios/v2-authorization-advertisement.test.ts +66 -0
- package/src/scenarios/v2-channel-presence-advertisement.test.ts +41 -0
- package/src/scenarios/v2-conversation-turn-model-provenance-advertisement.test.ts +41 -0
- package/src/scenarios/v2-conversation-turn-model-provenance.test.ts +110 -0
- package/src/scenarios/v2-credentials-advertisement.test.ts +66 -0
- package/src/scenarios/v2-data-residency-admission.test.ts +98 -0
- package/src/scenarios/v2-data-residency-advertisement.test.ts +52 -0
- package/src/scenarios/v2-dead-letter-advertisement.test.ts +54 -0
- package/src/scenarios/v2-discovery-etag.test.ts +86 -0
- package/src/scenarios/v2-dispatch-input-mapping.test.ts +70 -0
- package/src/scenarios/v2-dispatch-loop.test.ts +80 -0
- package/src/scenarios/v2-envelope-contracts-advertisement.test.ts +40 -0
- package/src/scenarios/v2-export-bundle-static.test.ts +108 -0
- package/src/scenarios/v2-multi-party-conversation-advertisement.test.ts +52 -0
- package/src/scenarios/v2-node-pack-runtimes-advertisement.test.ts +52 -0
- package/src/scenarios/v2-nondeterminism-policy-advertisement.test.ts +41 -0
- package/src/scenarios/v2-portability-advertisement.test.ts +63 -0
- package/src/scenarios/v2-prompt-library-advertisement.test.ts +40 -0
- package/src/scenarios/v2-prompt-list-and-fetch.test.ts +4 -2
- package/src/scenarios/v2-provider-usage-advertisement.test.ts +61 -0
- package/src/scenarios/v2-provider-usage-emission.test.ts +50 -0
- package/src/scenarios/v2-provider-usage-payload-static.test.ts +92 -0
- package/src/scenarios/v2-purpose-propagation-advertisement.test.ts +52 -0
- package/src/scenarios/v2-run-snapshot-etag.test.ts +35 -22
- package/src/scenarios/v2-scheduling-advertisement.test.ts +55 -0
- package/src/scenarios/v2-self-hosted-runner-advertisement.test.ts +52 -0
- package/src/scenarios/v2-self-hosted-runner-frames-static.test.ts +121 -0
- package/src/scenarios/v2-sub-workflow-advertisement.test.ts +53 -0
- package/src/scenarios/v2-subworkflow-input-mapping.test.ts +74 -0
- package/src/scenarios/v2-subworkflow-linkage.test.ts +76 -0
- package/src/scenarios/v2-ui-plugins-advertisement.test.ts +85 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `envelopeContracts` advertisement (`spec/v2/core/events.md` §Envelope contracts): the envelope contract claim. It had no v1 witness;
|
|
3
|
+
* the legs live in `lib/family-advert-witness.ts` (witness wave 3).
|
|
4
|
+
*
|
|
5
|
+
* record the record validates against its capabilities-schema seat;
|
|
6
|
+
*
|
|
7
|
+
* Not here: the refusal leg needs an error code in the prose and a fixture pack.
|
|
8
|
+
*
|
|
9
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `envelopeContracts` absent, or the
|
|
10
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
11
|
+
*
|
|
12
|
+
* @see spec/v2/core/events.md §Envelope contracts
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect } from 'vitest';
|
|
16
|
+
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
17
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
18
|
+
import { req } from '../lib/requirement-ids.js';
|
|
19
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
20
|
+
import { recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
21
|
+
|
|
22
|
+
const PROFILE = majorProfile(2);
|
|
23
|
+
const DOC = 'events.md §Envelope contracts';
|
|
24
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
25
|
+
const ID_RECORD = 'openwop.requirement.envelope-contracts.advert-record-schema';
|
|
26
|
+
|
|
27
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
28
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('v2 envelopeContracts advertisement (events.md §Envelope contracts)', () => {
|
|
32
|
+
it('the envelopeContracts record validates against its capabilities-schema seat', async () => {
|
|
33
|
+
const doc = await discovery();
|
|
34
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
35
|
+
if (!(await familyAdvertised('envelopeContracts'))) return softSkip('inapplicable', 'the host does not advertise envelopeContracts');
|
|
36
|
+
const out = recordSchemaLeg(PROFILE, doc, 'envelopeContracts', DOC);
|
|
37
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
38
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `multiPartyConversation` advertisement (`spec/v2/core/conversation.md` §`multiPartyConversation`): the multi-party conversation (council) claim. The v1 twin is
|
|
3
|
+
* `multi-party-conversation-shape`;
|
|
4
|
+
* the legs live in `lib/family-advert-witness.ts` (witness wave 3).
|
|
5
|
+
*
|
|
6
|
+
* record the record validates against its capabilities-schema seat;
|
|
7
|
+
* max-participants multiPartyConversation.maxParticipants is an integer of at least 2;
|
|
8
|
+
*
|
|
9
|
+
* Not here: roster enforcement and the over-cap refusal: v2 has no client route that opens a conversation with a roster, and the refusals name no error code (TODO §8).
|
|
10
|
+
*
|
|
11
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `multiPartyConversation` absent, or the
|
|
12
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
13
|
+
*
|
|
14
|
+
* @see spec/v2/core/conversation.md §`multiPartyConversation`
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect } from 'vitest';
|
|
18
|
+
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
19
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
20
|
+
import { req } from '../lib/requirement-ids.js';
|
|
21
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
22
|
+
import { multiPartyMaxParticipantsLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
23
|
+
|
|
24
|
+
const PROFILE = majorProfile(2);
|
|
25
|
+
const DOC = 'conversation.md §multiPartyConversation';
|
|
26
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
27
|
+
const ID_RECORD = 'openwop.requirement.multi-party-conversation.advert-record-schema';
|
|
28
|
+
const ID_MAX_PARTICIPANTS = 'openwop.requirement.multi-party-conversation.max-participants';
|
|
29
|
+
|
|
30
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
31
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('v2 multiPartyConversation advertisement (conversation.md §multiPartyConversation)', () => {
|
|
35
|
+
it('the multiPartyConversation record validates against its capabilities-schema seat', async () => {
|
|
36
|
+
const doc = await discovery();
|
|
37
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
38
|
+
if (!(await familyAdvertised('multiPartyConversation'))) return softSkip('inapplicable', 'the host does not advertise multiPartyConversation');
|
|
39
|
+
const out = recordSchemaLeg(PROFILE, doc, 'multiPartyConversation', DOC);
|
|
40
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
41
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('multiPartyConversation.maxParticipants is an integer of at least 2', async () => {
|
|
45
|
+
const doc = await discovery();
|
|
46
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
47
|
+
if (!(await familyAdvertised('multiPartyConversation'))) return softSkip('inapplicable', 'the host does not advertise multiPartyConversation');
|
|
48
|
+
const out = multiPartyMaxParticipantsLeg(PROFILE, doc);
|
|
49
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
50
|
+
for (const x of out.findings) expect(x.ok, req(ID_MAX_PARTICIPANTS, x.doc, x.message)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `nodePackRuntimes` advertisement (`spec/v2/core/node-pack-runtimes.md` §WASM): the node-pack runtimes a host loads. The v1 twin is
|
|
3
|
+
* `wasm-pack-load`;
|
|
4
|
+
* the legs live in `lib/family-advert-witness.ts` (witness wave 3).
|
|
5
|
+
*
|
|
6
|
+
* record the record validates against its capabilities-schema seat;
|
|
7
|
+
* wasm-facet-shape nodePackRuntimes.wasm lists ABI versions and a memory cap in range;
|
|
8
|
+
*
|
|
9
|
+
* Not here: ABI rejection at load and the memory-cap breach need an operator-installed fixture pack.
|
|
10
|
+
*
|
|
11
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `nodePackRuntimes` absent, or the
|
|
12
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
13
|
+
*
|
|
14
|
+
* @see spec/v2/core/node-pack-runtimes.md §WASM
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect } from 'vitest';
|
|
18
|
+
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
19
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
20
|
+
import { req } from '../lib/requirement-ids.js';
|
|
21
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
22
|
+
import { nodePackRuntimesWasmLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
23
|
+
|
|
24
|
+
const PROFILE = majorProfile(2);
|
|
25
|
+
const DOC = 'node-pack-runtimes.md §WASM';
|
|
26
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
27
|
+
const ID_RECORD = 'openwop.requirement.node-pack-runtimes.advert-record-schema';
|
|
28
|
+
const ID_WASM_FACET_SHAPE = 'openwop.requirement.node-pack-runtimes.wasm-facet-shape';
|
|
29
|
+
|
|
30
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
31
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('v2 nodePackRuntimes advertisement (node-pack-runtimes.md §WASM)', () => {
|
|
35
|
+
it('the nodePackRuntimes record validates against its capabilities-schema seat', async () => {
|
|
36
|
+
const doc = await discovery();
|
|
37
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
38
|
+
if (!(await familyAdvertised('nodePackRuntimes'))) return softSkip('inapplicable', 'the host does not advertise nodePackRuntimes');
|
|
39
|
+
const out = recordSchemaLeg(PROFILE, doc, 'nodePackRuntimes', DOC);
|
|
40
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
41
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('nodePackRuntimes.wasm lists ABI versions and a memory cap in range', async () => {
|
|
45
|
+
const doc = await discovery();
|
|
46
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
47
|
+
if (!(await familyAdvertised('nodePackRuntimes'))) return softSkip('inapplicable', 'the host does not advertise nodePackRuntimes');
|
|
48
|
+
const out = nodePackRuntimesWasmLeg(PROFILE, doc);
|
|
49
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
50
|
+
for (const x of out.findings) expect(x.ok, req(ID_WASM_FACET_SHAPE, x.doc, x.message)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `nondeterminismPolicy` advertisement (`spec/v2/core/replay.md` §Declared nondeterminism): the declared-nondeterminism claim. The v1 twin is
|
|
3
|
+
* `agent-platform-profile`;
|
|
4
|
+
* the legs live in `lib/family-advert-witness.ts` (witness wave 3).
|
|
5
|
+
*
|
|
6
|
+
* record the record validates against its capabilities-schema seat;
|
|
7
|
+
*
|
|
8
|
+
* Not here: recording every declared source needs a `sources[]` seat and a named event (TODO §8).
|
|
9
|
+
*
|
|
10
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `nondeterminismPolicy` absent, or the
|
|
11
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
12
|
+
*
|
|
13
|
+
* @see spec/v2/core/replay.md §Declared nondeterminism
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
18
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
19
|
+
import { req } from '../lib/requirement-ids.js';
|
|
20
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
21
|
+
import { recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
22
|
+
|
|
23
|
+
const PROFILE = majorProfile(2);
|
|
24
|
+
const DOC = 'replay.md §Declared nondeterminism';
|
|
25
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
26
|
+
const ID_RECORD = 'openwop.requirement.nondeterminism-policy.advert-record-schema';
|
|
27
|
+
|
|
28
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
29
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('v2 nondeterminismPolicy advertisement (replay.md §Declared nondeterminism)', () => {
|
|
33
|
+
it('the nondeterminismPolicy record validates against its capabilities-schema seat', async () => {
|
|
34
|
+
const doc = await discovery();
|
|
35
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
36
|
+
if (!(await familyAdvertised('nondeterminismPolicy'))) return softSkip('inapplicable', 'the host does not advertise nondeterminismPolicy');
|
|
37
|
+
const out = recordSchemaLeg(PROFILE, doc, 'nondeterminismPolicy', DOC);
|
|
38
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
39
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `portability` advertisement (`spec/v2/core/portability.md`; RFC
|
|
3
|
+
* 0098). The v1 twin is the advertisement leg of `export-bundle-portability`;
|
|
4
|
+
* the legs live in `lib/family-advert-witness.ts`.
|
|
5
|
+
*
|
|
6
|
+
* record the record validates against its capabilities-schema seat;
|
|
7
|
+
* import⇒dryRun "A host advertising `import` MUST advertise `dryRun: true`",
|
|
8
|
+
* read as the schema reads it: `import: true` ⇒ `dryRun: true`;
|
|
9
|
+
* kinds unique export-bundle item kinds.
|
|
10
|
+
*
|
|
11
|
+
* Not here: import refusal of credential material, dry-run no-write and
|
|
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`.
|
|
14
|
+
*
|
|
15
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `portability` absent, or the
|
|
16
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
17
|
+
*
|
|
18
|
+
* @see spec/v2/core/portability.md §The portability record
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from 'vitest';
|
|
22
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
23
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
24
|
+
import { req } from '../lib/requirement-ids.js';
|
|
25
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
26
|
+
import { portabilityImportDryRunLeg, portabilityKindsLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
27
|
+
|
|
28
|
+
const PROFILE = majorProfile(2);
|
|
29
|
+
const DOC = 'portability.md §The portability record';
|
|
30
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
31
|
+
const ID_RECORD = 'openwop.requirement.portability.advert-record-schema';
|
|
32
|
+
const ID_IMPORT_REQUIRES_DRY_RUN = 'openwop.requirement.portability.import-requires-dry-run';
|
|
33
|
+
const ID_KINDS_SHAPE = 'openwop.requirement.portability.kinds-shape';
|
|
34
|
+
|
|
35
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
36
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('v2 portability advertisement (portability.md §The portability record)', () => {
|
|
40
|
+
it('the portability record validates against its capabilities-schema seat', async () => {
|
|
41
|
+
const doc = await discovery();
|
|
42
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
43
|
+
const out = recordSchemaLeg(PROFILE, doc, 'portability', DOC);
|
|
44
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
45
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('a host advertising portability.import advertises dryRun: true', async () => {
|
|
49
|
+
const doc = await discovery();
|
|
50
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
51
|
+
const out = portabilityImportDryRunLeg(PROFILE, doc);
|
|
52
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
53
|
+
for (const x of out.findings) expect(x.ok, req(ID_IMPORT_REQUIRES_DRY_RUN, x.doc, x.message)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('portability.kinds lists unique export-bundle item kinds', async () => {
|
|
57
|
+
const doc = await discovery();
|
|
58
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
59
|
+
const out = portabilityKindsLeg(PROFILE, doc);
|
|
60
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
61
|
+
for (const x of out.findings) expect(x.ok, req(ID_KINDS_SHAPE, x.doc, x.message)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `promptLibrary` advertisement (`spec/v2/core/host-services.md` §`promptLibrary`): the prompt library claim. It had no v1 witness;
|
|
3
|
+
* the legs live in `lib/family-advert-witness.ts` (witness wave 3).
|
|
4
|
+
*
|
|
5
|
+
* record the record validates against its capabilities-schema seat;
|
|
6
|
+
*
|
|
7
|
+
* Not here: returning a pinned version verbatim needs a fixture pack.
|
|
8
|
+
*
|
|
9
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `promptLibrary` absent, or the
|
|
10
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
11
|
+
*
|
|
12
|
+
* @see spec/v2/core/host-services.md §`promptLibrary`
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect } from 'vitest';
|
|
16
|
+
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
17
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
18
|
+
import { req } from '../lib/requirement-ids.js';
|
|
19
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
20
|
+
import { recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
21
|
+
|
|
22
|
+
const PROFILE = majorProfile(2);
|
|
23
|
+
const DOC = 'host-services.md §promptLibrary';
|
|
24
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
25
|
+
const ID_RECORD = 'openwop.requirement.prompt-library.advert-record-schema';
|
|
26
|
+
|
|
27
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
28
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('v2 promptLibrary advertisement (host-services.md §promptLibrary)', () => {
|
|
32
|
+
it('the promptLibrary record validates against its capabilities-schema seat', async () => {
|
|
33
|
+
const doc = await discovery();
|
|
34
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
35
|
+
if (!(await familyAdvertised('promptLibrary'))) return softSkip('inapplicable', 'the host does not advertise promptLibrary');
|
|
36
|
+
const out = recordSchemaLeg(PROFILE, doc, 'promptLibrary', DOC);
|
|
37
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
38
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -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);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `providerUsage` advertisement (`spec/v2/core/events.md`
|
|
3
|
+
* §providerUsage; RFC 0026). The v1 twin is the advertisement leg of
|
|
4
|
+
* `provider-usage`; the legs live in `lib/family-advert-witness.ts`.
|
|
5
|
+
*
|
|
6
|
+
* record the record validates against its capabilities-schema seat;
|
|
7
|
+
* costEstimates one boolean;
|
|
8
|
+
* currency an ISO 4217 code, `^[A-Z]{3}$` (absent means USD).
|
|
9
|
+
*
|
|
10
|
+
* Not here: exactly-one-per-invocation emission needs a mock-provider run; the
|
|
11
|
+
* payload shape is `v2-provider-usage-payload-static`.
|
|
12
|
+
*
|
|
13
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `providerUsage` absent, or the
|
|
14
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
15
|
+
*
|
|
16
|
+
* @see spec/v2/core/events.md §providerUsage
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect } from 'vitest';
|
|
20
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
21
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
22
|
+
import { req } from '../lib/requirement-ids.js';
|
|
23
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
24
|
+
import { providerUsageCostEstimatesLeg, providerUsageCurrencyLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
25
|
+
|
|
26
|
+
const PROFILE = majorProfile(2);
|
|
27
|
+
const DOC = 'events.md §providerUsage';
|
|
28
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
29
|
+
const ID_RECORD = 'openwop.requirement.provider-usage.advert-record-schema';
|
|
30
|
+
const ID_COST_ESTIMATES_BOOLEAN = 'openwop.requirement.provider-usage.cost-estimates-boolean';
|
|
31
|
+
const ID_CURRENCY_ISO_4217 = 'openwop.requirement.provider-usage.currency-iso-4217';
|
|
32
|
+
|
|
33
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
34
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('v2 providerUsage advertisement (events.md §providerUsage)', () => {
|
|
38
|
+
it('the providerUsage record validates against its capabilities-schema seat', async () => {
|
|
39
|
+
const doc = await discovery();
|
|
40
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
41
|
+
const out = recordSchemaLeg(PROFILE, doc, 'providerUsage', DOC);
|
|
42
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
43
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('providerUsage.costEstimates is one boolean', async () => {
|
|
47
|
+
const doc = await discovery();
|
|
48
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
49
|
+
const out = providerUsageCostEstimatesLeg(PROFILE, doc);
|
|
50
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
51
|
+
for (const x of out.findings) expect(x.ok, req(ID_COST_ESTIMATES_BOOLEAN, x.doc, x.message)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('providerUsage.currency is an ISO 4217 code', async () => {
|
|
55
|
+
const doc = await discovery();
|
|
56
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
57
|
+
const out = providerUsageCurrencyLeg(PROFILE, doc);
|
|
58
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
59
|
+
for (const x of out.findings) expect(x.ok, req(ID_CURRENCY_ISO_4217, x.doc, x.message)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — `provider.usage` emission (`spec/v2/core/events.md` §`providerUsage`:
|
|
3
|
+
* "A host advertising `providerUsage` MUST emit exactly one `provider.usage`
|
|
4
|
+
* per LLM provider invocation, before that node's `node.completed`"). The leg
|
|
5
|
+
* lives in `lib/provider-usage-witness.ts`.
|
|
6
|
+
*
|
|
7
|
+
* A run of `conformance-context-budget-live` (a supervisor on the host's LIVE
|
|
8
|
+
* model — the fixture catalog forbids advertising it with a mock) is created
|
|
9
|
+
* with `POST /runs` and its log read with `events/poll`:
|
|
10
|
+
* - at least one `provider.usage` is on the log;
|
|
11
|
+
* - each precedes a `node.completed` of the node it names;
|
|
12
|
+
* - each payload validates against the v2 `providerUsage` def.
|
|
13
|
+
* "Exactly one per invocation" is NOT asserted: the invocation count is not
|
|
14
|
+
* observable from outside.
|
|
15
|
+
*
|
|
16
|
+
* Dispositions: `providerUsage` not advertised, or the fixture not in
|
|
17
|
+
* `fixtures[]` ⇒ `inapplicable`; the live run ends without completing and with
|
|
18
|
+
* no `provider.usage` ⇒ `blocked`; discovery unreadable ⇒ `blocked`.
|
|
19
|
+
*
|
|
20
|
+
* Proven against a double in `lib/provider-usage-witness.test.ts`.
|
|
21
|
+
*
|
|
22
|
+
* @see spec/v2/core/events.md §providerUsage
|
|
23
|
+
* @see conformance/fixtures.md conformance-context-budget-live
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, it, expect } from 'vitest';
|
|
27
|
+
import { familyAdvertised, v2Discovery, v2RefValidator } from '../lib/v2.js';
|
|
28
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
29
|
+
import { req } from '../lib/requirement-ids.js';
|
|
30
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
31
|
+
import { driveUsage, judgeUsage } from '../lib/provider-usage-witness.js';
|
|
32
|
+
|
|
33
|
+
const PROFILE = majorProfile(2);
|
|
34
|
+
const ID_EMITTED = 'openwop.requirement.provider-usage.emitted-before-node-completed';
|
|
35
|
+
const validate = v2RefValidator('run-event-payloads.schema.json#/$defs/providerUsage');
|
|
36
|
+
|
|
37
|
+
describe('v2 providerUsage: emission on a live model call (events.md §providerUsage)', () => {
|
|
38
|
+
it('a live-model run carries provider.usage, each before its node.completed and each payload valid', async () => {
|
|
39
|
+
if (!process.env['OPENWOP_BASE_URL']) return softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset');
|
|
40
|
+
let doc: Record<string, unknown> | null;
|
|
41
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
42
|
+
if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
|
|
43
|
+
if (!(await familyAdvertised('providerUsage'))) return softSkip('inapplicable', 'the host does not advertise providerUsage');
|
|
44
|
+
const o = await driveUsage(PROFILE, doc);
|
|
45
|
+
if ('kind' in o) return softSkip(o.disposition, o.reason);
|
|
46
|
+
const out = judgeUsage(o, validate);
|
|
47
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
48
|
+
for (const x of out.findings) expect(x.ok, req(ID_EMITTED, x.doc, x.message)).toBe(true);
|
|
49
|
+
}, 330_000);
|
|
50
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
});
|