@ggui-ai/protocol-conformance 0.2.0-alpha.3 → 0.3.0-rc.0
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/README.md +32 -13
- package/dist/cli.d.ts +11 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +48 -20
- package/dist/conformance-host.d.ts +81 -76
- package/dist/conformance-host.d.ts.map +1 -1
- package/dist/conformance-host.js +0 -44
- package/dist/fixtures/bootstrap-protocol/bootstrap-bundle-fetch-failed.json +3 -3
- package/dist/fixtures/bootstrap-protocol/bootstrap-meta-missing.json +3 -3
- package/dist/fixtures/bootstrap-protocol/bootstrap-success.json +3 -3
- package/dist/fixtures/consume-buffer/action-ack-sequence.json +28 -0
- package/dist/fixtures/consume-buffer/action-payload-schema-violation.json +37 -0
- package/dist/fixtures/consume-buffer/index.d.ts +6 -0
- package/dist/fixtures/consume-buffer/index.d.ts.map +1 -0
- package/dist/fixtures/consume-buffer/index.js +41 -0
- package/dist/fixtures/consume-buffer/undeclared-action-rejected.json +29 -0
- package/dist/fixtures/host-context/host-context-observed-persists.json +34 -0
- package/dist/fixtures/host-context/index.d.ts +5 -0
- package/dist/fixtures/host-context/index.d.ts.map +1 -0
- package/dist/fixtures/host-context/index.js +40 -0
- package/dist/fixtures/index.d.ts +28 -9
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +33 -14
- package/dist/fixtures/reserved-channel-authority/index.d.ts.map +1 -1
- package/dist/fixtures/reserved-channel-authority/index.js +17 -7
- package/dist/fixtures/reserved-channel-authority/props-update-roundtrip.json +4 -4
- package/dist/fixtures/schema-version-handshake/index.d.ts.map +1 -1
- package/dist/fixtures/schema-version-handshake/index.js +12 -4
- package/dist/fixtures/schema-version-handshake/version-match.json +7 -5
- package/dist/fixtures/schema-version-handshake/version-mismatch.json +9 -10
- package/dist/fixtures/subscribe-tenancy/absent-appid-defaults.json +18 -0
- package/dist/fixtures/subscribe-tenancy/app-mismatch.json +20 -0
- package/dist/fixtures/subscribe-tenancy/index.d.ts +6 -0
- package/dist/fixtures/subscribe-tenancy/index.d.ts.map +1 -0
- package/dist/fixtures/subscribe-tenancy/index.js +62 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/match-behavior.d.ts +19 -100
- package/dist/match-behavior.d.ts.map +1 -1
- package/dist/match-behavior.js +289 -246
- package/dist/registration-conformance/cases/gate-duplicate-hook.json +26 -0
- package/dist/registration-conformance/cases/gate-export-not-registered.json +1 -0
- package/dist/registration-conformance/cases/gate-multi-package-distinct-names-accept.json +26 -0
- package/dist/registration-conformance/cases/gate-no-gadgets-accept.json +1 -0
- package/dist/registration-conformance/cases/gate-package-mismatch.json +1 -0
- package/dist/registration-conformance/cases/gate-priority-not-registered-outranks-mismatch.json +1 -0
- package/dist/registration-conformance/cases/gate-priority-registry-outranks-public-env.json +22 -0
- package/dist/registration-conformance/cases/gate-public-env-missing.json +21 -0
- package/dist/registration-conformance/cases/gate-public-env-satisfied-accept.json +21 -0
- package/dist/registration-conformance/cases/gate-public-env-undeclared-wrapper-accept.json +26 -0
- package/dist/registration-conformance/cases/gate-registered-resolves-accept.json +1 -0
- package/dist/registration-conformance/cases/gate-stdlib-hook-accept.json +1 -0
- package/dist/registration-conformance/cases/gate-typo-classifies-as-not-registered.json +1 -0
- package/dist/registration-conformance/index.d.ts +64 -33
- package/dist/registration-conformance/index.d.ts.map +1 -1
- package/dist/registration-conformance/index.js +24 -8
- package/dist/run-conformance.d.ts +145 -6
- package/dist/run-conformance.d.ts.map +1 -1
- package/dist/run-conformance.js +531 -93
- package/dist/types.d.ts +319 -165
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +27 -23
- package/dist/ws-transport.d.ts.map +1 -1
- package/dist/ws-transport.js +3 -2
- package/package.json +3 -3
- package/dist/fixtures/observability-events/index.d.ts +0 -5
- package/dist/fixtures/observability-events/index.d.ts.map +0 -1
- package/dist/fixtures/observability-events/index.js +0 -22
- package/dist/fixtures/observability-events/observability-contract-error-emitted.json +0 -28
- package/dist/fixtures/observability-events/observability-wired-tool-invoked.json +0 -27
- package/dist/fixtures/refresh-semantics/index.d.ts +0 -5
- package/dist/fixtures/refresh-semantics/index.d.ts.map +0 -1
- package/dist/fixtures/refresh-semantics/index.js +0 -19
- package/dist/fixtures/refresh-semantics/stream-refresh-success.json +0 -39
- package/dist/fixtures/reserved-channel-authority/stream-schema-violation.json +0 -44
- package/dist/fixtures/wired-action-dispatch/index.d.ts +0 -5
- package/dist/fixtures/wired-action-dispatch/index.d.ts.map +0 -1
- package/dist/fixtures/wired-action-dispatch/index.js +0 -29
- package/dist/fixtures/wired-action-dispatch/wired-action-success.json +0 -35
- package/dist/fixtures/wired-action-dispatch/wired-action-tool-not-found.json +0 -30
- package/dist/fixtures/wired-action-dispatch/wired-action-tool-threw.json +0 -31
- package/dist/fixtures/wired-action-dispatch/wired-action-tool-timeout.json +0 -31
package/dist/run-conformance.js
CHANGED
|
@@ -6,31 +6,56 @@
|
|
|
6
6
|
* 1. Invoke `reporter.onStart(totalFixtures)`.
|
|
7
7
|
* 2. For each fixture (filtered by `config.only` if provided):
|
|
8
8
|
* a. If `fixture.skipReason !== null` → skip with the reason.
|
|
9
|
-
* b.
|
|
9
|
+
* b. Validate every authored setup/teardown directive against
|
|
10
|
+
* the closed `SetupStep` vocabulary (`parseSetupStep`), the
|
|
11
|
+
* authored `inputEnvelope` against the closed input-
|
|
12
|
+
* envelope dispatch vocabulary (`parseInputEnvelope`), and
|
|
13
|
+
* the authored `subscribe` shaping against the closed
|
|
14
|
+
* `SubscribeFrameShaping` vocabulary
|
|
15
|
+
* (`parseSubscribeShaping`). Unknown / malformed directives
|
|
16
|
+
* are fixture-authoring errors — the runner throws, aborting
|
|
17
|
+
* the run loudly (NOT a skip, NOT a fail of the
|
|
18
|
+
* implementation under test).
|
|
19
|
+
* c. If `fixture.setup` non-empty AND `config.host` absent →
|
|
10
20
|
* skip with "no host provided".
|
|
11
|
-
*
|
|
21
|
+
* d. Dispatch every setup step via `host.dispatchSetup()`.
|
|
12
22
|
* Throw → skip with the error message (NOT a fail).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
23
|
+
* e. Open a WS transport against `config.serverUrl`.
|
|
24
|
+
* f. Send the canonical `subscribe` frame (the runner knows the
|
|
15
25
|
* wire shape — fixture's `inputEnvelope` is NOT the subscribe
|
|
16
|
-
* frame; subscribe is always runner-owned)
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
26
|
+
* frame; subscribe is always runner-owned), shaped by the
|
|
27
|
+
* fixture's validated `subscribe` knob (`omitAppId`,
|
|
28
|
+
* `supportedVersions` with the `'current'` sentinel resolved
|
|
29
|
+
* to the kit's compiled `PROTOCOL_SCHEMA_VERSION`).
|
|
30
|
+
* g. If the fixture's `inputEnvelope` is a dispatchable C→S
|
|
31
|
+
* frame (`action` live-channel dispatch, or
|
|
32
|
+
* `host_context_observed` — the validated typed arm), send
|
|
33
|
+
* it AFTER subscribe.
|
|
34
|
+
* h. Observe frames for `config.observationTimeoutMs` (default
|
|
20
35
|
* 2000ms).
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
36
|
+
* i. Match observed frames against `fixture.expectedBehavior` —
|
|
37
|
+
* EXCEPT `session-state`, a stateful obligation with no wire
|
|
38
|
+
* response: the runner grades it after the observation window
|
|
39
|
+
* by reading the GguiSession field back via
|
|
40
|
+
* `host.readSessionField()` (`matchSessionState`). Absent
|
|
41
|
+
* host / absent method / a throwing read → skip-with-reason,
|
|
42
|
+
* never a weakened pass.
|
|
43
|
+
* j. Close the WS.
|
|
44
|
+
* k. Dispatch every teardown step via `host.dispatchTeardown()`.
|
|
24
45
|
* Throw → reporter warning; does NOT flip pass → fail.
|
|
25
|
-
*
|
|
46
|
+
* (No teardown vocabulary exists in this kit version, so the
|
|
47
|
+
* loop is empty today.)
|
|
48
|
+
* l. Record the outcome via reporter + accumulate in result.
|
|
26
49
|
* 3. Invoke `reporter.onComplete(result)` + return.
|
|
27
50
|
*
|
|
28
51
|
* Scope v1: pure WS transport. Browser-level fixtures (bootstrap-
|
|
29
|
-
* failure, props-update
|
|
30
|
-
*
|
|
52
|
+
* failure, props-update) automatically skip via the matcher's
|
|
53
|
+
* `unmatchable-on-ws` arm.
|
|
31
54
|
*/
|
|
55
|
+
import { PROTOCOL_SCHEMA_VERSION } from '@ggui-ai/protocol';
|
|
32
56
|
import { allFixtures, fixturesByContract } from './fixtures/index.js';
|
|
33
|
-
import {
|
|
57
|
+
import { isRecord } from '@ggui-ai/protocol';
|
|
58
|
+
import { behaviorIs, deepEqual, matchBehavior } from './match-behavior.js';
|
|
34
59
|
import { openWsTransport } from './ws-transport.js';
|
|
35
60
|
// =============================================================================
|
|
36
61
|
// Runner
|
|
@@ -82,23 +107,34 @@ async function runOneFixture(fixture, config, _reporter) {
|
|
|
82
107
|
if (fixture.skipReason !== null) {
|
|
83
108
|
return { kind: 'skip', reason: fixture.skipReason };
|
|
84
109
|
}
|
|
85
|
-
|
|
110
|
+
// Validate the authored directives BEFORE any host or transport
|
|
111
|
+
// work. An unknown / malformed directive is a fixture-authoring
|
|
112
|
+
// error, not a behavior of the implementation under test — the
|
|
113
|
+
// parse throws a descriptive error that aborts the whole run. The
|
|
114
|
+
// input envelope goes through the same gate: a malformed
|
|
115
|
+
// `host_context_observed` frame is a fixture-authoring error, never
|
|
116
|
+
// a verdict on the server.
|
|
117
|
+
const setupSteps = fixture.setup.map((step) => parseSetupStep(fixture.name, step));
|
|
118
|
+
const teardownSteps = (fixture.teardown ?? []).map((step) => parseTeardownStep(fixture.name, step));
|
|
119
|
+
const inputDispatch = parseInputEnvelope(fixture.name, fixture.inputEnvelope);
|
|
120
|
+
const subscribeShaping = parseSubscribeShaping(fixture.name, fixture.subscribe);
|
|
121
|
+
if (setupSteps.length > 0 && config.host === undefined) {
|
|
86
122
|
return {
|
|
87
123
|
kind: 'skip',
|
|
88
|
-
reason: `fixture has ${
|
|
124
|
+
reason: `fixture has ${setupSteps.length} setup step(s) and no \`host\` was provided — supply a ConformanceHost to drive setup directives.`,
|
|
89
125
|
};
|
|
90
126
|
}
|
|
91
127
|
// Dispatch setup via host. A throw means "host doesn't implement
|
|
92
128
|
// this directive" — record as skip, NOT fail.
|
|
93
129
|
if (config.host !== undefined) {
|
|
94
|
-
for (const step of
|
|
130
|
+
for (const step of setupSteps) {
|
|
95
131
|
try {
|
|
96
|
-
await config.host.dispatchSetup(
|
|
132
|
+
await config.host.dispatchSetup(step);
|
|
97
133
|
}
|
|
98
134
|
catch (err) {
|
|
99
135
|
return {
|
|
100
136
|
kind: 'skip',
|
|
101
|
-
reason: `host refused setup step
|
|
137
|
+
reason: `host refused setup step kind='${step.kind}': ${String(err.message ?? err)}`,
|
|
102
138
|
};
|
|
103
139
|
}
|
|
104
140
|
}
|
|
@@ -108,33 +144,55 @@ async function runOneFixture(fixture, config, _reporter) {
|
|
|
108
144
|
try {
|
|
109
145
|
const wsUrl = deriveWsUrl(config.serverUrl);
|
|
110
146
|
transport = await openWsTransport({ kind: 'ws', url: wsUrl, auth: config.auth });
|
|
111
|
-
const
|
|
147
|
+
const sessionId = extractSessionId(fixture, setupSteps);
|
|
148
|
+
// `subscribe` (SubscribeFrameShaping, validated above) shapes the
|
|
149
|
+
// runner-owned frame: `omitAppId` drops the conventional
|
|
150
|
+
// `appId: 'conformance'` stamp — the probe for SPEC §12.2's
|
|
151
|
+
// identity-default resolution — and `supportedVersions` declares
|
|
152
|
+
// the client half of §12.2.2's version handshake (the `'current'`
|
|
153
|
+
// sentinel already resolved to the kit's compiled
|
|
154
|
+
// PROTOCOL_SCHEMA_VERSION).
|
|
112
155
|
transport.send({
|
|
113
156
|
type: 'subscribe',
|
|
114
157
|
payload: {
|
|
115
|
-
|
|
116
|
-
appId: 'conformance',
|
|
158
|
+
sessionId,
|
|
159
|
+
...(subscribeShaping.omitAppId ? {} : { appId: 'conformance' }),
|
|
117
160
|
role: 'user',
|
|
118
|
-
...(
|
|
161
|
+
...(subscribeShaping.supportedVersions !== undefined
|
|
162
|
+
? { supportedVersions: subscribeShaping.supportedVersions }
|
|
163
|
+
: {}),
|
|
119
164
|
},
|
|
120
165
|
requestId: `conformance-subscribe-${fixture.name}`,
|
|
121
166
|
});
|
|
122
|
-
if (
|
|
123
|
-
transport.send(
|
|
167
|
+
if (inputDispatch.kind !== 'none') {
|
|
168
|
+
transport.send(inputDispatch.envelope);
|
|
124
169
|
}
|
|
125
170
|
const frames = await transport.observe({
|
|
126
171
|
timeoutMs: config.observationTimeoutMs ?? 2000,
|
|
127
172
|
});
|
|
128
|
-
|
|
173
|
+
// `session-state` is a stateful obligation — the input message
|
|
174
|
+
// produced no wire response, so the grade is a post-observation-
|
|
175
|
+
// window read-back of the GguiSession field via the host, not a
|
|
176
|
+
// frame match. Every other behavior kind grades against observed
|
|
177
|
+
// frames. (`behaviorIs` narrows to the specific arm — the
|
|
178
|
+
// extensibly-closed `UnknownBehavior` in the union has
|
|
179
|
+
// `kind: string & {}` which defeats bare literal narrowing.)
|
|
180
|
+
const expectedBehavior = fixture.expectedBehavior;
|
|
181
|
+
const match = behaviorIs(expectedBehavior, 'session-state')
|
|
182
|
+
? await matchSessionState(expectedBehavior, sessionId, config.host)
|
|
183
|
+
: matchBehavior(expectedBehavior, frames);
|
|
129
184
|
// Dispatch teardown regardless of match outcome. Failures in
|
|
130
185
|
// teardown surface as warnings; they do not flip pass → fail.
|
|
131
|
-
|
|
132
|
-
|
|
186
|
+
// (The teardown vocabulary is empty in this kit version, so
|
|
187
|
+
// `teardownSteps` is always empty today — the loop is the slot
|
|
188
|
+
// future cleanup directives flow through.)
|
|
189
|
+
if (config.host !== undefined) {
|
|
190
|
+
for (const step of teardownSteps) {
|
|
133
191
|
try {
|
|
134
|
-
await config.host.dispatchTeardown(
|
|
192
|
+
await config.host.dispatchTeardown(step);
|
|
135
193
|
}
|
|
136
194
|
catch (err) {
|
|
137
|
-
_reporter.onTeardownWarning?.(fixture.name, `teardown step
|
|
195
|
+
_reporter.onTeardownWarning?.(fixture.name, `teardown step threw: ${String(err.message ?? err)}`);
|
|
138
196
|
}
|
|
139
197
|
}
|
|
140
198
|
}
|
|
@@ -182,59 +240,303 @@ function filterFixtures(only) {
|
|
|
182
240
|
const allow = new Set(only);
|
|
183
241
|
return allFixtures.filter((fixture) => allow.has(fixture.name));
|
|
184
242
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Resolve the WS endpoint from `config.serverUrl`:
|
|
245
|
+
* - `http://` / `https://` schemes become `ws://` / `wss://`; a
|
|
246
|
+
* scheme-less value is assumed `ws://`.
|
|
247
|
+
* - A bare origin (no path) gets the default live-channel path
|
|
248
|
+
* `/ws` appended.
|
|
249
|
+
* - A URL that already carries a path is used exactly as given —
|
|
250
|
+
* the runner never appends to an explicit path, so
|
|
251
|
+
* `ws://host:3000/ws` (or a non-default mount point) works
|
|
252
|
+
* without double-appending.
|
|
253
|
+
*
|
|
254
|
+
* Exported for unit tests; not part of the package's public API.
|
|
255
|
+
*/
|
|
256
|
+
export function deriveWsUrl(serverUrl) {
|
|
257
|
+
const withScheme = /^(?:https?|wss?):\/\//.test(serverUrl)
|
|
258
|
+
? serverUrl
|
|
259
|
+
: `ws://${serverUrl}`;
|
|
260
|
+
const url = new URL(withScheme);
|
|
261
|
+
if (url.protocol === 'http:')
|
|
262
|
+
url.protocol = 'ws:';
|
|
263
|
+
if (url.protocol === 'https:')
|
|
264
|
+
url.protocol = 'wss:';
|
|
265
|
+
if (url.pathname === '' || url.pathname === '/') {
|
|
266
|
+
url.pathname = '/ws';
|
|
267
|
+
}
|
|
268
|
+
return url.toString();
|
|
196
269
|
}
|
|
197
|
-
function
|
|
198
|
-
// Prefer a
|
|
270
|
+
function extractSessionId(fixture, setupSteps) {
|
|
271
|
+
// Prefer a sessionId from the first `create-session` setup step —
|
|
199
272
|
// this is the fixture's declared render identity. Fall back to
|
|
200
|
-
// inputEnvelope.
|
|
201
|
-
for (const step of
|
|
202
|
-
if (step.
|
|
203
|
-
|
|
204
|
-
return step.renderId;
|
|
205
|
-
}
|
|
273
|
+
// inputEnvelope.sessionId, then to the fixture name.
|
|
274
|
+
for (const step of setupSteps) {
|
|
275
|
+
if (step.kind === 'create-session')
|
|
276
|
+
return step.sessionId;
|
|
206
277
|
}
|
|
207
278
|
const envelope = fixture.inputEnvelope;
|
|
208
|
-
if (envelope
|
|
209
|
-
|
|
210
|
-
'renderId' in envelope &&
|
|
211
|
-
typeof envelope.renderId === 'string') {
|
|
212
|
-
return envelope.renderId;
|
|
279
|
+
if (isRecord(envelope) && typeof envelope['sessionId'] === 'string') {
|
|
280
|
+
return envelope['sessionId'];
|
|
213
281
|
}
|
|
214
282
|
return fixture.name;
|
|
215
283
|
}
|
|
216
284
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
285
|
+
* Validating classifier for the fixture's `inputEnvelope`. The fixture
|
|
286
|
+
* JSON enters the type system through a compile-time cast, so —
|
|
287
|
+
* exactly like {@link parseSetupStep} — the dispatchable arms are
|
|
288
|
+
* re-validated structurally here, and a malformed
|
|
289
|
+
* `host_context_observed` frame throws a descriptive fixture-authoring
|
|
290
|
+
* error (never a skip, never a fail of the implementation under test).
|
|
291
|
+
*
|
|
292
|
+
* Exported for unit tests; not part of the package's public API.
|
|
221
293
|
*/
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
294
|
+
export function parseInputEnvelope(fixtureName, envelope) {
|
|
295
|
+
if (!isRecord(envelope) || typeof envelope['type'] !== 'string') {
|
|
296
|
+
return { kind: 'none' };
|
|
297
|
+
}
|
|
298
|
+
if (envelope['type'] === 'action') {
|
|
299
|
+
return { kind: 'action', envelope };
|
|
300
|
+
}
|
|
301
|
+
if (envelope['type'] === 'host_context_observed') {
|
|
302
|
+
return {
|
|
303
|
+
kind: 'host_context_observed',
|
|
304
|
+
envelope: parseHostContextObservedEnvelope(fixtureName, envelope),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return { kind: 'none' };
|
|
308
|
+
}
|
|
309
|
+
function parseHostContextObservedEnvelope(fixtureName, envelope) {
|
|
310
|
+
const requestId = envelope['requestId'];
|
|
311
|
+
if (requestId !== undefined && typeof requestId !== 'string') {
|
|
312
|
+
throw malformedInputEnvelope(fixtureName, "'requestId' must be a string when present", envelope);
|
|
313
|
+
}
|
|
314
|
+
const payload = envelope['payload'];
|
|
315
|
+
if (!isRecord(payload)) {
|
|
316
|
+
throw malformedInputEnvelope(fixtureName, "'payload' must be an object carrying {sessionId, hostContext}", envelope);
|
|
317
|
+
}
|
|
318
|
+
const sessionId = payload['sessionId'];
|
|
319
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
320
|
+
throw malformedInputEnvelope(fixtureName, "'payload.sessionId' must be a non-empty string", envelope);
|
|
321
|
+
}
|
|
322
|
+
const hostContext = parseHostContextProjection(fixtureName, payload['hostContext'], envelope);
|
|
323
|
+
return {
|
|
324
|
+
type: 'host_context_observed',
|
|
325
|
+
...(requestId !== undefined ? { requestId } : {}),
|
|
326
|
+
payload: { sessionId, hostContext },
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Validating narrower for the authored `hostContext` body against the
|
|
331
|
+
* live `HostContextProjection` (`@ggui-ai/protocol`,
|
|
332
|
+
* `types/host-context`). Every field is optional, so presence is never
|
|
333
|
+
* required — but a present field MUST carry the projection's shape,
|
|
334
|
+
* and unknown keys are REJECTED: a key outside the projection (e.g. a
|
|
335
|
+
* stale `theme` — theme flows through ggui's theming pipeline, not
|
|
336
|
+
* host context) is state no conformant server is obligated to read
|
|
337
|
+
* back, which would make the paired `session-state` grade dishonest.
|
|
338
|
+
*/
|
|
339
|
+
function parseHostContextProjection(fixtureName, value, envelope) {
|
|
340
|
+
if (!isRecord(value)) {
|
|
341
|
+
throw malformedInputEnvelope(fixtureName, "'payload.hostContext' must be an object (the HostContextProjection wire shape)", envelope);
|
|
342
|
+
}
|
|
343
|
+
const out = {};
|
|
344
|
+
for (const [key, field] of Object.entries(value)) {
|
|
345
|
+
switch (key) {
|
|
346
|
+
case 'availableDisplayModes': {
|
|
347
|
+
if (!Array.isArray(field)) {
|
|
348
|
+
throw malformedInputEnvelope(fixtureName, "'hostContext.availableDisplayModes' must be an array of display modes ('inline' | 'fullscreen' | 'pip')", envelope);
|
|
349
|
+
}
|
|
350
|
+
const modes = [];
|
|
351
|
+
for (const item of field) {
|
|
352
|
+
if (!isDisplayMode(item)) {
|
|
353
|
+
throw malformedInputEnvelope(fixtureName, `'hostContext.availableDisplayModes' carries ${JSON.stringify(item)} — display modes are 'inline' | 'fullscreen' | 'pip'`, envelope);
|
|
354
|
+
}
|
|
355
|
+
modes.push(item);
|
|
356
|
+
}
|
|
357
|
+
out.availableDisplayModes = modes;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case 'currentDisplayMode': {
|
|
361
|
+
if (!isDisplayMode(field)) {
|
|
362
|
+
throw malformedInputEnvelope(fixtureName, "'hostContext.currentDisplayMode' must be 'inline' | 'fullscreen' | 'pip'", envelope);
|
|
363
|
+
}
|
|
364
|
+
out.currentDisplayMode = field;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case 'containerDimensions': {
|
|
368
|
+
if (!isRecord(field)) {
|
|
369
|
+
throw malformedInputEnvelope(fixtureName, "'hostContext.containerDimensions' must be an object of numeric width/maxWidth/height/maxHeight", envelope);
|
|
370
|
+
}
|
|
371
|
+
const dims = {};
|
|
372
|
+
for (const [dimKey, dimValue] of Object.entries(field)) {
|
|
373
|
+
if (dimKey !== 'width' &&
|
|
374
|
+
dimKey !== 'maxWidth' &&
|
|
375
|
+
dimKey !== 'height' &&
|
|
376
|
+
dimKey !== 'maxHeight') {
|
|
377
|
+
throw malformedInputEnvelope(fixtureName, `'hostContext.containerDimensions' carries unknown key '${dimKey}' — the projection knows width, maxWidth, height, maxHeight`, envelope);
|
|
378
|
+
}
|
|
379
|
+
if (typeof dimValue !== 'number') {
|
|
380
|
+
throw malformedInputEnvelope(fixtureName, `'hostContext.containerDimensions.${dimKey}' must be a number`, envelope);
|
|
381
|
+
}
|
|
382
|
+
dims[dimKey] = dimValue;
|
|
383
|
+
}
|
|
384
|
+
out.containerDimensions = dims;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case 'platform': {
|
|
388
|
+
if (field !== 'web' && field !== 'desktop' && field !== 'mobile') {
|
|
389
|
+
throw malformedInputEnvelope(fixtureName, "'hostContext.platform' must be 'web' | 'desktop' | 'mobile'", envelope);
|
|
390
|
+
}
|
|
391
|
+
out.platform = field;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case 'deviceCapabilities': {
|
|
395
|
+
if (!isRecord(field)) {
|
|
396
|
+
throw malformedInputEnvelope(fixtureName, "'hostContext.deviceCapabilities' must be an object of boolean touch/hover", envelope);
|
|
397
|
+
}
|
|
398
|
+
const caps = {};
|
|
399
|
+
for (const [capKey, capValue] of Object.entries(field)) {
|
|
400
|
+
if (capKey !== 'touch' && capKey !== 'hover') {
|
|
401
|
+
throw malformedInputEnvelope(fixtureName, `'hostContext.deviceCapabilities' carries unknown key '${capKey}' — the projection knows touch, hover`, envelope);
|
|
402
|
+
}
|
|
403
|
+
if (typeof capValue !== 'boolean') {
|
|
404
|
+
throw malformedInputEnvelope(fixtureName, `'hostContext.deviceCapabilities.${capKey}' must be a boolean`, envelope);
|
|
405
|
+
}
|
|
406
|
+
caps[capKey] = capValue;
|
|
407
|
+
}
|
|
408
|
+
out.deviceCapabilities = caps;
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case 'locale': {
|
|
412
|
+
if (typeof field !== 'string' || field.length === 0) {
|
|
413
|
+
throw malformedInputEnvelope(fixtureName, "'hostContext.locale' must be a non-empty string", envelope);
|
|
414
|
+
}
|
|
415
|
+
out.locale = field;
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case 'timeZone': {
|
|
419
|
+
if (typeof field !== 'string' || field.length === 0) {
|
|
420
|
+
throw malformedInputEnvelope(fixtureName, "'hostContext.timeZone' must be a non-empty string", envelope);
|
|
421
|
+
}
|
|
422
|
+
out.timeZone = field;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
default:
|
|
426
|
+
throw malformedInputEnvelope(fixtureName, `'hostContext' carries unknown key '${key}' — the HostContextProjection vocabulary is availableDisplayModes, currentDisplayMode, containerDimensions, platform, deviceCapabilities, locale, timeZone`, envelope);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
function isDisplayMode(value) {
|
|
432
|
+
return value === 'inline' || value === 'fullscreen' || value === 'pip';
|
|
228
433
|
}
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
434
|
+
function malformedInputEnvelope(fixtureName, problem, envelope) {
|
|
435
|
+
return new Error(`protocol-conformance: fixture '${fixtureName}' authors a malformed 'host_context_observed' input envelope — ${problem}. Received: ${JSON.stringify(envelope)}`);
|
|
436
|
+
}
|
|
437
|
+
// =============================================================================
|
|
438
|
+
// Session-state grading (the kit's third grading mechanism)
|
|
439
|
+
// =============================================================================
|
|
440
|
+
/**
|
|
441
|
+
* Grade a `session-state` expectation — the input message left no wire
|
|
442
|
+
* trace, so the honest verdict is a post-observation-window read-back
|
|
443
|
+
* of the mutated GguiSession field via
|
|
444
|
+
* {@link ConformanceHost.readSessionField}, compared with the SAME
|
|
445
|
+
* exact {@link deepEqual} the frame matchers use.
|
|
446
|
+
*
|
|
447
|
+
* A missing host, a host without `readSessionField`, or a read that
|
|
448
|
+
* throws all yield `unmatchable-on-ws` — the runner records a SKIP
|
|
449
|
+
* with the reason. The kit refuses to grade a stateful obligation it
|
|
450
|
+
* has no introspection seam for (a host that cannot read state cannot
|
|
451
|
+
* grade it); it never converts that gap into a pass or a fail.
|
|
452
|
+
*
|
|
453
|
+
* Exported for unit tests; not part of the package's public API.
|
|
454
|
+
*/
|
|
455
|
+
export async function matchSessionState(behavior, sessionId, host) {
|
|
456
|
+
if (host === undefined || host.readSessionField === undefined) {
|
|
457
|
+
return {
|
|
458
|
+
kind: 'unmatchable-on-ws',
|
|
459
|
+
reason: host === undefined
|
|
460
|
+
? 'session-state expectation needs a ConformanceHost with readSessionField() — no host was provided.'
|
|
461
|
+
: 'session-state expectation needs ConformanceHost.readSessionField() — the provided host does not implement it, so the kit cannot observe the GguiSession-field mutation. SKIP, not a pass.',
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
let actual;
|
|
465
|
+
try {
|
|
466
|
+
actual = await host.readSessionField(sessionId, behavior.field);
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
return {
|
|
470
|
+
kind: 'unmatchable-on-ws',
|
|
471
|
+
reason: `host.readSessionField('${sessionId}', '${behavior.field}') threw — a host that cannot read state cannot grade it: ${String(err.message ?? err)}`,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
if (deepEqual(actual, behavior.expected)) {
|
|
475
|
+
return { kind: 'pass' };
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
kind: 'fail',
|
|
479
|
+
expected: { field: behavior.field, value: behavior.expected },
|
|
480
|
+
received: { field: behavior.field, value: actual },
|
|
481
|
+
message: `session field '${behavior.field}' did not hold the expected value after the input envelope was dispatched — the implementation under test may have dropped the message instead of persisting it.`,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Validating narrower for the fixture-authored `subscribe` knob
|
|
486
|
+
* (`SubscribeFrameShaping`, types.ts). Same trust-boundary posture as
|
|
487
|
+
* {@link parseSetupStep}: the fixture JSON enters the type system
|
|
488
|
+
* through a compile-time cast, so the shape is re-validated
|
|
489
|
+
* structurally here and any malformed or unknown shaping is a
|
|
490
|
+
* fixture-authoring error the runner throws on — never a skip, never
|
|
491
|
+
* a fail of the implementation under test.
|
|
492
|
+
*
|
|
493
|
+
* The vocabulary is CLOSED — exactly the shaping fields the shipped
|
|
494
|
+
* catalog authors (`omitAppId`, `supportedVersions`); an unknown key
|
|
495
|
+
* is rejected so a typo'd knob can never silently no-op into a
|
|
496
|
+
* vacuous grade. `supportedVersions: 'current'` resolves to
|
|
497
|
+
* `[PROTOCOL_SCHEMA_VERSION]` (the kit's compiled canonical), keeping
|
|
498
|
+
* fixtures evergreen across protocol version bumps.
|
|
499
|
+
*
|
|
500
|
+
* Exported for unit tests; not part of the package's public API.
|
|
501
|
+
*/
|
|
502
|
+
export function parseSubscribeShaping(fixtureName, shaping) {
|
|
503
|
+
if (shaping === undefined)
|
|
504
|
+
return { omitAppId: false };
|
|
505
|
+
if (!isRecord(shaping)) {
|
|
506
|
+
throw malformedSubscribeShaping(fixtureName, 'expected an object carrying the SubscribeFrameShaping fields', shaping);
|
|
507
|
+
}
|
|
508
|
+
for (const key of Object.keys(shaping)) {
|
|
509
|
+
if (key !== 'omitAppId' && key !== 'supportedVersions') {
|
|
510
|
+
throw malformedSubscribeShaping(fixtureName, `unknown key '${key}' — the subscribe-shaping vocabulary is closed: omitAppId, supportedVersions`, shaping);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const omitAppId = shaping['omitAppId'];
|
|
514
|
+
if (omitAppId !== undefined && typeof omitAppId !== 'boolean') {
|
|
515
|
+
throw malformedSubscribeShaping(fixtureName, "'omitAppId' must be a boolean when present", shaping);
|
|
516
|
+
}
|
|
517
|
+
const supportedVersions = parseSupportedVersionsDecl(fixtureName, shaping, shaping['supportedVersions']);
|
|
518
|
+
return {
|
|
519
|
+
omitAppId: omitAppId === true,
|
|
520
|
+
...(supportedVersions !== undefined ? { supportedVersions } : {}),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function parseSupportedVersionsDecl(fixtureName, shaping, decl) {
|
|
524
|
+
if (decl === undefined)
|
|
525
|
+
return undefined;
|
|
526
|
+
if (decl === 'current') {
|
|
527
|
+
// The evergreen sentinel — the version this kit release was
|
|
528
|
+
// compiled against, never a stale fixture literal.
|
|
529
|
+
return [PROTOCOL_SCHEMA_VERSION];
|
|
530
|
+
}
|
|
531
|
+
if (Array.isArray(decl) &&
|
|
532
|
+
decl.length > 0 &&
|
|
533
|
+
decl.every((version) => typeof version === 'string' && version.length > 0)) {
|
|
534
|
+
return decl;
|
|
535
|
+
}
|
|
536
|
+
throw malformedSubscribeShaping(fixtureName, "'supportedVersions' must be the sentinel 'current' or a non-empty array of non-empty strings", shaping);
|
|
537
|
+
}
|
|
538
|
+
function malformedSubscribeShaping(fixtureName, problem, shaping) {
|
|
539
|
+
return new Error(`protocol-conformance: fixture '${fixtureName}' authors a malformed 'subscribe' frame shaping — ${problem}. Received: ${JSON.stringify(shaping)}`);
|
|
238
540
|
}
|
|
239
541
|
/**
|
|
240
542
|
* Map fixture name → the bar criterion it exercises, for the
|
|
@@ -253,34 +555,170 @@ function slugToCriterion(slug) {
|
|
|
253
555
|
switch (slug) {
|
|
254
556
|
case 'bootstrap-protocol':
|
|
255
557
|
return 'Protocol #5 named failure modes — bootstrap contract';
|
|
256
|
-
case '
|
|
257
|
-
return '
|
|
258
|
-
case '
|
|
259
|
-
return '
|
|
558
|
+
case 'consume-buffer':
|
|
559
|
+
return 'Single action-routing model — consume-buffer persistence + declared-action contract (name membership + payload schema)';
|
|
560
|
+
case 'host-context':
|
|
561
|
+
return 'Host-context persistence — host_context_observed MUST persist onto GguiSession.hostContext';
|
|
260
562
|
case 'reserved-channel-authority':
|
|
261
563
|
return 'SPEC §4.4 reserved-channel authority';
|
|
262
564
|
case 'schema-version-handshake':
|
|
263
565
|
return 'Protocol #3 version negotiation';
|
|
264
|
-
case '
|
|
265
|
-
return '
|
|
566
|
+
case 'subscribe-tenancy':
|
|
567
|
+
return 'SPEC §12.2 subscribe tenancy — appId MUST match the bound app (§12.2.3 APP_MISMATCH)';
|
|
266
568
|
default:
|
|
267
569
|
return slug;
|
|
268
570
|
}
|
|
269
571
|
}
|
|
270
572
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* The
|
|
274
|
-
*
|
|
275
|
-
*
|
|
573
|
+
* Validating narrower from the fixture-JSON `SetupStep` (discriminated
|
|
574
|
+
* on `type`) to the host's runtime `SetupStep` (discriminated on
|
|
575
|
+
* `kind`). The fixture JSON enters the type system through a
|
|
576
|
+
* compile-time cast (tsc widens JSON-module string literals), so the
|
|
577
|
+
* static union cannot be trusted at runtime — every directive is
|
|
578
|
+
* re-validated structurally here.
|
|
579
|
+
*
|
|
580
|
+
* The setup vocabulary is CLOSED: an unknown or malformed directive is
|
|
581
|
+
* a fixture-authoring error, thrown with a descriptive message. It is
|
|
582
|
+
* never mapped to a skip or a fail — those verdicts describe the
|
|
583
|
+
* implementation under test, not the fixture catalog.
|
|
584
|
+
*
|
|
585
|
+
* The input is `unknown` by design — this function IS the trust
|
|
586
|
+
* boundary, so it never leans on the static union it produces
|
|
587
|
+
* evidence for.
|
|
588
|
+
*
|
|
589
|
+
* Exported for unit tests; not part of the package's public API.
|
|
276
590
|
*/
|
|
277
|
-
function
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
591
|
+
export function parseSetupStep(fixtureName, step) {
|
|
592
|
+
if (!isRecord(step) || typeof step['type'] !== 'string') {
|
|
593
|
+
throw new Error(`protocol-conformance: fixture '${fixtureName}' authors a malformed setup directive — expected an object with a string 'type'. Received: ${JSON.stringify(step)}`);
|
|
594
|
+
}
|
|
595
|
+
const type = step['type'];
|
|
596
|
+
if (type === 'create-session') {
|
|
597
|
+
const sessionId = requireStringField(fixtureName, type, step, 'sessionId');
|
|
598
|
+
const appId = step['appId'];
|
|
599
|
+
if (appId !== undefined && typeof appId !== 'string') {
|
|
600
|
+
throw malformedDirective(fixtureName, type, step, "'appId' must be a string when present");
|
|
601
|
+
}
|
|
602
|
+
const actionSpec = step['actionSpec'];
|
|
603
|
+
if (actionSpec !== undefined && !isActionSpecDecl(actionSpec)) {
|
|
604
|
+
throw malformedDirective(fixtureName, type, step, "'actionSpec' must be an object mapping action names to entry objects (each entry's optional 'schema' a shape-valid JSON-Schema node) when present");
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
kind: 'create-session',
|
|
608
|
+
sessionId,
|
|
609
|
+
...(appId !== undefined ? { appId } : {}),
|
|
610
|
+
...(actionSpec !== undefined ? { actionSpec } : {}),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
if (type === 'renderer-url-override') {
|
|
614
|
+
return {
|
|
615
|
+
kind: 'renderer-url-override',
|
|
616
|
+
sessionId: requireStringField(fixtureName, type, step, 'sessionId'),
|
|
617
|
+
url: requireStringField(fixtureName, type, step, 'url'),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
if (type === 'server-version-override') {
|
|
621
|
+
return {
|
|
622
|
+
kind: 'server-version-override',
|
|
623
|
+
sessionId: requireStringField(fixtureName, type, step, 'sessionId'),
|
|
624
|
+
advertiseVersion: requireStringField(fixtureName, type, step, 'advertiseVersion'),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
if (type === 'ui-initialize-response-override') {
|
|
628
|
+
const sessionId = requireStringField(fixtureName, type, step, 'sessionId');
|
|
629
|
+
if (!('override' in step)) {
|
|
630
|
+
throw malformedDirective(fixtureName, type, step, "missing the 'override' response body");
|
|
631
|
+
}
|
|
632
|
+
return { kind: 'ui-initialize-response-override', sessionId, override: step['override'] };
|
|
633
|
+
}
|
|
634
|
+
if (type === 'emit-envelope') {
|
|
635
|
+
const channel = requireStringField(fixtureName, type, step, 'channel');
|
|
636
|
+
if (!('payload' in step)) {
|
|
637
|
+
throw malformedDirective(fixtureName, type, step, "missing the 'payload' envelope body");
|
|
638
|
+
}
|
|
639
|
+
return { kind: 'emit-envelope', channel, payload: step['payload'] };
|
|
640
|
+
}
|
|
641
|
+
throw new Error(`protocol-conformance: fixture '${fixtureName}' authors unknown setup directive type='${type}'. The setup vocabulary is closed; known directives: create-session, renderer-url-override, server-version-override, ui-initialize-response-override, emit-envelope.`);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Teardown counterpart of {@link parseSetupStep}. The teardown
|
|
645
|
+
* vocabulary is empty in this kit version (renders decay via TTL), so
|
|
646
|
+
* ANY authored teardown directive is a fixture-authoring error.
|
|
647
|
+
*/
|
|
648
|
+
function parseTeardownStep(fixtureName, step) {
|
|
649
|
+
throw new Error(`protocol-conformance: fixture '${fixtureName}' authors a teardown directive, but this kit version defines no teardown vocabulary (renders decay via TTL). Received: ${JSON.stringify(step)}`);
|
|
650
|
+
}
|
|
651
|
+
function requireStringField(fixtureName, directiveType, step, field) {
|
|
652
|
+
const value = step[field];
|
|
653
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
654
|
+
throw malformedDirective(fixtureName, directiveType, step, `'${field}' must be a non-empty string`);
|
|
655
|
+
}
|
|
656
|
+
return value;
|
|
657
|
+
}
|
|
658
|
+
function malformedDirective(fixtureName, directiveType, step, problem) {
|
|
659
|
+
return new Error(`protocol-conformance: fixture '${fixtureName}' authors a malformed '${directiveType}' setup directive — ${problem}. Received: ${JSON.stringify(step)}`);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Validating narrower for `CreateGguiSessionStep.actionSpec`. Each
|
|
663
|
+
* entry must be an object whose optional `schema` field — when
|
|
664
|
+
* authored — is a shape-valid {@link JsonSchemaDecl} per
|
|
665
|
+
* {@link isJsonSchemaDecl}.
|
|
666
|
+
*/
|
|
667
|
+
function isActionSpecDecl(value) {
|
|
668
|
+
return (isRecord(value) &&
|
|
669
|
+
Object.values(value).every((entry) => isRecord(entry) &&
|
|
670
|
+
(entry['schema'] === undefined || isJsonSchemaDecl(entry['schema']))));
|
|
281
671
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
672
|
+
/** The `type` keyword values the kit's authored JSON-Schema subset names. */
|
|
673
|
+
const JSON_SCHEMA_DECL_TYPES = [
|
|
674
|
+
'string',
|
|
675
|
+
'number',
|
|
676
|
+
'integer',
|
|
677
|
+
'boolean',
|
|
678
|
+
'array',
|
|
679
|
+
'object',
|
|
680
|
+
'null',
|
|
681
|
+
];
|
|
682
|
+
/**
|
|
683
|
+
* Validating parse for the typed core of {@link JsonSchemaDecl} —
|
|
684
|
+
* recursively checks that every keyword the kit's authored vocabulary
|
|
685
|
+
* names carries the shape the type declares. Keywords outside the
|
|
686
|
+
* typed core pass through verbatim (the implementation under test
|
|
687
|
+
* owns their interpretation), mirroring the index-signature tail on
|
|
688
|
+
* the declared type. A fixture authoring a malformed schema node is a
|
|
689
|
+
* fixture-authoring error the runner throws on — never a skip or a
|
|
690
|
+
* fail of the implementation under test.
|
|
691
|
+
*/
|
|
692
|
+
function isJsonSchemaDecl(value) {
|
|
693
|
+
if (!isRecord(value))
|
|
694
|
+
return false;
|
|
695
|
+
if (value['type'] !== undefined && !JSON_SCHEMA_DECL_TYPES.includes(value['type'])) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
if (value['description'] !== undefined && typeof value['description'] !== 'string') {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
if (value['enum'] !== undefined && !Array.isArray(value['enum']))
|
|
702
|
+
return false;
|
|
703
|
+
if (value['items'] !== undefined && !isJsonSchemaDecl(value['items']))
|
|
704
|
+
return false;
|
|
705
|
+
const properties = value['properties'];
|
|
706
|
+
if (properties !== undefined) {
|
|
707
|
+
if (!isRecord(properties))
|
|
708
|
+
return false;
|
|
709
|
+
if (!Object.values(properties).every(isJsonSchemaDecl))
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
const required = value['required'];
|
|
713
|
+
if (required !== undefined &&
|
|
714
|
+
!(Array.isArray(required) && required.every((name) => typeof name === 'string'))) {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
const additionalProperties = value['additionalProperties'];
|
|
718
|
+
if (additionalProperties !== undefined &&
|
|
719
|
+
typeof additionalProperties !== 'boolean' &&
|
|
720
|
+
!isJsonSchemaDecl(additionalProperties)) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
return true;
|
|
286
724
|
}
|