@openwop/openwop-conformance 2.45.14 → 2.45.16
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 +45 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +2668 -4
- package/scenario-majors.json +164 -2
- package/schemas/CORPUS-STAMP.json +13 -13
- package/src/lib/artifact-schema-url-witness.ts +83 -0
- package/src/lib/backpressure-witness.ts +2 -2
- package/src/lib/budget-witness.ts +3 -3
- package/src/lib/dispatch-witness.ts +212 -0
- package/src/lib/envelope-schema-static.ts +166 -0
- package/src/lib/exhaustion-facet-witness.ts +1 -1
- package/src/lib/family-advert-witness.ts +411 -0
- package/src/lib/fixture-run-observer.ts +97 -0
- package/src/lib/i18n-double.ts +99 -0
- package/src/lib/i18n-negotiation-witness.ts +173 -0
- package/src/lib/localized-content-witness.ts +49 -0
- package/src/lib/model-capability-insufficient-witness.ts +121 -0
- package/src/lib/prompt-double.ts +217 -0
- package/src/lib/prompt-events-witness.ts +211 -0
- package/src/lib/prompt-library-witness.ts +406 -0
- package/src/lib/provider-usage-witness.ts +63 -0
- package/src/lib/run-bounds-witness.ts +111 -0
- package/src/lib/run-graph-double.ts +144 -0
- package/src/lib/scratch-host.ts +4 -1
- package/src/lib/subworkflow-witness.ts +192 -0
- package/src/lib/table-schema-witness.ts +3 -1
- 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-ai-envelope-shape-static.test.ts +93 -0
- package/src/scenarios/v2-ai-providers-advertisement.test.ts +94 -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-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-envelope-reasoning-shape.test.ts +81 -0
- package/src/scenarios/v2-envelope-tier-one-subset-static.test.ts +77 -0
- package/src/scenarios/v2-envelope-variant-discriminator-static.test.ts +57 -0
- package/src/scenarios/v2-envelopes-advertisement.test.ts +85 -0
- package/src/scenarios/v2-export-bundle-static.test.ts +108 -0
- package/src/scenarios/v2-i18n-negotiation.test.ts +91 -0
- package/src/scenarios/v2-limits-envelope-caps-advertisement.test.ts +61 -0
- package/src/scenarios/v2-localized-content-delivery.test.ts +136 -0
- package/src/scenarios/v2-media-inline-cap-static.test.ts +59 -0
- package/src/scenarios/v2-model-capabilities-advertisement.test.ts +61 -0
- package/src/scenarios/v2-model-capability-insufficient.test.ts +63 -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-all-four-kinds-events.test.ts +69 -0
- package/src/scenarios/v2-prompt-end-to-end-events.test.ts +71 -0
- package/src/scenarios/v2-prompt-library-advertisement.test.ts +40 -0
- package/src/scenarios/v2-prompt-list-and-fetch.test.ts +103 -0
- package/src/scenarios/v2-prompt-mutable-lifecycle.test.ts +130 -0
- package/src/scenarios/v2-prompt-pack-install.test.ts +78 -0
- package/src/scenarios/v2-prompt-render-deterministic.test.ts +87 -0
- package/src/scenarios/v2-prompt-resolution-chain-event.test.ts +46 -0
- package/src/scenarios/v2-prompt-template-shape.test.ts +112 -0
- 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-execution-bounds.test.ts +67 -0
- 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,57 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `envelopes` advertisement (`spec/v2/core/events.md` §`envelopes`;
|
|
3
|
+
* RFCs 0030, 0032, 0033). A claims-check family: what the host says about its
|
|
4
|
+
* envelope posture is what is witnessed. The v1 twins are the advertisement
|
|
5
|
+
* legs of `envelope-reasoning-shape`, `envelope-retry-attempted` /
|
|
6
|
+
* `envelope-refusal-shape` and `envelope-completion-distinguishes-truncation`;
|
|
7
|
+
* the legs live in `lib/family-advert-witness.ts`.
|
|
8
|
+
*
|
|
9
|
+
* record the record validates against its seat in
|
|
10
|
+
* `schemas/v2/capabilities.schema.json` (no v1 `supported` seat);
|
|
11
|
+
* reasoning `reasoning.promptDirective` is mandatory | advisory | off;
|
|
12
|
+
* tier-one `tierOneSubsetCompliance` is strict | warn | off;
|
|
13
|
+
* reliability `reliability.events[]` names only reliability events and, with
|
|
14
|
+
* the four v1 dotted names folded to their v2 equivalents
|
|
15
|
+
* (aliases until 3.0, RFC 0228 §G), lists the two MUST-tier
|
|
16
|
+
* events `envelope.retry-exhausted` and `envelope.refusal`;
|
|
17
|
+
* completion `reliability.completion.distinguishesTruncation` is a boolean
|
|
18
|
+
* and `truncationBudgetMultiplier` is in [1, 8].
|
|
19
|
+
*
|
|
20
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `envelopes` absent, or the
|
|
21
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
22
|
+
*
|
|
23
|
+
* @see spec/v2/core/events.md §envelopes
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, it, expect } from 'vitest';
|
|
27
|
+
import { v2Discovery } 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 { envelopesCompletionLeg, envelopesReasoningLeg, envelopesReliabilityLeg, envelopesTierOneLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
32
|
+
|
|
33
|
+
const PROFILE = majorProfile(2);
|
|
34
|
+
const ID_RECORD = 'openwop.requirement.envelopes.advert-record-schema';
|
|
35
|
+
const ID_REASONING = 'openwop.requirement.envelopes.reasoning-advert-shape';
|
|
36
|
+
const ID_TIER_ONE = 'openwop.requirement.envelopes.tier-one-subset-compliance-value';
|
|
37
|
+
const ID_RELIABILITY = 'openwop.requirement.envelopes.reliability-events-advert';
|
|
38
|
+
const ID_COMPLETION = 'openwop.requirement.envelopes.completion-advert-shape';
|
|
39
|
+
|
|
40
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
41
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
44
|
+
|
|
45
|
+
describe('v2 envelopes advertisement (events.md §envelopes)', () => {
|
|
46
|
+
it('the envelopes record validates against its capabilities-schema seat', async () => {
|
|
47
|
+
const doc = await discovery();
|
|
48
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
49
|
+
const out = recordSchemaLeg(PROFILE, doc, 'envelopes', 'events.md §envelopes');
|
|
50
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
51
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('envelopes.reasoning.promptDirective is mandatory, advisory or off', async () => {
|
|
55
|
+
const doc = await discovery();
|
|
56
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
57
|
+
const out = envelopesReasoningLeg(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_REASONING, x.doc, x.message)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('envelopes.tierOneSubsetCompliance is strict, warn or off', async () => {
|
|
63
|
+
const doc = await discovery();
|
|
64
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
65
|
+
const out = envelopesTierOneLeg(PROFILE, doc);
|
|
66
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
67
|
+
for (const x of out.findings) expect(x.ok, req(ID_TIER_ONE, x.doc, x.message)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('envelopes.reliability.events lists only reliability events and includes retry-exhausted and refusal', async () => {
|
|
71
|
+
const doc = await discovery();
|
|
72
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
73
|
+
const out = envelopesReliabilityLeg(PROFILE, doc);
|
|
74
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
75
|
+
for (const x of out.findings) expect(x.ok, req(ID_RELIABILITY, x.doc, x.message)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('envelopes.reliability.completion.distinguishesTruncation is a boolean', async () => {
|
|
79
|
+
const doc = await discovery();
|
|
80
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
81
|
+
const out = envelopesCompletionLeg(PROFILE, doc);
|
|
82
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
83
|
+
for (const x of out.findings) expect(x.ok, req(ID_COMPLETION, x.doc, x.message)).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — locale negotiation (`spec/v2/core/i18n.md` §`Accept-Language`,
|
|
3
|
+
* §Fallback, §Error envelopes, §The `i18n` record). The v1 twin is
|
|
4
|
+
* `i18n-negotiation`; the legs live in `lib/i18n-negotiation-witness.ts`, and
|
|
5
|
+
* what differs between majors (the unversioned run path, the `~`-projected
|
|
6
|
+
* bound id, presence-as-advertisement) is the profile row.
|
|
7
|
+
*
|
|
8
|
+
* shape the `i18n` record validates against the v2 capabilities
|
|
9
|
+
* schema, and `supportedLocales` contains `defaultLocale`;
|
|
10
|
+
* unsupported an unsupported `Accept-Language` is answered exactly as the
|
|
11
|
+
* same request without it; `Content-Language`, when sent, is a
|
|
12
|
+
* BCP 47 tag naming the default locale;
|
|
13
|
+
* malformed a malformed `Accept-Language` never yields `400`;
|
|
14
|
+
* error code the `error` code and the `details` keys are identical under
|
|
15
|
+
* the default and a negotiated locale.
|
|
16
|
+
*
|
|
17
|
+
* The probe reads a run id no host minted (`GET /runs/{bound id}`). Its answer
|
|
18
|
+
* is not pinned — `404 not_found`, or `403 id_tenant_mismatch` because the
|
|
19
|
+
* tenant segment is not the caller's — since every leg compares against the
|
|
20
|
+
* same request with no `Accept-Language`.
|
|
21
|
+
*
|
|
22
|
+
* Dispositions: no target ⇒ `inapplicable`; discovery unreadable ⇒ `blocked`;
|
|
23
|
+
* `i18n` not advertised ⇒ `inapplicable` (gated with the record's presence,
|
|
24
|
+
* never a strict-mode failure); a probe that answers no 4xx envelope ⇒
|
|
25
|
+
* `blocked` (no baseline).
|
|
26
|
+
*
|
|
27
|
+
* Proven against a scratch double in `lib/i18n-negotiation-witness.test.ts`.
|
|
28
|
+
*
|
|
29
|
+
* @see spec/v2/core/i18n.md
|
|
30
|
+
* @see schemas/v2/capabilities.schema.json §i18n
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { describe, it, expect } from 'vitest';
|
|
34
|
+
import { familyAdvertised, v2Discovery, v2RefValidator } from '../lib/v2.js';
|
|
35
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
36
|
+
import { req } from '../lib/requirement-ids.js';
|
|
37
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
38
|
+
import { errorCodeLeg, i18nAdverts, malformedLeg, shapeLeg, unsupportedLeg, type I18nAdverts } from '../lib/i18n-negotiation-witness.js';
|
|
39
|
+
|
|
40
|
+
const PROFILE = majorProfile(2);
|
|
41
|
+
const ID_SHAPE = 'openwop.requirement.i18n.record-shape';
|
|
42
|
+
const ID_UNSUPPORTED = 'openwop.requirement.i18n.unsupported-locale-falls-back';
|
|
43
|
+
const ID_MALFORMED = 'openwop.requirement.i18n.malformed-accept-language-not-400';
|
|
44
|
+
const ID_CODE = 'openwop.requirement.i18n.error-code-locale-invariant';
|
|
45
|
+
const HTTP_SKIP = !process.env['OPENWOP_BASE_URL'];
|
|
46
|
+
const validRecord = v2RefValidator('capabilities.schema.json#/properties/i18n');
|
|
47
|
+
|
|
48
|
+
type Ready = { ok: true; a: I18nAdverts } | { ok: false; skip: () => undefined };
|
|
49
|
+
async function ready(): Promise<Ready> {
|
|
50
|
+
if (HTTP_SKIP) return { ok: false, skip: () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset') };
|
|
51
|
+
let doc: Record<string, unknown> | null;
|
|
52
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
53
|
+
if (!doc) return { ok: false, skip: () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0') };
|
|
54
|
+
if ((await familyAdvertised('i18n')) === null) return { ok: false, skip: () => softSkip('inapplicable', 'the host does not advertise i18n — it serves one locale and ignores Accept-Language (i18n.md §The i18n record)') };
|
|
55
|
+
return { ok: true, a: i18nAdverts(PROFILE, doc) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('v2 i18n negotiation (i18n.md §Accept-Language, §Fallback, §Error envelopes)', () => {
|
|
59
|
+
it('the advertised i18n record is well-formed and lists its default locale', async () => {
|
|
60
|
+
const r = await ready();
|
|
61
|
+
if (!r.ok) return r.skip();
|
|
62
|
+
const v = validRecord(r.a.record);
|
|
63
|
+
const out = shapeLeg(r.a);
|
|
64
|
+
expect(v.ok, req(ID_SHAPE, 'schemas/v2/capabilities.schema.json §i18n', `the i18n record MUST validate: ${v.errors}`)).toBe(true);
|
|
65
|
+
if (out.kind === 'observed') for (const x of out.findings) expect(x.ok, req(ID_SHAPE, x.doc, x.message)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('an unsupported Accept-Language falls back and Content-Language never lies', async () => {
|
|
69
|
+
const r = await ready();
|
|
70
|
+
if (!r.ok) return r.skip();
|
|
71
|
+
const out = await unsupportedLeg(PROFILE, r.a);
|
|
72
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
73
|
+
for (const x of out.findings) expect(x.ok, req(ID_UNSUPPORTED, x.doc, x.message)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('a malformed Accept-Language does not cause 400', async () => {
|
|
77
|
+
const r = await ready();
|
|
78
|
+
if (!r.ok) return r.skip();
|
|
79
|
+
const out = await malformedLeg(PROFILE, r.a);
|
|
80
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
81
|
+
for (const x of out.findings) expect(x.ok, req(ID_MALFORMED, x.doc, x.message)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('the error code and details keys are identical under every negotiated locale', async () => {
|
|
85
|
+
const r = await ready();
|
|
86
|
+
if (!r.ok) return r.skip();
|
|
87
|
+
const out = await errorCodeLeg(PROFILE, r.a);
|
|
88
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
89
|
+
for (const x of out.findings) expect(x.ok, req(ID_CODE, x.doc, x.message)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the three envelope caps on `limits` (`spec/v2/core/runs.md` §Limits:
|
|
3
|
+
* "`limits` always carries `clarificationRounds` (per task), `schemaRounds`
|
|
4
|
+
* (per envelope) and `envelopesPerTurn` (per chat turn)"). The v1 twin is the
|
|
5
|
+
* advertisement half of `aiEnvelope.capBreached`; the legs live in
|
|
6
|
+
* `lib/family-advert-witness.ts`. The other `limits` facets
|
|
7
|
+
* (`maxRunDurationMs`, `maxLoopIterations`) are `v2-run-execution-bounds`.
|
|
8
|
+
*
|
|
9
|
+
* Each cap MUST be present and validate against its subschema in
|
|
10
|
+
* `schemas/v2/capabilities.schema.json` (a non-negative integer).
|
|
11
|
+
*
|
|
12
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `limits` absent ⇒
|
|
13
|
+
* `inapplicable` (at v2 presence of the record is the claim; the v1 rule that
|
|
14
|
+
* `limits` itself is always present has no v2 seat — the root schema requires
|
|
15
|
+
* only `protocolVersions` and `preferredVersion`).
|
|
16
|
+
*
|
|
17
|
+
* @see spec/v2/core/runs.md §Limits
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from 'vitest';
|
|
21
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
22
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
23
|
+
import { req } from '../lib/requirement-ids.js';
|
|
24
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
25
|
+
import { limitsCapLeg } from '../lib/family-advert-witness.js';
|
|
26
|
+
|
|
27
|
+
const PROFILE = majorProfile(2);
|
|
28
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
29
|
+
const ID_CLARIFICATION = 'openwop.requirement.limits.clarification-rounds-advertised';
|
|
30
|
+
const ID_SCHEMA = 'openwop.requirement.limits.schema-rounds-advertised';
|
|
31
|
+
const ID_ENVELOPES = 'openwop.requirement.limits.envelopes-per-turn-advertised';
|
|
32
|
+
|
|
33
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
34
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('v2 limits envelope caps (runs.md §Limits)', () => {
|
|
38
|
+
it('limits.clarificationRounds is present and a non-negative integer', async () => {
|
|
39
|
+
const doc = await discovery();
|
|
40
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
41
|
+
const out = limitsCapLeg(PROFILE, doc, 'clarificationRounds');
|
|
42
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
43
|
+
for (const x of out.findings) expect(x.ok, req(ID_CLARIFICATION, x.doc, x.message)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('limits.schemaRounds is present and a non-negative integer', async () => {
|
|
47
|
+
const doc = await discovery();
|
|
48
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
49
|
+
const out = limitsCapLeg(PROFILE, doc, 'schemaRounds');
|
|
50
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
51
|
+
for (const x of out.findings) expect(x.ok, req(ID_SCHEMA, x.doc, x.message)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('limits.envelopesPerTurn is present and a non-negative integer', async () => {
|
|
55
|
+
const doc = await discovery();
|
|
56
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
57
|
+
const out = limitsCapLeg(PROFILE, doc, 'envelopesPerTurn');
|
|
58
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
59
|
+
for (const x of out.findings) expect(x.ok, req(ID_ENVELOPES, x.doc, x.message)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — localized content (`spec/v2/core/i18n.md` §Localized content;
|
|
3
|
+
* `schemas/v2/localized-content-*.schema.json`; RFCs 0103, 0206). The v1 twin
|
|
4
|
+
* is `localized-content-delivery`.
|
|
5
|
+
*
|
|
6
|
+
* Two layers:
|
|
7
|
+
*
|
|
8
|
+
* A. Server-free — the v2 `content` capability record (no `supported` seat;
|
|
9
|
+
* presence is the claim), the four v2 content schemas, and the section
|
|
10
|
+
* merge every host MUST resolve identically (exact tag → `ll-Ssss` script
|
|
11
|
+
* family → language → `data`, shallow overlay), against the suite's one
|
|
12
|
+
* reference copy in `lib/localized-content.ts`.
|
|
13
|
+
*
|
|
14
|
+
* B. Live — the advertised `content` record is coherent with the advertised
|
|
15
|
+
* `i18n` record (`lib/localized-content-witness.ts`, a pure judge proven in
|
|
16
|
+
* `localized-content-witness.test.ts`). Gated by the presence of the v2
|
|
17
|
+
* `content` record: absent ⇒ `inapplicable`.
|
|
18
|
+
*
|
|
19
|
+
* Not ported here: the `/content/*` delivery legs (published-only, tenant
|
|
20
|
+
* isolation, no enumeration); v1 never drove them either.
|
|
21
|
+
*
|
|
22
|
+
* @see spec/v2/core/i18n.md §Localized content
|
|
23
|
+
* @see SECURITY/invariants.yaml id: content-published-cache-no-draft, content-response-tenant-scoped
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, it, expect } from 'vitest';
|
|
27
|
+
import { readFileSync } from 'node:fs';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
30
|
+
import { familyAdvertised, v2Discovery, v2RefValidator, v2Validator } from '../lib/v2.js';
|
|
31
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
32
|
+
import { req } from '../lib/requirement-ids.js';
|
|
33
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
34
|
+
import { resolveSection, type Section } from '../lib/localized-content.js';
|
|
35
|
+
import { coherenceLeg } from '../lib/localized-content-witness.js';
|
|
36
|
+
|
|
37
|
+
const PROFILE = majorProfile(2);
|
|
38
|
+
const DOC = 'spec/v2/core/i18n.md §Localized content';
|
|
39
|
+
const ID_RECORD = 'openwop.requirement.content.capability-record-shape';
|
|
40
|
+
const ID_SECTION = 'openwop.requirement.content.section-schema';
|
|
41
|
+
const ID_PAGE = 'openwop.requirement.content.page-schema';
|
|
42
|
+
const ID_SETTINGS = 'openwop.requirement.content.language-settings-schema';
|
|
43
|
+
const ID_RESPONSE = 'openwop.requirement.content.page-response-schema';
|
|
44
|
+
const ID_MERGE = 'openwop.requirement.content.section-merge';
|
|
45
|
+
const ID_COHERENT = 'openwop.requirement.content.advertisement-coherent';
|
|
46
|
+
const HTTP_SKIP = !process.env['OPENWOP_BASE_URL'];
|
|
47
|
+
|
|
48
|
+
const caps = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'capabilities.schema.json'), 'utf8')) as { properties: Record<string, { required?: string[]; properties?: Record<string, unknown> }> };
|
|
49
|
+
const section = v2Validator('localized-content-section');
|
|
50
|
+
const page = v2Validator('localized-content-page');
|
|
51
|
+
const settings = v2Validator('localized-content-language-settings');
|
|
52
|
+
const response = v2Validator('localized-content-page-response');
|
|
53
|
+
const contentRecord = v2RefValidator('capabilities.schema.json#/properties/content');
|
|
54
|
+
|
|
55
|
+
const goodSection = {
|
|
56
|
+
sectionId: 'hero', sectionType: 'hero', data: { heading: 'Welcome', cta: 'Get started' },
|
|
57
|
+
localizations: { es: { heading: 'Bienvenido', cta: 'Empezar' }, 'pt-BR': { heading: 'Bem-vindo' } },
|
|
58
|
+
status: 'published', enabled: true, order: 0,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe('v2 localized content: the capability record (server-free)', () => {
|
|
62
|
+
it('the v2 content record requires baseLocale and supportedLocales and has no supported seat', () => {
|
|
63
|
+
const content = caps.properties['content'];
|
|
64
|
+
expect(content?.required, req(ID_RECORD, 'schemas/v2/capabilities.schema.json §content', 'content MUST require baseLocale and supportedLocales')).toEqual(expect.arrayContaining(['baseLocale', 'supportedLocales']));
|
|
65
|
+
expect(content?.properties?.['supported'], req(ID_RECORD, 'spec/v2/core/capabilities.md (presence is the claim)', 'the v2 content record MUST NOT declare a supported seat')).toBeUndefined();
|
|
66
|
+
const base = { status: 'experimental', since: '2.0', until: '2.9', witness: 'witnessable-gated' };
|
|
67
|
+
expect(contentRecord({ ...base, baseLocale: 'en', supportedLocales: ['es'] }).ok, req(ID_RECORD, 'schemas/v2/capabilities.schema.json §content', 'a conforming content record MUST validate')).toBe(true);
|
|
68
|
+
expect(contentRecord({ ...base, supported: true, baseLocale: 'en', supportedLocales: ['es'] }).ok, req(ID_RECORD, 'schemas/v2/capabilities.schema.json §content', 'a content record carrying supported MUST be rejected (closed record)')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('v2 localized content: schema shapes (server-free)', () => {
|
|
73
|
+
it('a conforming section validates; a non-canonical or underscore locale key, a missing status, or an unknown status is rejected', () => {
|
|
74
|
+
expect(section(goodSection).ok, req(ID_SECTION, DOC, `a conforming section MUST validate: ${section(goodSection).errors}`)).toBe(true);
|
|
75
|
+
for (const key of ['zh-Hans', 'zh-Hant-TW', 'es-419', 'fil']) {
|
|
76
|
+
expect(section({ ...goodSection, localizations: { [key]: { heading: 'x' } } }).ok, req(ID_SECTION, 'RFC 0206 §A.1', `${key} MUST validate as a localizations key`)).toBe(true);
|
|
77
|
+
}
|
|
78
|
+
for (const key of ['EN', 'en_US', 'en-us', 'zh-hans', 'de-CH-1996']) {
|
|
79
|
+
expect(section({ ...goodSection, localizations: { [key]: { heading: 'x' } } }).ok, req(ID_SECTION, 'RFC 0206 §A.1', `${key} MUST be rejected as a localizations key`)).toBe(false);
|
|
80
|
+
}
|
|
81
|
+
const { status: _omit, ...noStatus } = goodSection;
|
|
82
|
+
expect(section(noStatus).ok, req(ID_SECTION, DOC, 'status is REQUIRED')).toBe(false);
|
|
83
|
+
expect(section({ ...goodSection, status: 'archived' }).ok, req(ID_SECTION, DOC, 'status MUST be draft or published')).toBe(false);
|
|
84
|
+
expect(section({ ...goodSection, extra: true }).ok, req(ID_SECTION, DOC, 'a section is a closed record')).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('a conforming page validates and a bad slug is rejected', () => {
|
|
88
|
+
const good = { pageId: 'home', slug: 'home', name: 'Home', status: 'published', sectionOrder: ['hero'] };
|
|
89
|
+
expect(page(good).ok, req(ID_PAGE, DOC, `a conforming page MUST validate: ${page(good).errors}`)).toBe(true);
|
|
90
|
+
expect(page({ ...good, slug: 'Home Page' }).ok, req(ID_PAGE, DOC, 'slug MUST match ^[a-z][a-z0-9-]*$')).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('a conforming language-settings document validates', () => {
|
|
94
|
+
const good = { baseLocale: 'en', supportedLocales: ['es', 'pt-BR', 'fr'], autoTranslateOnPublish: false };
|
|
95
|
+
expect(settings(good).ok, req(ID_SETTINGS, DOC, `settings MUST validate: ${settings(good).errors}`)).toBe(true);
|
|
96
|
+
expect(settings({ ...good, baseLocale: 'en_US' }).ok, req(ID_SETTINGS, DOC, 'baseLocale MUST be a canonical BCP 47 key')).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('a conforming page response validates', () => {
|
|
100
|
+
const good = {
|
|
101
|
+
version: '1', generatedAt: '2026-06-17T00:00:00Z', locale: 'pt-BR', slug: 'home',
|
|
102
|
+
page: { pageId: 'home', slug: 'home', name: 'Home' },
|
|
103
|
+
sections: [{ sectionId: 'hero', sectionType: 'hero', data: { heading: 'Bem-vindo', cta: 'Get started' } }],
|
|
104
|
+
};
|
|
105
|
+
expect(response(good).ok, req(ID_RESPONSE, DOC, `a resolved response MUST validate: ${response(good).errors}`)).toBe(true);
|
|
106
|
+
expect(response({ ...good, sections: [{ ...good.sections[0], localizations: {} }] }).ok, req(ID_RESPONSE, DOC, 'a delivered section carries resolved data, never its localizations')).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('v2 localized content: the section merge every host resolves identically (server-free)', () => {
|
|
111
|
+
it('exact tag, then script family, then language, then data — a shallow overlay', () => {
|
|
112
|
+
const s: Section = { data: { heading: 'Welcome', cta: 'Get started' }, localizations: { es: { heading: 'Bienvenido', cta: 'Empezar' }, 'pt-BR': { heading: 'Bem-vindo' } } };
|
|
113
|
+
expect(resolveSection(s, 'es', 'en'), req(ID_MERGE, DOC, 'an exact hit MUST overlay its fields onto data')).toEqual({ heading: 'Bienvenido', cta: 'Empezar' });
|
|
114
|
+
expect(resolveSection(s, 'pt-BR', 'en'), req(ID_MERGE, DOC, 'a field the overlay lacks MUST fall through to data')).toEqual({ heading: 'Bem-vindo', cta: 'Get started' });
|
|
115
|
+
expect(resolveSection({ data: { h: 'Hi' }, localizations: { pt: { h: 'Oi' } } }, 'pt-BR', 'en'), req(ID_MERGE, DOC, 'pt-BR MUST fall back to the pt language overlay')).toEqual({ h: 'Oi' });
|
|
116
|
+
expect(resolveSection({ data: { h: 'x' }, localizations: { 'zh-Hant': { h: '繁' }, zh: { h: '简' } } }, 'zh-Hant-TW', 'en'), req(ID_MERGE, DOC, 'zh-Hant-TW MUST take the zh-Hant script family before zh')).toEqual({ h: '繁' });
|
|
117
|
+
expect(resolveSection(s, 'de', 'en'), req(ID_MERGE, DOC, 'no overlay MUST resolve to data alone')).toEqual(s.data);
|
|
118
|
+
expect(resolveSection(s, 'en', 'en'), req(ID_MERGE, DOC, 'baseLocale MUST resolve to data')).toEqual(s.data);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('v2 localized content: live advertisement coherence (i18n.md §Localized content "Advertisement")', () => {
|
|
123
|
+
it('the advertised content record is coherent with the advertised i18n record', async () => {
|
|
124
|
+
if (HTTP_SKIP) return softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset');
|
|
125
|
+
let doc: Record<string, unknown> | null;
|
|
126
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
127
|
+
if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
|
|
128
|
+
const content = await familyAdvertised('content');
|
|
129
|
+
if (content === null) return softSkip('inapplicable', 'the host does not advertise content');
|
|
130
|
+
const out = coherenceLeg(PROFILE, doc);
|
|
131
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
132
|
+
const v = contentRecord(content);
|
|
133
|
+
expect(v.ok, req(ID_COHERENT, 'schemas/v2/capabilities.schema.json §content', `the content record MUST validate: ${v.errors}`)).toBe(true);
|
|
134
|
+
for (const x of out.findings) expect(x.ok, req(ID_COHERENT, x.doc, x.message)).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
});
|