@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.
Files changed (83) hide show
  1. package/README.md +32 -13
  2. package/dist/cli.d.ts +11 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +48 -20
  5. package/dist/conformance-host.d.ts +81 -76
  6. package/dist/conformance-host.d.ts.map +1 -1
  7. package/dist/conformance-host.js +0 -44
  8. package/dist/fixtures/bootstrap-protocol/bootstrap-bundle-fetch-failed.json +3 -3
  9. package/dist/fixtures/bootstrap-protocol/bootstrap-meta-missing.json +3 -3
  10. package/dist/fixtures/bootstrap-protocol/bootstrap-success.json +3 -3
  11. package/dist/fixtures/consume-buffer/action-ack-sequence.json +28 -0
  12. package/dist/fixtures/consume-buffer/action-payload-schema-violation.json +37 -0
  13. package/dist/fixtures/consume-buffer/index.d.ts +6 -0
  14. package/dist/fixtures/consume-buffer/index.d.ts.map +1 -0
  15. package/dist/fixtures/consume-buffer/index.js +41 -0
  16. package/dist/fixtures/consume-buffer/undeclared-action-rejected.json +29 -0
  17. package/dist/fixtures/host-context/host-context-observed-persists.json +34 -0
  18. package/dist/fixtures/host-context/index.d.ts +5 -0
  19. package/dist/fixtures/host-context/index.d.ts.map +1 -0
  20. package/dist/fixtures/host-context/index.js +40 -0
  21. package/dist/fixtures/index.d.ts +28 -9
  22. package/dist/fixtures/index.d.ts.map +1 -1
  23. package/dist/fixtures/index.js +33 -14
  24. package/dist/fixtures/reserved-channel-authority/index.d.ts.map +1 -1
  25. package/dist/fixtures/reserved-channel-authority/index.js +17 -7
  26. package/dist/fixtures/reserved-channel-authority/props-update-roundtrip.json +4 -4
  27. package/dist/fixtures/schema-version-handshake/index.d.ts.map +1 -1
  28. package/dist/fixtures/schema-version-handshake/index.js +12 -4
  29. package/dist/fixtures/schema-version-handshake/version-match.json +7 -5
  30. package/dist/fixtures/schema-version-handshake/version-mismatch.json +9 -10
  31. package/dist/fixtures/subscribe-tenancy/absent-appid-defaults.json +18 -0
  32. package/dist/fixtures/subscribe-tenancy/app-mismatch.json +20 -0
  33. package/dist/fixtures/subscribe-tenancy/index.d.ts +6 -0
  34. package/dist/fixtures/subscribe-tenancy/index.d.ts.map +1 -0
  35. package/dist/fixtures/subscribe-tenancy/index.js +62 -0
  36. package/dist/index.d.ts +4 -4
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -4
  39. package/dist/match-behavior.d.ts +19 -100
  40. package/dist/match-behavior.d.ts.map +1 -1
  41. package/dist/match-behavior.js +289 -246
  42. package/dist/registration-conformance/cases/gate-duplicate-hook.json +26 -0
  43. package/dist/registration-conformance/cases/gate-export-not-registered.json +1 -0
  44. package/dist/registration-conformance/cases/gate-multi-package-distinct-names-accept.json +26 -0
  45. package/dist/registration-conformance/cases/gate-no-gadgets-accept.json +1 -0
  46. package/dist/registration-conformance/cases/gate-package-mismatch.json +1 -0
  47. package/dist/registration-conformance/cases/gate-priority-not-registered-outranks-mismatch.json +1 -0
  48. package/dist/registration-conformance/cases/gate-priority-registry-outranks-public-env.json +22 -0
  49. package/dist/registration-conformance/cases/gate-public-env-missing.json +21 -0
  50. package/dist/registration-conformance/cases/gate-public-env-satisfied-accept.json +21 -0
  51. package/dist/registration-conformance/cases/gate-public-env-undeclared-wrapper-accept.json +26 -0
  52. package/dist/registration-conformance/cases/gate-registered-resolves-accept.json +1 -0
  53. package/dist/registration-conformance/cases/gate-stdlib-hook-accept.json +1 -0
  54. package/dist/registration-conformance/cases/gate-typo-classifies-as-not-registered.json +1 -0
  55. package/dist/registration-conformance/index.d.ts +64 -33
  56. package/dist/registration-conformance/index.d.ts.map +1 -1
  57. package/dist/registration-conformance/index.js +24 -8
  58. package/dist/run-conformance.d.ts +145 -6
  59. package/dist/run-conformance.d.ts.map +1 -1
  60. package/dist/run-conformance.js +531 -93
  61. package/dist/types.d.ts +319 -165
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js +27 -23
  64. package/dist/ws-transport.d.ts.map +1 -1
  65. package/dist/ws-transport.js +3 -2
  66. package/package.json +3 -3
  67. package/dist/fixtures/observability-events/index.d.ts +0 -5
  68. package/dist/fixtures/observability-events/index.d.ts.map +0 -1
  69. package/dist/fixtures/observability-events/index.js +0 -22
  70. package/dist/fixtures/observability-events/observability-contract-error-emitted.json +0 -28
  71. package/dist/fixtures/observability-events/observability-wired-tool-invoked.json +0 -27
  72. package/dist/fixtures/refresh-semantics/index.d.ts +0 -5
  73. package/dist/fixtures/refresh-semantics/index.d.ts.map +0 -1
  74. package/dist/fixtures/refresh-semantics/index.js +0 -19
  75. package/dist/fixtures/refresh-semantics/stream-refresh-success.json +0 -39
  76. package/dist/fixtures/reserved-channel-authority/stream-schema-violation.json +0 -44
  77. package/dist/fixtures/wired-action-dispatch/index.d.ts +0 -5
  78. package/dist/fixtures/wired-action-dispatch/index.d.ts.map +0 -1
  79. package/dist/fixtures/wired-action-dispatch/index.js +0 -29
  80. package/dist/fixtures/wired-action-dispatch/wired-action-success.json +0 -35
  81. package/dist/fixtures/wired-action-dispatch/wired-action-tool-not-found.json +0 -30
  82. package/dist/fixtures/wired-action-dispatch/wired-action-tool-threw.json +0 -31
  83. package/dist/fixtures/wired-action-dispatch/wired-action-tool-timeout.json +0 -31
