@ggui-ai/protocol-conformance 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -15
- package/dist/cli.d.ts +6 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +52 -6
- package/dist/domain-error-conformance/cases/amend-unknown-session.json +9 -0
- package/dist/domain-error-conformance/cases/consume-unknown-session.json +9 -0
- package/dist/domain-error-conformance/cases/emit-unknown-session.json +9 -0
- package/dist/domain-error-conformance/cases/get-session-unknown-session.json +6 -0
- package/dist/domain-error-conformance/cases/render-unknown-handshake.json +6 -0
- package/dist/domain-error-conformance/cases/update-unknown-session.json +9 -0
- package/dist/domain-error-conformance/index.d.ts +97 -0
- package/dist/domain-error-conformance/index.d.ts.map +1 -0
- package/dist/domain-error-conformance/index.js +97 -0
- package/dist/fixtures/index.d.ts +2 -2
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +3 -3
- package/dist/fixtures/{subscribe-tenancy → subscribe-app-scope}/absent-appid-defaults.json +1 -1
- package/dist/fixtures/{subscribe-tenancy → subscribe-app-scope}/app-mismatch.json +1 -1
- package/dist/fixtures/subscribe-app-scope/index.d.ts +6 -0
- package/dist/fixtures/subscribe-app-scope/index.d.ts.map +1 -0
- package/dist/fixtures/{subscribe-tenancy → subscribe-app-scope}/index.js +5 -5
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/refusal-envelope-conformance/cases/refuse-after-fix-caller.json +3 -3
- package/dist/refusal-envelope-conformance/cases/refuse-non-render-surface.json +4 -4
- package/dist/run-conformance.d.ts +13 -3
- package/dist/run-conformance.d.ts.map +1 -1
- package/dist/run-conformance.js +61 -7
- package/dist/theme-binding-conformance/cases/app-theme-v1-base.json +18 -0
- package/dist/theme-binding-conformance/cases/app-theme-v1-one-palette.json +11 -0
- package/dist/theme-binding-conformance/cases/app-theme-v2-accepted.json +18 -0
- package/dist/theme-binding-conformance/cases/app-theme-v2-keyframes-accepted.json +16 -0
- package/dist/theme-binding-conformance/cases/app-theme-v2-keyframes-not-keyframes.json +15 -0
- package/dist/theme-binding-conformance/cases/app-theme-v2-label-too-long.json +17 -0
- package/dist/theme-binding-conformance/cases/app-theme-v2-missing-dark.json +13 -0
- package/dist/theme-binding-conformance/cases/app-theme-v2-no-hash.json +15 -0
- package/dist/theme-binding-conformance/cases/app-theme-v2-platform-pin.json +17 -0
- package/dist/theme-binding-conformance/cases/overlay-hash-canonical.json +17 -0
- package/dist/theme-binding-conformance/cases/theme-mode-absence.json +10 -0
- package/dist/theme-binding-conformance/cases/theme-mode-default-when-silent-host.json +10 -0
- package/dist/theme-binding-conformance/cases/theme-mode-order.json +10 -0
- package/dist/theme-binding-conformance/index.d.ts +59 -0
- package/dist/theme-binding-conformance/index.d.ts.map +1 -0
- package/dist/theme-binding-conformance/index.js +110 -0
- package/dist/transport-refusal-conformance/cases/refuse-deprovisioned-endpoint.json +7 -5
- package/dist/transport-refusal-conformance/cases/refuse-render-only-code.json +3 -2
- package/dist/transport-refusal-conformance/index.d.ts +2 -0
- package/dist/transport-refusal-conformance/index.d.ts.map +1 -1
- package/dist/transport-refusal-conformance/index.js +2 -2
- package/dist/types.d.ts +1 -1
- package/package.json +12 -2
- package/dist/fixtures/subscribe-tenancy/index.d.ts +0 -6
- package/dist/fixtures/subscribe-tenancy/index.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ A fifth surface — the **Path-B browser-host driver** for DOM-level claims (`bo
|
|
|
19
19
|
|
|
20
20
|
**The behavioral fixture catalog is WebSocket-only.** The canonical ggui live-channel transport is WS (see SPEC §12 Transport Bindings). `TransportConfig` is shaped as an extensibly-closed union so later live-channel transports (HTTP long-poll) can be added without breaking the public API.
|
|
21
21
|
|
|
22
|
-
The MCP surface is graded separately, by catalog rather than by transport config — see `resource-read-conformance` below. Its cases are deliberately NOT registered in `fixturesByContract`: everything in that map is driven over a WebSocket, so an MCP case there would be a permanent skip on every WS run, which is the false gate the kit's exact skip-set pinning exists to prevent. `resources/read`
|
|
22
|
+
The MCP surface is graded separately, by catalog rather than by transport config — see `resource-read-conformance` below. Its cases are deliberately NOT registered in `fixturesByContract`: everything in that map is driven over a WebSocket, so an MCP case there would be a permanent skip on every WS run, which is the false gate the kit's exact skip-set pinning exists to prevent. `resources/read` and — since ggui#880 — one `tools/call` catalog (`domain-error`, SPEC §7.9 Plane 2) are the MCP methods bound today.
|
|
23
23
|
|
|
24
24
|
## Path-A vs Path-B matchability
|
|
25
25
|
|
|
@@ -61,7 +61,7 @@ Each catalog ships its cases as raw JSON so a non-TypeScript implementer can gra
|
|
|
61
61
|
- **`@ggui-ai/protocol-conformance/host-helper-conformance`** — which render coordinates a HOST-HELPER library MUST extract from a `tools/call` result, and which malformed results it MUST decline rather than guess at.
|
|
62
62
|
- **`@ggui-ai/protocol-conformance/refusal-envelope-conformance`** — the tool result a server MUST project for a PRE-GENERATION refusal (SPEC §7.1's refused arm): an in-result `isError` (never a throw), `content[0].text` leading with the code, `structuredContent` carrying `{outcome: 'refused', refusal}` and nothing else, no `_meta` — and no envelope at all for a registered code whose surfaces exclude the render gate.
|
|
63
63
|
- **`@ggui-ai/protocol-conformance/registry-completeness`** — the structural obligations a deployment's closed refusal-code registry MUST satisfy: `surfaces` non-empty, `code === key`, every `after-fix` entry naming who acts (`fixBy`), and `retry` inside the closed four-value set. Grades DATA rather than a function — the caller passes their registry.
|
|
64
|
-
- **`@ggui-ai/protocol-conformance/transport-refusal-conformance`** — the JSON-RPC error a per-app MCP endpoint MUST answer with when it refuses a request for a typed reason (SPEC §7.1's endpoint-level refusal, ggui#825): HTTP 403 with `{ code: -32003, message: 'App not found', data: { refusal } }`, where `refusal` is the registry projection without the render-only fields (`code`, `message`, `fix`, `retry`) — and no envelope at all for a registered code whose surfaces exclude `mcp-endpoint` (an untyped authorization failure answers 403 with `-
|
|
64
|
+
- **`@ggui-ai/protocol-conformance/transport-refusal-conformance`** — the JSON-RPC error a per-app MCP endpoint MUST answer with when it refuses a request for a typed reason (SPEC §7.1's endpoint-level refusal, ggui#825): HTTP 403 with `{ code: -32003, message: 'App not found', data: { refusal } }`, where `refusal` is the registry projection without the render-only fields (`code`, `message`, `fix`, `retry`) — and no envelope at all for a registered code whose surfaces exclude `mcp-endpoint` (an untyped authorization failure answers 403 with `-32007` and no `data`, by contract — ggui#836). Runner: `runTransportRefusalConformance(project)`; folded into `runConformance()` via `transportRefusalProjector`.
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
67
|
import { runSchemaConformance } from "@ggui-ai/protocol-conformance/schema-conformance";
|
|
@@ -116,25 +116,33 @@ The catalog grades all four failure classes (`NOT_FOUND` on `-32002`; `BLUEPRINT
|
|
|
116
116
|
|
|
117
117
|
It deliberately does **not** grade: the order in which a substrate-less server answers (`NOT_SUPPORTED` describes the deployment, so answering it immediately is correct); `detail` wording on any code; the `NOT_FOUND` message literal (its _constancy_ is what is normative); internal-error message text; the number a URI naming no locator receives (that belongs to the transport binding — only the negative is graded, that it must not be one of the four); and the shell's markup (success is graded on the projected render meta, never on DOM shape).
|
|
118
118
|
|
|
119
|
+
## Domain-error conformance — the `tools/call` binding (SPEC §7.9 Plane 2, ggui#880)
|
|
120
|
+
|
|
121
|
+
The kit's first `tools/call` driver. A Plane-2 failure — a missing session or handshake, props that do not satisfy the contract, an undeclared channel — is a tool-execution error, so it is a tool RESULT with `isError: true` (never a JSON-RPC error frame) whose `content[0].text` LEADS with the registered slug: `<code>: <detail>`. No `structuredContent`, no `_meta`. A raw MCP agent branches on `text.startsWith(code + ': ')` for a code in `@ggui-ai/protocol`'s `DOMAIN_ERROR_CODES` and on nothing else.
|
|
122
|
+
|
|
123
|
+
Vendor-neutral like `resources/read`: the kit imports no server. The adopter supplies a **driver** — `toolCallDriver` on `runConformance()`, or `--tool-call-driver <module>` on the CLI (an ES module exporting `drive(scenario)` or a default) — that performs ONE `tools/call` against the deployment and returns the raw result the MCP client received, or `null` when the tool is not bound there (that case is SKIPPED, named). The kit owns the scenarios and every assertion. Six no-setup cases under `src/domain-error-conformance/cases/`: an unknown `handshakeId` on `ggui_render` (`handshake_not_found`) and an unknown `sessionId` on `ggui_consume` / `ggui_get_session` / `ggui_update` / `ggui_amend` / `ggui_emit` (`session_not_found`). Each is graded on the raw result — `isError`, `content-text`, `slug-leads`, `no-structuredContent`, `no-meta` — and reported as `domain-error/<case>`; a driver that throws fails that case only.
|
|
124
|
+
|
|
125
|
+
The catalog exists for one receipt: before the slug led the text, every first-party server failed every case on `slug-leads` (executed, ggui#880). `src/cli-samples/tool-call-driver.mjs` is a spec-correct sample of what the flag imports.
|
|
126
|
+
|
|
119
127
|
## Conformance status
|
|
120
128
|
|
|
121
129
|
What is actually graded against shipping code today, and what is still awaiting a driver. This is an inventory, not a roadmap — a mechanism is listed as proven only if a test in the repo drives it against the real implementation on every push.
|
|
122
130
|
|
|
123
131
|
Every test below is a plain `*.test.ts` inside a package's `src/`, so it runs in that package's ordinary vitest suite. The ggui repo's per-push CI runs `turbo run test` across all non-e2e packages, so **all twelve rows below are gated on every push** — the kit needs no dedicated workflow, and adopters wiring it into their own CI need nothing more than their existing unit-test task.
|
|
124
132
|
|
|
125
|
-
| Obligation | Graded against
|
|
126
|
-
| ----------------------------------------------------------------------- |
|
|
127
|
-
| Gadget wire schema (§7.7.2) | the shipping `@ggui-ai/protocol` `clientCapabilitiesSpecSchema`
|
|
128
|
-
| Registration gate (§7.7.3, §7.9) | the shipping gate trio in `@ggui-ai/mcp-server-handlers`
|
|
129
|
-
| Bundle-URL resolution (§7.7.2) | the shipping `resolveGadgetUrls` in `@ggui-ai/mcp-server-handlers`
|
|
130
|
-
| Path-A WS fixtures — first-party | the real `@ggui-ai/mcp-server` GguiSession channel, booted in-process on an ephemeral port
|
|
131
|
-
| Path-A WS fixtures — vendor-neutral | `@ggui-ai/protocol-reference-server`
|
|
132
|
-
| `resources/read` typed failures + mount invariant | the shipping `registerGguiRenderResourceTemplate` in `@ggui-ai/mcp-server`
|
|
133
|
-
| Tool-binding resolution — declared wins, contract derivation (§7.7.4.1) | the shipping `resolveMcpToolBindings` in `@ggui-ai/artifact-manifest`
|
|
134
|
-
| Tool-binding search filters — `tool=` / `server=` semantics (§7.7.4.1) | the shipping `matchesMcpToolFilters` in `@ggui-ai/registry-core`
|
|
135
|
-
| Pre-generation refusal envelope (§7.1, refused arm) | `projectRenderRefusal` in `@ggui-ai/protocol` — the one source both the shipping `ggui_render` handler (`@ggui-ai/mcp-server-handlers`) and `@ggui-ai/protocol-reference-server` answer through
|
|
136
|
-
| Refusal-registry completeness (ggui#786 ruling 5b) | the shipping `PRE_GENERATION_REFUSAL_CODES` in `@ggui-ai/protocol`
|
|
137
|
-
| Endpoint-level refusal (§7.1, `mcp-endpoint` surface, ggui#825) | the catalog's kit-local reference projector only — no shipping deployment projector is graded yet: the OSS route is generic (`errorMapper`), the first-party server and the reference server supply no projector (SKIPPED, named), and
|
|
133
|
+
| Obligation | Graded against | Driver |
|
|
134
|
+
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
135
|
+
| Gadget wire schema (§7.7.2) | the shipping `@ggui-ai/protocol` `clientCapabilitiesSpecSchema` | this kit's own `src/schema-conformance/schema-conformance.test.ts` |
|
|
136
|
+
| Registration gate (§7.7.3, §7.9) | the shipping gate trio in `@ggui-ai/mcp-server-handlers` | `mcp-server-handlers/src/renders/assert-gadgets.conformance.test.ts` |
|
|
137
|
+
| Bundle-URL resolution (§7.7.2) | the shipping `resolveGadgetUrls` in `@ggui-ai/mcp-server-handlers` | `mcp-server-handlers/src/renders/resolve-gadget-urls.conformance.test.ts` |
|
|
138
|
+
| Path-A WS fixtures — first-party | the real `@ggui-ai/mcp-server` GguiSession channel, booted in-process on an ephemeral port | `mcp-server/src/ggui-session-channel.conformance.test.ts` — **24 pass / 12 skip / 0 fail** |
|
|
139
|
+
| Path-A WS fixtures — vendor-neutral | `@ggui-ai/protocol-reference-server` | `protocol-reference-server/src/conformance.test.ts` — **19 pass / 5 skip / 0 fail** |
|
|
140
|
+
| `resources/read` typed failures + mount invariant | the shipping `registerGguiRenderResourceTemplate` in `@ggui-ai/mcp-server` | `mcp-server/src/mcp-apps-outbound.resource-read.conformance.test.ts` — **12 pass / 0 skip / 0 fail** |
|
|
141
|
+
| Tool-binding resolution — declared wins, contract derivation (§7.7.4.1) | the shipping `resolveMcpToolBindings` in `@ggui-ai/artifact-manifest` | `artifact-manifest/src/mcp-tool-bindings.conformance.test.ts` |
|
|
142
|
+
| Tool-binding search filters — `tool=` / `server=` semantics (§7.7.4.1) | the shipping `matchesMcpToolFilters` in `@ggui-ai/registry-core` | `registry-core/src/mcp-tool-filters.conformance.test.ts` |
|
|
143
|
+
| Pre-generation refusal envelope (§7.1, refused arm) | `projectRenderRefusal` in `@ggui-ai/protocol` — the one source both the shipping `ggui_render` handler (`@ggui-ai/mcp-server-handlers`) and `@ggui-ai/protocol-reference-server` answer through | `mcp-server-handlers/src/renders/render-refusal-projection.conformance.test.ts` |
|
|
144
|
+
| Refusal-registry completeness (ggui#786 ruling 5b) | the shipping `PRE_GENERATION_REFUSAL_CODES` in `@ggui-ai/protocol` | this kit's own `src/registry-completeness/registry-completeness.test.ts` |
|
|
145
|
+
| Endpoint-level refusal (§7.1, `mcp-endpoint` surface, ggui#825) | the catalog's kit-local reference projector only — no shipping deployment projector is graded yet: the OSS route is generic (`errorMapper`), the first-party server and the reference server supply no projector (SKIPPED, named), and a hosted deployment's projector lands after this slice | this kit's own `src/transport-refusal-conformance/transport-refusal-conformance.test.ts` (coherence + discrimination) |
|
|
138
146
|
|
|
139
147
|
Both WS drivers pin their pass set _and_ their skip set as exact sets, and the resource-read driver pins its skip set as exactly empty. A fixture silently degrading to a skip fails the build, and so does a skipped fixture that quietly starts passing — a skip set that can grow unnoticed is a false gate, and re-pinning it is meant to be a deliberate act.
|
|
140
148
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -12,21 +12,25 @@ interface ParsedArgs {
|
|
|
12
12
|
readonly projector?: string;
|
|
13
13
|
/** Path to an ES module exporting the endpoint-level refusal projector. */
|
|
14
14
|
readonly transportProjector?: string;
|
|
15
|
+
/** Path to an ES module exporting a `tools/call` driver (SPEC §7.9 Plane 2, ggui#880). */
|
|
16
|
+
readonly toolCallDriver?: string;
|
|
15
17
|
readonly verbose: boolean;
|
|
16
18
|
readonly help: boolean;
|
|
17
19
|
}
|
|
18
20
|
declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
19
21
|
declare function parseAuth(spec: string): AuthConfig;
|
|
20
22
|
/** The three optional inputs a CLI run can hand the pure-function catalogs. */
|
|
21
|
-
export type CatalogInputs = Pick<RunConformanceConfig, 'refusalRegistry' | 'refusalProjector' | 'transportRefusalProjector'>;
|
|
23
|
+
export type CatalogInputs = Pick<RunConformanceConfig, 'refusalRegistry' | 'refusalProjector' | 'transportRefusalProjector' | 'toolCallDriver'>;
|
|
22
24
|
/** The flag values that name where each catalog input comes from. */
|
|
23
25
|
export interface CatalogInputPaths {
|
|
24
26
|
readonly registry?: string | undefined;
|
|
25
27
|
readonly projector?: string | undefined;
|
|
26
28
|
readonly transportProjector?: string | undefined;
|
|
29
|
+
readonly toolCallDriver?: string | undefined;
|
|
27
30
|
}
|
|
28
31
|
/**
|
|
29
|
-
* Resolve the `--registry` / `--projector` / `--transport-projector`
|
|
32
|
+
* Resolve the `--registry` / `--projector` / `--transport-projector` /
|
|
33
|
+
* `--tool-call-driver`
|
|
30
34
|
* flags into the runner's catalog inputs. A flag that is absent yields
|
|
31
35
|
* no input, and the runner reports that catalog SKIPPED with the flag
|
|
32
36
|
* named. Every failure names the flag and the path.
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA6DA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;AAK9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM7C,UAAU,UAAU;IAClB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,0FAA0F;IAC1F,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,iBAAS,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CA8DtD;AAOD,iBAAS,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAY3C;AAMD,+EAA+E;AAC/E,MAAM,MAAM,aAAa,GAAG,IAAI,CAC9B,oBAAoB,EACpB,iBAAiB,GAAG,kBAAkB,GAAG,2BAA2B,GAAG,gBAAgB,CACxF,CAAC;AAEF,qEAAqE;AACrE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9C;AA6KD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CAqCxF;AAMD,eAAO,MAAM,KAAK,QA8DL,CAAC;AAEd,iBAAe,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAoE5D;AAED;;;;;;;GAOG;AACH,iBAAS,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAI/D;AA+BD,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* The three PURE-FUNCTION catalogs (refusal-envelope,
|
|
20
20
|
* registry-completeness, transport-refusal) grade an in-process
|
|
21
21
|
* function and a data table, not the wire: `--registry <file.json>`,
|
|
22
|
-
* `--projector <module
|
|
22
|
+
* `--projector <module>`, `--transport-projector <module>` and
|
|
23
|
+
* `--tool-call-driver <module>` (the domain-error catalog, the kit's
|
|
24
|
+
* first tools/call driver, ggui#880) hand
|
|
23
25
|
* them their inputs (ggui#803 leg 3). A flag left out leaves its
|
|
24
26
|
* catalog SKIPPED with the flag named, never silently absent. Catalog
|
|
25
27
|
* rows count as executed fixtures, so a run given a catalog flag can
|
|
@@ -42,6 +44,7 @@
|
|
|
42
44
|
import { readFile } from 'node:fs/promises';
|
|
43
45
|
import { resolve } from 'node:path';
|
|
44
46
|
import { pathToFileURL } from 'node:url';
|
|
47
|
+
import { isRawToolCallResult, } from './domain-error-conformance/index.js';
|
|
45
48
|
import { createDefaultReporter, formatFailures, formatSkips } from './reporter.js';
|
|
46
49
|
import { runConformance, } from './run-conformance.js';
|
|
47
50
|
function parseArgs(argv) {
|
|
@@ -51,6 +54,7 @@ function parseArgs(argv) {
|
|
|
51
54
|
let timeoutMs;
|
|
52
55
|
let registry;
|
|
53
56
|
let projector;
|
|
57
|
+
let toolCallDriver;
|
|
54
58
|
let transportProjector;
|
|
55
59
|
let verbose = false;
|
|
56
60
|
let help = false;
|
|
@@ -88,6 +92,9 @@ function parseArgs(argv) {
|
|
|
88
92
|
case '--transport-projector':
|
|
89
93
|
transportProjector = next();
|
|
90
94
|
break;
|
|
95
|
+
case '--tool-call-driver':
|
|
96
|
+
toolCallDriver = next();
|
|
97
|
+
break;
|
|
91
98
|
case '--verbose':
|
|
92
99
|
case '-v':
|
|
93
100
|
verbose = true;
|
|
@@ -100,7 +107,7 @@ function parseArgs(argv) {
|
|
|
100
107
|
throw new Error(`unknown flag '${flag}' — run with --help for usage`);
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
|
-
return { url, auth, only, timeoutMs, registry, projector, transportProjector, verbose, help };
|
|
110
|
+
return { url, auth, only, timeoutMs, registry, projector, transportProjector, toolCallDriver, verbose, help };
|
|
104
111
|
}
|
|
105
112
|
/** The message of a thrown value, without pretending every throw is an Error. */
|
|
106
113
|
function errorMessage(err) {
|
|
@@ -188,7 +195,8 @@ function isTransportRefusalInput(value) {
|
|
|
188
195
|
typeof value.code === 'string' &&
|
|
189
196
|
typeof value.message === 'string' &&
|
|
190
197
|
typeof value.fix === 'string' &&
|
|
191
|
-
typeof value.retry === 'string'
|
|
198
|
+
typeof value.retry === 'string' &&
|
|
199
|
+
typeof value.appId === 'string');
|
|
192
200
|
}
|
|
193
201
|
function isProjectedTransportRefusal(value) {
|
|
194
202
|
return (isRecord(value) &&
|
|
@@ -234,7 +242,37 @@ async function loadProjector(flag, path, guard) {
|
|
|
234
242
|
};
|
|
235
243
|
}
|
|
236
244
|
/**
|
|
237
|
-
*
|
|
245
|
+
* Like {@link loadProjector} for a `tools/call` DRIVER: the export is
|
|
246
|
+
* `drive` (or the default export), it may be async, and `null` means
|
|
247
|
+
* "the tool is not bound on this deployment" (the catalog SKIPS that
|
|
248
|
+
* case, named). Every other value is held to the raw-result shape.
|
|
249
|
+
*/
|
|
250
|
+
async function loadDriver(flag, path, guard) {
|
|
251
|
+
const href = pathToFileURL(resolve(path)).href;
|
|
252
|
+
let mod;
|
|
253
|
+
try {
|
|
254
|
+
mod = await import(/* @vite-ignore */ href);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
throw new Error(`${flag} ${path} could not be imported: ${errorMessage(err)}`);
|
|
258
|
+
}
|
|
259
|
+
const candidate = mod.drive ?? mod.default;
|
|
260
|
+
if (typeof candidate !== 'function') {
|
|
261
|
+
throw new Error(`${flag} ${path} must export \`drive\` (or a default export) as a function`);
|
|
262
|
+
}
|
|
263
|
+
return async (input) => {
|
|
264
|
+
const out = await candidate(input);
|
|
265
|
+
if (out === null)
|
|
266
|
+
return null;
|
|
267
|
+
if (!guard(out)) {
|
|
268
|
+
throw new Error(`${flag} ${path} returned a value that is not a raw tools/call result: ${JSON.stringify(out)}`);
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Resolve the `--registry` / `--projector` / `--transport-projector` /
|
|
275
|
+
* `--tool-call-driver`
|
|
238
276
|
* flags into the runner's catalog inputs. A flag that is absent yields
|
|
239
277
|
* no input, and the runner reports that catalog SKIPPED with the flag
|
|
240
278
|
* named. Every failure names the flag and the path.
|
|
@@ -257,6 +295,9 @@ export async function loadCatalogInputs(paths) {
|
|
|
257
295
|
if (paths.transportProjector !== undefined) {
|
|
258
296
|
inputs.transportRefusalProjector = await loadProjector('--transport-projector', paths.transportProjector, isProjectedTransportRefusal);
|
|
259
297
|
}
|
|
298
|
+
if (paths.toolCallDriver !== undefined) {
|
|
299
|
+
inputs.toolCallDriver = await loadDriver('--tool-call-driver', paths.toolCallDriver, isRawToolCallResult);
|
|
300
|
+
}
|
|
260
301
|
return inputs;
|
|
261
302
|
}
|
|
262
303
|
// =============================================================================
|
|
@@ -296,16 +337,21 @@ Options:
|
|
|
296
337
|
{ httpStatus, error } for a refusal, or null when
|
|
297
338
|
the code has no transport envelope (grades
|
|
298
339
|
transport-refusal).
|
|
340
|
+
--tool-call-driver <module>
|
|
341
|
+
ES module exporting drive(scenario) (or default):
|
|
342
|
+
performs ONE tools/call and returns the raw result
|
|
343
|
+
(null = tool not bound). Grades the domain-error
|
|
344
|
+
catalog — SPEC §7.9 Plane 2 on the wire (ggui#880).
|
|
299
345
|
--verbose, -v Print failure details + skip reasons at the end.
|
|
300
346
|
--help, -h Show this help.
|
|
301
347
|
|
|
302
348
|
Pure-function catalogs (refusal-envelope, registry-completeness, transport-refusal):
|
|
303
349
|
These grade an in-process function and a data table, not the wire, so
|
|
304
|
-
a URL alone cannot reach them: the
|
|
350
|
+
a URL alone cannot reach them: the four flags above hand them their
|
|
305
351
|
inputs. A flag left out leaves its catalog on the scorecard as SKIPPED
|
|
306
352
|
with the flag named — an ungraded obligation stays visible rather
|
|
307
353
|
than vanishing. File and module paths resolve against the working
|
|
308
|
-
directory. A projector
|
|
354
|
+
directory. A projector is synchronous and a tools/call driver may be async; one that throws, returns
|
|
309
355
|
undefined, or returns a non-projection is graded as a FAIL on that
|
|
310
356
|
case (exit 1) and the remaining cases are still graded — never a
|
|
311
357
|
crash. Catalog rows count as executed fixtures: a run given a
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "amend-unknown-session",
|
|
3
|
+
"description": "An in-place amend on an unknown sessionId: `session_not_found` leads the text; nothing was mutated.",
|
|
4
|
+
"scenario": {
|
|
5
|
+
"tool": "ggui_amend",
|
|
6
|
+
"args": { "sessionId": "render_does_not_exist", "kind": "replace", "props": {} }
|
|
7
|
+
},
|
|
8
|
+
"expect": { "code": "session_not_found" }
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "consume-unknown-session",
|
|
3
|
+
"description": "A sessionId no session this caller can reach carries: `session_not_found` leads the text of the consume result — the same slug for never-minted, expired, closed and cross-app ids.",
|
|
4
|
+
"scenario": {
|
|
5
|
+
"tool": "ggui_consume",
|
|
6
|
+
"args": { "sessionId": "render_does_not_exist", "timeout": 0 }
|
|
7
|
+
},
|
|
8
|
+
"expect": { "code": "session_not_found" }
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "emit-unknown-session",
|
|
3
|
+
"description": "A stream emit on an unknown sessionId: `session_not_found` leads the text before any channel is checked.",
|
|
4
|
+
"scenario": {
|
|
5
|
+
"tool": "ggui_emit",
|
|
6
|
+
"args": { "sessionId": "render_does_not_exist", "channel": "x", "payload": {} }
|
|
7
|
+
},
|
|
8
|
+
"expect": { "code": "session_not_found" }
|
|
9
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "get-session-unknown-session",
|
|
3
|
+
"description": "The projection read on an unknown sessionId: `session_not_found` leads the text; no JSON-RPC error frame, no structuredContent.",
|
|
4
|
+
"scenario": { "tool": "ggui_get_session", "args": { "sessionId": "render_does_not_exist" } },
|
|
5
|
+
"expect": { "code": "session_not_found" }
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "render-unknown-handshake",
|
|
3
|
+
"description": "A handshakeId nothing minted: the render fails as not found — `handshake_not_found` leads the text. No handshake is consumed, nothing is committed; the caller re-handshakes.",
|
|
4
|
+
"scenario": { "tool": "ggui_render", "args": { "handshakeId": "h_does_not_exist", "props": {} } },
|
|
5
|
+
"expect": { "code": "handshake_not_found" }
|
|
6
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "update-unknown-session",
|
|
3
|
+
"description": "A history-advancing update on an unknown sessionId: `session_not_found` leads the text; nothing was mutated.",
|
|
4
|
+
"scenario": {
|
|
5
|
+
"tool": "ggui_update",
|
|
6
|
+
"args": { "sessionId": "render_does_not_exist", "kind": "replace", "props": {} }
|
|
7
|
+
},
|
|
8
|
+
"expect": { "code": "session_not_found" }
|
|
9
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `domain-error` catalog — SPEC §7.9 Plane 2 on the `tools/call` wire
|
|
3
|
+
* (ggui#880). The kit's FIRST `tools/call` driver.
|
|
4
|
+
*
|
|
5
|
+
* A Plane-2 failure (a missing session or handshake, props that do not
|
|
6
|
+
* satisfy the contract, an undeclared channel) is a tool-execution error,
|
|
7
|
+
* so it is a tool RESULT with `isError: true` — never a JSON-RPC error
|
|
8
|
+
* frame — whose `content[0].text` LEADS with the registered slug:
|
|
9
|
+
* `<code>: <detail>`. No `structuredContent`, no `_meta`. A raw MCP agent
|
|
10
|
+
* branches on `text.startsWith(code + ': ')` and on nothing else.
|
|
11
|
+
*
|
|
12
|
+
* Vendor-neutral by construction: the kit imports no server. The adopter
|
|
13
|
+
* supplies a {@link ToolCallDriver} that performs ONE `tools/call` against
|
|
14
|
+
* its deployment and returns the raw result the MCP client received (or
|
|
15
|
+
* `null` when the tool is not bound there — that case is SKIPPED, named).
|
|
16
|
+
* The kit owns the scenarios and every assertion. Each case ships as raw
|
|
17
|
+
* JSON under `./cases/`; the six here need no setup — an id nothing
|
|
18
|
+
* minted is refused the same way on every deployment.
|
|
19
|
+
*
|
|
20
|
+
* The catalog exists for one receipt: before the slug led the text, every
|
|
21
|
+
* first-party server failed every case on `slug-leads`.
|
|
22
|
+
*/
|
|
23
|
+
import type { JsonValue } from '@ggui-ai/protocol';
|
|
24
|
+
/** A JSON object — the `arguments` of one `tools/call`. */
|
|
25
|
+
export type ToolCallArgs = {
|
|
26
|
+
readonly [key: string]: JsonValue;
|
|
27
|
+
};
|
|
28
|
+
/** One `tools/call` the adopter's driver performs verbatim. */
|
|
29
|
+
export interface ToolCallScenario {
|
|
30
|
+
readonly tool: string;
|
|
31
|
+
readonly args: ToolCallArgs;
|
|
32
|
+
}
|
|
33
|
+
/** One content block of a tool result, as the MCP client received it. */
|
|
34
|
+
export interface ToolCallContent {
|
|
35
|
+
readonly type: string;
|
|
36
|
+
readonly text?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The raw `tools/call` result as the MCP client received it. Authored here
|
|
40
|
+
* rather than imported from an SDK so the driver is typed against the kit
|
|
41
|
+
* alone; `structuredContent` and `_meta` are read only for presence.
|
|
42
|
+
*/
|
|
43
|
+
export interface RawToolCallResult {
|
|
44
|
+
readonly isError?: boolean;
|
|
45
|
+
readonly content: readonly ToolCallContent[];
|
|
46
|
+
readonly structuredContent?: unknown;
|
|
47
|
+
readonly _meta?: unknown;
|
|
48
|
+
}
|
|
49
|
+
/** External-boundary guard for a value an adopter's driver returned. */
|
|
50
|
+
export declare function isRawToolCallResult(value: unknown): value is RawToolCallResult;
|
|
51
|
+
/**
|
|
52
|
+
* Performs one `tools/call` and returns the raw result — or `null` when
|
|
53
|
+
* the deployment does not bind that tool. May be async: a live call is the
|
|
54
|
+
* intended binding.
|
|
55
|
+
*/
|
|
56
|
+
export type ToolCallDriver = (scenario: ToolCallScenario) => RawToolCallResult | null | Promise<RawToolCallResult | null>;
|
|
57
|
+
/** One catalog case, authored as JSON under `./cases/`. */
|
|
58
|
+
export interface DomainErrorConformanceCase {
|
|
59
|
+
/** Reported as `domain-error/<name>` by the runner. */
|
|
60
|
+
readonly name: string;
|
|
61
|
+
readonly description: string;
|
|
62
|
+
readonly scenario: ToolCallScenario;
|
|
63
|
+
/** The registered slug that must lead the result text. */
|
|
64
|
+
readonly expect: {
|
|
65
|
+
readonly code: string;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** The catalog, in wire order: handshake first, then the session tools. */
|
|
69
|
+
export declare const domainErrorCases: readonly DomainErrorConformanceCase[];
|
|
70
|
+
/** The criterion a case failed on — the first that did not hold. */
|
|
71
|
+
export type DomainErrorCriterion = 'driver-threw' | 'isError' | 'content-text' | 'slug-leads' | 'no-structuredContent' | 'no-meta';
|
|
72
|
+
export interface DomainErrorMismatch {
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly criterion: DomainErrorCriterion;
|
|
75
|
+
readonly expected: unknown;
|
|
76
|
+
readonly actual: unknown;
|
|
77
|
+
}
|
|
78
|
+
export interface DomainErrorSkip {
|
|
79
|
+
readonly name: string;
|
|
80
|
+
readonly reason: string;
|
|
81
|
+
}
|
|
82
|
+
export interface DomainErrorConformanceResult {
|
|
83
|
+
readonly passed: readonly string[];
|
|
84
|
+
readonly failed: readonly DomainErrorMismatch[];
|
|
85
|
+
readonly skipped: readonly DomainErrorSkip[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Drive every case through `driver` and grade the raw results. A driver
|
|
89
|
+
* that throws fails THAT case (`driver-threw`) and the rest are still
|
|
90
|
+
* graded; a driver that returns `null` skips that case with the tool named.
|
|
91
|
+
*/
|
|
92
|
+
export interface RunDomainErrorConformanceOptions {
|
|
93
|
+
/** The cases to drive — the whole catalog by default. A live driver is real calls, so a runner selecting rows passes only those. */
|
|
94
|
+
readonly cases?: readonly DomainErrorConformanceCase[];
|
|
95
|
+
}
|
|
96
|
+
export declare function runDomainErrorConformance(driver: ToolCallDriver, options?: RunDomainErrorConformanceOptions): Promise<DomainErrorConformanceResult>;
|
|
97
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/domain-error-conformance/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAQnD,2DAA2D;AAC3D,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEjE,+DAA+D;AAC/D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;CAC7B;AAED,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAY9E;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,CAC3B,QAAQ,EAAE,gBAAgB,KACvB,iBAAiB,GAAG,IAAI,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;AAElE,2DAA2D;AAC3D,MAAM,WAAW,0BAA0B;IACzC,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C;AAED,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,EAAE,SAAS,0BAA0B,EAOjE,CAAC;AAEF,oEAAoE;AACpE,MAAM,MAAM,oBAAoB,GAC5B,cAAc,GACd,SAAS,GACT,cAAc,GACd,YAAY,GACZ,sBAAsB,GACtB,SAAS,CAAC;AAEd,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,oBAAoB,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;CAC9C;AAqCD;;;;GAIG;AACH,MAAM,WAAW,gCAAgC;IAC/C,oIAAoI;IACpI,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;CACxD;AAED,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,gCAAqC,GAC7C,OAAO,CAAC,4BAA4B,CAAC,CA6BvC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import amendUnknownSession from './cases/amend-unknown-session.json' with { type: 'json' };
|
|
2
|
+
import consumeUnknownSession from './cases/consume-unknown-session.json' with { type: 'json' };
|
|
3
|
+
import emitUnknownSession from './cases/emit-unknown-session.json' with { type: 'json' };
|
|
4
|
+
import getSessionUnknownSession from './cases/get-session-unknown-session.json' with { type: 'json' };
|
|
5
|
+
import renderUnknownHandshake from './cases/render-unknown-handshake.json' with { type: 'json' };
|
|
6
|
+
import updateUnknownSession from './cases/update-unknown-session.json' with { type: 'json' };
|
|
7
|
+
/** External-boundary guard for a value an adopter's driver returned. */
|
|
8
|
+
export function isRawToolCallResult(value) {
|
|
9
|
+
if (typeof value !== 'object' || value === null)
|
|
10
|
+
return false;
|
|
11
|
+
const content = Reflect.get(value, 'content');
|
|
12
|
+
if (!Array.isArray(content))
|
|
13
|
+
return false;
|
|
14
|
+
for (const block of content) {
|
|
15
|
+
if (typeof block !== 'object' || block === null)
|
|
16
|
+
return false;
|
|
17
|
+
if (typeof Reflect.get(block, 'type') !== 'string')
|
|
18
|
+
return false;
|
|
19
|
+
const text = Reflect.get(block, 'text');
|
|
20
|
+
if (text !== undefined && typeof text !== 'string')
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const isError = Reflect.get(value, 'isError');
|
|
24
|
+
return isError === undefined || typeof isError === 'boolean';
|
|
25
|
+
}
|
|
26
|
+
/** The catalog, in wire order: handshake first, then the session tools. */
|
|
27
|
+
export const domainErrorCases = [
|
|
28
|
+
renderUnknownHandshake,
|
|
29
|
+
consumeUnknownSession,
|
|
30
|
+
getSessionUnknownSession,
|
|
31
|
+
updateUnknownSession,
|
|
32
|
+
amendUnknownSession,
|
|
33
|
+
emitUnknownSession,
|
|
34
|
+
];
|
|
35
|
+
function errorText(err) {
|
|
36
|
+
return err instanceof Error ? err.message : String(err);
|
|
37
|
+
}
|
|
38
|
+
/** Grade one raw result against a case; `undefined` = every criterion held. */
|
|
39
|
+
function grade(testCase, result) {
|
|
40
|
+
const { name } = testCase;
|
|
41
|
+
if (result.isError !== true) {
|
|
42
|
+
return { name, criterion: 'isError', expected: true, actual: result.isError };
|
|
43
|
+
}
|
|
44
|
+
const first = result.content[0];
|
|
45
|
+
if (first === undefined || first.type !== 'text' || typeof first.text !== 'string') {
|
|
46
|
+
return { name, criterion: 'content-text', expected: 'content[0] of type text', actual: first };
|
|
47
|
+
}
|
|
48
|
+
const lead = `${testCase.expect.code}: `;
|
|
49
|
+
if (!first.text.startsWith(lead)) {
|
|
50
|
+
return { name, criterion: 'slug-leads', expected: `${lead}<detail>`, actual: first.text };
|
|
51
|
+
}
|
|
52
|
+
if (result.structuredContent !== undefined) {
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
criterion: 'no-structuredContent',
|
|
56
|
+
expected: undefined,
|
|
57
|
+
actual: result.structuredContent,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (result._meta !== undefined) {
|
|
61
|
+
return { name, criterion: 'no-meta', expected: undefined, actual: result._meta };
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
export async function runDomainErrorConformance(driver, options = {}) {
|
|
66
|
+
const passed = [];
|
|
67
|
+
const failed = [];
|
|
68
|
+
const skipped = [];
|
|
69
|
+
for (const testCase of options.cases ?? domainErrorCases) {
|
|
70
|
+
let result;
|
|
71
|
+
try {
|
|
72
|
+
result = await driver(testCase.scenario);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
failed.push({
|
|
76
|
+
name: testCase.name,
|
|
77
|
+
criterion: 'driver-threw',
|
|
78
|
+
expected: 'a raw tools/call result',
|
|
79
|
+
actual: errorText(err),
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (result === null) {
|
|
84
|
+
skipped.push({
|
|
85
|
+
name: testCase.name,
|
|
86
|
+
reason: `the driver returned null — \`${testCase.scenario.tool}\` is not bound on this deployment`,
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const mismatch = grade(testCase, result);
|
|
91
|
+
if (mismatch === undefined)
|
|
92
|
+
passed.push(testCase.name);
|
|
93
|
+
else
|
|
94
|
+
failed.push(mismatch);
|
|
95
|
+
}
|
|
96
|
+
return { passed, failed, skipped };
|
|
97
|
+
}
|
package/dist/fixtures/index.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export { consumeBufferFixtures } from './consume-buffer/index.js';
|
|
|
108
108
|
export { hostContextFixtures } from './host-context/index.js';
|
|
109
109
|
export { reservedChannelAuthorityFixtures } from './reserved-channel-authority/index.js';
|
|
110
110
|
export { schemaVersionHandshakeFixtures } from './schema-version-handshake/index.js';
|
|
111
|
-
export {
|
|
111
|
+
export { subscribeAppScopeFixtures } from './subscribe-app-scope/index.js';
|
|
112
112
|
import type { TestCase } from '../types.js';
|
|
113
113
|
/**
|
|
114
114
|
* Contract slugs — match the sub-module directory names. The
|
|
@@ -116,7 +116,7 @@ import type { TestCase } from '../types.js';
|
|
|
116
116
|
* Extensibly-closed via `(string & {})` so additive sub-modules
|
|
117
117
|
* register without an API break.
|
|
118
118
|
*/
|
|
119
|
-
export type ContractSlug = 'bootstrap-protocol' | 'consume-buffer' | 'host-context' | 'reserved-channel-authority' | 'schema-version-handshake' | 'subscribe-
|
|
119
|
+
export type ContractSlug = 'bootstrap-protocol' | 'consume-buffer' | 'host-context' | 'reserved-channel-authority' | 'schema-version-handshake' | 'subscribe-app-scope' | (string & {});
|
|
120
120
|
/**
|
|
121
121
|
* Every fixture the kit ships, grouped by the contract it exercises.
|
|
122
122
|
* Order within each group matches the sub-module's `export` order.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixtures/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwGG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,gCAAgC,EAAE,MAAM,uCAAuC,CAAC;AACzF,OAAO,EAAE,8BAA8B,EAAE,MAAM,qCAAqC,CAAC;AACrF,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixtures/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwGG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,gCAAgC,EAAE,MAAM,uCAAuC,CAAC;AACzF,OAAO,EAAE,8BAA8B,EAAE,MAAM,qCAAqC,CAAC;AACrF,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAQ5C;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GACpB,oBAAoB,GACpB,gBAAgB,GAChB,cAAc,GACd,4BAA4B,GAC5B,0BAA0B,GAC1B,qBAAqB,GACrB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,SAAS,QAAQ,EAAE,CAAC,CAOlF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,QAAQ,EAGwB,CAAC"}
|
package/dist/fixtures/index.js
CHANGED
|
@@ -108,13 +108,13 @@ export { consumeBufferFixtures } from './consume-buffer/index.js';
|
|
|
108
108
|
export { hostContextFixtures } from './host-context/index.js';
|
|
109
109
|
export { reservedChannelAuthorityFixtures } from './reserved-channel-authority/index.js';
|
|
110
110
|
export { schemaVersionHandshakeFixtures } from './schema-version-handshake/index.js';
|
|
111
|
-
export {
|
|
111
|
+
export { subscribeAppScopeFixtures } from './subscribe-app-scope/index.js';
|
|
112
112
|
import { bootstrapProtocolFixtures } from './bootstrap-protocol/index.js';
|
|
113
113
|
import { consumeBufferFixtures } from './consume-buffer/index.js';
|
|
114
114
|
import { hostContextFixtures } from './host-context/index.js';
|
|
115
115
|
import { reservedChannelAuthorityFixtures } from './reserved-channel-authority/index.js';
|
|
116
116
|
import { schemaVersionHandshakeFixtures } from './schema-version-handshake/index.js';
|
|
117
|
-
import {
|
|
117
|
+
import { subscribeAppScopeFixtures } from './subscribe-app-scope/index.js';
|
|
118
118
|
/**
|
|
119
119
|
* Every fixture the kit ships, grouped by the contract it exercises.
|
|
120
120
|
* Order within each group matches the sub-module's `export` order.
|
|
@@ -129,7 +129,7 @@ export const fixturesByContract = {
|
|
|
129
129
|
'host-context': hostContextFixtures,
|
|
130
130
|
'reserved-channel-authority': reservedChannelAuthorityFixtures,
|
|
131
131
|
'schema-version-handshake': schemaVersionHandshakeFixtures,
|
|
132
|
-
'subscribe-
|
|
132
|
+
'subscribe-app-scope': subscribeAppScopeFixtures,
|
|
133
133
|
};
|
|
134
134
|
/**
|
|
135
135
|
* Every fixture flat, in deterministic lexicographic order by
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "absent-appid-defaults",
|
|
3
|
-
"description": "A subscribe that OMITS `appId` resolves the caller's identity-default app and acks. SPEC §12.2's subscribe-payload table makes `appId` optional: absent ⇒ the server resolves it from the connection's authenticated identity (token binding, else identity mapping, else the deployment default) — clients never invent the default. Under the kit's harness the identity-default is the conventional
|
|
3
|
+
"description": "A subscribe that OMITS `appId` resolves the caller's identity-default app and acks. SPEC §12.2's subscribe-payload table makes `appId` optional: absent ⇒ the server resolves it from the connection's authenticated identity (token binding, else identity mapping, else the deployment default) — clients never invent the default. Under the kit's harness the identity-default is the conventional app 'conformance', so the provision-on-subscribe path must bind exactly that value to the fresh render row — a real string app id, never an undefined one (the corrupt-row failure mode this fixture locks out: a silent ack whose provisioned row carries appId undefined, unreachable by any later legal subscribe). The runner-owned subscribe frame IS the probe (`subscribe.omitAppId` drops the runner's conventional appId stamp; the inputEnvelope is descriptive, not dispatched); graded as a session-state read-back of the bound `appId` through the host's readSessionField seam. The §12.2.3 APP_MISMATCH app-scope gate is the sibling `app-mismatch` fixture's claim and still applies to the RESOLVED value.",
|
|
4
4
|
"skipReason": null,
|
|
5
5
|
"setup": [],
|
|
6
6
|
"subscribe": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "app-mismatch",
|
|
3
|
-
"description": "The GguiSession exists but is bound to a DIFFERENT app than the subscriber claims: `create-session` binds appId 'conformance-other', while the kit's runner always subscribes with appId 'conformance'. SPEC §12.2's subscribe-payload table makes the
|
|
3
|
+
"description": "The GguiSession exists but is bound to a DIFFERENT app than the subscriber claims: `create-session` binds appId 'conformance-other', while the kit's runner always subscribes with appId 'conformance'. SPEC §12.2's subscribe-payload table makes the app-scope check a MUST — appId 'MUST match the GguiSession's bound appId or subscribe fails APP_MISMATCH' — and §12.2.3 keeps the code distinct from SESSION_NOT_FOUND (the GguiSession exists; it is reachable only from a different app), so clients route 'fix your appId / API key' differently from 're-handshake'. The runner-owned subscribe frame IS the probe (the inputEnvelope is descriptive, not dispatched); expected: an `error` frame with `payload.code === 'APP_MISMATCH'`.",
|
|
4
4
|
"skipReason": null,
|
|
5
5
|
"setup": [
|
|
6
6
|
{
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TestCase } from '../../types.js';
|
|
2
|
+
/** All fixtures asserting subscribe-time app scope (SPEC §12.2 field
|
|
3
|
+
* table — identity-default resolution on absent `appId` — and
|
|
4
|
+
* §12.2.3 `APP_MISMATCH`). */
|
|
5
|
+
export declare const subscribeAppScopeFixtures: readonly TestCase[];
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/fixtures/subscribe-app-scope/index.ts"],"names":[],"mappings":"AAwDA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;+BAE+B;AAC/B,eAAO,MAAM,yBAAyB,EAAE,SAAS,QAAQ,EAGxD,CAAC"}
|