@ggui-ai/protocol-conformance 0.24.0 → 0.26.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 +45 -25
- package/dist/cli.d.ts +4 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -8
- package/dist/dispatch-idempotency-conformance/index.d.ts +78 -0
- package/dist/dispatch-idempotency-conformance/index.d.ts.map +1 -0
- package/dist/dispatch-idempotency-conformance/index.js +173 -0
- package/dist/fixtures/index.d.ts +22 -4
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +23 -3
- package/dist/fixtures/reserved-channel-replay/fresh-subscribe-replays-reserved-preview.json +30 -0
- package/dist/fixtures/reserved-channel-replay/index.d.ts +4 -0
- package/dist/fixtures/reserved-channel-replay/index.d.ts.map +1 -0
- package/dist/fixtures/reserved-channel-replay/index.js +18 -0
- package/dist/host-helper-conformance/index.d.ts +195 -1
- package/dist/host-helper-conformance/index.d.ts.map +1 -1
- package/dist/host-helper-conformance/index.js +383 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/match-behavior.d.ts.map +1 -1
- package/dist/match-behavior.js +47 -8
- package/dist/n1-compat-conformance/cases/forward-app-theme-carry-uninterpretable.json +21 -0
- package/dist/n1-compat-conformance/cases/forward-ops-list-blueprints-cloned.json +88 -0
- package/dist/n1-compat-conformance/cases/forward-ops-list-blueprints-stamped.json +77 -0
- package/dist/n1-compat-conformance/cases/forward-view-proof-later-version.json +14 -0
- package/dist/n1-compat-conformance/cases/forward-view-proof-root-later-claim.json +14 -0
- package/dist/n1-compat-conformance/cases/release-13-render-result.json +25 -0
- package/dist/n1-compat-conformance/cases/release-14-ops-list-blueprints.json +41 -0
- package/dist/n1-compat-conformance/index.d.ts +1 -1
- package/dist/n1-compat-conformance/index.d.ts.map +1 -1
- package/dist/n1-compat-conformance/index.js +138 -20
- package/dist/reporter.d.ts +5 -0
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +30 -4
- package/dist/run-conformance.d.ts +37 -1
- package/dist/run-conformance.d.ts.map +1 -1
- package/dist/run-conformance.js +32 -17
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -3
|
@@ -60,11 +60,279 @@
|
|
|
60
60
|
* reference validate is `@ggui-ai/design`'s `validateThemeCoverage`
|
|
61
61
|
* bound to the shipped `consumed-tokens.manifest.json` tokens.
|
|
62
62
|
*
|
|
63
|
+
* - `M1-model-tool-set` — OPTIONAL like T1, graded when the host supplies
|
|
64
|
+
* the filter it uses to derive the MODEL's tool list from a served
|
|
65
|
+
* `tools/list` (ggui#1414): the result must withhold every tool whose
|
|
66
|
+
* `_meta.ui.visibility` lacks `'model'` (ggui's six `ggui_runtime_*`
|
|
67
|
+
* app-only tools) and keep every other tool. SEP-1865 assigns this door
|
|
68
|
+
* to the host, and the server cannot observe a violation — a
|
|
69
|
+
* view-issued and a model-issued call are indistinguishable on the wire
|
|
70
|
+
* — so the kit is where a host proves it. Absent ⇒ `skip`.
|
|
71
|
+
*
|
|
72
|
+
* - `V1-view-material-withheld` — OPTIONAL (ggui#1415, SPEC §4.7): the
|
|
73
|
+
* host keeps view-delivered material out of the model's context. The
|
|
74
|
+
* kit feeds the host's own context rules a render result and a render
|
|
75
|
+
* read body whose `_meta["ai.ggui/render"]` slice carries a unique
|
|
76
|
+
* marker in every string value, and requires none of them (raw,
|
|
77
|
+
* JSON-escaped, percent-encoded, or inside a base64 / base64url run) in
|
|
78
|
+
* what the model sees, while the
|
|
79
|
+
* model still sees the result's model-visible marker; and the host's
|
|
80
|
+
* model-facing resource list, when it has one, must withhold
|
|
81
|
+
* `ui://ggui/render/*` while keeping an ordinary resource. Absent ⇒
|
|
82
|
+
* `skip`.
|
|
83
|
+
* - `L1-view-locator-binding` — OPTIONAL (ggui#1415, SPEC §4.7): the host
|
|
84
|
+
* binds a view's calls and reads to its own locator. The kit feeds the
|
|
85
|
+
* host's own relay decision a view mounted for one session, and reads
|
|
86
|
+
* (every locator form) and every session-naming call a view may make,
|
|
87
|
+
* for that session (relay) and another (refuse). Absent ⇒ `skip`.
|
|
88
|
+
*
|
|
89
|
+
* The three host-rule grades (M1, V1, L1) grade the host's RULE, fed
|
|
90
|
+
* fixtures: they prove the rule is right, not that a running host applies
|
|
91
|
+
* it (self-certification; the V1 and L1 pass details say so).
|
|
92
|
+
*
|
|
63
93
|
* A helper that refuses the relay honestly is graded **tier
|
|
64
94
|
* `read-only`** — a LEGAL grade, with the R cases skipped, never
|
|
65
95
|
* failed. `nonconforming` means a dishonesty case failed.
|
|
66
96
|
*/
|
|
97
|
+
import { toolVisibleToModel, } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
67
98
|
import { renderLocatorUri } from '../resource-read-conformance/index.js';
|
|
99
|
+
/**
|
|
100
|
+
* A served-equivalent shape the M1 grade feeds a host's filter (ggui#1414):
|
|
101
|
+
* agent tools as ggui declares them — `ggui_render` and `ggui_update` stamp
|
|
102
|
+
* `visibility: ['model']`, the others carry no marker (the spec's default)
|
|
103
|
+
* — and ggui's seven app-only `ggui_runtime_*` tools. The list is static and
|
|
104
|
+
* the names are the protocol's own, so it grades the FILTER a host applies,
|
|
105
|
+
* never the host's live `tools/list`: a host that filters by name rather
|
|
106
|
+
* than by visibility passes only as long as this list matches what its
|
|
107
|
+
* server lists, which is why the grade wants the filter and the SPEC
|
|
108
|
+
* points at the predicate.
|
|
109
|
+
*/
|
|
110
|
+
export const MODEL_TOOL_SET_FIXTURE = [
|
|
111
|
+
{ name: 'ggui_handshake' },
|
|
112
|
+
{ name: 'ggui_render', _meta: { ui: { visibility: ['model'] } } },
|
|
113
|
+
{ name: 'ggui_update', _meta: { ui: { visibility: ['model'] } } },
|
|
114
|
+
{ name: 'ggui_consume' },
|
|
115
|
+
{ name: 'ggui_amend' },
|
|
116
|
+
{ name: 'ggui_runtime_submit_action', _meta: { ui: { visibility: ['app'] } } },
|
|
117
|
+
{ name: 'ggui_runtime_pull', _meta: { ui: { visibility: ['app'] } } },
|
|
118
|
+
{ name: 'ggui_runtime_sync_context', _meta: { ui: { visibility: ['app'] } } },
|
|
119
|
+
{ name: 'ggui_runtime_refresh_ws_token', _meta: { ui: { visibility: ['app'] } } },
|
|
120
|
+
{ name: 'ggui_runtime_telemetry', _meta: { ui: { visibility: ['app'] } } },
|
|
121
|
+
{ name: 'ggui_runtime_report_render_failure', _meta: { ui: { visibility: ['app'] } } },
|
|
122
|
+
{ name: 'ggui_runtime_declare_tool_catalog', _meta: { ui: { visibility: ['app'] } } },
|
|
123
|
+
];
|
|
124
|
+
/**
|
|
125
|
+
* The render result V1 feeds a host's context builder (ggui#1415). Every
|
|
126
|
+
* string value in the `_meta["ai.ggui/render"]` slice is a unique marker
|
|
127
|
+
* ({@link VIEW_MATERIAL_CANARIES}); the model-visible parts carry values of
|
|
128
|
+
* their own, so a host that shows them cannot trip a canary, and one marker
|
|
129
|
+
* ({@link VIEW_MATERIAL_VISIBLE_MARKER}) the model must still see.
|
|
130
|
+
*/
|
|
131
|
+
export const VIEW_MATERIAL_RESULT_FIXTURE = {
|
|
132
|
+
structuredContent: { sessionId: 'vmx-visible-session-2a7d', status: 'rendered', note: 'vmx-visible-marker-5e1b' },
|
|
133
|
+
content: [{ type: 'text', text: 'Rendered the card (vmx-visible-marker-5e1b).' }],
|
|
134
|
+
_meta: {
|
|
135
|
+
'ai.ggui/render': {
|
|
136
|
+
sessionId: 'vmx-slice-session-3f9a1c',
|
|
137
|
+
appId: 'vmx-slice-app-8b2e',
|
|
138
|
+
runtimeUrl: 'https://vmx-slice-runtime.invalid/iframe-runtime.js',
|
|
139
|
+
wsUrl: 'wss://vmx-slice-wsurl-4c2d.invalid/ws',
|
|
140
|
+
wsToken: 'vmx-slice-wstoken-6d41e0a7',
|
|
141
|
+
viewKey: 'vmx-slice-viewkey-Q2FuYXJ5S2V5',
|
|
142
|
+
expiresAt: '2099-07-07T07:07:07.707Z',
|
|
143
|
+
propsJson: '{"vmxSliceProp":"vmx-slice-props-91c2"}',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* The marker V1 requires in the model's context: the result itself is not
|
|
149
|
+
* withheld. Both model-visible parts carry it, so a host that shows either
|
|
150
|
+
* one passes.
|
|
151
|
+
*/
|
|
152
|
+
export const VIEW_MATERIAL_VISIBLE_MARKER = 'vmx-visible-marker-5e1b';
|
|
153
|
+
/**
|
|
154
|
+
* Every string a value holds, walked deep: object values and keys, arrays,
|
|
155
|
+
* Map entries, Set members, and byte views read as UTF-8 text. Cycles are
|
|
156
|
+
* walked once.
|
|
157
|
+
*/
|
|
158
|
+
function stringValuesIn(value, out = [], seen = new Set()) {
|
|
159
|
+
if (typeof value === 'string') {
|
|
160
|
+
out.push(value);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
if (typeof value !== 'object' || value === null || seen.has(value))
|
|
164
|
+
return out;
|
|
165
|
+
seen.add(value);
|
|
166
|
+
if (ArrayBuffer.isView(value)) {
|
|
167
|
+
out.push(new TextDecoder().decode(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)));
|
|
168
|
+
}
|
|
169
|
+
else if (value instanceof ArrayBuffer) {
|
|
170
|
+
out.push(new TextDecoder().decode(new Uint8Array(value)));
|
|
171
|
+
}
|
|
172
|
+
else if (value instanceof Map) {
|
|
173
|
+
for (const [k, v] of value) {
|
|
174
|
+
stringValuesIn(k, out, seen);
|
|
175
|
+
stringValuesIn(v, out, seen);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (value instanceof Set || Array.isArray(value)) {
|
|
179
|
+
for (const item of value)
|
|
180
|
+
stringValuesIn(item, out, seen);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
for (const [k, v] of Object.entries(value)) {
|
|
184
|
+
out.push(k);
|
|
185
|
+
stringValuesIn(v, out, seen);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* One canary per string value of the fixture's `ai.ggui/render` slice: the
|
|
192
|
+
* value's unique `vmx-slice-…` mark, which survives JSON escaping and
|
|
193
|
+
* encoding, or the whole value when it carries none. None may reach the model.
|
|
194
|
+
*/
|
|
195
|
+
export const VIEW_MATERIAL_CANARIES = Object.values(VIEW_MATERIAL_RESULT_FIXTURE._meta['ai.ggui/render'])
|
|
196
|
+
.filter((value) => typeof value === 'string')
|
|
197
|
+
.map((value) => /vmx-slice-[A-Za-z0-9-]+/.exec(value)?.[0] ?? value);
|
|
198
|
+
/** The resources V1 feeds a host's model-facing resource list: the render template and one ordinary resource. */
|
|
199
|
+
export const MODEL_RESOURCE_FIXTURE = [
|
|
200
|
+
{ name: 'ggui render', uriTemplate: 'ui://ggui/render/{sessionId}' },
|
|
201
|
+
{ name: 'notes', uri: 'file:///vmx-notes.md' },
|
|
202
|
+
];
|
|
203
|
+
/**
|
|
204
|
+
* A render locator's `resources/read` result as a server answers it: the
|
|
205
|
+
* self-contained shell, with the view's slice inlined, canaries included.
|
|
206
|
+
*/
|
|
207
|
+
export const VIEW_MATERIAL_READ_FIXTURE = {
|
|
208
|
+
contents: [
|
|
209
|
+
{
|
|
210
|
+
uri: 'ui://ggui/render/vmx-slice-session-3f9a1c',
|
|
211
|
+
mimeType: 'text/html;profile=mcp-app',
|
|
212
|
+
text: `<!doctype html><html><body><script>globalThis.__GGUI_META__ = ${JSON.stringify(VIEW_MATERIAL_RESULT_FIXTURE._meta)};</script></body></html>`,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
/** The view L1's requests come from. */
|
|
217
|
+
export const VIEW_BINDING_MOUNT = { locator: 'ui://ggui/render/vmx-s1/bk-vmx1', sessionId: 'vmx-s1' };
|
|
218
|
+
const OTHER_SESSION = 'vmx-s2';
|
|
219
|
+
/**
|
|
220
|
+
* Every tool a view may call (no `model`-only marker) that names a session in
|
|
221
|
+
* its arguments, with arguments its input schema accepts. Left out:
|
|
222
|
+
* `ggui_runtime_declare_tool_catalog` (app-scoped, names none),
|
|
223
|
+
* `ggui_runtime_refresh_ws_token` (its session is inside the envelope, and
|
|
224
|
+
* the server admits the caller to it), `ggui_handshake` and
|
|
225
|
+
* `ggui_list_sessions` (name none), and `ggui_render` / `ggui_update`
|
|
226
|
+
* (model-only).
|
|
227
|
+
*/
|
|
228
|
+
export const VIEW_SESSION_BOUND_CALLS = [
|
|
229
|
+
{
|
|
230
|
+
name: 'ggui_runtime_submit_action',
|
|
231
|
+
args: (sessionId) => ({
|
|
232
|
+
kind: 'dispatch',
|
|
233
|
+
payload: { intent: 'confirm', actionData: null, uiContext: {} },
|
|
234
|
+
sessionId,
|
|
235
|
+
appId: 'vmx-app',
|
|
236
|
+
actionId: 'a3f2b1d4',
|
|
237
|
+
firedAt: '2026-09-29T00:00:00.000Z',
|
|
238
|
+
}),
|
|
239
|
+
},
|
|
240
|
+
{ name: 'ggui_runtime_sync_context', args: (sessionId) => ({ sessionId, appId: 'vmx-app', snapshot: {} }) },
|
|
241
|
+
{ name: 'ggui_runtime_pull', args: (sessionId) => ({ sessionId }) },
|
|
242
|
+
{ name: 'ggui_runtime_telemetry', args: (sessionId) => ({ sessionId, events: [{ at: 0, kind: 'boot.path' }] }) },
|
|
243
|
+
{
|
|
244
|
+
name: 'ggui_runtime_report_render_failure',
|
|
245
|
+
args: (sessionId) => ({ sessionId, appId: 'vmx-app', phase: 'mount', errorName: 'TypeError', catches: 2 }),
|
|
246
|
+
},
|
|
247
|
+
{ name: 'ggui_consume', args: (sessionId) => ({ sessionId }) },
|
|
248
|
+
{ name: 'ggui_amend', args: (sessionId) => ({ sessionId, kind: 'merge', patch: { count: 1 } }) },
|
|
249
|
+
{ name: 'ggui_emit', args: (sessionId) => ({ sessionId, channel: 'status', payload: { text: 'hi' } }) },
|
|
250
|
+
{ name: 'ggui_get_session', args: (sessionId) => ({ sessionId }) },
|
|
251
|
+
{ name: 'ggui_get_render_source', args: (sessionId) => ({ sessionId }) },
|
|
252
|
+
];
|
|
253
|
+
/**
|
|
254
|
+
* L1's requests and the decision each must get (ggui#1415). A view's own
|
|
255
|
+
* session relays and another's refuses, for a read of either locator form
|
|
256
|
+
* (`{sessionId}`, `{sessionId}/{blueprintKey}`, and an epoch pin `#N`) and
|
|
257
|
+
* for every call in {@link VIEW_SESSION_BOUND_CALLS}.
|
|
258
|
+
*/
|
|
259
|
+
export const VIEW_BINDING_CASES = [
|
|
260
|
+
{ request: { kind: 'resources/read', uri: VIEW_BINDING_MOUNT.locator }, expect: 'relay' },
|
|
261
|
+
{ request: { kind: 'resources/read', uri: `ui://ggui/render/${VIEW_BINDING_MOUNT.sessionId}` }, expect: 'relay' },
|
|
262
|
+
{ request: { kind: 'resources/read', uri: `ui://ggui/render/${VIEW_BINDING_MOUNT.sessionId}#2` }, expect: 'relay' },
|
|
263
|
+
{ request: { kind: 'resources/read', uri: `ui://ggui/render/${OTHER_SESSION}` }, expect: 'refuse' },
|
|
264
|
+
{ request: { kind: 'resources/read', uri: `ui://ggui/render/${OTHER_SESSION}/bk-vmx2` }, expect: 'refuse' },
|
|
265
|
+
{ request: { kind: 'resources/read', uri: `ui://ggui/render/${OTHER_SESSION}/bk-vmx2#1` }, expect: 'refuse' },
|
|
266
|
+
...VIEW_SESSION_BOUND_CALLS.flatMap((call) => [
|
|
267
|
+
{ request: { kind: 'tools/call', name: call.name, arguments: call.args(VIEW_BINDING_MOUNT.sessionId) }, expect: 'relay' },
|
|
268
|
+
{ request: { kind: 'tools/call', name: call.name, arguments: call.args(OTHER_SESSION) }, expect: 'refuse' },
|
|
269
|
+
]),
|
|
270
|
+
];
|
|
271
|
+
/** The base64 / base64url runs inside a string, decoded; a run that does not decode yields nothing. */
|
|
272
|
+
function decodedBase64Runs(text) {
|
|
273
|
+
const runs = text.match(/[A-Za-z0-9+/_-]{16,}={0,2}/g) ?? [];
|
|
274
|
+
// A blob can start anywhere in a run (after a URL path, say), and base64
|
|
275
|
+
// decodes in 4-character groups, so each run is decoded from all four
|
|
276
|
+
// offsets: one of them lines up with the blob.
|
|
277
|
+
return runs.flatMap((run) => [0, 1, 2, 3].flatMap((offset) => {
|
|
278
|
+
const normal = run.slice(offset).replace(/-/g, '+').replace(/_/g, '/').replace(/=+$/, '');
|
|
279
|
+
const usable = normal.slice(0, normal.length - (normal.length % 4 === 1 ? 1 : 0));
|
|
280
|
+
try {
|
|
281
|
+
const binary = atob(usable + '==='.slice((usable.length + 3) % 4));
|
|
282
|
+
return [new TextDecoder().decode(Uint8Array.from(binary, (ch) => ch.charCodeAt(0)))];
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Not base64 from this offset: the run is searched raw already.
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* A string with its `%XX` escapes decoded, where it carries any: the form
|
|
292
|
+
* state takes in a URL's query string. A malformed escape is left as is,
|
|
293
|
+
* one escape at a time, so a stray `%` never hides the rest.
|
|
294
|
+
*/
|
|
295
|
+
function percentDecoded(text) {
|
|
296
|
+
if (!/%[0-9A-Fa-f]{2}/.test(text))
|
|
297
|
+
return [];
|
|
298
|
+
try {
|
|
299
|
+
return [decodeURIComponent(text)];
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// A malformed escape somewhere: decode the well-formed ones in place.
|
|
303
|
+
return [text.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {
|
|
304
|
+
try {
|
|
305
|
+
return decodeURIComponent(run);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// Bytes that are not UTF-8 stay escaped; the rest still decode.
|
|
309
|
+
return run;
|
|
310
|
+
}
|
|
311
|
+
})];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Which canaries appear in a model context: raw (JSON-escaped values
|
|
316
|
+
* included, since every mark is escape-free), or inside a percent-encoded
|
|
317
|
+
* string or a base64 / base64url run, decoded (the first match per canary).
|
|
318
|
+
*/
|
|
319
|
+
function leakedCanaries(context) {
|
|
320
|
+
const strings = stringValuesIn(context);
|
|
321
|
+
const unescaped = strings.flatMap(percentDecoded);
|
|
322
|
+
const decoded = [...unescaped, ...[...strings, ...unescaped].flatMap(decodedBase64Runs)];
|
|
323
|
+
const raw = VIEW_MATERIAL_CANARIES.filter((c) => strings.some((s) => s.includes(c)));
|
|
324
|
+
const encoded = VIEW_MATERIAL_CANARIES.filter((c) => !raw.includes(c) && decoded.some((s) => s.includes(c)));
|
|
325
|
+
return { raw, encoded };
|
|
326
|
+
}
|
|
327
|
+
/** The slice field a canary is the value of, for a failure detail. */
|
|
328
|
+
function canaryField(canary) {
|
|
329
|
+
const slice = VIEW_MATERIAL_RESULT_FIXTURE._meta['ai.ggui/render'];
|
|
330
|
+
const entry = typeof slice === 'object' && slice !== null
|
|
331
|
+
? Object.entries(slice).find(([, v]) => typeof v === 'string' && /vmx-slice-[A-Za-z0-9-]+/.exec(v)?.[0] === canary) ??
|
|
332
|
+
Object.entries(slice).find(([, v]) => v === canary)
|
|
333
|
+
: undefined;
|
|
334
|
+
return entry !== undefined ? entry[0] : canary;
|
|
335
|
+
}
|
|
68
336
|
const METHOD_NOT_SUPPORTED = -32601;
|
|
69
337
|
const DEFAULT_REFUSAL_TIMEOUT_MS = 2_000;
|
|
70
338
|
/** A method no ggui helper answers — the honest-refusal probe. */
|
|
@@ -72,8 +340,7 @@ const UNSUPPORTED_PROBE_METHOD = 'ggui-conformance/unsupported-probe';
|
|
|
72
340
|
/**
|
|
73
341
|
* The `resources/read` probe's URI: a well-formed render locator naming
|
|
74
342
|
* a render that does not exist, built with the kit's own locator grammar
|
|
75
|
-
* (one owner, `resource-read-conformance`; the kit never
|
|
76
|
-
* live protocol types). It is a locator on purpose, so a helper that
|
|
343
|
+
* (one owner, `resource-read-conformance`; the kit compiles against the protocol's published integrations only where a predicate IS the contract (`toolVisibleToModel`), never against live wire types). It is a locator on purpose, so a helper that
|
|
77
344
|
* forwards only `ui://ggui/render/` reads still forwards it; a relaying
|
|
78
345
|
* helper then hands back the server's own classification of the miss,
|
|
79
346
|
* which is an answer (ggui#1304).
|
|
@@ -438,6 +705,120 @@ export async function runHostHelperConformance(port, options = {}) {
|
|
|
438
705
|
});
|
|
439
706
|
}
|
|
440
707
|
}
|
|
708
|
+
// ── M1: the model's tool set withholds app-only tools ─────────────
|
|
709
|
+
if (options.modelToolSet === undefined) {
|
|
710
|
+
cases.push({
|
|
711
|
+
id: 'M1-model-tool-set',
|
|
712
|
+
outcome: 'skip',
|
|
713
|
+
detail: 'no model-facing filter supplied — the visibility door is still the host\'s obligation (SEP-1865), just not graded here (supply modelToolSet to grade)',
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
const offered = new Set(options.modelToolSet.offeredToModel(MODEL_TOOL_SET_FIXTURE));
|
|
718
|
+
const leaked = MODEL_TOOL_SET_FIXTURE.filter((t) => !toolVisibleToModel(t._meta?.ui?.visibility) && offered.has(t.name)).map((t) => t.name);
|
|
719
|
+
const withheld = MODEL_TOOL_SET_FIXTURE.filter((t) => toolVisibleToModel(t._meta?.ui?.visibility) && !offered.has(t.name)).map((t) => t.name);
|
|
720
|
+
const served = new Set(MODEL_TOOL_SET_FIXTURE.map((t) => t.name));
|
|
721
|
+
const unknown = [...offered].filter((name) => !served.has(name));
|
|
722
|
+
if (leaked.length === 0 && withheld.length === 0 && unknown.length === 0) {
|
|
723
|
+
cases.push({
|
|
724
|
+
id: 'M1-model-tool-set',
|
|
725
|
+
outcome: 'pass',
|
|
726
|
+
detail: `the model's tool set withholds every app-only tool and keeps every model-visible one (${offered.size} offered)`,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
const parts = [
|
|
731
|
+
leaked.length > 0
|
|
732
|
+
? `app-only tools offered to the model (${leaked.length}): ${leaked.join(', ')} — a model holding the app credential can call them`
|
|
733
|
+
: undefined,
|
|
734
|
+
withheld.length > 0
|
|
735
|
+
? `model-visible tools withheld (${withheld.length}): ${withheld.join(', ')} — the agent cannot run the loop without them`
|
|
736
|
+
: undefined,
|
|
737
|
+
unknown.length > 0
|
|
738
|
+
? `names offered that the server never listed (${unknown.length}): ${unknown.join(', ')} — the list is not derived from tools/list`
|
|
739
|
+
: undefined,
|
|
740
|
+
].filter((part) => part !== undefined);
|
|
741
|
+
cases.push({
|
|
742
|
+
id: 'M1-model-tool-set',
|
|
743
|
+
outcome: 'fail',
|
|
744
|
+
detail: parts.join('; '),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// ── V1: view-delivered material stays out of the model's context ──
|
|
749
|
+
if (options.modelContext === undefined) {
|
|
750
|
+
cases.push({
|
|
751
|
+
id: 'V1-view-material-withheld',
|
|
752
|
+
outcome: 'skip',
|
|
753
|
+
detail: "no model-context rule supplied: keeping view material from the model is still the host's obligation (SPEC §4.7), just not graded here (supply modelContext to grade)",
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
const rules = options.modelContext;
|
|
758
|
+
try {
|
|
759
|
+
const context = await rules.modelContextOf(VIEW_MATERIAL_RESULT_FIXTURE);
|
|
760
|
+
const readContext = await rules.modelContextOfRead(VIEW_MATERIAL_READ_FIXTURE);
|
|
761
|
+
const fromResult = leakedCanaries(context);
|
|
762
|
+
const fromRead = leakedCanaries(readContext);
|
|
763
|
+
const seesResult = stringValuesIn(context).some((s) => s.includes(VIEW_MATERIAL_VISIBLE_MARKER));
|
|
764
|
+
const offered = rules.resourcesOfferedToModel?.(MODEL_RESOURCE_FIXTURE);
|
|
765
|
+
const offersRender = (offered ?? []).filter((u) => u.startsWith('ui://ggui/render'));
|
|
766
|
+
const keepsOrdinary = offered === undefined || offered.includes('file:///vmx-notes.md');
|
|
767
|
+
const parts = [
|
|
768
|
+
fromResult.raw.length > 0 ? `view material from a tool result in the model's context: ${fromResult.raw.map(canaryField).join(', ')}` : undefined,
|
|
769
|
+
fromResult.encoded.length > 0 ? `view material from a tool result inside a percent-encoded or base64 run: ${fromResult.encoded.map(canaryField).join(', ')}` : undefined,
|
|
770
|
+
fromRead.raw.length > 0 ? `a render read body in the model's context: ${fromRead.raw.map(canaryField).join(', ')}` : undefined,
|
|
771
|
+
fromRead.encoded.length > 0 ? `a render read body inside a percent-encoded or base64 run: ${fromRead.encoded.map(canaryField).join(', ')}` : undefined,
|
|
772
|
+
seesResult ? undefined : `the model does not see the result's own marker ${VIEW_MATERIAL_VISIBLE_MARKER}: withholding the result is not withholding the view material`,
|
|
773
|
+
offersRender.length > 0 ? `a render read offered to the model: ${offersRender.join(', ')}` : undefined,
|
|
774
|
+
keepsOrdinary ? undefined : 'the ordinary resource was not offered: a list that hides everything is not a rule (omit resourcesOfferedToModel when the model reads no resources)',
|
|
775
|
+
].filter((part) => part !== undefined);
|
|
776
|
+
cases.push(parts.length === 0
|
|
777
|
+
? {
|
|
778
|
+
id: 'V1-view-material-withheld',
|
|
779
|
+
outcome: 'pass',
|
|
780
|
+
detail: `none of the slice's ${VIEW_MATERIAL_CANARIES.length} values reaches the model from a tool result or a render read body, raw, JSON-escaped, percent-encoded, base64 or base64url; the result itself does; ${offered === undefined ? 'the host offers the model no resource reads' : 'no render read is offered'} (the host's rule, self-certified)`,
|
|
781
|
+
}
|
|
782
|
+
: { id: 'V1-view-material-withheld', outcome: 'fail', detail: parts.join('; ') });
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
cases.push({ id: 'V1-view-material-withheld', outcome: 'fail', detail: `the host's rule threw: ${err instanceof Error ? err.message : String(err)}` });
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
// ── L1: a view's calls and reads are bound to its own locator ─────
|
|
789
|
+
if (options.viewBinding === undefined) {
|
|
790
|
+
cases.push({
|
|
791
|
+
id: 'L1-view-locator-binding',
|
|
792
|
+
outcome: 'skip',
|
|
793
|
+
detail: "no view-binding rule supplied: binding a view to its own locator is still the host's obligation (SPEC §4.7), just not graded here (supply viewBinding to grade)",
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
const describe = (r) => r.kind === 'resources/read' ? `resources/read ${r.uri}` : `${r.name} for session ${String(r.arguments['sessionId'])}`;
|
|
798
|
+
const decide = options.viewBinding.decide;
|
|
799
|
+
const decided = VIEW_BINDING_CASES.map((c) => {
|
|
800
|
+
try {
|
|
801
|
+
return { ...c, got: decide(VIEW_BINDING_MOUNT, c.request) };
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
// A rule that throws decides nothing: the request is neither relayed nor refused, and the case says so.
|
|
805
|
+
return { ...c, got: `threw: ${err instanceof Error ? err.message : String(err)}` };
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
const relayedForeign = decided.filter((c) => c.expect === 'refuse' && c.got !== 'refuse').map((c) => describe(c.request));
|
|
809
|
+
const refusedOwn = decided.filter((c) => c.expect === 'relay' && c.got !== 'relay').map((c) => describe(c.request));
|
|
810
|
+
const parts = [
|
|
811
|
+
relayedForeign.length > 0 ? `relayed another view's request (${relayedForeign.length}): ${relayedForeign.join('; ')}` : undefined,
|
|
812
|
+
refusedOwn.length > 0 ? `refused its own view's request (${refusedOwn.length}): ${refusedOwn.join('; ')}` : undefined,
|
|
813
|
+
].filter((part) => part !== undefined);
|
|
814
|
+
cases.push(parts.length === 0
|
|
815
|
+
? {
|
|
816
|
+
id: 'L1-view-locator-binding',
|
|
817
|
+
outcome: 'pass',
|
|
818
|
+
detail: `the view's own reads and session-bound calls relay and another session's refuse, ${VIEW_BINDING_CASES.length} requests (the host's rule, self-certified)`,
|
|
819
|
+
}
|
|
820
|
+
: { id: 'L1-view-locator-binding', outcome: 'fail', detail: parts.join('; ') });
|
|
821
|
+
}
|
|
441
822
|
const failures = cases
|
|
442
823
|
.filter((c) => c.outcome === 'fail')
|
|
443
824
|
.map((c) => c.id);
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export type { TransportConfig, WebSocketTransportConfig, UnknownTransportConfig, AuthConfig, TestCase, SubscribeFrameShaping, SetupStep, CreateGguiSessionStep, ActionSpecEntryDecl, JsonSchemaDecl, RendererUrlOverrideStep, ServerVersionOverrideStep, UiInitializeResponseOverrideStep, EmitEnvelopeStep, TeardownStep, ExpectedBehavior, ActionAckBehavior, ErrorFrameBehavior, StreamUpdateBehavior, BootstrapFailureBehavior, BootstrapSuccessBehavior, VersionMismatchBehavior, PropsUpdateBehavior, SessionStateBehavior, NoOpBehavior, UnknownBehavior, ProtocolError, BootstrapFailureReason, } from './types.js';
|
|
14
14
|
export type { ConformanceHost, SetupStep as HostSetupStep, CreateGguiSessionSetup, RendererUrlOverrideSetup, ServerVersionOverrideSetup, UiInitializeResponseOverrideSetup, EmitEnvelopeSetup, TeardownStep as HostTeardownStep, } from './conformance-host.js';
|
|
15
|
-
export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
|
|
15
|
+
export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, reservedChannelReplayFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
|
|
16
16
|
export type { ContractSlug } from './fixtures/index.js';
|
|
17
17
|
export { gadgetWireSchemaCases, runSchemaConformance, } from './schema-conformance/index.js';
|
|
18
18
|
export type { SchemaConformanceCase, SchemaConformanceMismatch, SchemaConformanceResult, } from './schema-conformance/index.js';
|
|
@@ -37,10 +37,12 @@ export type { PreparedReadPlaneOnlyScenario, ReadPlaneOnlyConformanceCase, ReadP
|
|
|
37
37
|
export { loadFixture, listFixtures, loadAllFixtures } from './loader.js';
|
|
38
38
|
export { runConformance } from './run-conformance.js';
|
|
39
39
|
export type { RunConformanceConfig, ConformanceResult, ConformanceFailure, SkippedFixture, ConformanceReporter, InputEnvelopeDispatch, HostContextObservedInputEnvelope, } from './run-conformance.js';
|
|
40
|
-
export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatSkips, } from './reporter.js';
|
|
40
|
+
export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatWarnings, formatSkips, } from './reporter.js';
|
|
41
41
|
export type { DefaultReporterOptions } from './reporter.js';
|
|
42
42
|
export { domainErrorCases, isRawToolCallResult, runDomainErrorConformance, } from './domain-error-conformance/index.js';
|
|
43
43
|
export type { DomainErrorConformanceCase, DomainErrorConformanceResult, DomainErrorCriterion, DomainErrorMismatch, DomainErrorSkip, RawToolCallResult, RunDomainErrorConformanceOptions, ToolCallArgs, ToolCallContent, ToolCallDriver, ToolCallScenario, } from './domain-error-conformance/index.js';
|
|
44
|
+
export { DISPATCH_IDEMPOTENCY_ACTION, DISPATCH_IDEMPOTENCY_ACTION_SPEC, runDispatchIdempotencyConformance, } from './dispatch-idempotency-conformance/index.js';
|
|
45
|
+
export type { DispatchIdempotencyCaseName, DispatchIdempotencyCaseResult, DispatchIdempotencyHost, DispatchIdempotencyResult, DispatchIdempotencySession, } from './dispatch-idempotency-conformance/index.js';
|
|
44
46
|
export { APP_THEME_CASES, OVERLAY_HASH_CASES, THEME_MODE_CASES, runThemeBindingConformance, } from './theme-binding-conformance/index.js';
|
|
45
47
|
export type { AppThemeCase, OverlayHashCase, ThemeBindingResult, ThemeModeCase, } from './theme-binding-conformance/index.js';
|
|
46
48
|
export { N1_COMPAT_CASES, N1_COMPAT_DIRECTIONS, N1_COMPAT_WIRES, runN1CompatConformance } from './n1-compat-conformance/index.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// Fixture catalog — every authored conformance case, classified by
|
|
14
14
|
// contract. Third-party runners + the kit's own `runConformance()`
|
|
15
15
|
// consume this surface.
|
|
16
|
-
export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
|
|
16
|
+
export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, reservedChannelReplayFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
|
|
17
17
|
// Pure-function conformance catalogs — SPEC §7.7.2's gadget
|
|
18
18
|
// obligations are deterministic validation functions, not transport-
|
|
19
19
|
// observable behaviors. Each catalog grades a caller-supplied function
|
|
@@ -78,9 +78,11 @@ export { runConformance } from './run-conformance.js';
|
|
|
78
78
|
// Default reporter — stdout-based bar-scorecard implementation +
|
|
79
79
|
// formatters programmatic consumers can reuse when building their
|
|
80
80
|
// own reporter (CI annotations, vitest integration, etc.).
|
|
81
|
-
export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatSkips, } from './reporter.js';
|
|
81
|
+
export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatWarnings, formatSkips, } from './reporter.js';
|
|
82
82
|
// ── domain-error (SPEC §7.9 Plane 2 on the tools/call wire, ggui#880) ──
|
|
83
83
|
export { domainErrorCases, isRawToolCallResult, runDomainErrorConformance, } from './domain-error-conformance/index.js';
|
|
84
|
+
// ── dispatch-idempotency (SPEC §11.1 "A retried dispatch is one gesture", ggui#1519; SHOULD level) ──
|
|
85
|
+
export { DISPATCH_IDEMPOTENCY_ACTION, DISPATCH_IDEMPOTENCY_ACTION_SPEC, runDispatchIdempotencyConformance, } from './dispatch-idempotency-conformance/index.js';
|
|
84
86
|
// ── theme binding (ggui#987 — the theming wave's arbiter; guuey 1.3 judges against it) ──
|
|
85
87
|
export { APP_THEME_CASES, OVERLAY_HASH_CASES, THEME_MODE_CASES, runThemeBindingConformance, } from './theme-binding-conformance/index.js';
|
|
86
88
|
// ── n1-compat (ggui#1014 §3.6 — previous-release payloads against today's parsers; ggui#1093 — a later release's payload against today's READ door) ──
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"match-behavior.d.ts","sourceRoot":"","sources":["../src/match-behavior.ts"],"names":[],"mappings":"AA+FA,OAAO,KAAK,EAIV,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,gBAAgB,CAAC,MAAM,CAAC,EAC3D,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,CAAC,GACN,QAAQ,IAAI,OAAO,CAAC,gBAAgB,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,CAE7D;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,SAAS,aAAa,EAAE,GAC/B,WAAW,CAkDb;
|
|
1
|
+
{"version":3,"file":"match-behavior.d.ts","sourceRoot":"","sources":["../src/match-behavior.ts"],"names":[],"mappings":"AA+FA,OAAO,KAAK,EAIV,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,gBAAgB,CAAC,MAAM,CAAC,EAC3D,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,CAAC,GACN,QAAQ,IAAI,OAAO,CAAC,gBAAgB,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,CAE7D;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,SAAS,aAAa,EAAE,GAC/B,WAAW,CAkDb;AAkXD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAgBzD"}
|
package/dist/match-behavior.js
CHANGED
|
@@ -340,19 +340,21 @@ function matchContractViolationPayload(assertions, parsed, frames) {
|
|
|
340
340
|
* non-conformant envelope must not satisfy the assertion.
|
|
341
341
|
*/
|
|
342
342
|
function matchStreamUpdate(behavior, frames) {
|
|
343
|
-
const envelopes = frames.flatMap((f) => {
|
|
344
|
-
if (f.kind !== 'frame' || f.parsed['type'] !== 'data')
|
|
345
|
-
return [];
|
|
346
|
-
const body = f.parsed['payload'];
|
|
347
|
-
return isStreamEnvelope(body) ? [body] : [];
|
|
348
|
-
});
|
|
349
343
|
// `valueMatch: 'subset'` relaxes the value comparison to "every
|
|
350
344
|
// declared key is present + matching; extra observed keys ignored".
|
|
351
345
|
// Default ('exact' / absent) is the exact deep-equal.
|
|
352
346
|
const subset = behavior.valueMatch === 'subset';
|
|
353
347
|
const valueMatches = (observed) => subset ? deepMatchSubset(behavior.value, observed) : deepEqual(observed, behavior.value);
|
|
354
|
-
|
|
355
|
-
|
|
348
|
+
// Every matching envelope, with where it arrived among the frames.
|
|
349
|
+
const matches = frames.flatMap((f, index) => {
|
|
350
|
+
if (f.kind !== 'frame' || f.parsed['type'] !== 'data')
|
|
351
|
+
return [];
|
|
352
|
+
const body = f.parsed['payload'];
|
|
353
|
+
return isStreamEnvelope(body) && body.channel === behavior.channel && valueMatches(body.payload)
|
|
354
|
+
? [{ index, envelope: body }]
|
|
355
|
+
: [];
|
|
356
|
+
});
|
|
357
|
+
if (matches.length === 0) {
|
|
356
358
|
return {
|
|
357
359
|
kind: 'fail',
|
|
358
360
|
expected: {
|
|
@@ -366,6 +368,43 @@ function matchStreamUpdate(behavior, frames) {
|
|
|
366
368
|
message: `expected a \`data\` frame whose StreamEnvelope names channel '${behavior.channel}' and carries the declared value as its payload (${subset ? 'subset match — every declared key present + matching' : 'exact match'}); none matched.`,
|
|
367
369
|
};
|
|
368
370
|
}
|
|
371
|
+
if (behavior.replayAfterAck !== true)
|
|
372
|
+
return { kind: 'pass' };
|
|
373
|
+
return matchReplayPlacement(behavior.channel, frames, matches);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The replay placement a `stream-update` with `replayAfterAck` grades
|
|
377
|
+
* (SPEC §12.2.1, ggui#1526): a matching frame that arrives after the
|
|
378
|
+
* subscribe's `ack` and carries a `seq` at or below that ack's
|
|
379
|
+
* `streamSeq`. The first placement failure found names what is wrong.
|
|
380
|
+
*/
|
|
381
|
+
function matchReplayPlacement(channel, frames, matches) {
|
|
382
|
+
const ackIndex = frames.findIndex((f) => f.kind === 'frame' && f.parsed['type'] === 'ack');
|
|
383
|
+
const expected = { type: 'data', channel, after: 'the subscribe ack', seq: '<= ack.payload.streamSeq' };
|
|
384
|
+
const fail = (message) => ({
|
|
385
|
+
kind: 'fail',
|
|
386
|
+
expected,
|
|
387
|
+
received: frames.filter((f) => f.kind === 'frame').map((f) => (f.kind === 'frame' ? f.parsed : f)),
|
|
388
|
+
message,
|
|
389
|
+
});
|
|
390
|
+
if (ackIndex === -1) {
|
|
391
|
+
return fail(`a matching frame on '${channel}' was observed, but no subscribe \`ack\` was, so it cannot be shown to be the replay.`);
|
|
392
|
+
}
|
|
393
|
+
const ackFrame = frames[ackIndex];
|
|
394
|
+
const ackPayload = ackFrame !== undefined && ackFrame.kind === 'frame' ? ackFrame.parsed['payload'] : undefined;
|
|
395
|
+
const streamSeq = isRecord(ackPayload) ? ackPayload['streamSeq'] : undefined;
|
|
396
|
+
if (typeof streamSeq !== 'number') {
|
|
397
|
+
return fail(`the subscribe \`ack\` carries no numeric \`streamSeq\`, so a frame on '${channel}' cannot be shown to be at or below it.`);
|
|
398
|
+
}
|
|
399
|
+
const afterAck = matches.filter((m) => m.index > ackIndex);
|
|
400
|
+
if (afterAck.length === 0) {
|
|
401
|
+
return fail(`the only matching frames on '${channel}' arrived before the subscribe's \`ack\`; the replay belongs after it.`);
|
|
402
|
+
}
|
|
403
|
+
const replayed = afterAck.find((m) => typeof m.envelope.seq === 'number' && m.envelope.seq <= streamSeq);
|
|
404
|
+
if (replayed === undefined) {
|
|
405
|
+
const seqs = afterAck.map((m) => (typeof m.envelope.seq === 'number' ? m.envelope.seq : 'none')).join(', ');
|
|
406
|
+
return fail(`the matching frames on '${channel}' after the \`ack\` carry seq ${seqs}, none at or below the ack's \`streamSeq\` (${streamSeq}): each is above the ack's \`streamSeq\` (${streamSeq}) or unnumbered, so none is the replay.`);
|
|
407
|
+
}
|
|
369
408
|
return { kind: 'pass' };
|
|
370
409
|
}
|
|
371
410
|
function matchNoOp(behavior, frames) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "forward-app-theme-carry-uninterpretable",
|
|
3
|
+
"description": "FORWARD tolerance on the theme CARRY read (ggui#1175, VERSION-POLICY §3.6): a theme GET response for a stored document that fails the carry shape, as a door that names it returns it — `theme: null` beside `uninterpretable`, whose issues carry a path and a protocol-owned code and never a stored value. Today's response schema must accept it, and must read it as a document that is not a theme, never as \"no theme\".",
|
|
4
|
+
"direction": "forward",
|
|
5
|
+
"release": {
|
|
6
|
+
"label": "a later ggui release (synthetic): the theme GET naming an uninterpretable stored document",
|
|
7
|
+
"sha": "synthetic"
|
|
8
|
+
},
|
|
9
|
+
"wire": "app-theme-carry",
|
|
10
|
+
"payload": {
|
|
11
|
+
"theme": null,
|
|
12
|
+
"uninterpretable": {
|
|
13
|
+
"issueCount": 2,
|
|
14
|
+
"issues": [
|
|
15
|
+
{ "path": ["overlays", "light", "not-a-ggui-var"], "code": "unknown_key" },
|
|
16
|
+
{ "path": ["mode"], "code": "value_not_allowed" }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"expect": "accepted"
|
|
21
|
+
}
|