package/dist/types.d.ts CHANGED
@@ -13,27 +13,31 @@
13
13
  * {@link TeardownStep}, and {@link ExpectedBehavior} IS the public
14
14
  * API. Renaming a field breaks every third-party consumer of the
15
15
  * conformance kit. Treat this file the way `@ggui-ai/protocol`'s
16
- * type declarations get treated: additive changes only, extensibility
17
- * via `(string & {})` tails.
16
+ * type declarations get treated: additive changes only; where a
17
+ * union is extensible, it is via `(string & {})` tails (see the next
18
+ * section for which unions those are).
18
19
  *
19
- * ## Extensibly-closed unions
20
+ * ## Closed vs extensibly-closed unions
20
21
  *
21
- * Every discriminator in this module carries a `(string & {})` tail —
22
- * see SPEC §extensibility and `ContractErrorCode` in
23
- * `@ggui-ai/protocol`. Third-party conformance-host implementations
24
- * MAY introduce setup / teardown / expectation kinds the reference
25
- * kit doesn't recognize; the runner SHOULD skip such cases with a
26
- * warning rather than failing loudly, exactly the way
27
- * `ContractErrorCode`'s extensibility works.
22
+ * Two different evolution postures coexist in this module:
28
23
  *
29
- * Only these discriminators are extensibly closed:
30
- * - `SetupStep['type']`
31
- * - `TeardownStep['type']`
32
- * - `ExpectedBehavior['kind']`
33
- * - `ExpectedObservabilityEvent['kind']`
34
- * - `TransportConfig['kind']`
24
+ * - `SetupStep['type']` is **closed** — exactly the directive
25
+ * vocabulary the shipped fixture catalog authors. An unknown or
26
+ * malformed directive is a fixture-authoring error the runner
27
+ * rejects loudly (it can never be a legitimate runtime extension
28
+ * point, because the catalog and the vocabulary ship together).
29
+ * New directives land as new union arms in a kit version bump.
30
+ * `TeardownStep` is the same posture with a currently-empty
31
+ * vocabulary.
32
+ * - `ExpectedBehavior['kind']` and `TransportConfig['kind']` carry a
33
+ * `(string & {})` tail — see SPEC §11.4 (extensibly-closed
34
+ * unions, e.g. `SubmitActionKind` in `@ggui-ai/protocol`). Runners
35
+ * that
36
+ * receive a behavior kind they don't recognize SHOULD skip with a
37
+ * warning; an unrecognized transport kind is a configuration
38
+ * error.
35
39
  *
36
- * Everything else — tool names, envelope field names, contract error
40
+ * Everything else — tool names, envelope field names, wire error
37
41
  * codes — is passed verbatim and the implementation under test owns
38
42
  * the recognition set.
39
43
  *
@@ -51,8 +55,8 @@
51
55
  * pinned here, deliberately decoupled from the live source tree. A
52
56
  * drift check between these authored unions and the live protocol /
53
57
  * renderer types belongs in the kit's own meta-tests, NOT in this
54
- * file's import graph. If `@ggui-ai/protocol` adds a new
55
- * `ContractErrorCode` canonical member tomorrow, conformance kit
58
+ * file's import graph. If `@ggui-ai/protocol` adds a new canonical
59
+ * member to an extensibly-closed union tomorrow, conformance kit
56
60
  * consumers MUST NOT be forced to recompile — their fixtures
57
61
  * continue to reference the value they authored, and the runner's
58
62
  * extensible `(string & {})` tail handles recognition. This mirrors
@@ -70,8 +74,9 @@
70
74
  * unknown arms are accepted as opaque records so third-party runners
71
75
  * can extend without a package bump.
72
76
  *
