@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,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gate-typo-classifies-as-not-registered",
|
|
3
|
+
"description": "The contract references `useGeoLocation` (capital L) under `@ggui-ai/gadgets` — a near-miss typo of the registered `useGeolocation`. A typo'd export name is still an export name absent from every registered package, so the gate MUST classify it as `gadget_not_registered`, NOT `gadget_package_mismatch` (the name does not resolve under any package). The reference gate additionally attaches a did-you-mean suggestion when a registered name is within Levenshtein distance < 3; the suggestion is a message-level aid and does not change the outcome code this catalog asserts.",
|
|
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": "reject", "code": "gadget_not_registered" }
|
|
27
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration-conformance catalog — accept / reject cases for the
|
|
3
|
+
* gadget push-time registry gate (SPEC §7.7.2 — `assertGadgetsRegistered`).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a pure-function catalog, not a behavioral fixture
|
|
6
|
+
*
|
|
7
|
+
* The push-time gadget gate answers a pure question: given a contract
|
|
8
|
+
* and the app's registered `App.gadgets` catalog, does every
|
|
9
|
+
* `(package, export)` the contract references resolve — and if not,
|
|
10
|
+
* which precise reject code applies? There is no session, no
|
|
11
|
+
* transport, no wire frame in that question — it is a deterministic
|
|
12
|
+
* classification, exactly like the `clientCapabilities` schema check
|
|
13
|
+
* in `../schema-conformance`.
|
|
14
|
+
*
|
|
15
|
+
* Modeling it as a WebSocket behavioral fixture would mean inventing a
|
|
16
|
+
* wire frame the real protocol never emits (the gate runs at MCP-push
|
|
17
|
+
* time, before any live channel exists) — the dishonest modeling the
|
|
18
|
+
* kit's Path-A / Path-B partition exists to forbid. So the gate gets
|
|
19
|
+
* the same treatment as the wire schema: a catalog of cases graded
|
|
20
|
+
* against a caller-supplied gate function.
|
|
21
|
+
*
|
|
22
|
+
* ## Polyglot, decoupled
|
|
23
|
+
*
|
|
24
|
+
* Each case ships as raw JSON under `./cases/`. {@link runRegistrationConformance}
|
|
25
|
+
* takes the gate as a callback — the kit never imports a concrete gate
|
|
26
|
+
* implementation here, so adopters drive their OWN gate. The reference
|
|
27
|
+
* binding (`./registration-conformance.test.ts`) wraps
|
|
28
|
+
* `@ggui-ai/mcp-server-handlers`'s `assertGadgetsRegistered`.
|
|
29
|
+
*
|
|
30
|
+
* The `DataContract` / `GadgetDescriptor` types ARE imported (type-only)
|
|
31
|
+
* from `@ggui-ai/protocol` — unlike the behavioral fixture vocabulary
|
|
32
|
+
* in `../types.ts`, these are the gate's actual input contract, not a
|
|
33
|
+
* frozen authored union; the runner's `gate` signature MUST match the
|
|
34
|
+
* real function for the reference binding to be cast-free.
|
|
35
|
+
*/
|
|
36
|
+
import type { DataContract, GadgetDescriptor } from '@ggui-ai/protocol';
|
|
37
|
+
/**
|
|
38
|
+
* The precise reject codes the gadget gate classifies a miss into.
|
|
39
|
+
* Mirrors `GadgetGateErrorCode`'s registry-gate arms in
|
|
40
|
+
* `@ggui-ai/mcp-server-handlers`. Extensibly-closed `(string & {})`
|
|
41
|
+
* tail so a later protocol revision's code rides through without a kit
|
|
42
|
+
* bump — same discipline as `ContractErrorCode` in `../types.ts`.
|
|
43
|
+
*/
|
|
44
|
+
export type GadgetGateRejectCode = 'gadget_not_registered' | 'gadget_package_mismatch' | (string & {});
|
|
45
|
+
/**
|
|
46
|
+
* The outcome of running the gate against one `(contract, appGadgets)`
|
|
47
|
+
* pair. A conformant gate either accepts the push or rejects it with
|
|
48
|
+
* one classified code.
|
|
49
|
+
*/
|
|
50
|
+
export type GateOutcome = {
|
|
51
|
+
readonly outcome: 'accept';
|
|
52
|
+
} | {
|
|
53
|
+
readonly outcome: 'reject';
|
|
54
|
+
readonly code: GadgetGateRejectCode;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* One registration-conformance case. Authored as JSON under `./cases/`,
|
|
58
|
+
* consumed via {@link gadgetRegistrationCases}, graded by
|
|
59
|
+
* {@link runRegistrationConformance}.
|
|
60
|
+
*
|
|
61
|
+
* The shape IS the public API — additive changes only, mirroring the
|
|
62
|
+
* `TestCase` / `SchemaConformanceCase` discipline.
|
|
63
|
+
*/
|
|
64
|
+
export interface RegistrationConformanceCase {
|
|
65
|
+
/** Unique case name. MUST match the JSON filename without `.json`. */
|
|
66
|
+
readonly name: string;
|
|
67
|
+
/** What gate obligation this case proves. */
|
|
68
|
+
readonly description: string;
|
|
69
|
+
/** The contract whose `clientCapabilities.gadgets` the gate walks. */
|
|
70
|
+
readonly contract: DataContract;
|
|
71
|
+
/** The app's registered gadget catalog the gate resolves against. */
|
|
72
|
+
readonly appGadgets: readonly GadgetDescriptor[];
|
|
73
|
+
/** The outcome a spec-conformant gate MUST produce. */
|
|
74
|
+
readonly expect: GateOutcome;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Every gadget registration-gate case the kit ships, in deterministic
|
|
78
|
+
* order — rejects first (grouped by the obligation they probe), then
|
|
79
|
+
* accepts.
|
|
80
|
+
*/
|
|
81
|
+
export declare const gadgetRegistrationCases: readonly RegistrationConformanceCase[];
|
|
82
|
+
/** One case the gate under test graded wrong. */
|
|
83
|
+
export interface RegistrationConformanceMismatch {
|
|
84
|
+
readonly name: string;
|
|
85
|
+
/** The outcome the catalog says a conformant gate MUST produce. */
|
|
86
|
+
readonly expected: GateOutcome;
|
|
87
|
+
/** The outcome the gate under test actually produced. */
|
|
88
|
+
readonly actual: GateOutcome;
|
|
89
|
+
}
|
|
90
|
+
/** Outcome of grading a gate against the catalog. */
|
|
91
|
+
export interface RegistrationConformanceResult {
|
|
92
|
+
/** Names of cases the gate graded correctly. */
|
|
93
|
+
readonly passed: readonly string[];
|
|
94
|
+
/** Cases the gate graded wrong — empty iff fully conformant. */
|
|
95
|
+
readonly failed: readonly RegistrationConformanceMismatch[];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Grade a gadget registration gate against the catalog.
|
|
99
|
+
*
|
|
100
|
+
* `gate` MUST be a pure classification of `(contract, appGadgets)` —
|
|
101
|
+
* return `{outcome:'accept'}` when every referenced `(package, export)`
|
|
102
|
+
* resolves, or `{outcome:'reject', code}` with the precise reject code
|
|
103
|
+
* otherwise. The kit deliberately does NOT import a concrete gate —
|
|
104
|
+
* adopters pass their own (the reference binding wraps
|
|
105
|
+
* `assertGadgetsRegistered`; see `./registration-conformance.test.ts`).
|
|
106
|
+
* A conformant gate produces an empty `failed` array.
|
|
107
|
+
*
|
|
108
|
+
* `gate` is invoked exactly once per case, in {@link gadgetRegistrationCases}
|
|
109
|
+
* order; it MUST be pure and MUST NOT throw (a throwing gate is itself
|
|
110
|
+
* non-conformant — wrap it so a classified rejection becomes a
|
|
111
|
+
* returned `GateOutcome`).
|
|
112
|
+
*/
|
|
113
|
+
export declare function runRegistrationConformance(gate: (contract: DataContract, appGadgets: readonly GadgetDescriptor[]) => GateOutcome): RegistrationConformanceResult;
|
|
114
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/registration-conformance/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAUxE;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAC5B,uBAAuB,GACvB,yBAAyB,GACzB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAExE;;;;;;;GAOG;AACH,MAAM,WAAW,2BAA2B;IAC1C,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,UAAU,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACjD,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,SAAS,2BAA2B,EAQzE,CAAC;AAEF,iDAAiD;AACjD,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED,qDAAqD;AACrD,MAAM,WAAW,6BAA6B;IAC5C,gDAAgD;IAChD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,SAAS,+BAA+B,EAAE,CAAC;CAC7D;AAUD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,CACJ,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,SAAS,gBAAgB,EAAE,KACpC,WAAW,GACf,6BAA6B,CAgB/B"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import gateExportNotRegistered from './cases/gate-export-not-registered.json' with { type: 'json' };
|
|
2
|
+
import gateNoGadgetsAccept from './cases/gate-no-gadgets-accept.json' with { type: 'json' };
|
|
3
|
+
import gatePackageMismatch from './cases/gate-package-mismatch.json' with { type: 'json' };
|
|
4
|
+
import gatePriorityNotRegisteredOutranksMismatch from './cases/gate-priority-not-registered-outranks-mismatch.json' with { type: 'json' };
|
|
5
|
+
import gateRegisteredResolvesAccept from './cases/gate-registered-resolves-accept.json' with { type: 'json' };
|
|
6
|
+
import gateStdlibHookAccept from './cases/gate-stdlib-hook-accept.json' with { type: 'json' };
|
|
7
|
+
import gateTypoClassifiesAsNotRegistered from './cases/gate-typo-classifies-as-not-registered.json' with { type: 'json' };
|
|
8
|
+
/**
|
|
9
|
+
* Every gadget registration-gate case the kit ships, in deterministic
|
|
10
|
+
* order — rejects first (grouped by the obligation they probe), then
|
|
11
|
+
* accepts.
|
|
12
|
+
*/
|
|
13
|
+
export const gadgetRegistrationCases = [
|
|
14
|
+
gateExportNotRegistered,
|
|
15
|
+
gatePackageMismatch,
|
|
16
|
+
gatePriorityNotRegisteredOutranksMismatch,
|
|
17
|
+
gateTypoClassifiesAsNotRegistered,
|
|
18
|
+
gateStdlibHookAccept,
|
|
19
|
+
gateRegisteredResolvesAccept,
|
|
20
|
+
gateNoGadgetsAccept,
|
|
21
|
+
];
|
|
22
|
+
function gateOutcomeEquals(a, b) {
|
|
23
|
+
if (a.outcome !== b.outcome)
|
|
24
|
+
return false;
|
|
25
|
+
if (a.outcome === 'reject' && b.outcome === 'reject') {
|
|
26
|
+
return a.code === b.code;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Grade a gadget registration gate against the catalog.
|
|
32
|
+
*
|
|
33
|
+
* `gate` MUST be a pure classification of `(contract, appGadgets)` —
|
|
34
|
+
* return `{outcome:'accept'}` when every referenced `(package, export)`
|
|
35
|
+
* resolves, or `{outcome:'reject', code}` with the precise reject code
|
|
36
|
+
* otherwise. The kit deliberately does NOT import a concrete gate —
|
|
37
|
+
* adopters pass their own (the reference binding wraps
|
|
38
|
+
* `assertGadgetsRegistered`; see `./registration-conformance.test.ts`).
|
|
39
|
+
* A conformant gate produces an empty `failed` array.
|
|
40
|
+
*
|
|
41
|
+
* `gate` is invoked exactly once per case, in {@link gadgetRegistrationCases}
|
|
42
|
+
* order; it MUST be pure and MUST NOT throw (a throwing gate is itself
|
|
43
|
+
* non-conformant — wrap it so a classified rejection becomes a
|
|
44
|
+
* returned `GateOutcome`).
|
|
45
|
+
*/
|
|
46
|
+
export function runRegistrationConformance(gate) {
|
|
47
|
+
const passed = [];
|
|
48
|
+
const failed = [];
|
|
49
|
+
for (const testCase of gadgetRegistrationCases) {
|
|
50
|
+
const actual = gate(testCase.contract, testCase.appGadgets);
|
|
51
|
+
if (gateOutcomeEquals(actual, testCase.expect)) {
|
|
52
|
+
passed.push(testCase.name);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
failed.push({
|
|
56
|
+
name: testCase.name,
|
|
57
|
+
expected: testCase.expect,
|
|
58
|
+
actual,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { passed, failed };
|
|
63
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ConformanceFailure, ConformanceReporter, ConformanceResult, SkippedFixture } from './run-conformance.js';
|
|
2
|
+
export interface DefaultReporterOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Destination for all output. Defaults to
|
|
5
|
+
* `process.stdout.write`-bound. Tests swap this for capture.
|
|
6
|
+
*/
|
|
7
|
+
readonly write?: (line: string) => void;
|
|
8
|
+
/**
|
|
9
|
+
* If `true`, emit one line per fixture as it completes (streaming).
|
|
10
|
+
* If `false`, only emit the final summary block. Default `true`.
|
|
11
|
+
*/
|
|
12
|
+
readonly streaming?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build a {@link ConformanceReporter} that writes the bar-scorecard
|
|
16
|
+
* report. The return value plugs directly into
|
|
17
|
+
* `runConformance({reporter: createDefaultReporter()})`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createDefaultReporter(options?: DefaultReporterOptions): ConformanceReporter;
|
|
20
|
+
export declare function formatScorecard(result: ConformanceResult): string;
|
|
21
|
+
export declare function formatSummary(result: ConformanceResult): string;
|
|
22
|
+
/**
|
|
23
|
+
* Emit the full set of failure messages — for CLI `--verbose` mode +
|
|
24
|
+
* programmatic consumers that want a post-run diagnostic dump.
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatFailures(failures: readonly ConformanceFailure[]): string;
|
|
27
|
+
/**
|
|
28
|
+
* Emit the full set of skip reasons — useful for `--verbose` mode to
|
|
29
|
+
* see which directives an implementation hasn't wired yet.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatSkips(skipped: readonly SkippedFixture[]): string;
|
|
32
|
+
//# sourceMappingURL=reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACf,MAAM,sBAAsB,CAAC;AAM9B,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,sBAA2B,GACnC,mBAAmB,CA0CrB;AAMD,wBAAgB,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAmBjE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAW/D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,GAAG,MAAM,CAW9E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAStE"}
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default reporter implementation for `runConformance()`.
|
|
3
|
+
*
|
|
4
|
+
* Prints a scorecard-style report to stdout as fixtures complete,
|
|
5
|
+
* then a summary block at the end grouped by contract slug. The
|
|
6
|
+
* output groups results by the protocol/contract criterion each
|
|
7
|
+
* fixture exercises so adopters can see at a glance which obligations
|
|
8
|
+
* their implementation satisfies.
|
|
9
|
+
*
|
|
10
|
+
* The default reporter is `process.stdout`-bound. Third-party
|
|
11
|
+
* consumers wanting programmatic access should implement
|
|
12
|
+
* {@link ConformanceReporter} directly against their own sink (JSON,
|
|
13
|
+
* vitest, CI annotations, etc.) and pass it via `runConformance()`'s
|
|
14
|
+
* `reporter` config.
|
|
15
|
+
*/
|
|
16
|
+
import { fixturesByContract } from './fixtures/index.js';
|
|
17
|
+
/**
|
|
18
|
+
* Build a {@link ConformanceReporter} that writes the bar-scorecard
|
|
19
|
+
* report. The return value plugs directly into
|
|
20
|
+
* `runConformance({reporter: createDefaultReporter()})`.
|
|
21
|
+
*/
|
|
22
|
+
export function createDefaultReporter(options = {}) {
|
|
23
|
+
const write = options.write ??
|
|
24
|
+
((line) => {
|
|
25
|
+
process.stdout.write(line);
|
|
26
|
+
if (!line.endsWith('\n'))
|
|
27
|
+
process.stdout.write('\n');
|
|
28
|
+
});
|
|
29
|
+
const streaming = options.streaming ?? true;
|
|
30
|
+
return {
|
|
31
|
+
onStart(total) {
|
|
32
|
+
if (!streaming)
|
|
33
|
+
return;
|
|
34
|
+
write(`ggui protocol conformance — driving ${total} fixture(s) against the implementation under test`);
|
|
35
|
+
write(RULE);
|
|
36
|
+
},
|
|
37
|
+
onFixturePass(name, elapsedMs) {
|
|
38
|
+
if (!streaming)
|
|
39
|
+
return;
|
|
40
|
+
write(` ${MARK_PASS} ${padRight(name, 48)} ${elapsedMs}ms`);
|
|
41
|
+
},
|
|
42
|
+
onFixtureFail(failure) {
|
|
43
|
+
if (!streaming)
|
|
44
|
+
return;
|
|
45
|
+
write(` ${MARK_FAIL} ${padRight(failure.name, 48)} ${failure.criterion}`);
|
|
46
|
+
write(` → ${failure.message}`);
|
|
47
|
+
},
|
|
48
|
+
onFixtureSkip(name, reason) {
|
|
49
|
+
if (!streaming)
|
|
50
|
+
return;
|
|
51
|
+
const shortReason = reason.length > 80 ? `${reason.slice(0, 77)}…` : reason;
|
|
52
|
+
write(` ${MARK_SKIP} ${padRight(name, 48)} ${shortReason}`);
|
|
53
|
+
},
|
|
54
|
+
onTeardownWarning(name, message) {
|
|
55
|
+
if (!streaming)
|
|
56
|
+
return;
|
|
57
|
+
write(` ${MARK_WARN} ${padRight(name, 48)} teardown: ${message}`);
|
|
58
|
+
},
|
|
59
|
+
onComplete(result) {
|
|
60
|
+
write(RULE);
|
|
61
|
+
write(formatScorecard(result));
|
|
62
|
+
write(RULE);
|
|
63
|
+
write(formatSummary(result));
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Scorecard formatting
|
|
69
|
+
// =============================================================================
|
|
70
|
+
export function formatScorecard(result) {
|
|
71
|
+
// Group fixtures by contract so operators see pass rates per
|
|
72
|
+
// criterion, mirroring `fixturesByContract`'s classification.
|
|
73
|
+
const lines = [];
|
|
74
|
+
for (const [slug, fixtures] of Object.entries(fixturesByContract)) {
|
|
75
|
+
const names = new Set(fixtures.map((f) => f.name));
|
|
76
|
+
const passedCount = result.passed.filter((n) => names.has(n)).length;
|
|
77
|
+
const failedCount = result.failed.filter((f) => names.has(f.name)).length;
|
|
78
|
+
const skippedCount = result.skipped.filter((s) => names.has(s.name)).length;
|
|
79
|
+
const total = passedCount + failedCount + skippedCount;
|
|
80
|
+
if (total === 0)
|
|
81
|
+
continue;
|
|
82
|
+
const tone = failedCount > 0 ? MARK_FAIL : passedCount > 0 ? MARK_PASS : MARK_SKIP;
|
|
83
|
+
lines.push(` ${tone} ${padRight(slug, 38)} ${passedCount}/${total} pass${failedCount > 0 ? ` · ${failedCount} fail` : ''}${skippedCount > 0 ? ` · ${skippedCount} skip` : ''}`);
|
|
84
|
+
}
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
export function formatSummary(result) {
|
|
88
|
+
const passed = result.passed.length;
|
|
89
|
+
const failed = result.failed.length;
|
|
90
|
+
const skipped = result.skipped.length;
|
|
91
|
+
const total = passed + failed + skipped;
|
|
92
|
+
return [
|
|
93
|
+
` Passed: ${String(passed).padStart(3)}`,
|
|
94
|
+
` Failed: ${String(failed).padStart(3)}`,
|
|
95
|
+
` Skipped: ${String(skipped).padStart(3)}`,
|
|
96
|
+
` Total: ${String(total).padStart(3)} (${result.totalMs}ms)`,
|
|
97
|
+
].join('\n');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Emit the full set of failure messages — for CLI `--verbose` mode +
|
|
101
|
+
* programmatic consumers that want a post-run diagnostic dump.
|
|
102
|
+
*/
|
|
103
|
+
export function formatFailures(failures) {
|
|
104
|
+
if (failures.length === 0)
|
|
105
|
+
return '';
|
|
106
|
+
const lines = ['', 'Failures:', ''];
|
|
107
|
+
for (const failure of failures) {
|
|
108
|
+
lines.push(`${MARK_FAIL} ${failure.name} (${failure.criterion})`);
|
|
109
|
+
lines.push(` ${failure.message}`);
|
|
110
|
+
lines.push(` expected: ${safeStringify(failure.expected)}`);
|
|
111
|
+
lines.push(` received: ${safeStringify(failure.received)}`);
|
|
112
|
+
lines.push('');
|
|
113
|
+
}
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Emit the full set of skip reasons — useful for `--verbose` mode to
|
|
118
|
+
* see which directives an implementation hasn't wired yet.
|
|
119
|
+
*/
|
|
120
|
+
export function formatSkips(skipped) {
|
|
121
|
+
if (skipped.length === 0)
|
|
122
|
+
return '';
|
|
123
|
+
const lines = ['', 'Skipped:', ''];
|
|
124
|
+
for (const skip of skipped) {
|
|
125
|
+
lines.push(`${MARK_SKIP} ${skip.name}`);
|
|
126
|
+
lines.push(` ${skip.reason}`);
|
|
127
|
+
lines.push('');
|
|
128
|
+
}
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// Shared glyphs + helpers
|
|
133
|
+
// =============================================================================
|
|
134
|
+
const RULE = '─'.repeat(70);
|
|
135
|
+
const MARK_PASS = 'PASS';
|
|
136
|
+
const MARK_FAIL = 'FAIL';
|
|
137
|
+
const MARK_SKIP = 'SKIP';
|
|
138
|
+
const MARK_WARN = 'WARN';
|
|
139
|
+
function padRight(s, width) {
|
|
140
|
+
if (s.length >= width)
|
|
141
|
+
return s;
|
|
142
|
+
return s + ' '.repeat(width - s.length);
|
|
143
|
+
}
|
|
144
|
+
function safeStringify(value) {
|
|
145
|
+
try {
|
|
146
|
+
return JSON.stringify(value);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return String(value);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resolve-bundle-host-computed",
|
|
3
|
+
"description": "A descriptor with a hostname-only `bundleHost` (no scheme, no path) and no explicit `bundleUrl`. The resolver MUST compute both the bundle and style URLs as `https://<bundleHost>/bundles/<package>/<version>/<file>` — the path layout matching `@ggui-ai/registry-core`'s `BundleStorage` so a bundle published to one registry resolves cleanly from the other.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"package": "@acme/leaflet-wrapper",
|
|
6
|
+
"version": "2.3.1",
|
|
7
|
+
"bundleHost": "cdn.example.com"
|
|
8
|
+
},
|
|
9
|
+
"expect": {
|
|
10
|
+
"bundleUrl": "https://cdn.example.com/bundles/@acme/leaflet-wrapper/2.3.1/bundle.js",
|
|
11
|
+
"styleUrl": "https://cdn.example.com/bundles/@acme/leaflet-wrapper/2.3.1/style.css"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resolve-default-host",
|
|
3
|
+
"description": "A descriptor with `package` + `version` but neither `bundleHost` nor `bundleUrl`. The resolver MUST fall through to the spec default host `registry.ggui.ai` — first-party and hosted-registry publishes resolve with zero operator config.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"package": "@acme/leaflet-wrapper",
|
|
6
|
+
"version": "2.3.1"
|
|
7
|
+
},
|
|
8
|
+
"expect": {
|
|
9
|
+
"bundleUrl": "https://registry.ggui.ai/bundles/@acme/leaflet-wrapper/2.3.1/bundle.js",
|
|
10
|
+
"styleUrl": "https://registry.ggui.ai/bundles/@acme/leaflet-wrapper/2.3.1/style.css"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resolve-explicit-bundle-and-style-url",
|
|
3
|
+
"description": "Both `bundleUrl` and `styleUrl` declared explicitly. Each is a full-URL escape hatch and MUST be used verbatim — no hostname computation, no path assembly.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"package": "@acme/leaflet-wrapper",
|
|
6
|
+
"version": "2.3.1",
|
|
7
|
+
"bundleUrl": "https://esm.sh/@acme/leaflet-wrapper@2.3.1/bundle.js",
|
|
8
|
+
"styleUrl": "https://esm.sh/@acme/leaflet-wrapper@2.3.1/style.css"
|
|
9
|
+
},
|
|
10
|
+
"expect": {
|
|
11
|
+
"bundleUrl": "https://esm.sh/@acme/leaflet-wrapper@2.3.1/bundle.js",
|
|
12
|
+
"styleUrl": "https://esm.sh/@acme/leaflet-wrapper@2.3.1/style.css"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resolve-explicit-bundle-url",
|
|
3
|
+
"description": "A descriptor with an explicit full `bundleUrl` — the escape hatch for wrappers shipped via a non-registry CDN (esm.sh, jsdelivr, …). The resolver MUST use `bundleUrl` verbatim and MUST NOT auto-synthesize a `styleUrl`: an operator who escape-hatched the bundle declares `styleUrl` themselves, and a synthesized `registry.ggui.ai/.../style.css` would 404 and pollute the iframe CSP `style-src`.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"package": "@acme/leaflet-wrapper",
|
|
6
|
+
"version": "2.3.1",
|
|
7
|
+
"bundleUrl": "https://esm.sh/@acme/leaflet-wrapper@2.3.1/bundle.js"
|
|
8
|
+
},
|
|
9
|
+
"expect": {
|
|
10
|
+
"bundleUrl": "https://esm.sh/@acme/leaflet-wrapper@2.3.1/bundle.js"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resolve-loopback-host-http-scheme",
|
|
3
|
+
"description": "A `bundleHost` resolving to localhost (`localhost`, `127.0.0.1`, or `0.0.0.0`, each optionally with a port). The resolver MUST emit the `http://` scheme for loopback hosts: a local-registry workflow publishes over `http://`, and an `https://` render URL would mixed-content-block in the sandboxed iframe. Every non-loopback host gets `https://`.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"package": "@acme/leaflet-wrapper",
|
|
6
|
+
"version": "2.3.1",
|
|
7
|
+
"bundleHost": "localhost:4873"
|
|
8
|
+
},
|
|
9
|
+
"expect": {
|
|
10
|
+
"bundleUrl": "http://localhost:4873/bundles/@acme/leaflet-wrapper/2.3.1/bundle.js",
|
|
11
|
+
"styleUrl": "http://localhost:4873/bundles/@acme/leaflet-wrapper/2.3.1/style.css"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resolve-package-only-no-version",
|
|
3
|
+
"description": "A descriptor with a `package` but no `version` — not enough to assemble a hostable bundle path. The resolver MUST return no URLs (such an entry is resolved iframe-side via a bare-specifier `package` import, no bundle URL needed). Guards against a resolver that emits a malformed `.../undefined/bundle.js`.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"package": "@acme/leaflet-wrapper"
|
|
6
|
+
},
|
|
7
|
+
"expect": {}
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resolve-stdlib-no-urls",
|
|
3
|
+
"description": "The first-party `@ggui-ai/gadgets` stdlib package. Its exports ship with the iframe runtime — no bundle is fetched. The resolver MUST return no URLs for it, and MUST do so even when a `bundleUrl` is present: the stdlib's transport is first-party by construction and operators do not override it.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"package": "@ggui-ai/gadgets",
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"bundleUrl": "https://example.com/should-be-ignored.js"
|
|
8
|
+
},
|
|
9
|
+
"expect": {}
|
|
10
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transport-relevant subset of a gadget descriptor the resolver
|
|
3
|
+
* reads. Authored loose — every field optional — because the resolver
|
|
4
|
+
* is defined to defensively handle incomplete entries (a `package`
|
|
5
|
+
* with no `version`, etc.). Decoupled from `@ggui-ai/protocol`'s
|
|
6
|
+
* `GadgetDescriptor` on purpose: this is the kit's authored vocabulary.
|
|
7
|
+
*/
|
|
8
|
+
export interface GadgetUrlEntry {
|
|
9
|
+
readonly package?: string;
|
|
10
|
+
readonly version?: string;
|
|
11
|
+
readonly bundleUrl?: string;
|
|
12
|
+
readonly bundleHost?: string;
|
|
13
|
+
readonly styleUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What the resolver produces — the bundle + style URLs (either may be
|
|
17
|
+
* absent: an escape-hatched bundle synthesizes no style, a stdlib or
|
|
18
|
+
* version-less entry resolves to neither).
|
|
19
|
+
*/
|
|
20
|
+
export interface ResolvedGadgetUrls {
|
|
21
|
+
readonly bundleUrl?: string;
|
|
22
|
+
readonly styleUrl?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* One resolution-conformance case. Authored as JSON under `./cases/`,
|
|
26
|
+
* consumed via {@link gadgetResolutionCases}, graded by
|
|
27
|
+
* {@link runResolutionConformance}.
|
|
28
|
+
*
|
|
29
|
+
* The shape IS the public API — additive changes only.
|
|
30
|
+
*/
|
|
31
|
+
export interface ResolutionConformanceCase {
|
|
32
|
+
/** Unique case name. MUST match the JSON filename without `.json`. */
|
|
33
|
+
readonly name: string;
|
|
34
|
+
/** Which resolution obligation this case proves. */
|
|
35
|
+
readonly description: string;
|
|
36
|
+
/** The descriptor transport fields fed to the resolver. */
|
|
37
|
+
readonly entry: GadgetUrlEntry;
|
|
38
|
+
/** The URLs a spec-conformant resolver MUST produce. */
|
|
39
|
+
readonly expect: ResolvedGadgetUrls;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Every gadget URL-resolution case the kit ships, in deterministic
|
|
43
|
+
* order — explicit-URL cases, then host-computed, then the
|
|
44
|
+
* no-resolution edges.
|
|
45
|
+
*/
|
|
46
|
+
export declare const gadgetResolutionCases: readonly ResolutionConformanceCase[];
|
|
47
|
+
/** One case the resolver under test graded wrong. */
|
|
48
|
+
export interface ResolutionConformanceMismatch {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
/** The URLs the catalog says a conformant resolver MUST produce. */
|
|
51
|
+
readonly expected: ResolvedGadgetUrls;
|
|
52
|
+
/** The URLs the resolver under test actually produced. */
|
|
53
|
+
readonly actual: ResolvedGadgetUrls;
|
|
54
|
+
}
|
|
55
|
+
/** Outcome of grading a resolver against the catalog. */
|
|
56
|
+
export interface ResolutionConformanceResult {
|
|
57
|
+
/** Names of cases the resolver graded correctly. */
|
|
58
|
+
readonly passed: readonly string[];
|
|
59
|
+
/** Cases the resolver graded wrong — empty iff fully conformant. */
|
|
60
|
+
readonly failed: readonly ResolutionConformanceMismatch[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Grade a gadget URL resolver against the catalog.
|
|
64
|
+
*
|
|
65
|
+
* `resolve` MUST be a pure computation of `(entry) → {bundleUrl?,
|
|
66
|
+
* styleUrl?}`. The kit deliberately does NOT import a concrete
|
|
67
|
+
* resolver — adopters pass their own (the reference binding uses a
|
|
68
|
+
* faithful in-test resolver; see `./resolution-conformance.test.ts`).
|
|
69
|
+
* A conformant resolver produces an empty `failed` array.
|
|
70
|
+
*
|
|
71
|
+
* `resolve` is invoked exactly once per case, in
|
|
72
|
+
* {@link gadgetResolutionCases} order; it MUST be pure and MUST NOT
|
|
73
|
+
* throw.
|
|
74
|
+
*/
|
|
75
|
+
export declare function runResolutionConformance(resolve: (entry: GadgetUrlEntry) => ResolvedGadgetUrls): ResolutionConformanceResult;
|
|
76
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resolution-conformance/index.ts"],"names":[],"mappings":"AAkCA;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;CACrC;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,yBAAyB,EAQrE,CAAC;AAEF,qDAAqD;AACrD,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;CACrC;AAED,yDAAyD;AACzD,MAAM,WAAW,2BAA2B;IAC1C,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,SAAS,6BAA6B,EAAE,CAAC;CAC3D;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,kBAAkB,GACrD,2BAA2B,CAgB7B"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution-conformance catalog — accept cases for gadget bundle-URL
|
|
3
|
+
* resolution (SPEC §7.7.2 — `resolveGadgetUrls`).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a pure-function catalog
|
|
6
|
+
*
|
|
7
|
+
* Given a gadget descriptor's transport fields, the server computes
|
|
8
|
+
* the bundle + style URLs the iframe sees in
|
|
9
|
+
* `_meta.ggui.bootstrap.gadgets[*]`. That computation is a pure,
|
|
10
|
+
* deterministic function — no session, transport, or wire frame — so
|
|
11
|
+
* it gets the same treatment as the wire schema (`../schema-conformance`)
|
|
12
|
+
* and the registry gate (`../registration-conformance`): a catalog of
|
|
13
|
+
* cases graded against a caller-supplied resolver.
|
|
14
|
+
*
|
|
15
|
+
* Every case here is an "accept"-shaped check — the resolver always
|
|
16
|
+
* produces *some* result; the obligation is which URLs (or none).
|
|
17
|
+
*
|
|
18
|
+
* ## Polyglot, decoupled
|
|
19
|
+
*
|
|
20
|
+
* Each case ships as raw JSON under `./cases/`.
|
|
21
|
+
* {@link runResolutionConformance} takes the resolver as a callback —
|
|
22
|
+
* the kit never imports a concrete resolver here, so adopters drive
|
|
23
|
+
* their OWN. The reference binding (`./resolution-conformance.test.ts`)
|
|
24
|
+
* uses a faithful in-test resolver built from `@ggui-ai/protocol`'s
|
|
25
|
+
* `DEFAULT_BUNDLE_HOST` + `bundleHostScheme` primitives.
|
|
26
|
+
*/
|
|
27
|
+
import resolveBundleHostComputed from './cases/resolve-bundle-host-computed.json' with { type: 'json' };
|
|
28
|
+
import resolveDefaultHost from './cases/resolve-default-host.json' with { type: 'json' };
|
|
29
|
+
import resolveExplicitBundleAndStyleUrl from './cases/resolve-explicit-bundle-and-style-url.json' with { type: 'json' };
|
|
30
|
+
import resolveExplicitBundleUrl from './cases/resolve-explicit-bundle-url.json' with { type: 'json' };
|
|
31
|
+
import resolveLoopbackHostHttpScheme from './cases/resolve-loopback-host-http-scheme.json' with { type: 'json' };
|
|
32
|
+
import resolvePackageOnlyNoVersion from './cases/resolve-package-only-no-version.json' with { type: 'json' };
|
|
33
|
+
import resolveStdlibNoUrls from './cases/resolve-stdlib-no-urls.json' with { type: 'json' };
|
|
34
|
+
/**
|
|
35
|
+
* Every gadget URL-resolution case the kit ships, in deterministic
|
|
36
|
+
* order — explicit-URL cases, then host-computed, then the
|
|
37
|
+
* no-resolution edges.
|
|
38
|
+
*/
|
|
39
|
+
export const gadgetResolutionCases = [
|
|
40
|
+
resolveExplicitBundleUrl,
|
|
41
|
+
resolveExplicitBundleAndStyleUrl,
|
|
42
|
+
resolveBundleHostComputed,
|
|
43
|
+
resolveDefaultHost,
|
|
44
|
+
resolveLoopbackHostHttpScheme,
|
|
45
|
+
resolveStdlibNoUrls,
|
|
46
|
+
resolvePackageOnlyNoVersion,
|
|
47
|
+
];
|
|
48
|
+
function resolvedUrlsEqual(a, b) {
|
|
49
|
+
return a.bundleUrl === b.bundleUrl && a.styleUrl === b.styleUrl;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Grade a gadget URL resolver against the catalog.
|
|
53
|
+
*
|
|
54
|
+
* `resolve` MUST be a pure computation of `(entry) → {bundleUrl?,
|
|
55
|
+
* styleUrl?}`. The kit deliberately does NOT import a concrete
|
|
56
|
+
* resolver — adopters pass their own (the reference binding uses a
|
|
57
|
+
* faithful in-test resolver; see `./resolution-conformance.test.ts`).
|
|
58
|
+
* A conformant resolver produces an empty `failed` array.
|
|
59
|
+
*
|
|
60
|
+
* `resolve` is invoked exactly once per case, in
|
|
61
|
+
* {@link gadgetResolutionCases} order; it MUST be pure and MUST NOT
|
|
62
|
+
* throw.
|
|
63
|
+
*/
|
|
64
|
+
export function runResolutionConformance(resolve) {
|
|
65
|
+
const passed = [];
|
|
66
|
+
const failed = [];
|
|
67
|
+
for (const testCase of gadgetResolutionCases) {
|
|
68
|
+
const actual = resolve(testCase.entry);
|
|
69
|
+
if (resolvedUrlsEqual(actual, testCase.expect)) {
|
|
70
|
+
passed.push(testCase.name);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
failed.push({
|
|
74
|
+
name: testCase.name,
|
|
75
|
+
expected: testCase.expect,
|
|
76
|
+
actual,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { passed, failed };
|
|
81
|
+
}
|