@openwop/openwop-conformance 2.46.35 → 2.46.37
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 +14 -0
- package/README.md +3 -3
- package/coverage.md +5 -6
- package/dist/spec-artifacts.lock.json +2 -2
- package/fixtures/conformance-clarification.json +1 -1
- package/fixtures/conformance-interrupt-form-refused.json +108 -0
- package/fixtures/conformance-interrupt-form-unbound.json +108 -0
- package/fixtures/conformance-interrupt-form.json +107 -0
- package/fixtures/conformance-stream-text.json +1 -2
- package/fixtures.md +20 -5
- package/package.json +2 -2
- package/requirement-aliases.json +98 -1
- package/requirements.json +596 -804
- package/scenario-majors.json +16 -19
- package/schemas/CORPUS-STAMP.json +56 -69
- package/src/lib/callai-attempts-witness.ts +13 -1
- package/src/lib/family-advert-witness.ts +37 -50
- package/src/scenarios/stream-text-fixture.test.ts +13 -5
- package/src/scenarios/v2-clarification-resume-answers.test.ts +66 -0
- package/src/scenarios/v2-envelope-surfaces-absent.test.ts +55 -0
- package/src/scenarios/v2-interrupt-form-rules.test.ts +143 -0
- package/src/scenarios/v2-limits-clarification-rounds-advertisement.test.ts +42 -0
- package/src/scenarios/v2-peer-dependency-declared.test.ts +1 -1
- package/src/scenarios/v2-provider-output-reliability.test.ts +94 -0
- package/src/scenarios/v2-structured-output-advertisement.test.ts +82 -0
- package/src/lib/envelope-schema-static.ts +0 -167
- package/src/scenarios/v2-a2ui-v09-surface.test.ts +0 -323
- package/src/scenarios/v2-ai-envelope-advertisement.test.ts +0 -43
- package/src/scenarios/v2-envelope-contracts-advertisement.test.ts +0 -48
- package/src/scenarios/v2-envelope-reasoning-shape.test.ts +0 -81
- package/src/scenarios/v2-envelope-tier-one-subset-static.test.ts +0 -64
- package/src/scenarios/v2-envelopes-advertisement.test.ts +0 -85
- package/src/scenarios/v2-limits-envelope-caps-advertisement.test.ts +0 -61
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Static walkers over envelope payload schemas, shared by the v2 static twins:
|
|
3
|
-
* the scenario `v2-envelope-tier-one-subset-static` and the corpus gates
|
|
4
|
-
* `v2-envelope-tier-one-universal-static` and `v2-envelope-variant-discriminator-static`.
|
|
5
|
-
*
|
|
6
|
-
* The walk rules are the v1 rules unchanged (RFC 0030 §B, RFC 0031 §A); what
|
|
7
|
-
* moved at major 2 is where the schemas live (`schemas/v2/envelopes/`) and
|
|
8
|
-
* where a host's kind catalog is advertised (`supportedEnvelopes.kinds`, a
|
|
9
|
-
* family record, instead of a root `string[]`).
|
|
10
|
-
*
|
|
11
|
-
* Every function here is pure and is proven in both directions in
|
|
12
|
-
* `envelope-schema-static.test.ts`: a conforming schema yields no violation,
|
|
13
|
-
* and each defect yields the one it names.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
import { SCHEMAS_DIR } from './paths.js';
|
|
19
|
-
|
|
20
|
-
export interface SchemaViolation { readonly path: string; readonly rule: string; readonly detail?: string }
|
|
21
|
-
|
|
22
|
-
const isRecord = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
23
|
-
|
|
24
|
-
/** The v2 envelope payload schema directory. */
|
|
25
|
-
export const V2_ENVELOPES_DIR = join(SCHEMAS_DIR, 'v2', 'envelopes');
|
|
26
|
-
|
|
27
|
-
/** The four kinds every engine recognizes (`events.md` §"AI envelopes"). */
|
|
28
|
-
export const UNIVERSAL_KINDS = ['clarification.request', 'schema.request', 'schema.response', 'error'] as const;
|
|
29
|
-
|
|
30
|
-
/** A v2 envelope payload schema by kind, or `null` when the corpus has none. */
|
|
31
|
-
export function loadV2EnvelopeSchema(kind: string): Record<string, unknown> | null {
|
|
32
|
-
const p = join(V2_ENVELOPES_DIR, `${kind}.schema.json`);
|
|
33
|
-
if (!existsSync(p)) return null;
|
|
34
|
-
const v: unknown = JSON.parse(readFileSync(p, 'utf8'));
|
|
35
|
-
return isRecord(v) ? v : null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Every kind with a payload schema under `schemas/v2/envelopes/`. */
|
|
39
|
-
export function listV2EnvelopeKinds(): string[] {
|
|
40
|
-
if (!existsSync(V2_ENVELOPES_DIR)) return [];
|
|
41
|
-
return readdirSync(V2_ENVELOPES_DIR).filter((f) => f.endsWith('.schema.json')).map((f) => f.replace(/\.schema\.json$/, '')).sort();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// RFC 0030 §B — the Tier-1 structured-output subset.
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
/** Keywords that fail across more than one Tier-1 vendor (Gemini drops `oneOf` silently). Checked in both modes. */
|
|
49
|
-
const LOAD_BEARING_KEYWORDS = ['oneOf', 'allOf', 'not', 'if', 'then', 'else', 'dependencies', 'prefixItems', 'propertyNames'] as const;
|
|
50
|
-
export const MAX_DEPTH = 5;
|
|
51
|
-
export const MAX_PROPERTIES = 100;
|
|
52
|
-
|
|
53
|
-
const hasType = (schema: Record<string, unknown>, t: string): boolean => schema['type'] === t || (Array.isArray(schema['type']) && (schema['type'] as unknown[]).includes(t));
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Walk a payload schema for Tier-1 subset violations.
|
|
57
|
-
*
|
|
58
|
-
* `load-bearing`: only the rules that fail across several vendors — the floor
|
|
59
|
-
* every corpus schema is held to. `strict`: every rule of the OpenAI-strict ∩
|
|
60
|
-
* Anthropic-strict ∩ Gemini intersection, applied only when a host advertises
|
|
61
|
-
* `envelopes.tierOneSubsetCompliance: "strict"`.
|
|
62
|
-
*/
|
|
63
|
-
export function tierOneViolations(schema: Record<string, unknown>, mode: 'load-bearing' | 'strict'): SchemaViolation[] {
|
|
64
|
-
const out: SchemaViolation[] = [];
|
|
65
|
-
const count = { n: 0 };
|
|
66
|
-
walkTierOne(schema, '#', 0, count, out, mode);
|
|
67
|
-
if (count.n > MAX_PROPERTIES) out.push({ path: '#', rule: 'max-property-count-100-exceeded', detail: `count=${count.n}` });
|
|
68
|
-
return out;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function walkTierOne(schema: Record<string, unknown>, path: string, depth: number, count: { n: number }, out: SchemaViolation[], mode: 'load-bearing' | 'strict'): void {
|
|
72
|
-
if (depth > MAX_DEPTH) { out.push({ path, rule: 'max-nesting-depth-5', detail: `depth=${depth}` }); return; }
|
|
73
|
-
for (const kw of LOAD_BEARING_KEYWORDS) if (kw in schema) out.push({ path, rule: 'forbidden-keyword', detail: kw });
|
|
74
|
-
if (Array.isArray(schema['anyOf'])) {
|
|
75
|
-
(schema['anyOf'] as unknown[]).forEach((b, i) => { if (isRecord(b)) walkTierOne(b, `${path}/anyOf/${i}`, depth + 1, count, out, mode); });
|
|
76
|
-
}
|
|
77
|
-
if (hasType(schema, 'object')) {
|
|
78
|
-
if (mode === 'strict' && schema['additionalProperties'] !== false) out.push({ path, rule: 'additionalProperties-must-be-false-on-object-strict-only' });
|
|
79
|
-
const props = isRecord(schema['properties']) ? schema['properties'] : {};
|
|
80
|
-
const required = Array.isArray(schema['required']) ? (schema['required'] as unknown[]) : [];
|
|
81
|
-
for (const [name, sub] of Object.entries(props)) {
|
|
82
|
-
count.n++;
|
|
83
|
-
if (mode === 'strict' && !required.includes(name)) out.push({ path: `${path}/properties/${name}`, rule: 'property-not-in-required-strict-mode-only' });
|
|
84
|
-
if (isRecord(sub)) walkTierOne(sub, `${path}/properties/${name}`, depth + 1, count, out, mode);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (mode === 'strict') {
|
|
88
|
-
const bounded: Array<[boolean, readonly string[], string]> = [
|
|
89
|
-
[hasType(schema, 'string'), ['minLength', 'maxLength', 'pattern', 'format'], 'forbidden-string-constraint-strict-only'],
|
|
90
|
-
[hasType(schema, 'number') || hasType(schema, 'integer'), ['minimum', 'maximum', 'multipleOf'], 'forbidden-number-constraint-strict-only'],
|
|
91
|
-
[hasType(schema, 'array'), ['minItems', 'maxItems', 'uniqueItems'], 'forbidden-array-constraint-strict-only'],
|
|
92
|
-
];
|
|
93
|
-
for (const [applies, kws, rule] of bounded) if (applies) for (const kw of kws) if (kw in schema) out.push({ path, rule, detail: kw });
|
|
94
|
-
}
|
|
95
|
-
if (hasType(schema, 'array') && isRecord(schema['items'])) walkTierOne(schema['items'], `${path}/items`, depth + 1, count, out, mode);
|
|
96
|
-
const defs = isRecord(schema['$defs']) ? schema['$defs'] : {};
|
|
97
|
-
for (const [name, sub] of Object.entries(defs)) if (isRecord(sub)) walkTierOne(sub, `${path}/$defs/${name}`, depth + 1, count, out, mode);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
// RFC 0031 §A — variant payloads: no `oneOf`; every `anyOf` branch discriminated.
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
|
|
104
|
-
/** `oneOf` anywhere in a payload schema. */
|
|
105
|
-
export function oneOfViolations(schema: unknown, path = '#'): SchemaViolation[] {
|
|
106
|
-
const out: SchemaViolation[] = [];
|
|
107
|
-
const walk = (s: unknown, p: string): void => {
|
|
108
|
-
if (Array.isArray(s)) { s.forEach((x, i) => walk(x, `${p}/${i}`)); return; }
|
|
109
|
-
if (!isRecord(s)) return;
|
|
110
|
-
if ('oneOf' in s) out.push({ path: p, rule: 'oneOf-forbidden', detail: 'use anyOf with a single-string-enum discriminator (RFC 0031 §A)' });
|
|
111
|
-
for (const [k, v] of Object.entries(s)) walk(v, `${p}/${k}`);
|
|
112
|
-
};
|
|
113
|
-
walk(schema, path);
|
|
114
|
-
return out;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* A branch is discriminated when one of its `required` properties is
|
|
119
|
-
* `type: string` with an `enum` of exactly one value. A `$ref` branch is
|
|
120
|
-
* accepted unresolved, as at v1: the referenced shape is walked where it is
|
|
121
|
-
* defined, and a version-selected union (one branch per `schemaVersion`, as
|
|
122
|
-
* `ui.a2ui-surface` declares) is not a payload-field discrimination at all.
|
|
123
|
-
*/
|
|
124
|
-
export function branchDiscriminated(branch: unknown): boolean {
|
|
125
|
-
if (!isRecord(branch)) return false;
|
|
126
|
-
if ('$ref' in branch) return true;
|
|
127
|
-
const required = Array.isArray(branch['required']) ? (branch['required'] as unknown[]) : [];
|
|
128
|
-
const props = isRecord(branch['properties']) ? branch['properties'] : {};
|
|
129
|
-
return required.some((name) => {
|
|
130
|
-
const p = typeof name === 'string' ? props[name] : undefined;
|
|
131
|
-
return isRecord(p) && p['type'] === 'string' && Array.isArray(p['enum']) && (p['enum'] as unknown[]).length === 1;
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Whether a branch can match a JSON object: an explicit object type, a `$ref`, or object keywords with no type. */
|
|
136
|
-
function admitsObject(branch: unknown): boolean {
|
|
137
|
-
if (!isRecord(branch)) return false;
|
|
138
|
-
if ('$ref' in branch) return true;
|
|
139
|
-
const t = branch['type'];
|
|
140
|
-
if (t === undefined) return 'properties' in branch || 'required' in branch;
|
|
141
|
-
return t === 'object' || (Array.isArray(t) && t.includes('object'));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Every `anyOf` branch, at any depth, that carries no single-string-enum
|
|
146
|
-
* discriminator — in a VARIANT union, one where two or more branches can be
|
|
147
|
-
* objects. A union with at most one object branch (`string | binding`,
|
|
148
|
-
* `string | number | boolean | array | binding`, as `ui.a2ui-surface`
|
|
149
|
-
* payload v2 declares) is a type union: JSON type alone selects the branch, no
|
|
150
|
-
* field is inspected, and a scalar branch has no property to discriminate on.
|
|
151
|
-
* RFC 0031 §A governs variant payloads; it is not read as forbidding these.
|
|
152
|
-
*/
|
|
153
|
-
export function anyOfDiscriminatorViolations(schema: unknown, path = '#'): SchemaViolation[] {
|
|
154
|
-
const out: SchemaViolation[] = [];
|
|
155
|
-
const walk = (s: unknown, p: string): void => {
|
|
156
|
-
if (Array.isArray(s)) { s.forEach((x, i) => walk(x, `${p}/${i}`)); return; }
|
|
157
|
-
if (!isRecord(s)) return;
|
|
158
|
-
if (Array.isArray(s['anyOf']) && (s['anyOf'] as unknown[]).filter(admitsObject).length >= 2) {
|
|
159
|
-
(s['anyOf'] as unknown[]).forEach((b, i) => {
|
|
160
|
-
if (admitsObject(b) && !branchDiscriminated(b)) out.push({ path: `${p}/anyOf/${i}`, rule: 'anyOf-branch-missing-discriminator', detail: 'a branch MUST declare a single-string-enum discriminator property in required (RFC 0031 §A)' });
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
for (const [k, v] of Object.entries(s)) walk(v, `${p}/${k}`);
|
|
164
|
-
};
|
|
165
|
-
walk(schema, path);
|
|
166
|
-
return out;
|
|
167
|
-
}
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2-a2ui-v09-surface — RFC 0209 §A.1, §A.2, §C.9, §C.11, §C.12, the seam-gated
|
|
3
|
-
* legs (suite 2.36.0, target major 2).
|
|
4
|
-
*
|
|
5
|
-
* `ui.a2ui-surface` at per-kind schema version 2 carries A2UI v0.9 messages. The
|
|
6
|
-
* suite cannot make a model emit a chosen envelope, so every leg here drives the
|
|
7
|
-
* v2 emit-surface seam (`POST /conformance/seams/sample/a2ui/emit-surface`,
|
|
8
|
-
* api/seams-v2.yaml), which a host MUST route through its production envelope
|
|
9
|
-
* admission. The server-free legs (profile ⊂ upstream, the excluded affordances,
|
|
10
|
-
* the pinned catalog, the kind carve-out) are corpus rows in
|
|
11
|
-
* `src/coherence/a2ui-v09-profile.test.ts`.
|
|
12
|
-
*
|
|
13
|
-
* Gates, all decided before any assertion so a skipped leg never records a
|
|
14
|
-
* partial witness: the seams profile advertised; `schemaVersions.kinds` giving
|
|
15
|
-
* the kind a floor of at least 2 (version-2 legs) or exactly 1 (the legacy leg);
|
|
16
|
-
* the seam answering something other than 404/405. Any of these absent ⇒
|
|
17
|
-
* `inapplicable` — a leg never passes on a non-answer.
|
|
18
|
-
*
|
|
19
|
-
* Every refusal leg carries an admitted control in the same run, so a host that
|
|
20
|
-
* refuses everything fails as surely as one that refuses nothing.
|
|
21
|
-
*
|
|
22
|
-
* The last describe accounts for §C.9's `render-needs-root`, whose
|
|
23
|
-
* Falsifiability verdict is `unwitnessable`: the guarantee is render-side, and
|
|
24
|
-
* a server-oriented suite cannot observe a renderer. It records `inapplicable`
|
|
25
|
-
* on EVERY host, before any request and before the seam gate, and names where
|
|
26
|
-
* the real witness lives: a reference-app client probe, `tier: reference-impl`,
|
|
27
|
-
* following the `a2ui-surface-no-code-exec` precedent. It never asserts and
|
|
28
|
-
* cannot become a pass.
|
|
29
|
-
*
|
|
30
|
-
* @see RFCS/0209-v2-a2ui-surfaces-are-a2ui-v0-9.md
|
|
31
|
-
* @see spec/v2/ext/a2uiSurface/README.md
|
|
32
|
-
* @see spec/v2/core/events.md §"The envelope-kind catalog"
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import { describe, it, expect } from 'vitest';
|
|
36
|
-
import { readFileSync } from 'node:fs';
|
|
37
|
-
import { join } from 'node:path';
|
|
38
|
-
import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
39
|
-
import { v2Discovery } from '../lib/v2.js';
|
|
40
|
-
import { seamsProfileAdvertised, SEAMS_PREFIX } from '../lib/seams.js';
|
|
41
|
-
import { readErrorCode } from '../lib/error-envelope.js';
|
|
42
|
-
import { FIXTURES_DIR } from '../lib/paths.js';
|
|
43
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
44
|
-
import { req } from '../lib/requirement-ids.js';
|
|
45
|
-
import { carriesPayload } from '../lib/carries-payload.js';
|
|
46
|
-
|
|
47
|
-
type Json = Record<string, unknown>;
|
|
48
|
-
const KIND = 'ui.a2ui-surface';
|
|
49
|
-
const SEAM = `${SEAMS_PREFIX}/sample/a2ui/emit-surface`;
|
|
50
|
-
const FIXTURE = 'conformance-approval';
|
|
51
|
-
const GATE_NODE = 'gate';
|
|
52
|
-
const CATALOG = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json';
|
|
53
|
-
const clone = <T>(o: T): T => JSON.parse(JSON.stringify(o)) as T;
|
|
54
|
-
const POSITIVE = JSON.parse(readFileSync(join(FIXTURES_DIR, 'a2ui-v09', 'positive-approve-brief.json'), 'utf8')) as Json;
|
|
55
|
-
const V1_SURFACE: Json = { catalogVersion: '0.9.1', surface: { title: 'Kickoff', components: [{ component: 'heading', text: 'Kickoff', level: 2 }, { component: 'action.button', id: 'go', label: 'Go', action: { target: 'resume' } }] } };
|
|
56
|
-
const V1_LATER: Json = { catalogVersion: '0.9.1', surface: { title: 'Kickoff, later', components: [{ component: 'heading', text: 'Kickoff, later', level: 2 }] } };
|
|
57
|
-
|
|
58
|
-
let seq = 0;
|
|
59
|
-
const nonce = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
60
|
-
function envelope(schemaVersion: number, payload: unknown, opts: { trust?: 'trusted' | 'untrusted'; nodeId?: string } = {}): Json {
|
|
61
|
-
seq += 1;
|
|
62
|
-
const meta: Json = { source: 'ai-generation', ts: new Date().toISOString() };
|
|
63
|
-
if (opts.trust) meta['contentTrust'] = opts.trust;
|
|
64
|
-
return { type: KIND, schemaVersion, envelopeId: `env-0209-${nonce}-${seq}`, correlationId: `corr-0209-${nonce}-${seq}`, ...(opts.nodeId ? { nodeId: opts.nodeId } : {}), payload, meta };
|
|
65
|
-
}
|
|
66
|
-
/** A version-2 payload for `surfaceId` carrying `messages` (surfaceId stamped into each). */
|
|
67
|
-
function v2(surfaceId: string, messages: Json[]): Json {
|
|
68
|
-
return { version: 'v0.9', catalogId: CATALOG, surfaceId, messages: messages.map((m) => { const c = clone(m); const body = Object.entries(c).find(([k]) => k !== 'version')?.[1] as Json; body['surfaceId'] = surfaceId; return c; }) };
|
|
69
|
-
}
|
|
70
|
-
const createMsg = (): Json => ({ version: 'v0.9', createSurface: { surfaceId: '', catalogId: CATALOG } });
|
|
71
|
-
const componentsMsg = (): Json => clone((POSITIVE['messages'] as Json[])[1]);
|
|
72
|
-
const dataMsg = (): Json => ({ version: 'v0.9', updateDataModel: { surfaceId: '', value: { name: 'x' } } });
|
|
73
|
-
const deleteMsg = (): Json => ({ version: 'v0.9', deleteSurface: { surfaceId: '' } });
|
|
74
|
-
const sid = (tag: string): string => `s-${tag}-${nonce}`;
|
|
75
|
-
|
|
76
|
-
async function emit(runId: string, env: Json): Promise<OpenWOPResponse> {
|
|
77
|
-
return driver.post(SEAM, { runId, envelope: env });
|
|
78
|
-
}
|
|
79
|
-
const code = (r: OpenWOPResponse): string => `${r.status} ${readErrorCode(r.json) ?? ''}`.trim();
|
|
80
|
-
|
|
81
|
-
async function waitStatus(runId: string, wanted: ReadonlySet<string>, timeoutMs: number): Promise<string | null> {
|
|
82
|
-
const deadline = Date.now() + timeoutMs;
|
|
83
|
-
for (;;) {
|
|
84
|
-
const res = await driver.get(`/runs/${encodeURIComponent(runId)}`);
|
|
85
|
-
const status = res.status === 200 ? String((res.json as { status?: unknown } | null)?.status ?? '') : null;
|
|
86
|
-
if (status !== null && wanted.has(status)) return status;
|
|
87
|
-
if (Date.now() > deadline) return status;
|
|
88
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
type Ctx = { readonly floor: number; readonly strict: boolean; readonly runId: string };
|
|
93
|
-
type Gate = { ok: true; ctx: Ctx } | { ok: false; kind: 'inapplicable' | 'blocked'; reason: string };
|
|
94
|
-
|
|
95
|
-
/** Everything a leg needs, decided before its first assertion. `want` is the floor predicate. */
|
|
96
|
-
async function gate(want: (floor: number) => boolean, wantText: string): Promise<Gate> {
|
|
97
|
-
const doc = await v2Discovery();
|
|
98
|
-
if (!doc) return { ok: false, kind: 'blocked', reason: 'v2 discovery unreachable' };
|
|
99
|
-
if (!seamsProfileAdvertised(doc)) return { ok: false, kind: 'inapplicable', reason: 'host does not advertise conformance.seamsProfile: openwop-conformance-seams-v2 — the emit-surface seam is the only way the suite can choose an envelope (RFC 0209 Falsifiability)' };
|
|
100
|
-
const kinds = ((doc['schemaVersions'] as Json | undefined)?.['kinds'] ?? {}) as Json;
|
|
101
|
-
const floor = typeof kinds[KIND] === 'number' ? (kinds[KIND] as number) : 0;
|
|
102
|
-
if (!want(floor)) return { ok: false, kind: 'inapplicable', reason: `schemaVersions.kinds["${KIND}"] is ${floor}; this leg needs ${wantText}` };
|
|
103
|
-
const strict = ((doc['envelopeStrictness'] as Json | undefined)?.['mode']) === 'strict';
|
|
104
|
-
const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
|
|
105
|
-
if (!fixtures.includes(FIXTURE)) return { ok: false, kind: 'inapplicable', reason: `the host does not advertise the ${FIXTURE} fixture the legs run inside` };
|
|
106
|
-
const create = await driver.post('/runs', { workflowId: FIXTURE });
|
|
107
|
-
if (create.status !== 201) return { ok: false, kind: 'blocked', reason: `POST /runs {workflowId: ${FIXTURE}} answered ${code(create)}` };
|
|
108
|
-
const runId = (create.json as { runId: string }).runId;
|
|
109
|
-
const status = await waitStatus(runId, new Set(['waiting-approval', 'completed', 'failed', 'cancelled']), 10_000);
|
|
110
|
-
if (status !== 'waiting-approval') return { ok: false, kind: 'blocked', reason: `${FIXTURE} did not suspend (status ${status})` };
|
|
111
|
-
// Probe the seam with a payload every host that has it must refuse (no createSurface first), so the probe records nothing.
|
|
112
|
-
const probe = await emit(runId, envelope(floor, floor >= 2 ? v2(sid('probe'), [dataMsg()]) : { catalogVersion: '9.9.9', surface: { components: [] } }));
|
|
113
|
-
if (probe.status === 404 || probe.status === 405) {
|
|
114
|
-
await driver.post(`/runs/${encodeURIComponent(runId)}/cancel`, {});
|
|
115
|
-
return { ok: false, kind: 'inapplicable', reason: `${SEAM} answered ${probe.status} — the emit-surface seam is not wired` };
|
|
116
|
-
}
|
|
117
|
-
return { ok: true, ctx: { floor, strict, runId } };
|
|
118
|
-
}
|
|
119
|
-
async function done(runId: string): Promise<void> { await driver.post(`/runs/${encodeURIComponent(runId)}/cancel`, {}); }
|
|
120
|
-
|
|
121
|
-
// RFC 0209 §C.11 compares as JSON, not as text: member order carries no meaning
|
|
122
|
-
// (RFC 8259 §4), and a JSONB host re-sorts it. See lib/carries-payload.ts.
|
|
123
|
-
const carries = carriesPayload;
|
|
124
|
-
async function eventsOf(runId: string): Promise<unknown[]> {
|
|
125
|
-
const r = await driver.get(`/runs/${encodeURIComponent(runId)}/events/poll?timeout=1`);
|
|
126
|
-
const ev = (r.json as { events?: unknown } | null)?.events;
|
|
127
|
-
return Array.isArray(ev) ? ev : [];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
describe('RFC 0209 §A.1 — the schema version selects the branch (seam-gated)', () => {
|
|
131
|
-
it('a version-2 envelope carrying a version-1 body is refused; the positive is admitted; above the floor is refused; below it follows envelopeStrictness', async () => {
|
|
132
|
-
const g = await gate((f) => f >= 2, 'a floor of at least 2');
|
|
133
|
-
if (!g.ok) return softSkip(g.kind, g.reason);
|
|
134
|
-
const { runId, floor, strict } = g.ctx;
|
|
135
|
-
try {
|
|
136
|
-
const good = await emit(runId, envelope(2, v2(sid('vsb-good'), (POSITIVE['messages'] as Json[]))));
|
|
137
|
-
expect(good.status, req('openwop.requirement.0209.version-selects-branch', 'RFC 0209 §A.1', `the positive version-2 surface MUST be admitted (201), got ${code(good)}`)).toBe(201);
|
|
138
|
-
expect(typeof (good.json as { sequence?: unknown } | null)?.sequence, req('openwop.requirement.0209.version-selects-branch', 'api/seams-v2.yaml emitA2uiSurface', 'a 201 names the recording event sequence')).toBe('number');
|
|
139
|
-
const mixed = await emit(runId, envelope(2, V1_SURFACE));
|
|
140
|
-
expect(mixed.status, req('openwop.requirement.0209.version-selects-branch', 'RFC 0209 §A.1', `a schemaVersion 2 envelope with a payloadV1 body MUST be refused — validating against the anyOf union would admit it (got ${code(mixed)})`)).toBe(422);
|
|
141
|
-
expect(readErrorCode(mixed.json), req('openwop.requirement.0209.version-selects-branch', 'spec/v2/ext/a2uiSurface/README.md §Versions', 'the refusal is envelope_invalid')).toBe('envelope_invalid');
|
|
142
|
-
const above = await emit(runId, envelope(floor + 1, v2(sid('vsb-above'), [createMsg()])));
|
|
143
|
-
expect(code(above), req('openwop.requirement.0209.version-selects-branch', 'events.md §"The envelope-kind catalog"', 'a schemaVersion above the floor MUST be refused with unknown_schema_version whatever the strictness')).toBe('422 unknown_schema_version');
|
|
144
|
-
const below = await emit(runId, envelope(1, v2(sid('vsb-below'), [createMsg()])));
|
|
145
|
-
if (strict) {
|
|
146
|
-
expect(code(below), req('openwop.requirement.0209.version-selects-branch', 'events.md §"The envelope-kind catalog"', 'under strict, a schemaVersion 1 envelope below floor 2 MUST be refused with unknown_schema_version')).toBe('422 unknown_schema_version');
|
|
147
|
-
} else {
|
|
148
|
-
expect(below.status, req('openwop.requirement.0209.version-selects-branch', 'events.md §"The envelope-kind catalog"', `under warn, a schemaVersion 1 envelope is validated against the advertised version 2; a payloadV2 body is therefore admitted with a drift log (got ${code(below)})`)).toBe(201);
|
|
149
|
-
const belowV1 = await emit(runId, envelope(1, V1_SURFACE));
|
|
150
|
-
expect(code(belowV1), req('openwop.requirement.0209.version-selects-branch', 'RFC 0209 §A.1', 'under warn, a schemaVersion 1 envelope with a payloadV1 body is validated against version 2 and MUST be refused')).toBe('422 envelope_invalid');
|
|
151
|
-
}
|
|
152
|
-
} finally { await done(runId); }
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe('RFC 0209 §C.9 — the fold is guarded at record time (seam-gated)', () => {
|
|
157
|
-
it('no first createSurface, a message after deleteSurface, and a second createSurface are each refused; the valid steps are admitted', async () => {
|
|
158
|
-
const g = await gate((f) => f >= 2, 'a floor of at least 2');
|
|
159
|
-
if (!g.ok) return softSkip(g.kind, g.reason);
|
|
160
|
-
const { runId } = g.ctx;
|
|
161
|
-
const R = (why: string) => req('openwop.requirement.0209.fold-guarded', 'RFC 0209 §C.9', why);
|
|
162
|
-
try {
|
|
163
|
-
const orphan = await emit(runId, envelope(2, v2(sid('fold-a'), [componentsMsg()])));
|
|
164
|
-
expect(code(orphan), R('the first envelope for a surfaceId MUST begin with createSurface — updateComponents alone is refused, not recorded')).toBe('422 envelope_invalid');
|
|
165
|
-
const bCreate = await emit(runId, envelope(2, v2(sid('fold-b'), [createMsg(), componentsMsg()])));
|
|
166
|
-
expect(bCreate.status, R(`createSurface + updateComponents for a new surfaceId MUST be admitted (control), got ${code(bCreate)}`)).toBe(201);
|
|
167
|
-
const bDelete = await emit(runId, envelope(2, v2(sid('fold-b'), [deleteMsg()])));
|
|
168
|
-
expect(bDelete.status, R(`deleteSurface on a live surface MUST be admitted (control), got ${code(bDelete)}`)).toBe(201);
|
|
169
|
-
const bAfter = await emit(runId, envelope(2, v2(sid('fold-b'), [dataMsg()])));
|
|
170
|
-
expect(code(bAfter), R('a message after deleteSurface without a new createSurface MUST be refused')).toBe('422 envelope_invalid');
|
|
171
|
-
const cCreate = await emit(runId, envelope(2, v2(sid('fold-c'), [createMsg()])));
|
|
172
|
-
expect(cCreate.status, R(`createSurface for a new surfaceId MUST be admitted (control), got ${code(cCreate)}`)).toBe(201);
|
|
173
|
-
const cAgain = await emit(runId, envelope(2, v2(sid('fold-c'), [createMsg()])));
|
|
174
|
-
expect(code(cAgain), R('createSurface for a live surfaceId MUST be refused')).toBe('422 envelope_invalid');
|
|
175
|
-
const bRecreate = await emit(runId, envelope(2, v2(sid('fold-b'), [createMsg(), dataMsg()])));
|
|
176
|
-
expect(bRecreate.status, R(`a new createSurface after deleteSurface MUST be admitted (control), got ${code(bRecreate)}`)).toBe(201);
|
|
177
|
-
} finally { await done(runId); }
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe('RFC 0209 §A.2 — the cross-field rules are checked at admission (seam-gated)', () => {
|
|
182
|
-
it('a createSurface.catalogId differing from the payload catalogId is refused', async () => {
|
|
183
|
-
const g = await gate((f) => f >= 2, 'a floor of at least 2');
|
|
184
|
-
if (!g.ok) return softSkip(g.kind, g.reason);
|
|
185
|
-
const { runId } = g.ctx;
|
|
186
|
-
try {
|
|
187
|
-
const p = v2(sid('cat'), [createMsg()]);
|
|
188
|
-
((p['messages'] as Json[])[0]['createSurface'] as Json)['catalogId'] = 'https://a2ui.org/specification/v0_9/catalogs/other/catalog.json';
|
|
189
|
-
const r = await emit(runId, envelope(2, p));
|
|
190
|
-
expect(code(r), req('openwop.requirement.0209.catalog-equality', 'RFC 0209 §A.2', 'a createSurface.catalogId MUST equal the payload catalogId; a differing one is refused, not recorded')).toBe('422 envelope_invalid');
|
|
191
|
-
const ok = await emit(runId, envelope(2, v2(sid('cat-ok'), [createMsg()])));
|
|
192
|
-
expect(ok.status, req('openwop.requirement.0209.catalog-equality', 'RFC 0209 §A.2', `the same payload with equal catalogIds MUST be admitted (control), got ${code(ok)}`)).toBe(201);
|
|
193
|
-
} finally { await done(runId); }
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('a message whose surfaceId differs from the payload surfaceId is refused', async () => {
|
|
197
|
-
const g = await gate((f) => f >= 2, 'a floor of at least 2');
|
|
198
|
-
if (!g.ok) return softSkip(g.kind, g.reason);
|
|
199
|
-
const { runId } = g.ctx;
|
|
200
|
-
try {
|
|
201
|
-
const p = v2(sid('sid'), [createMsg(), componentsMsg()]);
|
|
202
|
-
((p['messages'] as Json[])[1]['updateComponents'] as Json)['surfaceId'] = sid('sid-other');
|
|
203
|
-
const r = await emit(runId, envelope(2, p));
|
|
204
|
-
expect(code(r), req('openwop.it.v2-a2ui-v09-surface.surface-id-equality', 'RFC 0209 §A.2', 'every message surfaceId MUST equal the payload surfaceId — the schema cannot express it, so the engine MUST refuse a mismatch')).toBe('422 envelope_invalid');
|
|
205
|
-
const ok = await emit(runId, envelope(2, v2(sid('sid'), [createMsg(), componentsMsg()])));
|
|
206
|
-
expect(ok.status, req('openwop.it.v2-a2ui-v09-surface.surface-id-equality', 'RFC 0209 §A.2', `the refused envelope left no trace: the same surfaceId is still new and admits createSurface (got ${code(ok)})`)).toBe(201);
|
|
207
|
-
} finally { await done(runId); }
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
describe('RFC 0209 §C.11 — recorded surfaces are returned as recorded (seam-gated)', () => {
|
|
212
|
-
// "byte-equal" in these two titles means JCS-equal (lib/carries-payload.ts). The titles
|
|
213
|
-
// are kept because they mint the rows' title ids.
|
|
214
|
-
it('a recorded version-2 surface is returned byte-equal on poll and carried by a fork at a later fromSeq', async () => {
|
|
215
|
-
const g = await gate((f) => f >= 2, 'a floor of at least 2');
|
|
216
|
-
if (!g.ok) return softSkip(g.kind, g.reason);
|
|
217
|
-
const { runId } = g.ctx;
|
|
218
|
-
try {
|
|
219
|
-
const payload = v2(sid('rec'), (POSITIVE['messages'] as Json[]));
|
|
220
|
-
const r = await emit(runId, envelope(2, payload));
|
|
221
|
-
expect(r.status, req('openwop.it.v2-a2ui-v09-surface.recorded-as-recorded', 'RFC 0209 §C.11', `the positive surface MUST be admitted, got ${code(r)}`)).toBe(201);
|
|
222
|
-
const sequence = (r.json as { sequence: number }).sequence;
|
|
223
|
-
const events = await eventsOf(runId);
|
|
224
|
-
expect(carries(events, payload), req('openwop.it.v2-a2ui-v09-surface.recorded-as-recorded', 'RFC 0209 §C.11', 'the run\'s poll MUST return the recorded surface payload unchanged as JSON (JCS-equal; member order carries no meaning) — no surface is regenerated')).toBe(true);
|
|
225
|
-
// The surface is the last event of the suspended run, so `sequence + 1`
|
|
226
|
-
// names no event and runs.md §Fork REQUIRES 422 fork_point_invalid for it.
|
|
227
|
-
// Record one more envelope on the same surface and fork AT that event:
|
|
228
|
-
// the surface (sequence < fromSeq) is fixed history, the later one re-executes.
|
|
229
|
-
const later = await emit(runId, envelope(2, v2(sid('rec'), [dataMsg()])));
|
|
230
|
-
expect(later.status, req('openwop.it.v2-a2ui-v09-surface.recorded-as-recorded', 'RFC 0209 §C.9', `an updateDataModel on the live surface MUST be admitted (it supplies the fork point), got ${code(later)}`)).toBe(201);
|
|
231
|
-
const fromSeq = (later.json as { sequence: number }).sequence;
|
|
232
|
-
expect(fromSeq > sequence, req('openwop.it.v2-a2ui-v09-surface.recorded-as-recorded', 'api/seams-v2.yaml emitA2uiSurface', `a later recording names a later sequence (surface ${sequence}, update ${fromSeq})`)).toBe(true);
|
|
233
|
-
const fork = await driver.post(`/runs/${encodeURIComponent(runId)}:fork`, { mode: 'replay', fromSeq });
|
|
234
|
-
expect(fork.status, req('openwop.it.v2-a2ui-v09-surface.recorded-as-recorded', 'RFC 0209 §C.11', `POST :fork at fromSeq ${fromSeq} (a recorded event after the surface) MUST answer 201, got ${code(fork)}`)).toBe(201);
|
|
235
|
-
const forkId = (fork.json as { runId: string }).runId;
|
|
236
|
-
expect(carries(await eventsOf(forkId), payload), req('openwop.it.v2-a2ui-v09-surface.recorded-as-recorded', 'RFC 0209 §C.11', 'a fork at a fromSeq after the surface MUST carry it unchanged')).toBe(true);
|
|
237
|
-
await done(forkId);
|
|
238
|
-
} finally { await done(runId); }
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('a version-1 surface recorded at floor 1 is returned byte-equal on poll and carried by a fork', async () => {
|
|
242
|
-
const g = await gate((f) => f === 1, 'a floor of exactly 1 — a host whose floor is 2 has no admission path for a version-1 surface, and the suite cannot seed one (RFC 0209 §C.11 holds for what it recorded before raising the floor)');
|
|
243
|
-
if (!g.ok) return softSkip(g.kind, g.reason);
|
|
244
|
-
const { runId } = g.ctx;
|
|
245
|
-
try {
|
|
246
|
-
const r = await emit(runId, envelope(1, V1_SURFACE));
|
|
247
|
-
expect(r.status, req('openwop.requirement.0209.legacy-readable', 'RFC 0209 §C.11', `a version-1 surface at floor 1 MUST be admitted, got ${code(r)}`)).toBe(201);
|
|
248
|
-
const sequence = (r.json as { sequence: number }).sequence;
|
|
249
|
-
expect(carries(await eventsOf(runId), V1_SURFACE), req('openwop.requirement.0209.legacy-readable', 'RFC 0209 §C.11', 'the poll MUST return the version-1 surface unchanged as JSON (JCS-equal)')).toBe(true);
|
|
250
|
-
// As in the version-2 leg: fork at a recorded event after the surface, never at `sequence + 1` (no such event; 422 fork_point_invalid).
|
|
251
|
-
const later = await emit(runId, envelope(1, V1_LATER));
|
|
252
|
-
expect(later.status, req('openwop.requirement.0209.legacy-readable', 'RFC 0209 §C.11', `a second version-1 surface at floor 1 MUST be admitted (it supplies the fork point), got ${code(later)}`)).toBe(201);
|
|
253
|
-
const fromSeq = (later.json as { sequence: number }).sequence;
|
|
254
|
-
expect(fromSeq > sequence, req('openwop.requirement.0209.legacy-readable', 'api/seams-v2.yaml emitA2uiSurface', `a later recording names a later sequence (surface ${sequence}, next ${fromSeq})`)).toBe(true);
|
|
255
|
-
const fork = await driver.post(`/runs/${encodeURIComponent(runId)}:fork`, { mode: 'replay', fromSeq });
|
|
256
|
-
expect(fork.status, req('openwop.requirement.0209.legacy-readable', 'RFC 0209 §C.11', `POST :fork at fromSeq ${fromSeq} (a recorded event after the surface) MUST answer 201, got ${code(fork)}`)).toBe(201);
|
|
257
|
-
const forkId = (fork.json as { runId: string }).runId;
|
|
258
|
-
expect(carries(await eventsOf(forkId), V1_SURFACE), req('openwop.requirement.0209.legacy-readable', 'RFC 0209 §C.11', 'a fork at a later fromSeq MUST carry the version-1 surface unchanged')).toBe(true);
|
|
259
|
-
await done(forkId);
|
|
260
|
-
} finally { await done(runId); }
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
describe('RFC 0209 §C.12 — trust is sticky across the fold (seam-gated)', () => {
|
|
265
|
-
it('one untrusted update taints a trusted surface, a later trusted update does not launder it, and the approval does not advance; an all-trusted surface does', async () => {
|
|
266
|
-
const g = await gate((f) => f >= 2, 'a floor of at least 2');
|
|
267
|
-
if (!g.ok) return softSkip(g.kind, g.reason);
|
|
268
|
-
const { runId } = g.ctx;
|
|
269
|
-
const R = (why: string) => req('openwop.requirement.0209.taint-sticky', 'RFC 0209 §C.12', why);
|
|
270
|
-
let controlRun: string | null = null;
|
|
271
|
-
try {
|
|
272
|
-
const s = sid('taint');
|
|
273
|
-
const steps = [
|
|
274
|
-
await emit(runId, envelope(2, v2(s, [createMsg(), componentsMsg()]), { nodeId: GATE_NODE })),
|
|
275
|
-
await emit(runId, envelope(2, v2(s, [componentsMsg()]), { nodeId: GATE_NODE, trust: 'untrusted' })),
|
|
276
|
-
await emit(runId, envelope(2, v2(s, [componentsMsg()]), { nodeId: GATE_NODE, trust: 'trusted' })),
|
|
277
|
-
];
|
|
278
|
-
steps.forEach((r, i) => expect(r.status, R(`surface step ${i} MUST be recorded (201) — an untrusted envelope is recorded and tainted, not refused; got ${code(r)}`)).toBe(201));
|
|
279
|
-
const resolve = await driver.post(`/runs/${encodeURIComponent(runId)}/interrupts/${GATE_NODE}`, { resumeValue: { action: 'accept' } });
|
|
280
|
-
expect(resolve.status >= 400 && resolve.status < 500, R(`resolving the approval bound to a tainted surface MUST be refused (4xx), got ${code(resolve)}`)).toBe(true);
|
|
281
|
-
const text = JSON.stringify(resolve.json ?? {});
|
|
282
|
-
expect(text.includes('untrusted_content_blocks_approval'), R(`the refusal MUST name untrusted_content_blocks_approval (in error.details or message): ${text.slice(0, 300)}`)).toBe(true);
|
|
283
|
-
expect(await waitStatus(runId, new Set(['waiting-approval']), 1_000), R('a refused resolve MUST NOT advance the run — the approval stays pending')).toBe('waiting-approval');
|
|
284
|
-
|
|
285
|
-
// Control: the same fold with no untrusted envelope MUST let the approval advance, or the refusal above proves nothing.
|
|
286
|
-
const ctl = await driver.post('/runs', { workflowId: FIXTURE });
|
|
287
|
-
expect(ctl.status, R(`control run MUST start, got ${code(ctl)}`)).toBe(201);
|
|
288
|
-
controlRun = (ctl.json as { runId: string }).runId;
|
|
289
|
-
expect(await waitStatus(controlRun, new Set(['waiting-approval', 'completed', 'failed', 'cancelled']), 10_000), R('the control run MUST suspend on its approval')).toBe('waiting-approval');
|
|
290
|
-
const c = sid('trusted');
|
|
291
|
-
const c1 = await emit(controlRun, envelope(2, v2(c, [createMsg(), componentsMsg()]), { nodeId: GATE_NODE }));
|
|
292
|
-
const c2 = await emit(controlRun, envelope(2, v2(c, [componentsMsg()]), { nodeId: GATE_NODE, trust: 'trusted' }));
|
|
293
|
-
expect([c1.status, c2.status], R('the control surface MUST be recorded')).toEqual([201, 201]);
|
|
294
|
-
const ok = await driver.post(`/runs/${encodeURIComponent(controlRun)}/interrupts/${GATE_NODE}`, { resumeValue: { action: 'accept' } });
|
|
295
|
-
expect(ok.status, R(`resolving the approval bound to an all-trusted surface MUST succeed (control), got ${code(ok)}`)).toBe(200);
|
|
296
|
-
} finally {
|
|
297
|
-
await done(runId);
|
|
298
|
-
if (controlRun) await done(controlRun);
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
describe('RFC 0209 §C.9 — no render before root (declared unwitnessable here; reference-impl witness)', () => {
|
|
304
|
-
it('records why render-needs-root has no server-side witness, and where its witness lives', () => {
|
|
305
|
-
req('openwop.requirement.0209.render-needs-root', 'RFC 0209 §C.9', 'a renderer does not render a surface before its root component exists');
|
|
306
|
-
return softSkip('inapplicable', 'unwitnessable by this suite (RFC 0209 §C.9 Falsifiability verdict): a render-side guarantee, and a server-oriented suite cannot observe a renderer. The fold guard the server CAN observe is openwop.requirement.0209.fold-guarded, above. The render half is witnessed as a reference-app client probe (tier: reference-impl, the a2ui-surface-no-code-exec precedent): openwop/openwop-app frontend/react/src/chat/a2ui/__tests__/a2ui-v09-render-needs-root.test.tsx (openwop-app#4121, ADR 0749). Removing the `if (!state.renderable)` guard in frontend/react/src/chat/a2ui/v09/A2uiV09Surface.tsx turns 2 of its 4 cases red. This row records that the suite reached the requirement, and it is not a pass.');
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
describe('RFC 0220 §C — the a2uiSurface family witness (seam-gated)', () => {
|
|
311
|
-
it('a version-2 surface is admitted and a version-1 body under schema version 2 is refused, in the same run', async () => {
|
|
312
|
-
const g = await gate((f) => f >= 2, 'a floor of at least 2');
|
|
313
|
-
if (!g.ok) return softSkip(g.kind, g.reason);
|
|
314
|
-
const { runId } = g.ctx;
|
|
315
|
-
const R = (why: string) => req('openwop.family.a2uiSurface', 'spec/v2/ext/a2uiSurface/README.md §Conformance', why);
|
|
316
|
-
try {
|
|
317
|
-
const good = await emit(runId, envelope(2, v2(sid('fam-good'), (POSITIVE['messages'] as Json[]))));
|
|
318
|
-
expect(good.status, R(`the positive version-2 surface MUST be admitted (201), got ${code(good)}`)).toBe(201);
|
|
319
|
-
const mixed = await emit(runId, envelope(2, V1_SURFACE));
|
|
320
|
-
expect(code(mixed), R('a version-1 body under schema version 2 MUST be refused with envelope_invalid (control: a host that admits everything fails here)')).toBe('422 envelope_invalid');
|
|
321
|
-
} finally { await done(runId); }
|
|
322
|
-
});
|
|
323
|
-
});
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2 — the `aiEnvelope` advertisement (`spec/v2/core/host-services.md`
|
|
3
|
-
* §aiEnvelope; RFC 0144). The v1 twin is the record shape `aiEnvelope` carried
|
|
4
|
-
* at v1; the legs live in `lib/family-advert-witness.ts`.
|
|
5
|
-
*
|
|
6
|
-
* record the record (with its optional `await` boolean) validates against
|
|
7
|
-
* its capabilities-schema seat.
|
|
8
|
-
*
|
|
9
|
-
* Not here: exposing `ctx.aiEnvelope.generate` / `await` to pack code is a
|
|
10
|
-
* pack-runtime surface with no wire witness.
|
|
11
|
-
*
|
|
12
|
-
* Dispositions: discovery unreadable ⇒ `blocked`; `aiEnvelope` absent, or the
|
|
13
|
-
* facet a leg reads absent ⇒ `inapplicable`.
|
|
14
|
-
*
|
|
15
|
-
* @see spec/v2/core/host-services.md §aiEnvelope
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { describe, it, expect } from 'vitest';
|
|
19
|
-
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
20
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
21
|
-
import { req } from '../lib/requirement-ids.js';
|
|
22
|
-
import { majorProfile } from '../lib/major-profile.js';
|
|
23
|
-
import { recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
24
|
-
|
|
25
|
-
const PROFILE = majorProfile(2);
|
|
26
|
-
const DOC = 'host-services.md §aiEnvelope';
|
|
27
|
-
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
28
|
-
const ID_RECORD = 'openwop.requirement.ai-envelope.advert-record-schema';
|
|
29
|
-
|
|
30
|
-
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
31
|
-
try { return await v2Discovery(); } catch { return null; }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('v2 aiEnvelope advertisement (host-services.md §aiEnvelope)', () => {
|
|
35
|
-
it('the aiEnvelope record validates against its capabilities-schema seat', async () => {
|
|
36
|
-
const doc = await discovery();
|
|
37
|
-
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
38
|
-
if (!(await familyAdvertised('aiEnvelope'))) return softSkip('inapplicable', 'the host does not advertise aiEnvelope');
|
|
39
|
-
const out = recordSchemaLeg(PROFILE, doc, 'aiEnvelope', 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
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
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
|
-
|
|
41
|
-
it('envelopeContracts.advertised is not false: a host that does not offer the family omits it', async () => {
|
|
42
|
-
const doc = await discovery();
|
|
43
|
-
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
44
|
-
const rec = await familyAdvertised('envelopeContracts');
|
|
45
|
-
if (!rec) return softSkip('inapplicable', 'the host does not advertise envelopeContracts');
|
|
46
|
-
expect(rec['advertised'], req('openwop.requirement.0237.no-false-advertisement', 'RFC 0237 §C; capabilities.md §2', 'envelopeContracts.advertised: false is not a v2 state; a host that does not offer the family MUST omit it')).not.toBe(false);
|
|
47
|
-
});
|
|
48
|
-
});
|