73
- * Pattern mirrored from `ContractErrorCode` in
74
- * `@ggui-ai/protocol/types/data-contract`.
77
+ * Pattern mirrored from the protocol's extensibly-closed unions
78
+ * (e.g. `SubmitActionKind` in
79
+ * `@ggui-ai/protocol/integrations/mcp-apps`).
75
80
  */
76
81
  export type TransportConfig = WebSocketTransportConfig | UnknownTransportConfig;
77
82
  export interface WebSocketTransportConfig {
@@ -109,35 +114,112 @@ export type AuthConfig = {
109
114
  * this module authors the declarative intent; the host owns the
110
115
  * implementation.
111
116
  *
112
- * Discriminator is extensibly closed: unknown `type` values skip the
113
- * fixture with a warning rather than erroring. Third-party hosts MAY
114
- * extend the setup vocabulary without a kit version bump.
117
+ * Discriminator is CLOSED: exactly the directive vocabulary the
118
+ * shipped fixture catalog authors. The runner validates every step
119
+ * against this union before dispatching and throws a descriptive
120
+ * fixture-authoring error on an unknown or malformed directive — a
121
+ * typo'd `type` can never silently skip. Hosts that don't implement
122
+ * a (valid) directive signal it by throwing from `dispatchSetup`,
123
+ * which the runner records as a SKIP.
115
124
  */
116
- export type SetupStep = CreateRenderStep | RegisterToolStep | EmitEnvelopeStep | SeedChannelStep | UnknownSetupStep;
117
- export interface CreateRenderStep {
118
- readonly type: 'create-render';
125
+ export type SetupStep = CreateGguiSessionStep | RendererUrlOverrideStep | ServerVersionOverrideStep | UiInitializeResponseOverrideStep | EmitEnvelopeStep;
126
+ /**
127
+ * Declared-action entry for {@link CreateGguiSessionStep.actionSpec}.
128
+ * An empty entry declares a void-payload action — fixtures over it
129
+ * exercise the name-membership half of the action contract. An entry
130
+ * carrying `schema` ALSO declares the action's payload contract: the
131
+ * implementation under test MUST validate a `data:submit` payload's
132
+ * `data` against it at receipt (SPEC §4.6) and reject non-conforming
133
+ * payloads with a `CONTRACT_VIOLATION` error frame — the same wire
134
+ * surface as an undeclared name. A host that cannot install
135
+ * per-action payload schemas MUST refuse the directive (throw, so the
136
+ * fixture skips honestly) rather than silently downgrading the
137
+ * declaration to name-membership.
138
+ */
139
+ export interface ActionSpecEntryDecl {
140
+ readonly schema?: JsonSchemaDecl;
141
+ }
142
+ /**
143
+ * JSON-Schema node authored on {@link ActionSpecEntryDecl.schema} —
144
+ * the kit's decoupled copy of the protocol's JSON Schema draft-07
145
+ * subset (`JsonSchema` in `@ggui-ai/protocol`), same authoring
146
+ * posture as {@link BootstrapFailureReason}: fixtures compile against
147
+ * the
148
+ * kit's frozen vocabulary, never the live protocol types. The typed
149
+ * core names exactly the keywords the shipped fixtures author and
150
+ * the runner's validating parse recognizes; the index-signature tail
151
+ * carries any further draft-07 keyword verbatim — the implementation
152
+ * under test owns their interpretation, exactly as with tool names
153
+ * and wire error codes.
154
+ */
155
+ export interface JsonSchemaDecl {
156
+ readonly [keyword: string]: unknown;
157
+ readonly type?: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null';
158
+ readonly description?: string;
159
+ readonly enum?: readonly unknown[];
160
+ readonly items?: JsonSchemaDecl;
161
+ readonly properties?: Readonly<Record<string, JsonSchemaDecl>>;
162
+ readonly required?: readonly string[];
163
+ readonly additionalProperties?: JsonSchemaDecl | boolean;
164
+ }
165
+ export interface CreateGguiSessionStep {
166
+ readonly type: 'create-session';
119
167
  /** Opaque render id the host allocates. Downstream steps + the
120
168
  * fixture's `inputEnvelope` reference it by value. */
121
- readonly renderId: string;
169
+ readonly sessionId: string;
122
170
  /** Optional app (tenant) id the render scopes to. Defaults to the
123
171
  * host's implementation-defined "default app". */
124
172
  readonly appId?: string;
173
+ /**
174
+ * Optional actionSpec declared on the GguiSession at creation —
175
+ * mirrors how a real ggui server derives the declared-action set
176
+ * from the render's data contract (actions are part of the render's
177
+ * identity, not separately registered). Servers MUST reject
178
+ * `data:submit` envelopes naming actions absent from this record —
179
+ * and, for entries that declare a `schema`, envelopes whose `data`
180
+ * does not conform to it (SPEC §4.6 receipt validation) — with an
181
+ * `error` frame, code `CONTRACT_VIOLATION`. Omitting the field
182
+ * declares no contract — all actions are accepted.
183
+ */
184
+ readonly actionSpec?: Readonly<Record<string, ActionSpecEntryDecl>>;
125
185
  }
126
- export interface RegisterToolStep {
127
- readonly type: 'register-tool';
128
- readonly toolName: string;
129
- /** Handler identifier the host maps to a real implementation. The
130
- * kit is authoring-only — it doesn't ship handler bodies. Known
131
- * values the reference `@ggui-ai/mcp-server`-backed host supports:
132
- *
133
- * - `'echo'` — returns `{received: args}`.
134
- * - `'throw'` — rejects with `Error('tool_threw_for_fixture')`.
135
- * - `'timeout'`— never resolves (host enforces its own timeout).
136
- * - `'malformed-stream'` — returns `{wrong:'shape'}`.
137
- *
138
- * Third-party hosts MAY recognize additional handlers; the kit
139
- * records the string verbatim. */
140
- readonly handler: string;
186
+ /**
187
+ * Point the render's bootstrap at a renderer-bundle URL the host
188
+ * controls — typically an unreachable one, to drive the
189
+ * `BUNDLE_FETCH_FAILED` bootstrap-failure path. A browser-host (Path-B)
190
+ * concern: WS-only hosts throw on it and the fixture skips.
191
+ */
192
+ export interface RendererUrlOverrideStep {
193
+ readonly type: 'renderer-url-override';
194
+ /** GguiSession the override scopes to (a prior `create-session`). */
195
+ readonly sessionId: string;
196
+ /** Renderer-bundle URL the host substitutes for the real one. */
197
+ readonly url: string;
198
+ }
199
+ /**
200
+ * Advertise a server schema version other than the implementation's
201
+ * real one — drives the version-handshake rejection path
202
+ * (`UPGRADE_REQUIRED`).
203
+ */
204
+ export interface ServerVersionOverrideStep {
205
+ readonly type: 'server-version-override';
206
+ /** GguiSession the override scopes to (a prior `create-session`). */
207
+ readonly sessionId: string;
208
+ /** Schema version the server advertises on the wire for this render. */
209
+ readonly advertiseVersion: string;
210
+ }
211
+ /**
212
+ * Substitute the MCP Apps `ui/initialize` response the host returns to
213
+ * the iframe — drives bootstrap-failure paths like
214
+ * `BOOTSTRAP_META_MISSING`. A browser-host (Path-B) concern: WS-only
215
+ * hosts throw on it and the fixture skips.
216
+ */
217
+ export interface UiInitializeResponseOverrideStep {
218
+ readonly type: 'ui-initialize-response-override';
219
+ /** GguiSession the override scopes to (a prior `create-session`). */
220
+ readonly sessionId: string;
221
+ /** Response body the host returns from `ui/initialize` verbatim. */
222
+ readonly override: unknown;
141
223
  }
142
224
  export interface EmitEnvelopeStep {
143
225
  readonly type: 'emit-envelope';
@@ -147,78 +229,88 @@ export interface EmitEnvelopeStep {
147
229
  * format (sequence stamp, timestamp, etc.). */
148
230
  readonly payload: unknown;
149
231
  }
150
- export interface SeedChannelStep {
151
- readonly type: 'seed-channel';
152
- readonly renderId: string;
153
- readonly channel: string;
154
- readonly value: unknown;
155
- }
156
232
  /**
157
- * Future-compat catch. Hosts that do not recognize the `type` SHOULD
158
- * skip the case with a warning.
233
+ * Teardown directive — mirrors {@link SetupStep}'s closed posture,
234
+ * scoped to cleanup. The vocabulary is EMPTY in this kit version: no
235
+ * fixture authors a teardown directive (renders decay via TTL), so
236
+ * the union has no members and the runner rejects any authored
237
+ * teardown step as a fixture-authoring error. Future cleanup
238
+ * directives land additively as union arms.
159
239
  */
160
- export interface UnknownSetupStep {
161
- readonly type: string & {};
162
- readonly [field: string]: unknown;
163
- }
164
- /**
165
- * Teardown directive — mirrors {@link SetupStep}'s vocabulary, scoped
166
- * to cleanup.
167
- */
168
- export type TeardownStep = UnregisterToolStep | UnknownTeardownStep;
169
- export interface UnregisterToolStep {
170
- readonly type: 'unregister-tool';
171
- readonly toolName: string;
172
- }
173
- export interface UnknownTeardownStep {
174
- readonly type: string & {};
175
- readonly [field: string]: unknown;
176
- }
240
+ export type TeardownStep = never;
177
241
  /**
178
242
  * What the test asserts after the input envelope drives the host.
179
243
  *
180
244
  * Discriminator is extensibly closed via `(string & {})`. Each arm
181
245
  * carries only the fields the assertion semantically requires — no
182
- * optional-everything-bags. Cross-cuts (e.g. every failure-path case
183
- * also expects a matching observability event) are authored as
184
- * {@link TestCase.expectedBehavior} of one kind PLUS
185
- * {@link TestCase.expectedObservability} alongside, not as nested
186
- * unions.
246
+ * optional-everything-bags.
187
247
  */
188
- export type ExpectedBehavior = ContractErrorBehavior | StreamUpdateBehavior | ObservabilityBehavior | BootstrapFailureBehavior | BootstrapSuccessBehavior | VersionMismatchBehavior | PropsUpdateBehavior | NoOpBehavior | UnknownBehavior;
248
+ export type ExpectedBehavior = ActionAckBehavior | ErrorFrameBehavior | StreamUpdateBehavior | BootstrapFailureBehavior | BootstrapSuccessBehavior | VersionMismatchBehavior | PropsUpdateBehavior | SessionStateBehavior | NoOpBehavior | UnknownBehavior;
189
249
  /**
190
- * Expect a `_ggui:contract-error` envelope with the given code + tool
191
- * + optional actionName + optional sourceAction.type. Matches the
192
- * renderer's {@link ProtocolError} kind `'contract'` variant AND the
193
- * server-wire `ContractErrorPayload` shape.
250
+ * Expect the action's `ack` frame to carry a numeric
251
+ * `payload.sequence` — the monotonic event sequence the server
252
+ * assigned when it appended the action to the GguiSession's consume
253
+ * buffer. The ack-carries-sequence contract is the wire-observable
254
+ * proof the event persisted; the retrieval half (the agent draining
255
+ * the buffer via `ggui_consume`) is an MCP tool call outside this WS
256
+ * kit's observation window.
194
257
  */
195
- export interface ContractErrorBehavior {
196
- readonly kind: 'contract-error';
197
- readonly code: ContractErrorCode;
198
- readonly toolName: string;
199
- readonly actionName?: string;
200
- readonly sourceAction?: 'wired-action' | 'refresh-stream' | (string & {});
258
+ export interface ActionAckBehavior {
259
+ readonly kind: 'action-ack';
201
260
  /**
202
- * Optional observability co-assertion. Most contract-error paths
203
- * also emit a `contract-error-emitted` observability event with the
204
- * same `code` + `toolName`. Authoring this here keeps the
205
- * cross-cut in one place.
261
+ * `requestId` the fixture's `inputEnvelope` stamps on the action
262
+ * frame. The matcher requires the ack to echo it — distinguishing
263
+ * the action's ack from the ack the runner's own subscribe frame
264
+ * produces.
206
265
  */
207
- readonly observability?: ExpectedObservabilityEvent;
266
+ readonly requestId: string;
267
+ }
268
+ /**
269
+ * Expect an `error` frame whose `payload.code` matches. Generic
270
+ * error-frame vocabulary: `version-mismatch`'s `UPGRADE_REQUIRED`
271
+ * match is the same read narrowed to one code, and future rejection
272
+ * fixtures (`SESSION_NOT_FOUND`, `APP_MISMATCH`, …) author this arm
273
+ * with their expected code.
274
+ */
275
+ export interface ErrorFrameBehavior {
276
+ readonly kind: 'error-frame';
277
+ /** Expected `payload.code` on the error frame. */
278
+ readonly code: string;
279
+ /** Optional `requestId` the error frame must echo — pins the error
280
+ * to the fixture's own dispatched frame rather than any error the
281
+ * observation window happens to catch. */
282
+ readonly requestId?: string;
208
283
  }
209
- /** Expect a stream-update on the named channel matching the given value
210
- * shape (deep-equal, via the host's matcher). */
284
+ /**
285
+ * Expect a canonical channel-3 delivery frame (SPEC §12.2):
286
+ * `{type: 'data', payload: StreamEnvelope}` whose envelope names the
287
+ * declared `channel` and whose `payload.payload` body deep-equals the
288
+ * declared `value`. The `StreamEnvelope` shape is defined by
289
+ * `@ggui-ai/protocol` (`types/live-channel`). Frames of any other
290
+ * `type` never satisfy this expectation.
291
+ */
211
292
  export interface StreamUpdateBehavior {
212
293
  readonly kind: 'stream-update';
213
294
  readonly channel: string;
214
295
  readonly value: unknown;
215
- }
216
- /** Expect a standalone observability event (no wired-action / refresh
217
- * side-effect paired). Mostly used by happy-path rows + the
218
- * `wired-tool-invoked` event. */
219
- export interface ObservabilityBehavior {
220
- readonly kind: 'observability-event';
221
- readonly event: ExpectedObservabilityEvent;
296
+ /**
297
+ * How {@link value} is compared against the observed envelope's
298
+ * `payload` body.
299
+ *
300
+ * - `'exact'` (default) — deep-equal. The observed body MUST have
301
+ * exactly the declared keys, no more.
302
+ * - `'subset'` — every key declared in {@link value} MUST be
303
+ * present and matching in the observed body; extra observed keys
304
+ * are ignored. Subset semantics relax object key sets only —
305
+ * primitives and arrays still compare exact (an array with a
306
+ * missing element is a different array, not a subset).
307
+ *
308
+ * Use `'subset'` when the real payload carries non-deterministic
309
+ * fields the fixture cannot pin (generated ids, timestamps). Pinning
310
+ * only the deterministic keys keeps the assertion honest without
311
+ * wrongly rejecting a correct server over a random id.
312
+ */
313
+ readonly valueMatch?: 'exact' | 'subset';
222
314
  }
223
315
  /**
224
316
  * Expect a bootstrap-failure postMessage envelope + the renderer's
@@ -237,24 +329,51 @@ export interface BootstrapFailureBehavior {
237
329
  }
238
330
  /**
239
331
  * Expect the renderer to reach the "ready to accept actions" state —
240
- * no bootstrap failure, no version rejection, the first stack item
241
- * rendered with `data-ggui-code-ready="true"` (or the host's
332
+ * no bootstrap failure, no version rejection, the render reaching
333
+ * `data-ggui-code-ready="true"` (or the host's
242
334
  * equivalent signal).
243
335
  */
244
336
  export interface BootstrapSuccessBehavior {
245
337
  readonly kind: 'bootstrap-success';
246
- /** Optional: observability events the host MUST emit in order. */
247
- readonly observabilitySequence?: readonly ExpectedObservabilityEvent[];
338
+ /**
339
+ * Optional ack-field assertion — the server half of SPEC §12.2.2's
340
+ * version handshake. When authored, the subscribe `ack`'s
341
+ * `payload.serverVersion` MUST be present and equal the expected
342
+ * version; an ack that omits the field (legacy-pass-through) or
343
+ * advertises a different version FAILS the fixture.
344
+ *
345
+ * The only authored value is the `'current'` sentinel: the matcher
346
+ * resolves it to `PROTOCOL_SCHEMA_VERSION`, the canonical schema
347
+ * version this kit release was compiled against
348
+ * (`@ggui-ai/protocol`) — the same resolution
349
+ * {@link SubscribeFrameShaping.supportedVersions} uses, so the
350
+ * declared set and the asserted advertisement can never drift.
351
+ * Evergreen by construction; a fixture pinning a version literal
352
+ * would go stale at the next protocol bump. An explicit-literal arm
353
+ * can land additively if a fixture ever needs one.
354
+ *
355
+ * Absent → the assertion is ack presence alone (plus no error
356
+ * frame), the pre-handshake bootstrap claim.
357
+ */
358
+ readonly serverVersion?: 'current';
248
359
  }
249
360
  /**
250
- * Expect a version-handshake rejection. Maps to the
251
- * `{type:'ggui:upgrade-required', server, client}` postMessage
252
- * envelope + the renderer's {@link ProtocolError} kind `'version'`.
361
+ * Expect a version-handshake rejection — an `error` frame with
362
+ * `payload.code === 'UPGRADE_REQUIRED'` (SPEC §12.2.2) after the
363
+ * runner's subscribe. The client-side declaration that PROVOKES the
364
+ * rejection is NOT authored here — it lives on the fixture's
365
+ * {@link SubscribeFrameShaping.supportedVersions} knob, the single
366
+ * place the runner sources its subscribe declaration from.
253
367
  */
254
368
  export interface VersionMismatchBehavior {
255
369
  readonly kind: 'version-mismatch';
370
+ /**
371
+ * Version the server is expected to be advertising when it rejects
372
+ * — for the shipped fixture, the value its `server-version-override`
373
+ * setup directive installed. Surfaced in failure diagnostics so a
374
+ * miss names the version the override should have advertised.
375
+ */
256
376
  readonly serverVersion: string;
257
- readonly clientAccepts: readonly string[];
258
377
  }
259
378
  /**
260
379
  * Expect a `props_update` round-trip — hosts deliver new props via
@@ -272,6 +391,45 @@ export interface PropsUpdateBehavior {
272
391
  readonly expected: string;
273
392
  };
274
393
  }
394
+ /**
395
+ * Expect a render-state mutation — the input message produced no wire
396
+ * response, but it MUST have changed observable GguiSession state.
397
+ *
398
+ * This is the kit's THIRD grading mechanism, alongside the Path-A WS
399
+ * fixtures (wire-observable) and the pure-function catalogs
400
+ * (deterministic validation): a *stateful* obligation. A
401
+ * Client→Server message like `host_context_observed` has no
402
+ * synchronous response envelope — its whole contract is the
403
+ * persistence onto the GguiSession. Asserting `no-op` (wire silence)
404
+ * would certify a server that drops the message entirely, so the
405
+ * honest grade reads the field back.
406
+ *
407
+ * The runner resolves this AFTER the observation window via
408
+ * {@link ConformanceHost.readSessionField} — it asks the host for the
409
+ * field's post-dispatch value and deep-equals it against
410
+ * {@link expected} (the same exact deep-equal the frame matchers
411
+ * use). A host that does not provide `readSessionField` makes the
412
+ * fixture SKIP (the kit cannot observe a mutation it has no
413
+ * introspection seam for) — never a silent pass. `matchBehavior`
414
+ * returns `unmatchable-on-ws` for this kind: frames cannot prove
415
+ * state.
416
+ */
417
+ export interface SessionStateBehavior {
418
+ readonly kind: 'session-state';
419
+ /**
420
+ * The GguiSession field whose post-dispatch value the kit asserts
421
+ * (e.g. `hostContext`). Passed verbatim to
422
+ * {@link ConformanceHost.readSessionField}; the host owns the
423
+ * field-name recognition set.
424
+ */
425
+ readonly field: string;
426
+ /**
427
+ * The value the field MUST hold after the input message has been
428
+ * processed by the implementation under test. Compared with exact
429
+ * deep equality.
430
+ */
431
+ readonly expected: unknown;
432
+ }
275
433
  /**
276
434
  * No observable side-effect expected. Used for inputs the protocol
277
435
  * intentionally drops (malformed envelopes, unrecognized JSON-RPC
@@ -289,16 +447,45 @@ export interface UnknownBehavior {
289
447
  readonly [field: string]: unknown;
290
448
  }
291
449
  /**
292
- * Narrowed observability-event shape for fixture assertions. Parallel
293
- * to `@ggui-ai/iframe-runtime`'s `ObservabilityEvent` but with every field
294
- * optional-by-default so fixtures can assert "at least these values"
295
- * without over-specifying (e.g. `latencyMs` is nondeterministic).
450
+ * Optional shaping of the RUNNER-OWNED subscribe frame.
451
+ *
452
+ * The subscribe frame is not part of a fixture's `inputEnvelope` — the
453
+ * runner always sends it first, with its conventional payload
454
+ * (`{sessionId, appId: 'conformance', role: 'user'}`). Some contracts
455
+ * are only drivable by varying that frame itself; this object is the
456
+ * fixture's declarative knob for those variations. Additive
457
+ * vocabulary: new shaping fields land as optional members, mirroring
458
+ * the kit's authored-shape evolution discipline.
296
459
  */
297
- export interface ExpectedObservabilityEvent {
298
- readonly kind: 'wired-tool-invoked' | 'contract-error-emitted' | 'schema-version-mismatch' | 'subscribe-failed' | (string & {});
299
- readonly toolName?: string;
300
- readonly actionName?: string;
301
- readonly code?: string;
460
+ export interface SubscribeFrameShaping {
461
+ /**
462
+ * When `true`, the runner OMITS `appId` from its subscribe payload.
463
+ * Drives SPEC §12.2's identity-default resolution path: `appId` is
464
+ * optional on the wire, and absence means the server resolves the
465
+ * caller's identity-default app (token binding, identity mapping,
466
+ * or deployment default). Default `false` — the runner stamps its
467
+ * conventional `appId: 'conformance'`.
468
+ */
469
+ readonly omitAppId?: boolean;
470
+ /**
471
+ * Protocol schema versions the runner DECLARES on its subscribe
472
+ * payload (`SubscribePayload.supportedVersions`) — the client half
473
+ * of SPEC §12.2.2's opt-in version handshake.
474
+ *
475
+ * - Absent → the runner declares nothing; the subscribe is
476
+ * version-agnostic (legacy-pass-through), exactly as before the
477
+ * handshake existed.
478
+ * - `'current'` (sentinel) → the runner resolves the declaration
479
+ * to `[PROTOCOL_SCHEMA_VERSION]`, the canonical schema version
480
+ * this kit release was compiled against (`@ggui-ai/protocol`).
481
+ * The sentinel keeps fixtures evergreen across protocol version
482
+ * bumps — no fixture pins a version literal that goes stale.
483
+ * - Explicit `string[]` → declared on the wire verbatim.
484
+ *
485
+ * The runner's validating parse rejects any other shape as a
486
+ * fixture-authoring error (closed vocabulary, like {@link SetupStep}).
487
+ */
488
+ readonly supportedVersions?: 'current' | readonly string[];
302
489
  }
303
490
  /**
304
491
  * One conformance-kit fixture. Authored as JSON under `./fixtures/**`,
@@ -324,14 +511,20 @@ export interface TestCase {
324
511
  * be empty. See {@link SetupStep}.
325
512
  */
326
513
  readonly setup: readonly SetupStep[];
514
+ /**
515
+ * Optional shaping of the runner-owned subscribe frame — see
516
+ * {@link SubscribeFrameShaping}. Absent means the runner's
517
+ * conventional subscribe payload is sent unmodified.
518
+ */
519
+ readonly subscribe?: SubscribeFrameShaping;
327
520
  /**
328
521
  * The envelope the runner feeds to the system under test. Opaque
329
522
  * shape — the runner passes it verbatim to the host's transport.
330
523
  *
331
- * Typically an object like
332
- * `{type: 'action', channel: 0, renderId: 'test-r1', action: {…}}`
333
- * (channel 0 = wired-action dispatch) or
334
- * `{type: 'render', renderId: 'test-r1', resource: {…}}` for
524
+ * Typically a live-channel action message like
525
+ * `{type: 'action', requestId, payload: {sessionId, type:
526
+ * 'data:submit', payload: {action, data}}}` or
527
+ * `{type: 'render', sessionId: 'test-r1', resource: {…}}` for
335
528
  * bootstrap-path fixtures.
336
529
  *
337
530
  * `unknown` rather than a typed union because the kit may drive
@@ -342,41 +535,19 @@ export interface TestCase {
342
535
  /** What the runner asserts after the envelope is dispatched. */
343
536
  readonly expectedBehavior: ExpectedBehavior;
344
537
  /**
345
- * Optional observability assertion, independent of
346
- * {@link expectedBehavior}. When `expectedBehavior.kind ===
347
- * 'contract-error'` and `expectedBehavior.observability` is set,
348
- * this field SHOULD be omitted — the nested one is canonical.
349
- */
350
- readonly expectedObservability?: readonly ExpectedObservabilityEvent[];
351
- /**
352
- * Cleanup directives the host executes AFTER the assertion. Optional —
353
- * most fixtures need no teardown post the `close-render` drop (renders
354
- * decay via TTL); `unregister-tool` is the only remaining kind, used
355
- * by fixtures that mutate the tool registry.
538
+ * Cleanup directives the host executes AFTER the assertion.
539
+ * Optional — no current fixture needs teardown (renders decay via
540
+ * TTL). The slot stays so future cleanup directives land additively.
356
541
  */
357
542
  readonly teardown?: readonly TeardownStep[];
358
543
  }
359
- /**
360
- * Canonical contract-error codes. Mirrors `@ggui-ai/protocol`'s
361
- * `ContractErrorCode` at the time this kit version was authored. The
362
- * `(string & {})` tail lets fixtures reference codes introduced by
363
- * later protocol revisions without a kit bump — the runner's
364
- * recognition set is its own concern.
365
- *
366
- * Drift discipline: the kit ships a meta-test that walks
367
- * `@ggui-ai/protocol`'s `ContractErrorCode` union against this
368
- * declaration and warns when a new canonical member appears. The
369
- * runner's behaviour on unknown codes (skip with warning) is
370
- * additive-safe: adding a code here is a kit minor; removing one is
371
- * a major.
372
- */
373
- export type ContractErrorCode = 'TOOL_NOT_FOUND' | 'TOOL_THREW' | 'TOOL_TIMEOUT' | 'SCHEMA_VIOLATION' | 'SCHEMA_MISMATCH_ERROR' | 'SESSION_NOT_FOUND' | 'AUTH_REJECTED' | (string & {});
374
544
  /**
375
545
  * Boot-path failure reasons. Mirrors `@ggui-ai/iframe-runtime`'s
376
546
  * `BootstrapFailureReason` — combines parse-time, post-parse
377
547
  * orchestration, and pre-WS transport-observable failures. Authored
378
- * copy for the same drift-decoupling reason as
379
- * {@link ContractErrorCode}.
548
+ * copy per the drift-decoupling posture in the module docstring:
549
+ * fixtures compile against the kit's frozen vocabulary, never the
550
+ * live protocol or renderer types.
380
551
  */
381
552
  export type BootstrapFailureReason = 'MISSING_TOOL_OUTPUT' | 'MISSING_META_GGUI_BOOTSTRAP' | 'BOOTSTRAP_META_MISSING' | 'MALFORMED_BOOTSTRAP' | 'EXPIRED_BOOTSTRAP' | 'UI_INITIALIZE_FAILED' | 'WS_HANDSHAKE_FAILED' | 'UPGRADE_REQUIRED' | 'BUNDLE_FETCH_FAILED' | 'CSP_VIOLATION' | 'SESSION_NOT_FOUND' | 'AUTH_REJECTED' | (string & {});
382
553
  /**
@@ -395,30 +566,13 @@ export type ProtocolError = {
395
566
  readonly message?: string;
396
567
  } | {
397
568
  readonly kind: 'auth';
398
- readonly code: 'SESSION_NOT_FOUND' | 'TOKEN_EXPIRED' | 'AUTH_REJECTED';
569
+ readonly code: 'SESSION_NOT_FOUND' | 'BOOTSTRAP_EXPIRED' | 'BOOTSTRAP_INVALID' | 'BOOTSTRAP_SESSION_MISMATCH' | 'BOOTSTRAP_APP_MISMATCH' | 'UNAUTHENTICATED';
399
570
  readonly message?: string;
400
571
  } | {
401
572
  readonly kind: 'protocol';
402
573
  readonly code: 'SESSION_MISMATCH' | 'APP_MISMATCH' | 'MALFORMED_ENVELOPE' | (string & {});
403
574
  readonly message?: string;
404
575
  readonly details?: unknown;
405
- } | {
406
- readonly kind: 'contract';
407
- readonly payload: {
408
- readonly toolName: string;
409
- readonly actionName?: string;
410
- readonly sourceAction?: {
411
- readonly type: 'wired-action' | 'refresh-stream' | (string & {});
412
- readonly dispatchedAt: string;
413
- };
414
- readonly error: {
415
- readonly code: ContractErrorCode;
416
- readonly message: string;
417
- readonly causedBy?: string;
418
- };
419
- readonly timestamp: string;
420
- readonly schemaVersion?: string;
421
- };
422
576
  } | {
423
577
  readonly kind: 'bootstrap';
424
578
  readonly reason: BootstrapFailureReason;