@ggui-ai/protocol-conformance 0.1.0-rc.1
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/LICENSE +201 -0
- package/README.md +89 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +193 -0
- package/dist/conformance-host.d.ts +155 -0
- package/dist/conformance-host.d.ts.map +1 -0
- package/dist/conformance-host.js +45 -0
- package/dist/fixtures/bootstrap-protocol/bootstrap-bundle-fetch-failed.json +26 -0
- package/dist/fixtures/bootstrap-protocol/bootstrap-meta-missing.json +32 -0
- package/dist/fixtures/bootstrap-protocol/bootstrap-success.json +17 -0
- package/dist/fixtures/bootstrap-protocol/index.d.ts +5 -0
- package/dist/fixtures/bootstrap-protocol/index.d.ts.map +1 -0
- package/dist/fixtures/bootstrap-protocol/index.js +24 -0
- package/dist/fixtures/canvas-mode-wire-shapes/canvas-bootstrap-mutual-exclusion.json +25 -0
- package/dist/fixtures/canvas-mode-wire-shapes/canvas-lifecycle-channel-emits-handshake-started.json +30 -0
- package/dist/fixtures/canvas-mode-wire-shapes/canvas-navigated-updates-active-stack-item.json +30 -0
- package/dist/fixtures/canvas-mode-wire-shapes/host-context-observed-persists.json +22 -0
- package/dist/fixtures/canvas-mode-wire-shapes/index.d.ts +5 -0
- package/dist/fixtures/canvas-mode-wire-shapes/index.d.ts.map +1 -0
- package/dist/fixtures/canvas-mode-wire-shapes/index.js +47 -0
- package/dist/fixtures/index.d.ts +78 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +87 -0
- package/dist/fixtures/observability-events/index.d.ts +5 -0
- package/dist/fixtures/observability-events/index.d.ts.map +1 -0
- package/dist/fixtures/observability-events/index.js +22 -0
- package/dist/fixtures/observability-events/observability-contract-error-emitted.json +29 -0
- package/dist/fixtures/observability-events/observability-wired-tool-invoked.json +28 -0
- package/dist/fixtures/refresh-semantics/index.d.ts +5 -0
- package/dist/fixtures/refresh-semantics/index.d.ts.map +1 -0
- package/dist/fixtures/refresh-semantics/index.js +19 -0
- package/dist/fixtures/refresh-semantics/stream-refresh-success.json +40 -0
- package/dist/fixtures/reserved-channel-authority/index.d.ts +4 -0
- package/dist/fixtures/reserved-channel-authority/index.d.ts.map +1 -0
- package/dist/fixtures/reserved-channel-authority/index.js +21 -0
- package/dist/fixtures/reserved-channel-authority/props-update-roundtrip.json +34 -0
- package/dist/fixtures/reserved-channel-authority/stream-schema-violation.json +45 -0
- package/dist/fixtures/schema-version-handshake/index.d.ts +4 -0
- package/dist/fixtures/schema-version-handshake/index.d.ts.map +1 -0
- package/dist/fixtures/schema-version-handshake/index.js +20 -0
- package/dist/fixtures/schema-version-handshake/version-match.json +16 -0
- package/dist/fixtures/schema-version-handshake/version-mismatch.json +26 -0
- package/dist/fixtures/wired-action-dispatch/index.d.ts +5 -0
- package/dist/fixtures/wired-action-dispatch/index.d.ts.map +1 -0
- package/dist/fixtures/wired-action-dispatch/index.js +29 -0
- package/dist/fixtures/wired-action-dispatch/wired-action-success.json +36 -0
- package/dist/fixtures/wired-action-dispatch/wired-action-tool-not-found.json +31 -0
- package/dist/fixtures/wired-action-dispatch/wired-action-tool-threw.json +32 -0
- package/dist/fixtures/wired-action-dispatch/wired-action-tool-timeout.json +32 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/loader.d.ts +19 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +46 -0
- package/dist/match-behavior.d.ts +121 -0
- package/dist/match-behavior.d.ts.map +1 -0
- package/dist/match-behavior.js +385 -0
- package/dist/registration-conformance/cases/gate-export-not-registered.json +19 -0
- package/dist/registration-conformance/cases/gate-no-gadgets-accept.json +13 -0
- package/dist/registration-conformance/cases/gate-package-mismatch.json +19 -0
- package/dist/registration-conformance/cases/gate-priority-not-registered-outranks-mismatch.json +20 -0
- package/dist/registration-conformance/cases/gate-registered-resolves-accept.json +25 -0
- package/dist/registration-conformance/cases/gate-stdlib-hook-accept.json +27 -0
- package/dist/registration-conformance/cases/gate-typo-classifies-as-not-registered.json +27 -0
- package/dist/registration-conformance/index.d.ts +114 -0
- package/dist/registration-conformance/index.d.ts.map +1 -0
- package/dist/registration-conformance/index.js +63 -0
- package/dist/reporter.d.ts +32 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +151 -0
- package/dist/resolution-conformance/cases/resolve-bundle-host-computed.json +13 -0
- package/dist/resolution-conformance/cases/resolve-default-host.json +12 -0
- package/dist/resolution-conformance/cases/resolve-explicit-bundle-and-style-url.json +14 -0
- package/dist/resolution-conformance/cases/resolve-explicit-bundle-url.json +12 -0
- package/dist/resolution-conformance/cases/resolve-loopback-host-http-scheme.json +13 -0
- package/dist/resolution-conformance/cases/resolve-package-only-no-version.json +8 -0
- package/dist/resolution-conformance/cases/resolve-stdlib-no-urls.json +10 -0
- package/dist/resolution-conformance/index.d.ts +76 -0
- package/dist/resolution-conformance/index.d.ts.map +1 -0
- package/dist/resolution-conformance/index.js +81 -0
- package/dist/run-conformance.d.ts +58 -0
- package/dist/run-conformance.d.ts.map +1 -0
- package/dist/run-conformance.js +286 -0
- package/dist/schema-conformance/cases/wire-bad-export-name-reject.json +11 -0
- package/dist/schema-conformance/cases/wire-bad-package-name-reject.json +11 -0
- package/dist/schema-conformance/cases/wire-component-export-accept.json +10 -0
- package/dist/schema-conformance/cases/wire-empty-package-reject.json +11 -0
- package/dist/schema-conformance/cases/wire-minimal-accept.json +10 -0
- package/dist/schema-conformance/cases/wire-multi-package-accept.json +14 -0
- package/dist/schema-conformance/cases/wire-prose-accept.json +15 -0
- package/dist/schema-conformance/cases/wire-registry-metadata-reject.json +13 -0
- package/dist/schema-conformance/cases/wire-stale-libraries-reject.json +14 -0
- package/dist/schema-conformance/cases/wire-stdlib-accept.json +10 -0
- package/dist/schema-conformance/cases/wire-transport-field-reject.json +15 -0
- package/dist/schema-conformance/cases/wire-version-field-reject.json +13 -0
- package/dist/schema-conformance/index.d.ts +71 -0
- package/dist/schema-conformance/index.d.ts.map +1 -0
- package/dist/schema-conformance/index.js +103 -0
- package/dist/types.d.ts +434 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +62 -0
- package/dist/ws-transport.d.ts +42 -0
- package/dist/ws-transport.d.ts.map +1 -0
- package/dist/ws-transport.js +135 -0
- package/package.json +83 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match observed WS frames against a fixture's {@link ExpectedBehavior}.
|
|
3
|
+
*
|
|
4
|
+
* Each matcher is pure — it consumes a read-only array of frames the
|
|
5
|
+
* runner collected from the transport and returns either a pass or a
|
|
6
|
+
* {@link MatchFailure} the runner lifts into a
|
|
7
|
+
* {@link ConformanceFailure}. No side-effects, no hidden state — the
|
|
8
|
+
* runner owns orchestration.
|
|
9
|
+
*
|
|
10
|
+
* ## Path-A vs Path-B partition
|
|
11
|
+
*
|
|
12
|
+
* The kit's behavior vocabulary spans both wire-observable claims (the
|
|
13
|
+
* subscribe ack, contract-error envelopes on reserved channels, stream
|
|
14
|
+
* updates) AND surface-observable claims (DOM state after a props
|
|
15
|
+
* update, browser-side bootstrap failure modes). This file matches the
|
|
16
|
+
* **Path-A** subset — the wire-observable behaviors a runner can
|
|
17
|
+
* assert from frames alone, with no MCP-Apps-host adapter and no
|
|
18
|
+
* Playwright page. The Path-B subset (browser-host harness) is
|
|
19
|
+
* inherently NOT WS-reducible: bootstrap-failure modes are caused by
|
|
20
|
+
* the renderer-bundle fetch + `ui/initialize` round-trip the host
|
|
21
|
+
* speaks over postMessage; props-update assertions are assertions on
|
|
22
|
+
* the rendered DOM the renderer paints, not on WS frames the server
|
|
23
|
+
* emits. For these kinds we return `unmatchable-on-ws` with a sharp
|
|
24
|
+
* reason and the runner records SKIP — they are NOT failures of the
|
|
25
|
+
* server, they are claims a different driver must drive.
|
|
26
|
+
*
|
|
27
|
+
* The Path-B driver is a separate Playwright-based test suite — it
|
|
28
|
+
* picks up the `bootstrap-failure` fixtures and asserts on the
|
|
29
|
+
* iframe-host's `data-ggui-console-iframe-error` pane after
|
|
30
|
+
* route-based fault injection. A future packaged browser-host adapter
|
|
31
|
+
* will fold that capability into the kit so third-party adopters
|
|
32
|
+
* don't have to reimplement it. Until then, the partition is honest:
|
|
33
|
+
* Path A handles wire claims, Path B (out-of-process) handles
|
|
34
|
+
* browser claims, and `unmatchable-on-ws` is the partition seam.
|
|
35
|
+
*
|
|
36
|
+
* v1 Path-A scope:
|
|
37
|
+
*
|
|
38
|
+
* - `bootstrap-success` — subscribe produced an `ack` frame.
|
|
39
|
+
* - `version-mismatch` — subscribe produced an `error` frame with
|
|
40
|
+
* `code: 'UPGRADE_REQUIRED'`.
|
|
41
|
+
* - `contract-error` — stream frame on `_ggui:contract-error`
|
|
42
|
+
* channel matches `code` + `toolName` (+ optional `actionName` +
|
|
43
|
+
* `sourceAction`).
|
|
44
|
+
* - `stream-update` — stream frame on named channel matches
|
|
45
|
+
* declared value.
|
|
46
|
+
* - `no-op` — no frames observed after input dispatch.
|
|
47
|
+
* - `observability-event` — see below.
|
|
48
|
+
*
|
|
49
|
+
* v1 Path-B-only kinds (return `unmatchable-on-ws`):
|
|
50
|
+
*
|
|
51
|
+
* - `bootstrap-failure` — the fault surface lives in the host's
|
|
52
|
+
* bootstrap-fetch + `ui/initialize` round-trip; the fixture's
|
|
53
|
+
* `renderer-url-override` / `ui-initialize-response-override`
|
|
54
|
+
* setup directives are MCP-Apps-host concerns, not WS server
|
|
55
|
+
* concerns. The reference server's host adapter correctly throws
|
|
56
|
+
* "out of scope" on these directives — that's not a bug, it's
|
|
57
|
+
* the partition being respected.
|
|
58
|
+
* - `props-update` — the assertion is on rendered DOM after the
|
|
59
|
+
* server emits a `_ggui:props` frame; matchable on WS only as
|
|
60
|
+
* "frame was emitted", not as "DOM reflects it" — and the fixture
|
|
61
|
+
* vocabulary asserts the latter.
|
|
62
|
+
*
|
|
63
|
+
* ## `observability-event` — WS-evidence matcher
|
|
64
|
+
*
|
|
65
|
+
* The ggui protocol bar mandates that observability events follow
|
|
66
|
+
* from WS-observable evidence. The bar's claim is "any conformant host
|
|
67
|
+
* MUST emit `wired-tool-invoked` when the WS observes a wired-action's
|
|
68
|
+
* tool dispatch + result frame; any conformant host MUST emit
|
|
69
|
+
* `contract-error-emitted` when the WS observes a `_ggui:contract-
|
|
70
|
+
* error` envelope." So a Path-A pass on an `observability-event`
|
|
71
|
+
* fixture is the kit certifying "WS evidence the bar mandates a host
|
|
72
|
+
* mirror-emission for is present" — a true vendor-neutral conformance
|
|
73
|
+
* claim, not a renderer-internal probe.
|
|
74
|
+
*
|
|
75
|
+
* Two recognized event arms (extensibly closed via `(string & {})`):
|
|
76
|
+
*
|
|
77
|
+
* - `wired-tool-invoked` — assert a `stream` frame on the
|
|
78
|
+
* `_ggui:wired-tool-invoked` channel carrying
|
|
79
|
+
* `value: {toolName, actionName?}` matching `event.toolName`
|
|
80
|
+
* (+ optional `event.actionName`). The reference server emits
|
|
81
|
+
* this signal from `action-router.ts::dispatchAction()` after a
|
|
82
|
+
* successful handler resolution — every conformant host MUST
|
|
83
|
+
* produce equivalent WS evidence (or a richer mirror-emission a
|
|
84
|
+
* Path-B browser host can observe).
|
|
85
|
+
*
|
|
86
|
+
* - `contract-error-emitted` — assert a `stream` frame on the
|
|
87
|
+
* `_ggui:contract-error` channel whose canonical SPEC §4.4
|
|
88
|
+
* `ContractErrorPayload` carries `error.code === event.code` and
|
|
89
|
+
* `toolName === event.toolName` (+ optional `event.actionName`).
|
|
90
|
+
* This is the SAME frame {@link matchContractError} matches; the
|
|
91
|
+
* observability arm asserts the bar's "every live-channel envelope
|
|
92
|
+
* becomes an observability event" mirror without requiring a
|
|
93
|
+
* postMessage-capture harness.
|
|
94
|
+
*
|
|
95
|
+
* Other arms (`schema-version-mismatch`, `subscribe-failed`,
|
|
96
|
+
* future `(string & {})` tails) the matcher cannot ground in current
|
|
97
|
+
* WS evidence — return `unmatchable-on-ws` with a sharp reason so the
|
|
98
|
+
* runner records SKIP, not FAIL. These are Path-B candidates for the
|
|
99
|
+
* future browser-host adapter.
|
|
100
|
+
*/
|
|
101
|
+
import type { ExpectedBehavior } from './types.js';
|
|
102
|
+
import type { ObservedFrame } from './ws-transport.js';
|
|
103
|
+
/**
|
|
104
|
+
* Matcher outcome. `pass` → the fixture's assertion is satisfied.
|
|
105
|
+
* `fail` → wire-level mismatch; the runner records a
|
|
106
|
+
* {@link ConformanceFailure}. `unmatchable-on-ws` → this behavior is
|
|
107
|
+
* not drivable over pure WS; the runner records a skip.
|
|
108
|
+
*/
|
|
109
|
+
export type MatchResult = {
|
|
110
|
+
readonly kind: 'pass';
|
|
111
|
+
} | {
|
|
112
|
+
readonly kind: 'fail';
|
|
113
|
+
readonly expected: unknown;
|
|
114
|
+
readonly received: unknown;
|
|
115
|
+
readonly message: string;
|
|
116
|
+
} | {
|
|
117
|
+
readonly kind: 'unmatchable-on-ws';
|
|
118
|
+
readonly reason: string;
|
|
119
|
+
};
|
|
120
|
+
export declare function matchBehavior(behavior: ExpectedBehavior, frames: readonly ObservedFrame[]): MatchResult;
|
|
121
|
+
//# sourceMappingURL=match-behavior.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"match-behavior.d.ts","sourceRoot":"","sources":["../src/match-behavior.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmGG;AACH,OAAO,KAAK,EAGV,gBAAgB,EAMjB,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,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,SAAS,aAAa,EAAE,GAC/B,WAAW,CA2Cb"}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
export function matchBehavior(behavior, frames) {
|
|
2
|
+
// Dispatch on the `kind` discriminator with explicit casts because
|
|
3
|
+
// the extensibly-closed `UnknownBehavior` (`kind: string & {}`) in
|
|
4
|
+
// the union widens the discriminant, preventing TS from narrowing
|
|
5
|
+
// on literals. We checked the literal above — the cast is sound.
|
|
6
|
+
if (behavior.kind === 'bootstrap-success') {
|
|
7
|
+
return matchBootstrapSuccess(behavior, frames);
|
|
8
|
+
}
|
|
9
|
+
if (behavior.kind === 'version-mismatch') {
|
|
10
|
+
return matchVersionMismatch(behavior, frames);
|
|
11
|
+
}
|
|
12
|
+
if (behavior.kind === 'contract-error') {
|
|
13
|
+
return matchContractError(behavior, frames);
|
|
14
|
+
}
|
|
15
|
+
if (behavior.kind === 'stream-update') {
|
|
16
|
+
return matchStreamUpdate(behavior, frames);
|
|
17
|
+
}
|
|
18
|
+
if (behavior.kind === 'no-op') {
|
|
19
|
+
return matchNoOp(behavior, frames);
|
|
20
|
+
}
|
|
21
|
+
if (behavior.kind === 'bootstrap-failure') {
|
|
22
|
+
return {
|
|
23
|
+
kind: 'unmatchable-on-ws',
|
|
24
|
+
reason: 'bootstrap-failure is a Path-B (browser-host) claim — the fault surface is the host\'s bootstrap-fetch + `ui/initialize` postMessage round-trip, not a WS frame the server emits. The `renderer-url-override` / `ui-initialize-response-override` setup directives are MCP-Apps-host concerns; the reference server\'s host adapter throws "out of scope" on them by design. Drive these fixtures via a Path-B browser-host harness (today: Slice M Playwright dispatch in `e2e/ggui-oss/tests/mcp-app-iframe.spec.ts`; future: packaged Phase-3.2 adapter inside the kit).',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (behavior.kind === 'observability-event') {
|
|
28
|
+
return matchObservabilityEvent(behavior, frames);
|
|
29
|
+
}
|
|
30
|
+
if (behavior.kind === 'props-update') {
|
|
31
|
+
return {
|
|
32
|
+
kind: 'unmatchable-on-ws',
|
|
33
|
+
reason: 'props-update is a Path-B (browser-host) claim — the assertion is on rendered DOM (selector + attribute/text), not on the `_ggui:props` WS frame in isolation. Matchable on WS only as "frame was emitted"; the fixture vocabulary asserts "DOM reflects the update". Drive via a Path-B browser-host harness (today: Slice M Playwright dispatch in `e2e/ggui-oss/tests/mcp-app-iframe.spec.ts`; future: packaged Phase-3.2 adapter inside the kit).',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Extensibly-closed union catch — unknown `kind` = new fixture
|
|
37
|
+
// vocabulary the runner doesn't recognize; skip cleanly.
|
|
38
|
+
return {
|
|
39
|
+
kind: 'unmatchable-on-ws',
|
|
40
|
+
reason: `Unknown expectedBehavior.kind='${behavior.kind}' — kit runner does not recognize this behavior. Check kit version.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Per-behavior matchers
|
|
45
|
+
// =============================================================================
|
|
46
|
+
function matchBootstrapSuccess(_behavior, frames) {
|
|
47
|
+
const ack = frames.find((f) => f.kind === 'frame' && f.parsed['type'] === 'ack');
|
|
48
|
+
if (ack === undefined) {
|
|
49
|
+
return {
|
|
50
|
+
kind: 'fail',
|
|
51
|
+
expected: { type: 'ack' },
|
|
52
|
+
received: frames.map((f) => (f.kind === 'frame' ? f.parsed['type'] : 'unparseable')),
|
|
53
|
+
message: 'expected an `ack` frame after subscribe; received none within the observation window.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// If an error frame arrived BEFORE the ack, that's a bootstrap
|
|
57
|
+
// failure we misclassified — surface it explicitly.
|
|
58
|
+
const error = frames.find((f) => f.kind === 'frame' && f.parsed['type'] === 'error');
|
|
59
|
+
if (error !== undefined) {
|
|
60
|
+
return {
|
|
61
|
+
kind: 'fail',
|
|
62
|
+
expected: { type: 'ack', precededByError: false },
|
|
63
|
+
received: { errorFrame: error.kind === 'frame' ? error.parsed : error },
|
|
64
|
+
message: 'expected bootstrap-success but received an error frame — the subscribe was rejected.',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { kind: 'pass' };
|
|
68
|
+
}
|
|
69
|
+
function matchVersionMismatch(behavior, frames) {
|
|
70
|
+
const error = frames.find((f) => f.kind === 'frame' &&
|
|
71
|
+
f.parsed['type'] === 'error' &&
|
|
72
|
+
isRecord(f.parsed['payload']) &&
|
|
73
|
+
f.parsed['payload']['code'] === 'UPGRADE_REQUIRED');
|
|
74
|
+
if (error === undefined) {
|
|
75
|
+
return {
|
|
76
|
+
kind: 'fail',
|
|
77
|
+
expected: {
|
|
78
|
+
type: 'error',
|
|
79
|
+
payload: { code: 'UPGRADE_REQUIRED' },
|
|
80
|
+
serverVersion: behavior.serverVersion,
|
|
81
|
+
},
|
|
82
|
+
received: frames.map((f) => (f.kind === 'frame' ? f.parsed : f)),
|
|
83
|
+
message: 'expected an `error` frame with `payload.code === UPGRADE_REQUIRED` after subscribe; received none.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { kind: 'pass' };
|
|
87
|
+
}
|
|
88
|
+
function matchContractError(behavior, frames) {
|
|
89
|
+
const candidates = frames.filter((f) => f.kind === 'frame' &&
|
|
90
|
+
f.parsed['type'] === 'stream' &&
|
|
91
|
+
isRecord(f.parsed['payload']) &&
|
|
92
|
+
f.parsed['payload']['channel'] === '_ggui:contract-error');
|
|
93
|
+
if (candidates.length === 0) {
|
|
94
|
+
return {
|
|
95
|
+
kind: 'fail',
|
|
96
|
+
expected: {
|
|
97
|
+
channel: '_ggui:contract-error',
|
|
98
|
+
code: behavior.code,
|
|
99
|
+
toolName: behavior.toolName,
|
|
100
|
+
},
|
|
101
|
+
received: frames.map((f) => (f.kind === 'frame' ? f.parsed : f)),
|
|
102
|
+
message: 'expected a `stream` frame on the `_ggui:contract-error` reserved channel; none observed.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Check at least one candidate carries the expected code + tool.
|
|
106
|
+
//
|
|
107
|
+
// Canonical shape per SPEC §4.4 `ContractErrorPayload`: `code` is
|
|
108
|
+
// NESTED under `value.error.code`, alongside `value.error.message`
|
|
109
|
+
// and `value.error.causedBy`. `toolName` / `actionName` /
|
|
110
|
+
// `sourceAction` / `timestamp` live at the top of `value`. The
|
|
111
|
+
// matcher reads the canonical shape exclusively — the reference
|
|
112
|
+
// server (and `@ggui-ai/mcp-server`'s router) emits via
|
|
113
|
+
// `makeContractErrorPayload`, which is the authoritative builder.
|
|
114
|
+
// Any producer emitting a flat `code` field is a spec violation and
|
|
115
|
+
// will correctly fail this match — that's the point of Protocol #5
|
|
116
|
+
// (named failure modes) being assertable.
|
|
117
|
+
const match = candidates.find((f) => {
|
|
118
|
+
if (f.kind !== 'frame')
|
|
119
|
+
return false;
|
|
120
|
+
const payload = f.parsed['payload'];
|
|
121
|
+
if (!isRecord(payload))
|
|
122
|
+
return false;
|
|
123
|
+
const value = payload['value'];
|
|
124
|
+
if (!isRecord(value))
|
|
125
|
+
return false;
|
|
126
|
+
const error = value['error'];
|
|
127
|
+
if (!isRecord(error))
|
|
128
|
+
return false;
|
|
129
|
+
if (error['code'] !== behavior.code)
|
|
130
|
+
return false;
|
|
131
|
+
if (value['toolName'] !== behavior.toolName)
|
|
132
|
+
return false;
|
|
133
|
+
if (behavior.actionName !== undefined &&
|
|
134
|
+
value['actionName'] !== behavior.actionName) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (behavior.sourceAction !== undefined) {
|
|
138
|
+
const source = value['sourceAction'];
|
|
139
|
+
const sourceType = isRecord(source) ? source['type'] : source;
|
|
140
|
+
if (sourceType !== behavior.sourceAction)
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
});
|
|
145
|
+
if (match === undefined) {
|
|
146
|
+
return {
|
|
147
|
+
kind: 'fail',
|
|
148
|
+
expected: {
|
|
149
|
+
code: behavior.code,
|
|
150
|
+
toolName: behavior.toolName,
|
|
151
|
+
actionName: behavior.actionName,
|
|
152
|
+
sourceAction: behavior.sourceAction,
|
|
153
|
+
},
|
|
154
|
+
received: candidates.map((f) => (f.kind === 'frame' ? f.parsed : f)),
|
|
155
|
+
message: '`_ggui:contract-error` frame observed but payload did not match the expected code/tool/action triple.',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { kind: 'pass' };
|
|
159
|
+
}
|
|
160
|
+
function matchStreamUpdate(behavior, frames) {
|
|
161
|
+
const match = frames.find((f) => f.kind === 'frame' &&
|
|
162
|
+
f.parsed['type'] === 'stream' &&
|
|
163
|
+
isRecord(f.parsed['payload']) &&
|
|
164
|
+
f.parsed['payload']['channel'] === behavior.channel &&
|
|
165
|
+
deepEqual(f.parsed['payload']['value'], behavior.value));
|
|
166
|
+
if (match === undefined) {
|
|
167
|
+
return {
|
|
168
|
+
kind: 'fail',
|
|
169
|
+
expected: { channel: behavior.channel, value: behavior.value },
|
|
170
|
+
received: frames
|
|
171
|
+
.filter((f) => f.kind === 'frame' && f.parsed['type'] === 'stream')
|
|
172
|
+
.map((f) => (f.kind === 'frame' ? f.parsed['payload'] : f)),
|
|
173
|
+
message: `expected a \`stream\` frame on channel '${behavior.channel}' with the declared value shape; none matched.`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return { kind: 'pass' };
|
|
177
|
+
}
|
|
178
|
+
function matchNoOp(behavior, frames) {
|
|
179
|
+
// Exclude the initial `ack` (subscribe always produces one) — the
|
|
180
|
+
// no-op claim is about frames observed AFTER input dispatch, which
|
|
181
|
+
// the runner collects starting post-subscribe. If the runner is
|
|
182
|
+
// correctly collecting post-input frames, `ack` never appears here
|
|
183
|
+
// anyway, but double-filtering guards against caller bugs.
|
|
184
|
+
const interesting = frames.filter((f) => f.kind !== 'frame' || f.parsed['type'] !== 'ack');
|
|
185
|
+
if (interesting.length > 0) {
|
|
186
|
+
return {
|
|
187
|
+
kind: 'fail',
|
|
188
|
+
expected: { frames: [] },
|
|
189
|
+
received: interesting.map((f) => (f.kind === 'frame' ? f.parsed : f)),
|
|
190
|
+
message: `no-op behavior expected silence after input dispatch; observed ${interesting.length} frame(s). Reason given: ${behavior.reason}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return { kind: 'pass' };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Match an observability-event behavior against WS evidence the
|
|
197
|
+
* protocol-and-contract bar mandates a conformant host mirror-emit on.
|
|
198
|
+
*
|
|
199
|
+
* Two arms are matchable on pure WS today; everything else returns
|
|
200
|
+
* `unmatchable-on-ws` with a sharp reason — see the file-level
|
|
201
|
+
* docstring for the rationale.
|
|
202
|
+
*/
|
|
203
|
+
function matchObservabilityEvent(behavior, frames) {
|
|
204
|
+
const { event } = behavior;
|
|
205
|
+
if (event.kind === 'wired-tool-invoked') {
|
|
206
|
+
return matchWiredToolInvoked(event, frames);
|
|
207
|
+
}
|
|
208
|
+
if (event.kind === 'contract-error-emitted') {
|
|
209
|
+
return matchContractErrorEmitted(event, frames);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
kind: 'unmatchable-on-ws',
|
|
213
|
+
reason: `observability-event of kind '${event.kind}' is a Path-B (browser-host) claim — not grounded in WS-observable evidence by the kit's matcher. Drive via a Path-B browser-host harness (today: \`page.exposeBinding\` in spec-side dispatch; future: packaged Phase-3.2 adapter inside the kit).`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* `wired-tool-invoked` — the WS evidence is a `stream` frame on the
|
|
218
|
+
* canonical `_ggui:wired-tool-invoked` channel carrying
|
|
219
|
+
* `value: {toolName, actionName?}`. The reference server's
|
|
220
|
+
* `action-router.ts::dispatchAction()` emits this after a successful
|
|
221
|
+
* handler resolution; the bar mandates equivalent WS evidence from any
|
|
222
|
+
* conformant host on the wired-tool-invoked path.
|
|
223
|
+
*/
|
|
224
|
+
function matchWiredToolInvoked(event, frames) {
|
|
225
|
+
if (event.toolName === undefined) {
|
|
226
|
+
return {
|
|
227
|
+
kind: 'fail',
|
|
228
|
+
expected: { kind: 'wired-tool-invoked', toolName: '<required>' },
|
|
229
|
+
received: { event },
|
|
230
|
+
message: 'observability-event of kind `wired-tool-invoked` MUST declare `toolName` for the matcher to assert against; fixture is under-specified.',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const candidates = frames.filter((f) => f.kind === 'frame' &&
|
|
234
|
+
f.parsed['type'] === 'stream' &&
|
|
235
|
+
isRecord(f.parsed['payload']) &&
|
|
236
|
+
f.parsed['payload']['channel'] === '_ggui:wired-tool-invoked');
|
|
237
|
+
if (candidates.length === 0) {
|
|
238
|
+
return {
|
|
239
|
+
kind: 'fail',
|
|
240
|
+
expected: {
|
|
241
|
+
channel: '_ggui:wired-tool-invoked',
|
|
242
|
+
value: { toolName: event.toolName, actionName: event.actionName },
|
|
243
|
+
},
|
|
244
|
+
received: frames.map((f) => (f.kind === 'frame' ? f.parsed : f)),
|
|
245
|
+
message: 'expected a `stream` frame on `_ggui:wired-tool-invoked` (the WS-observable evidence the protocol bar mandates a conformant host mirror-emit `wired-tool-invoked` on); none observed.',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const match = candidates.find((f) => {
|
|
249
|
+
if (f.kind !== 'frame')
|
|
250
|
+
return false;
|
|
251
|
+
const payload = f.parsed['payload'];
|
|
252
|
+
if (!isRecord(payload))
|
|
253
|
+
return false;
|
|
254
|
+
const value = payload['value'];
|
|
255
|
+
if (!isRecord(value))
|
|
256
|
+
return false;
|
|
257
|
+
if (value['toolName'] !== event.toolName)
|
|
258
|
+
return false;
|
|
259
|
+
if (event.actionName !== undefined &&
|
|
260
|
+
value['actionName'] !== event.actionName) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
});
|
|
265
|
+
if (match === undefined) {
|
|
266
|
+
return {
|
|
267
|
+
kind: 'fail',
|
|
268
|
+
expected: {
|
|
269
|
+
channel: '_ggui:wired-tool-invoked',
|
|
270
|
+
value: { toolName: event.toolName, actionName: event.actionName },
|
|
271
|
+
},
|
|
272
|
+
received: candidates.map((f) => (f.kind === 'frame' ? f.parsed : f)),
|
|
273
|
+
message: '`_ggui:wired-tool-invoked` frame observed but value did not match the expected toolName/actionName.',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return { kind: 'pass' };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* `contract-error-emitted` — the WS evidence is the canonical SPEC
|
|
280
|
+
* §4.4 `_ggui:contract-error` envelope itself. Per the bar, every such
|
|
281
|
+
* envelope MUST be mirrored as a `contract-error-emitted` observability
|
|
282
|
+
* event on the host side; asserting on the WS frame is asserting on
|
|
283
|
+
* the cause the bar makes load-bearing for the mirror.
|
|
284
|
+
*/
|
|
285
|
+
function matchContractErrorEmitted(event, frames) {
|
|
286
|
+
if (event.code === undefined || event.toolName === undefined) {
|
|
287
|
+
return {
|
|
288
|
+
kind: 'fail',
|
|
289
|
+
expected: {
|
|
290
|
+
kind: 'contract-error-emitted',
|
|
291
|
+
code: event.code ?? '<required>',
|
|
292
|
+
toolName: event.toolName ?? '<required>',
|
|
293
|
+
},
|
|
294
|
+
received: { event },
|
|
295
|
+
message: 'observability-event of kind `contract-error-emitted` MUST declare both `code` and `toolName` for the matcher to assert against; fixture is under-specified.',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const candidates = frames.filter((f) => f.kind === 'frame' &&
|
|
299
|
+
f.parsed['type'] === 'stream' &&
|
|
300
|
+
isRecord(f.parsed['payload']) &&
|
|
301
|
+
f.parsed['payload']['channel'] === '_ggui:contract-error');
|
|
302
|
+
if (candidates.length === 0) {
|
|
303
|
+
return {
|
|
304
|
+
kind: 'fail',
|
|
305
|
+
expected: {
|
|
306
|
+
channel: '_ggui:contract-error',
|
|
307
|
+
code: event.code,
|
|
308
|
+
toolName: event.toolName,
|
|
309
|
+
},
|
|
310
|
+
received: frames.map((f) => (f.kind === 'frame' ? f.parsed : f)),
|
|
311
|
+
message: 'expected a `stream` frame on `_ggui:contract-error` (the WS-observable evidence the protocol bar mandates a conformant host mirror-emit `contract-error-emitted` on); none observed.',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
// Same canonical-shape reader as `matchContractError` — code is
|
|
315
|
+
// nested under `payload.value.error.code`, toolName at the top of
|
|
316
|
+
// `payload.value`.
|
|
317
|
+
const match = candidates.find((f) => {
|
|
318
|
+
if (f.kind !== 'frame')
|
|
319
|
+
return false;
|
|
320
|
+
const payload = f.parsed['payload'];
|
|
321
|
+
if (!isRecord(payload))
|
|
322
|
+
return false;
|
|
323
|
+
const value = payload['value'];
|
|
324
|
+
if (!isRecord(value))
|
|
325
|
+
return false;
|
|
326
|
+
const error = value['error'];
|
|
327
|
+
if (!isRecord(error))
|
|
328
|
+
return false;
|
|
329
|
+
if (error['code'] !== event.code)
|
|
330
|
+
return false;
|
|
331
|
+
if (value['toolName'] !== event.toolName)
|
|
332
|
+
return false;
|
|
333
|
+
if (event.actionName !== undefined &&
|
|
334
|
+
value['actionName'] !== event.actionName) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
return true;
|
|
338
|
+
});
|
|
339
|
+
if (match === undefined) {
|
|
340
|
+
return {
|
|
341
|
+
kind: 'fail',
|
|
342
|
+
expected: {
|
|
343
|
+
channel: '_ggui:contract-error',
|
|
344
|
+
code: event.code,
|
|
345
|
+
toolName: event.toolName,
|
|
346
|
+
actionName: event.actionName,
|
|
347
|
+
},
|
|
348
|
+
received: candidates.map((f) => (f.kind === 'frame' ? f.parsed : f)),
|
|
349
|
+
message: '`_ggui:contract-error` frame observed but payload did not match the expected code/tool/action triple for the mandated mirror-emission.',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return { kind: 'pass' };
|
|
353
|
+
}
|
|
354
|
+
// =============================================================================
|
|
355
|
+
// Helpers
|
|
356
|
+
// =============================================================================
|
|
357
|
+
function isRecord(value) {
|
|
358
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
359
|
+
}
|
|
360
|
+
function deepEqual(a, b) {
|
|
361
|
+
if (a === b)
|
|
362
|
+
return true;
|
|
363
|
+
if (typeof a !== typeof b)
|
|
364
|
+
return false;
|
|
365
|
+
if (a === null || b === null)
|
|
366
|
+
return false;
|
|
367
|
+
if (typeof a !== 'object')
|
|
368
|
+
return false;
|
|
369
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
370
|
+
return false;
|
|
371
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
372
|
+
if (a.length !== b.length)
|
|
373
|
+
return false;
|
|
374
|
+
return a.every((item, i) => deepEqual(item, b[i]));
|
|
375
|
+
}
|
|
376
|
+
const aObj = a;
|
|
377
|
+
const bObj = b;
|
|
378
|
+
const aKeys = Object.keys(aObj).sort();
|
|
379
|
+
const bKeys = Object.keys(bObj).sort();
|
|
380
|
+
if (aKeys.length !== bKeys.length)
|
|
381
|
+
return false;
|
|
382
|
+
if (!aKeys.every((k, i) => k === bKeys[i]))
|
|
383
|
+
return false;
|
|
384
|
+
return aKeys.every((k) => deepEqual(aObj[k], bObj[k]));
|
|
385
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gate-export-not-registered",
|
|
3
|
+
"description": "The contract's `clientCapabilities.gadgets` references the export `useDoorDashCheckout` under `@acme/checkout` — a (package, export) pair absent from every descriptor in the app's registered `App.gadgets` catalog. The push-time gadget gate (SPEC §7.7.2 — `assertGadgetsRegistered`) MUST reject with code `gadget_not_registered`: the most-fundamental miss class, where the export name itself is unknown.",
|
|
4
|
+
"contract": {
|
|
5
|
+
"clientCapabilities": {
|
|
6
|
+
"gadgets": {
|
|
7
|
+
"@acme/checkout": { "useDoorDashCheckout": {} }
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"appGadgets": [
|
|
12
|
+
{
|
|
13
|
+
"package": "@ggui-samples/leaflet",
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"exports": [{ "hook": "useLeafletMap" }]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"expect": { "outcome": "reject", "code": "gadget_not_registered" }
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gate-no-gadgets-accept",
|
|
3
|
+
"description": "A contract that declares no `clientCapabilities.gadgets` at all. The gate has nothing to check — a contract that uses no gadgets references no registry membership — so it MUST accept unconditionally, even against a non-empty app catalog. Guards against a gate that over-fires on the common gadget-free contract.",
|
|
4
|
+
"contract": {},
|
|
5
|
+
"appGadgets": [
|
|
6
|
+
{
|
|
7
|
+
"package": "@ggui-samples/leaflet",
|
|
8
|
+
"version": "2.3.1",
|
|
9
|
+
"exports": [{ "hook": "useLeafletMap" }]
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"expect": { "outcome": "accept" }
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gate-package-mismatch",
|
|
3
|
+
"description": "The contract references `useLeafletMap` under `@acme/maps`, but the app registers an export of that exact name only under `@ggui-samples/leaflet`. The export NAME resolves — so this is not a typo or a hallucinated capability — but the (package, export) identity pair does not. The gate MUST reject with code `gadget_package_mismatch`: the lower-priority category, which fires only when no `gadget_not_registered` miss outranks it on the same push.",
|
|
4
|
+
"contract": {
|
|
5
|
+
"clientCapabilities": {
|
|
6
|
+
"gadgets": {
|
|
7
|
+
"@acme/maps": { "useLeafletMap": {} }
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"appGadgets": [
|
|
12
|
+
{
|
|
13
|
+
"package": "@ggui-samples/leaflet",
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"exports": [{ "hook": "useLeafletMap" }]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"expect": { "outcome": "reject", "code": "gadget_package_mismatch" }
|
|
19
|
+
}
|
package/dist/registration-conformance/cases/gate-priority-not-registered-outranks-mismatch.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gate-priority-not-registered-outranks-mismatch",
|
|
3
|
+
"description": "A single push that references BOTH an unknown export (`useDoorDashCheckout` under `@acme/checkout`) AND a package-mismatched export (`useLeafletMap` under `@acme/maps`, registered only under `@ggui-samples/leaflet`). The gate classifies every miss and MUST throw the most-fundamental category FIRST — `gadget_not_registered` outranks `gadget_package_mismatch`, because an author who got the export name wrong cannot act on a 'wrong package' message. The lower-priority package miss rides the rejection's `secondary` so the author corrects both in one round trip; the catalog asserts the primary outcome code.",
|
|
4
|
+
"contract": {
|
|
5
|
+
"clientCapabilities": {
|
|
6
|
+
"gadgets": {
|
|
7
|
+
"@acme/checkout": { "useDoorDashCheckout": {} },
|
|
8
|
+
"@acme/maps": { "useLeafletMap": {} }
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"appGadgets": [
|
|
13
|
+
{
|
|
14
|
+
"package": "@ggui-samples/leaflet",
|
|
15
|
+
"version": "1.0.0",
|
|
16
|
+
"exports": [{ "hook": "useLeafletMap" }]
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"expect": { "outcome": "reject", "code": "gadget_not_registered" }
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gate-registered-resolves-accept",
|
|
3
|
+
"description": "The positive baseline. The contract references a hook export (`useLeafletMap` from `@ggui-samples/leaflet`) and a package that exposes both a component and a hook export (`RevenueChart` + `useChartTheme` from `@ggui-samples/charts`). Every `(package, export)` pair the contract names resolves to a registered export by identity. The gate MUST accept — no reject, regardless of export kind.",
|
|
4
|
+
"contract": {
|
|
5
|
+
"clientCapabilities": {
|
|
6
|
+
"gadgets": {
|
|
7
|
+
"@ggui-samples/leaflet": { "useLeafletMap": {} },
|
|
8
|
+
"@ggui-samples/charts": { "RevenueChart": {}, "useChartTheme": {} }
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"appGadgets": [
|
|
13
|
+
{
|
|
14
|
+
"package": "@ggui-samples/leaflet",
|
|
15
|
+
"version": "2.3.1",
|
|
16
|
+
"exports": [{ "hook": "useLeafletMap" }]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"package": "@ggui-samples/charts",
|
|
20
|
+
"version": "1.0.0",
|
|
21
|
+
"exports": [{ "component": "RevenueChart" }, { "hook": "useChartTheme" }]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"expect": { "outcome": "accept" }
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gate-stdlib-hook-accept",
|
|
3
|
+
"description": "The contract references `useGeolocation` under `@ggui-ai/gadgets` — one of the seven first-party browser-capability hooks. The app's catalog is the `STDLIB_GADGETS` baseline: the single `@ggui-ai/gadgets` package descriptor whose `exports` carry all seven stdlib hooks. The `(package, export)` pair resolves, so the gate MUST accept. (When a real app row carries no explicit catalog, the push handler substitutes `STDLIB_GADGETS` before calling the gate — this case provides that baseline directly.)",
|
|
4
|
+
"contract": {
|
|
5
|
+
"clientCapabilities": {
|
|
6
|
+
"gadgets": {
|
|
7
|
+
"@ggui-ai/gadgets": { "useGeolocation": {} }
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"appGadgets": [
|
|
12
|
+
{
|
|
13
|
+
"package": "@ggui-ai/gadgets",
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"exports": [
|
|
16
|
+
{ "hook": "useCamera" },
|
|
17
|
+
{ "hook": "useClipboardPaste" },
|
|
18
|
+
{ "hook": "useClipboardWrite" },
|
|
19
|
+
{ "hook": "useFilePicker" },
|
|
20
|
+
{ "hook": "useGeolocation" },
|
|
21
|
+
{ "hook": "useMicrophone" },
|
|
22
|
+
{ "hook": "useNotifications" }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"expect": { "outcome": "accept" }
|
|
27
|
+
}
|