@aexhq/sdk 1.0.5-canary → 1.0.7-canary
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/dist/_contracts/schemas/wire.js +12 -0
- package/dist/_contracts/testing/response-bindings.d.ts +45 -0
- package/dist/_contracts/testing/response-bindings.js +256 -0
- package/dist/_contracts/testing/wire-conformance-entry.d.ts +10 -0
- package/dist/_contracts/testing/wire-conformance-entry.js +8 -0
- package/dist/_contracts/testing/wire-conformance.d.ts +169 -0
- package/dist/_contracts/testing/wire-conformance.js +276 -0
- package/dist/cli.mjs +8 -0
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -131,6 +131,18 @@ export function errorFromZod(error) {
|
|
|
131
131
|
}
|
|
132
132
|
/** Parse `input` against `schema`, throwing the family's own error text on failure. */
|
|
133
133
|
export function parseWire(schema, input) {
|
|
134
|
+
const meta = wireObjectMeta.get(schema);
|
|
135
|
+
if (meta && input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
136
|
+
// Zod deliberately skips `__proto__` while collecting unknown keys so a
|
|
137
|
+
// plain-object output cannot have its prototype replaced. That safety
|
|
138
|
+
// rule must not turn an attacker-controlled field into an accepted field;
|
|
139
|
+
// recover the strict wire-object diagnostic before Zod sees the input.
|
|
140
|
+
const unknownKey = Object.keys(input).find((key) => !meta.permitted.includes(key));
|
|
141
|
+
if (unknownKey !== undefined) {
|
|
142
|
+
const diagnostic = meta.unknownKey(meta.resolvePath([]), unknownKey, meta.permitted);
|
|
143
|
+
throw typeof diagnostic === "string" ? new Error(diagnostic) : diagnostic;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
134
146
|
const result = z.safeParse(schema, input);
|
|
135
147
|
if (!result.success) {
|
|
136
148
|
throw structuredError(schema, result.error) ?? errorFromZod(result.error);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ResponseSchemaBinding } from "./wire-conformance.js";
|
|
2
|
+
/**
|
|
3
|
+
* Turn a dispatch RegExp into a MATCH pattern for
|
|
4
|
+
* {@link import("./wire-conformance.js").pathMatches}.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately not the OpenAPI path form: `pathMatches` only asks whether a
|
|
7
|
+
* segment is a `{…}` placeholder, so naming the parameter would be decoration
|
|
8
|
+
* that has to agree with a second implementation. Cross-reference to the
|
|
9
|
+
* generated document goes through the operation NAME, which both sides carry.
|
|
10
|
+
*/
|
|
11
|
+
export declare function routeMatchTemplate(pattern: RegExp): string;
|
|
12
|
+
export interface UnschemadRoute {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly reason: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Data-plane routes with NO response schema, and why not.
|
|
18
|
+
*
|
|
19
|
+
* Every entry is here because a JSON response schema cannot describe what the
|
|
20
|
+
* route returns, or because the shape belongs to another package. None is here
|
|
21
|
+
* because it was awkward: an empty schema that asserts nothing would be worse
|
|
22
|
+
* than this list, since it would inflate the "validated" count with routes
|
|
23
|
+
* nothing was checked on.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ROUTES_WITHOUT_RESPONSE_SCHEMA: readonly UnschemadRoute[];
|
|
26
|
+
/**
|
|
27
|
+
* Routes that HAVE a schema but that no client in this repository can produce.
|
|
28
|
+
*
|
|
29
|
+
* They will appear in `unexercised` on every run, forever, and that is not a
|
|
30
|
+
* gap in test coverage — there is no call site to add a test to. Stated so a
|
|
31
|
+
* reader of the report is not misled in the other direction from a false green.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ROUTES_OFF_THE_SDK_SEAM: readonly UnschemadRoute[];
|
|
34
|
+
/** Every data-plane route that has a response schema, bound to it. */
|
|
35
|
+
export declare const DATA_PLANE_RESPONSE_SCHEMAS: readonly ResponseSchemaBinding[];
|
|
36
|
+
/**
|
|
37
|
+
* Render the STATIC coverage picture — what could be checked, before a single
|
|
38
|
+
* response arrives.
|
|
39
|
+
*
|
|
40
|
+
* Complements `formatWireConformanceReport`, which renders what actually was.
|
|
41
|
+
* Both belong in a suite's output: the dynamic report alone cannot tell a reader
|
|
42
|
+
* whether the 40 operations it validated are most of the surface or a third of
|
|
43
|
+
* it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatResponseSchemaCoverage(): string;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { AUTHENTICATED_API_ROUTE_DESCRIPTORS } from "../api-routes.js";
|
|
2
|
+
import { NoContentResponseSchema } from "../schemas/response-common.js";
|
|
3
|
+
import { AssetFinalizeResponseSchema, AssetMpuAbortResponseSchema, AssetMpuPresignPartsResponseSchema, AssetPresignResponseSchema } from "../schemas/response-assets.js";
|
|
4
|
+
import { AdminBillingAccountTypeResponseSchema, AdminBillingPaymentMethodResponseSchema, AdminBillingTopupResponseSchema, BillingAutoTopupResponseSchema, BillingHostedSessionResponseSchema, BillingLedgerResponseSchema, BillingStatementListResponseSchema, BillingSummaryResponseSchema } from "../schemas/response-billing.js";
|
|
5
|
+
import { WhoAmIResponseSchema } from "../schemas/response-identity.js";
|
|
6
|
+
import { McpServerListResponseSchema, McpServerResponseSchema } from "../schemas/response-mcp-servers.js";
|
|
7
|
+
import { SecretListResponseSchema, SecretResponseSchema } from "../schemas/response-secrets.js";
|
|
8
|
+
import { AcknowledgedResponseSchema, CoordinatorTicketResponseSchema, EventArchiveLinkResponseSchema, SessionChildrenResponseSchema, SessionDeleteResponseSchema, SessionEnvelopeResponseSchema, SessionEventsPageResponseSchema, SessionFileLinkResponseSchema, SessionFilesResponseSchema, SessionListResponseSchema, SessionMessageAcceptedResponseSchema, SessionMessagesPageResponseSchema, SessionWebhookDeliveriesResponseSchema } from "../schemas/response-sessions.js";
|
|
9
|
+
import { ChildFinalizeResponseSchema, ChildResultResponseSchema, SessionOtlpResponseSchema } from "../schemas/response-sessions-internal.js";
|
|
10
|
+
import { WebhookSigningSecretResponseSchema, WorkspaceWebhookDeliveriesResponseSchema } from "../schemas/response-webhooks.js";
|
|
11
|
+
import { WorkspaceEraseResponseSchema, WorkspaceFilePageResponseSchema, WorkspaceFileResponseSchema, WorkspaceInstructionPageResponseSchema, WorkspaceInstructionResponseSchema, WorkspaceSkillPageResponseSchema, WorkspaceSkillResponseSchema, WorkspaceToolPageResponseSchema, WorkspaceToolResponseSchema } from "../schemas/response-workspace.js";
|
|
12
|
+
/**
|
|
13
|
+
* Turn a dispatch RegExp into a MATCH pattern for
|
|
14
|
+
* {@link import("./wire-conformance.js").pathMatches}.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately not the OpenAPI path form: `pathMatches` only asks whether a
|
|
17
|
+
* segment is a `{…}` placeholder, so naming the parameter would be decoration
|
|
18
|
+
* that has to agree with a second implementation. Cross-reference to the
|
|
19
|
+
* generated document goes through the operation NAME, which both sides carry.
|
|
20
|
+
*/
|
|
21
|
+
export function routeMatchTemplate(pattern) {
|
|
22
|
+
const source = pattern.source
|
|
23
|
+
.replace(/^\^/, "")
|
|
24
|
+
.replace(/\$$/, "")
|
|
25
|
+
// Collapse variable segments BEFORE unescaping: `[^/]+` contains a literal
|
|
26
|
+
// `/`, so unescaping first tears the class in two.
|
|
27
|
+
.replace(/\[\^\\?\/\]\+/g, "{param}")
|
|
28
|
+
.replace(/\\\//g, "/");
|
|
29
|
+
return source;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The path the harness sees, which is NOT the path the route table declares.
|
|
33
|
+
*
|
|
34
|
+
* `api-routes.ts` describes the surface as the lambda dispatches it (`/sessions`);
|
|
35
|
+
* every client reaches it under an `/api` prefix, and `HttpClient` reports
|
|
36
|
+
* `url.pathname`. The generated OpenAPI document applies the same prefix.
|
|
37
|
+
*/
|
|
38
|
+
const API_PREFIX = "/api";
|
|
39
|
+
/**
|
|
40
|
+
* Operation name -> the schema its 2xx JSON body must satisfy.
|
|
41
|
+
*
|
|
42
|
+
* Keyed by name so this table and the route table are joined by the identifier
|
|
43
|
+
* both already carry, rather than by a re-stated method and path.
|
|
44
|
+
*/
|
|
45
|
+
const RESPONSE_SCHEMA_BY_OPERATION = {
|
|
46
|
+
whoami: WhoAmIResponseSchema,
|
|
47
|
+
// Sessions — every state change answers a `{ session }` envelope.
|
|
48
|
+
"sessions.create": SessionEnvelopeResponseSchema,
|
|
49
|
+
"sessions.get": SessionEnvelopeResponseSchema,
|
|
50
|
+
"sessions.suspend": SessionEnvelopeResponseSchema,
|
|
51
|
+
"sessions.cancel": SessionEnvelopeResponseSchema,
|
|
52
|
+
"sessions.resume": SessionEnvelopeResponseSchema,
|
|
53
|
+
"sessions.requestApproval": SessionEnvelopeResponseSchema,
|
|
54
|
+
"sessions.approve": SessionEnvelopeResponseSchema,
|
|
55
|
+
"sessions.deny": SessionEnvelopeResponseSchema,
|
|
56
|
+
"sessions.list": SessionListResponseSchema,
|
|
57
|
+
"sessions.sendMessage": SessionMessageAcceptedResponseSchema,
|
|
58
|
+
"sessions.delete": SessionDeleteResponseSchema,
|
|
59
|
+
"sessions.listMessages": SessionMessagesPageResponseSchema,
|
|
60
|
+
"sessions.listEvents": SessionEventsPageResponseSchema,
|
|
61
|
+
"sessions.eventsTicket": CoordinatorTicketResponseSchema,
|
|
62
|
+
"sessions.listChildren": SessionChildrenResponseSchema,
|
|
63
|
+
"sessions.eventArchiveLink": EventArchiveLinkResponseSchema,
|
|
64
|
+
"sessions.listFiles": SessionFilesResponseSchema,
|
|
65
|
+
"sessions.fileLink": SessionFileLinkResponseSchema,
|
|
66
|
+
"sessions.listWebhookDeliveries": SessionWebhookDeliveriesResponseSchema,
|
|
67
|
+
"sessions.redeliverWebhook": AcknowledgedResponseSchema,
|
|
68
|
+
"sessions.otel": SessionOtlpResponseSchema,
|
|
69
|
+
"sessions.childResult": ChildResultResponseSchema,
|
|
70
|
+
"sessions.finalize": ChildFinalizeResponseSchema,
|
|
71
|
+
// Assets.
|
|
72
|
+
"assets.presign": AssetPresignResponseSchema,
|
|
73
|
+
"assets.finalize": AssetFinalizeResponseSchema,
|
|
74
|
+
"assets.mpuPresignParts": AssetMpuPresignPartsResponseSchema,
|
|
75
|
+
"assets.mpuAbort": AssetMpuAbortResponseSchema,
|
|
76
|
+
"assets.delete": NoContentResponseSchema,
|
|
77
|
+
// Versioned workspace resources.
|
|
78
|
+
"workspace.files.publish": WorkspaceFileResponseSchema,
|
|
79
|
+
"workspace.files.get": WorkspaceFileResponseSchema,
|
|
80
|
+
"workspace.files.list": WorkspaceFilePageResponseSchema,
|
|
81
|
+
"workspace.files.delete": NoContentResponseSchema,
|
|
82
|
+
"workspace.skills.publish": WorkspaceSkillResponseSchema,
|
|
83
|
+
"workspace.skills.get": WorkspaceSkillResponseSchema,
|
|
84
|
+
"workspace.skills.list": WorkspaceSkillPageResponseSchema,
|
|
85
|
+
"workspace.skills.delete": NoContentResponseSchema,
|
|
86
|
+
"workspace.tools.publish": WorkspaceToolResponseSchema,
|
|
87
|
+
"workspace.tools.get": WorkspaceToolResponseSchema,
|
|
88
|
+
"workspace.tools.list": WorkspaceToolPageResponseSchema,
|
|
89
|
+
"workspace.tools.delete": NoContentResponseSchema,
|
|
90
|
+
"workspace.instructions.publish": WorkspaceInstructionResponseSchema,
|
|
91
|
+
"workspace.instructions.get": WorkspaceInstructionResponseSchema,
|
|
92
|
+
"workspace.instructions.list": WorkspaceInstructionPageResponseSchema,
|
|
93
|
+
"workspace.instructions.delete": NoContentResponseSchema,
|
|
94
|
+
// Workspace secret store.
|
|
95
|
+
"secrets.create": SecretResponseSchema,
|
|
96
|
+
"secrets.get": SecretResponseSchema,
|
|
97
|
+
"secrets.rotate": SecretResponseSchema,
|
|
98
|
+
"secrets.list": SecretListResponseSchema,
|
|
99
|
+
"secrets.delete": NoContentResponseSchema,
|
|
100
|
+
// Workspace MCP server config.
|
|
101
|
+
"mcpServers.create": McpServerResponseSchema,
|
|
102
|
+
"mcpServers.get": McpServerResponseSchema,
|
|
103
|
+
"mcpServers.list": McpServerListResponseSchema,
|
|
104
|
+
"mcpServers.delete": NoContentResponseSchema,
|
|
105
|
+
// Billing.
|
|
106
|
+
"billing.get": BillingSummaryResponseSchema,
|
|
107
|
+
"billing.ledger": BillingLedgerResponseSchema,
|
|
108
|
+
"billing.topupCheckout": BillingHostedSessionResponseSchema,
|
|
109
|
+
"billing.autoTopup": BillingAutoTopupResponseSchema,
|
|
110
|
+
"billing.statements": BillingStatementListResponseSchema,
|
|
111
|
+
"billing.portal": BillingHostedSessionResponseSchema,
|
|
112
|
+
"adminBilling.topup": AdminBillingTopupResponseSchema,
|
|
113
|
+
"adminBilling.paymentMethod": AdminBillingPaymentMethodResponseSchema,
|
|
114
|
+
"adminBilling.accountType": AdminBillingAccountTypeResponseSchema,
|
|
115
|
+
// Workspace webhooks and GDPR erase.
|
|
116
|
+
"webhook.signingSecret": WebhookSigningSecretResponseSchema,
|
|
117
|
+
"webhook.listDeliveries": WorkspaceWebhookDeliveriesResponseSchema,
|
|
118
|
+
"workspaces.erase": WorkspaceEraseResponseSchema
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Data-plane routes with NO response schema, and why not.
|
|
122
|
+
*
|
|
123
|
+
* Every entry is here because a JSON response schema cannot describe what the
|
|
124
|
+
* route returns, or because the shape belongs to another package. None is here
|
|
125
|
+
* because it was awkward: an empty schema that asserts nothing would be worse
|
|
126
|
+
* than this list, since it would inflate the "validated" count with routes
|
|
127
|
+
* nothing was checked on.
|
|
128
|
+
*/
|
|
129
|
+
export const ROUTES_WITHOUT_RESPONSE_SCHEMA = [
|
|
130
|
+
{
|
|
131
|
+
name: "sessions.downloadFile",
|
|
132
|
+
reason: "Not JSON on success: a small file is 200 raw bytes with the FILE's own content-type, " +
|
|
133
|
+
"a large one is a 302 to presigned storage."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "sessions.archiveInternal",
|
|
137
|
+
reason: "Not JSON on success: 302 with an empty body and content-type application/zip."
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "runtime.writerAuthority",
|
|
141
|
+
reason: "204 with no body and no headers at all, and writer-token only — it is a liveness probe " +
|
|
142
|
+
"for the writer baton, not a bearer/SDK surface."
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "runtime.journalCommit",
|
|
146
|
+
reason: "Writer-token only, and its 200 body is the platform's journal control-item shape " +
|
|
147
|
+
"(lambda-runtime-contracts), not a shape this package declares. Three of its six ops " +
|
|
148
|
+
"answer 204 with no body."
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "billing.statement",
|
|
152
|
+
reason: "Not always JSON: the single statement is content-negotiated — `application/pdf` and " +
|
|
153
|
+
"`text/plain` renders of the same document alongside the JSON default — and the JSON " +
|
|
154
|
+
"body's `document` is the platform's stored render, not a shape this package declares. " +
|
|
155
|
+
"The LIST at `billing.statements` is plain JSON and IS bound."
|
|
156
|
+
}
|
|
157
|
+
];
|
|
158
|
+
/**
|
|
159
|
+
* Routes that HAVE a schema but that no client in this repository can produce.
|
|
160
|
+
*
|
|
161
|
+
* They will appear in `unexercised` on every run, forever, and that is not a
|
|
162
|
+
* gap in test coverage — there is no call site to add a test to. Stated so a
|
|
163
|
+
* reader of the report is not misled in the other direction from a false green.
|
|
164
|
+
*/
|
|
165
|
+
export const ROUTES_OFF_THE_SDK_SEAM = [
|
|
166
|
+
{
|
|
167
|
+
name: "sessions.otel",
|
|
168
|
+
reason: "`getSessionOtlpPage` reads the body through `HttpClient.download()`, which reports only " +
|
|
169
|
+
"its FAILURES to the wire observer — its 2xx body is handed back as a `Response` for the " +
|
|
170
|
+
"caller to read, and reporting it would mean cloning every download. Its error envelope " +
|
|
171
|
+
"IS checked; its success body is what stays unobserved."
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "sessions.childResult",
|
|
175
|
+
reason: "Writer-token only; called by the in-container subagent runtime, not by `HttpClient`."
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "sessions.finalize",
|
|
179
|
+
reason: "Writer-token only; the child-settle hop, called from inside the runtime."
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "mcpServers.create",
|
|
183
|
+
reason: "No client function exists in `operations.ts` — the dashboard BFF reaches it directly."
|
|
184
|
+
},
|
|
185
|
+
{ name: "mcpServers.list", reason: "No client function exists in `operations.ts`." },
|
|
186
|
+
{ name: "mcpServers.get", reason: "No client function exists in `operations.ts`." },
|
|
187
|
+
{ name: "mcpServers.delete", reason: "No client function exists in `operations.ts`." },
|
|
188
|
+
{
|
|
189
|
+
name: "webhook.listDeliveries",
|
|
190
|
+
reason: "No client function exists in `operations.ts`; only the session-scoped " +
|
|
191
|
+
"`sessions.listWebhookDeliveries` has one."
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "billing.statements",
|
|
195
|
+
reason: "No client function exists in `operations.ts`. The descriptor is what makes the route " +
|
|
196
|
+
"reachable at all — the platform 404s an undeclared path before its handler ladder runs — " +
|
|
197
|
+
"and the dashboard reaches it directly."
|
|
198
|
+
},
|
|
199
|
+
{ name: "adminBilling.topup", reason: "Operator route; no client function in `operations.ts`." },
|
|
200
|
+
{
|
|
201
|
+
name: "adminBilling.paymentMethod",
|
|
202
|
+
reason: "Operator route; no client function in `operations.ts`."
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "adminBilling.accountType",
|
|
206
|
+
reason: "Operator route; no client function in `operations.ts`."
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "workspaces.erase",
|
|
210
|
+
reason: "No DATA-plane client function. `deleteWorkspace()` in `operations.ts` calls the " +
|
|
211
|
+
"identically-shaped CONTROL-plane path — see the plane-collision note above."
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
function bindingFor(descriptor) {
|
|
215
|
+
const schema = RESPONSE_SCHEMA_BY_OPERATION[descriptor.name];
|
|
216
|
+
if (schema === undefined) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
method: descriptor.method.toUpperCase(),
|
|
221
|
+
path: `${API_PREFIX}${routeMatchTemplate(descriptor.pattern)}`,
|
|
222
|
+
name: descriptor.name,
|
|
223
|
+
schema
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Every data-plane route that has a response schema, bound to it. */
|
|
227
|
+
export const DATA_PLANE_RESPONSE_SCHEMAS = AUTHENTICATED_API_ROUTE_DESCRIPTORS.flatMap((descriptor) => {
|
|
228
|
+
const binding = bindingFor(descriptor);
|
|
229
|
+
return binding === undefined ? [] : [binding];
|
|
230
|
+
});
|
|
231
|
+
/**
|
|
232
|
+
* Render the STATIC coverage picture — what could be checked, before a single
|
|
233
|
+
* response arrives.
|
|
234
|
+
*
|
|
235
|
+
* Complements `formatWireConformanceReport`, which renders what actually was.
|
|
236
|
+
* Both belong in a suite's output: the dynamic report alone cannot tell a reader
|
|
237
|
+
* whether the 40 operations it validated are most of the surface or a third of
|
|
238
|
+
* it.
|
|
239
|
+
*/
|
|
240
|
+
export function formatResponseSchemaCoverage() {
|
|
241
|
+
const total = AUTHENTICATED_API_ROUTE_DESCRIPTORS.length;
|
|
242
|
+
const bound = DATA_PLANE_RESPONSE_SCHEMAS.length;
|
|
243
|
+
const lines = [
|
|
244
|
+
`response schemas: ${bound}/${total} data-plane route(s) have one`,
|
|
245
|
+
` NO SCHEMA (${ROUTES_WITHOUT_RESPONSE_SCHEMA.length}):`
|
|
246
|
+
];
|
|
247
|
+
for (const route of ROUTES_WITHOUT_RESPONSE_SCHEMA) {
|
|
248
|
+
lines.push(` - ${route.name}: ${route.reason}`);
|
|
249
|
+
}
|
|
250
|
+
lines.push(` SCHEMA BUT UNREACHABLE from any client in this repo — expect these in ` +
|
|
251
|
+
`"NOT EXERCISED" forever (${ROUTES_OFF_THE_SDK_SEAM.length}):`);
|
|
252
|
+
for (const route of ROUTES_OFF_THE_SDK_SEAM) {
|
|
253
|
+
lines.push(` - ${route.name}: ${route.reason}`);
|
|
254
|
+
}
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The minimal C4 entrypoint copied into the SDK's private inlined contracts.
|
|
3
|
+
*
|
|
4
|
+
* Keep the broader `@aexhq/contracts/testing` kit out of customer packages;
|
|
5
|
+
* live user tests only need the response schemas and observer installer.
|
|
6
|
+
*/
|
|
7
|
+
export { installWireConformance, formatWireConformanceReport, mergeWireConformanceReports, pathMatches, wireOrigin } from "./wire-conformance.js";
|
|
8
|
+
export type { ResponseSchemaBinding, WireConformanceOptions, WireConformanceReport, WireConformanceViolation } from "./wire-conformance.js";
|
|
9
|
+
export { DATA_PLANE_RESPONSE_SCHEMAS, ROUTES_WITHOUT_RESPONSE_SCHEMA, ROUTES_OFF_THE_SDK_SEAM, formatResponseSchemaCoverage } from "./response-bindings.js";
|
|
10
|
+
export type { UnschemadRoute } from "./response-bindings.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The minimal C4 entrypoint copied into the SDK's private inlined contracts.
|
|
3
|
+
*
|
|
4
|
+
* Keep the broader `@aexhq/contracts/testing` kit out of customer packages;
|
|
5
|
+
* live user tests only need the response schemas and observer installer.
|
|
6
|
+
*/
|
|
7
|
+
export { installWireConformance, formatWireConformanceReport, mergeWireConformanceReports, pathMatches, wireOrigin } from "./wire-conformance.js";
|
|
8
|
+
export { DATA_PLANE_RESPONSE_SCHEMAS, ROUTES_WITHOUT_RESPONSE_SCHEMA, ROUTES_OFF_THE_SDK_SEAM, formatResponseSchemaCoverage } from "./response-bindings.js";
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C4 — the wire-conformance harness.
|
|
3
|
+
*
|
|
4
|
+
* Validates the bytes a real server actually returned against the response
|
|
5
|
+
* schemas. This is the only gate in the contract pipeline that observes reality
|
|
6
|
+
* rather than comparing two of our own artefacts to each other, and it is the
|
|
7
|
+
* one the plan flags as easiest to skip and most costly to skip.
|
|
8
|
+
*
|
|
9
|
+
* It attaches to `HttpClient` through {@link observeWireResponses}, so every
|
|
10
|
+
* response the live and user-test suites already receive is checked — no new
|
|
11
|
+
* suite, no new requests, no server changes.
|
|
12
|
+
*
|
|
13
|
+
* **Coverage is reported, not assumed.** A green run over 12 of 120 routes is
|
|
14
|
+
* not a verified surface, and calling it one is the specific failure this
|
|
15
|
+
* harness exists to prevent. {@link WireConformanceReport} names the routes that
|
|
16
|
+
* were checked, the routes that have a schema but were never exercised, the
|
|
17
|
+
* responses that arrived with no schema to check them against, and the responses
|
|
18
|
+
* that were deliberately skipped because they came from another plane.
|
|
19
|
+
*
|
|
20
|
+
* ## What it checks, and what it used to miss
|
|
21
|
+
*
|
|
22
|
+
* - **2xx bodies** against the operation's response schema.
|
|
23
|
+
* - **non-2xx bodies** against {@link ApiErrorEnvelopeSchema}. Every generated
|
|
24
|
+
* operation declares that envelope, and until `HttpClient` learned to report
|
|
25
|
+
* before it throws, nothing had ever checked it.
|
|
26
|
+
*
|
|
27
|
+
* ## The plane collision, and how it is closed
|
|
28
|
+
*
|
|
29
|
+
* A PATH does not identify a route. The control plane serves different bodies at
|
|
30
|
+
* two of the data plane's paths (`GET /api/whoami`,
|
|
31
|
+
* `DELETE /api/workspaces/{id}`), so a process driving both planes — the CLI
|
|
32
|
+
* does, `aex login` is control-plane — would validate a control-plane body
|
|
33
|
+
* against a data-plane schema and report a violation that is not one.
|
|
34
|
+
* {@link WireResponse} therefore carries the ORIGIN it came from, and
|
|
35
|
+
* {@link WireConformanceOptions.origin} says which plane the bindings describe.
|
|
36
|
+
* Responses from anywhere else are counted and named under
|
|
37
|
+
* {@link WireConformanceReport.offPlane} rather than silently dropped.
|
|
38
|
+
*
|
|
39
|
+
* Installing WITHOUT an origin is still allowed, because a data-plane-only suite
|
|
40
|
+
* has nothing to collide with — but the report says so in as many words, so a
|
|
41
|
+
* reader can tell a filtered run from an unfiltered one.
|
|
42
|
+
*/
|
|
43
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
44
|
+
/** A response schema bound to the route whose responses it describes. */
|
|
45
|
+
export interface ResponseSchemaBinding {
|
|
46
|
+
/** Uppercase HTTP method. */
|
|
47
|
+
readonly method: string;
|
|
48
|
+
/**
|
|
49
|
+
* Path pattern with `{param}` placeholders, matched segment-wise:
|
|
50
|
+
* `/api/sessions/{sessionId}/messages`.
|
|
51
|
+
*/
|
|
52
|
+
readonly path: string;
|
|
53
|
+
/** Operation id, used in the report. */
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly schema: StandardSchemaV1;
|
|
56
|
+
}
|
|
57
|
+
export interface WireConformanceOptions {
|
|
58
|
+
/**
|
|
59
|
+
* The plane the bindings describe, as an origin or any URL on it —
|
|
60
|
+
* `https://dev-api.aex.dev`, or the `AEX_API_URL` the suite already reads.
|
|
61
|
+
*
|
|
62
|
+
* Responses from any other origin are recorded in
|
|
63
|
+
* {@link WireConformanceReport.offPlane} and NOT validated. Omit only when the
|
|
64
|
+
* process provably drives one plane; the report states which case it was.
|
|
65
|
+
*/
|
|
66
|
+
readonly origin?: string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Schema for non-2xx bodies. Defaults to {@link ApiErrorEnvelopeSchema} — the
|
|
69
|
+
* shape the generated document declares as every operation's `default`
|
|
70
|
+
* response. Pass `null` to stop checking error bodies, which throws away the
|
|
71
|
+
* only assertion that covers 68 declared-but-unchecked responses; there is no
|
|
72
|
+
* good reason to.
|
|
73
|
+
*/
|
|
74
|
+
readonly errorEnvelope?: StandardSchemaV1 | null;
|
|
75
|
+
}
|
|
76
|
+
export interface WireConformanceViolation {
|
|
77
|
+
/**
|
|
78
|
+
* Which contract was broken: the operation's own 2xx schema, or the error
|
|
79
|
+
* envelope every operation declares.
|
|
80
|
+
*/
|
|
81
|
+
readonly kind: "response" | "error-envelope";
|
|
82
|
+
/** Operation id, or `METHOD /path` when no binding matched. */
|
|
83
|
+
readonly name: string;
|
|
84
|
+
readonly method: string;
|
|
85
|
+
readonly origin: string;
|
|
86
|
+
readonly path: string;
|
|
87
|
+
readonly status: number;
|
|
88
|
+
readonly issues: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* The body that failed, verbatim.
|
|
91
|
+
*
|
|
92
|
+
* Carried because a complaint without the bytes that caused it cannot be
|
|
93
|
+
* triaged — the reader has to be able to tell a server defect from a schema
|
|
94
|
+
* defect, and only the actual body settles that.
|
|
95
|
+
*/
|
|
96
|
+
readonly body: unknown;
|
|
97
|
+
}
|
|
98
|
+
export interface WireConformanceReport {
|
|
99
|
+
/**
|
|
100
|
+
* The origin responses were required to come from, or `undefined` when the
|
|
101
|
+
* harness was installed without a plane filter.
|
|
102
|
+
*/
|
|
103
|
+
readonly originFilter: string | undefined;
|
|
104
|
+
/** Operation ids whose 2xx responses were seen and validated. */
|
|
105
|
+
readonly validated: readonly string[];
|
|
106
|
+
/** Operation ids with a response schema that no request exercised. */
|
|
107
|
+
readonly unexercised: readonly string[];
|
|
108
|
+
/** `METHOD /path` for responses that arrived with no schema bound. */
|
|
109
|
+
readonly unschemad: readonly string[];
|
|
110
|
+
/** `METHOD /path -> status` for non-2xx bodies checked against the envelope. */
|
|
111
|
+
readonly errorsValidated: readonly string[];
|
|
112
|
+
/** `origin METHOD /path` for responses from a plane these bindings do not describe. */
|
|
113
|
+
readonly offPlane: readonly string[];
|
|
114
|
+
readonly violations: readonly WireConformanceViolation[];
|
|
115
|
+
/** Responses observed in total, including ones with no schema and off-plane ones. */
|
|
116
|
+
readonly observed: number;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Match a concrete path against a `{param}` pattern.
|
|
120
|
+
*
|
|
121
|
+
* Segment-wise rather than by regex so a placeholder cannot accidentally span a
|
|
122
|
+
* `/` and make two different routes look like one.
|
|
123
|
+
*/
|
|
124
|
+
export declare function pathMatches(pattern: string, actual: string): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Reduce whatever the caller had to hand — a base URL, a URL with a path, a bare
|
|
127
|
+
* origin — to the origin `WireResponse` reports.
|
|
128
|
+
*
|
|
129
|
+
* Throws rather than falling back to "match everything": an unparseable
|
|
130
|
+
* `AEX_API_URL` that silently disabled the filter would reintroduce exactly the
|
|
131
|
+
* false violations the filter exists to prevent, and would do it quietly.
|
|
132
|
+
*/
|
|
133
|
+
export declare function wireOrigin(baseUrl: string): string;
|
|
134
|
+
/**
|
|
135
|
+
* Start validating responses.
|
|
136
|
+
*
|
|
137
|
+
* Returns a handle carrying the report and a `stop()`. Validation is
|
|
138
|
+
* synchronous: a schema whose `~standard.validate` returns a promise is treated
|
|
139
|
+
* as unvalidatable and recorded as such rather than silently skipped, because
|
|
140
|
+
* `HttpClient` cannot await an observer without changing request timing.
|
|
141
|
+
*/
|
|
142
|
+
export declare function installWireConformance(bindings: readonly ResponseSchemaBinding[], options?: WireConformanceOptions): {
|
|
143
|
+
readonly report: () => WireConformanceReport;
|
|
144
|
+
readonly stop: () => void;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Fold reports from several processes into one.
|
|
148
|
+
*
|
|
149
|
+
* The suites that matter drive the SDK out of process — one bun child per
|
|
150
|
+
* scenario, one test process per file — so no single process sees the whole
|
|
151
|
+
* surface, and a per-process report would understate coverage by construction.
|
|
152
|
+
*
|
|
153
|
+
* `unexercised` is INTERSECTED, not concatenated: a name is unexercised overall
|
|
154
|
+
* only when every fragment failed to exercise it. Concatenating would report a
|
|
155
|
+
* route as unexercised because some other process did not happen to call it,
|
|
156
|
+
* which is the mirror image of the false-green this harness exists to prevent.
|
|
157
|
+
* That identity only holds when every fragment was produced from the same
|
|
158
|
+
* binding table; {@link installWireConformance} is the only producer.
|
|
159
|
+
*/
|
|
160
|
+
export declare function mergeWireConformanceReports(reports: readonly WireConformanceReport[]): WireConformanceReport;
|
|
161
|
+
/**
|
|
162
|
+
* Render the report for a suite's output.
|
|
163
|
+
*
|
|
164
|
+
* Always prints the unexercised and unschemad lists, including when there are no
|
|
165
|
+
* violations — a run that reports only "0 violations" invites the reader to
|
|
166
|
+
* conclude the surface is verified. A violation always prints the BODY that
|
|
167
|
+
* caused it as well as the schema's complaint, because triage needs both.
|
|
168
|
+
*/
|
|
169
|
+
export declare function formatWireConformanceReport(report: WireConformanceReport): string;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { ApiErrorEnvelopeSchema } from "../schemas/response-common.js";
|
|
2
|
+
import { observeWireResponses } from "../wire-observer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Match a concrete path against a `{param}` pattern.
|
|
5
|
+
*
|
|
6
|
+
* Segment-wise rather than by regex so a placeholder cannot accidentally span a
|
|
7
|
+
* `/` and make two different routes look like one.
|
|
8
|
+
*/
|
|
9
|
+
export function pathMatches(pattern, actual) {
|
|
10
|
+
const patternSegments = pattern.split("/");
|
|
11
|
+
const actualSegments = actual.split("/");
|
|
12
|
+
if (patternSegments.length !== actualSegments.length) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return patternSegments.every((segment, index) => (segment.startsWith("{") && segment.endsWith("}")) || segment === actualSegments[index]);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reduce whatever the caller had to hand — a base URL, a URL with a path, a bare
|
|
19
|
+
* origin — to the origin `WireResponse` reports.
|
|
20
|
+
*
|
|
21
|
+
* Throws rather than falling back to "match everything": an unparseable
|
|
22
|
+
* `AEX_API_URL` that silently disabled the filter would reintroduce exactly the
|
|
23
|
+
* false violations the filter exists to prevent, and would do it quietly.
|
|
24
|
+
*/
|
|
25
|
+
export function wireOrigin(baseUrl) {
|
|
26
|
+
try {
|
|
27
|
+
return new URL(baseUrl).origin;
|
|
28
|
+
}
|
|
29
|
+
catch (cause) {
|
|
30
|
+
throw new Error(`wire conformance: could not read an origin from ${JSON.stringify(baseUrl)} — ` +
|
|
31
|
+
`expected an absolute URL like "https://api.aex.dev"`, { cause });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function issuesOf(result) {
|
|
35
|
+
return (result.issues ?? []).map((issue) => {
|
|
36
|
+
const path = (issue.path ?? [])
|
|
37
|
+
.map((segment) => (typeof segment === "object" ? String(segment.key) : String(segment)))
|
|
38
|
+
.join(".");
|
|
39
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Which contract a status is judged against.
|
|
44
|
+
*
|
|
45
|
+
* The generated document is coarser than this — it declares `2XX` and a
|
|
46
|
+
* `default` that catches everything else — but judging a redirect against the
|
|
47
|
+
* error envelope would manufacture a violation out of an empty body, which is
|
|
48
|
+
* noise rather than a finding. A 3xx is recorded, named and not judged.
|
|
49
|
+
*
|
|
50
|
+
* In practice `fetch` follows redirects, so `HttpClient` sees the final
|
|
51
|
+
* response; this exists so that a caller who stops following them gets a
|
|
52
|
+
* comprehensible report rather than a wall of false failures.
|
|
53
|
+
*/
|
|
54
|
+
function contractFor(status) {
|
|
55
|
+
if (status < 300)
|
|
56
|
+
return "response";
|
|
57
|
+
if (status < 400)
|
|
58
|
+
return "redirect";
|
|
59
|
+
return "error-envelope";
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Start validating responses.
|
|
63
|
+
*
|
|
64
|
+
* Returns a handle carrying the report and a `stop()`. Validation is
|
|
65
|
+
* synchronous: a schema whose `~standard.validate` returns a promise is treated
|
|
66
|
+
* as unvalidatable and recorded as such rather than silently skipped, because
|
|
67
|
+
* `HttpClient` cannot await an observer without changing request timing.
|
|
68
|
+
*/
|
|
69
|
+
export function installWireConformance(bindings, options = {}) {
|
|
70
|
+
const originFilter = options.origin === undefined ? undefined : wireOrigin(options.origin);
|
|
71
|
+
const errorEnvelope = options.errorEnvelope === undefined ? ApiErrorEnvelopeSchema : options.errorEnvelope;
|
|
72
|
+
const validated = new Set();
|
|
73
|
+
const unschemad = new Set();
|
|
74
|
+
const errorsValidated = new Set();
|
|
75
|
+
const offPlane = new Set();
|
|
76
|
+
const violations = [];
|
|
77
|
+
let observed = 0;
|
|
78
|
+
/** Validate `body`, recording a violation on failure. Returns false if async. */
|
|
79
|
+
const check = (schema, kind, name, response) => {
|
|
80
|
+
const result = schema["~standard"].validate(response.body);
|
|
81
|
+
if (result instanceof Promise) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (result.issues) {
|
|
85
|
+
violations.push({
|
|
86
|
+
kind,
|
|
87
|
+
name,
|
|
88
|
+
method: response.method.toUpperCase(),
|
|
89
|
+
origin: response.origin,
|
|
90
|
+
path: response.path,
|
|
91
|
+
status: response.status,
|
|
92
|
+
issues: issuesOf(result),
|
|
93
|
+
body: response.body
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
const stop = observeWireResponses((response) => {
|
|
99
|
+
observed += 1;
|
|
100
|
+
const method = response.method.toUpperCase();
|
|
101
|
+
if (originFilter !== undefined && response.origin !== originFilter) {
|
|
102
|
+
offPlane.add(`${response.origin} ${method} ${response.path}`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const contract = contractFor(response.status);
|
|
106
|
+
if (contract === "redirect") {
|
|
107
|
+
unschemad.add(`${method} ${response.path} -> ${response.status} (redirect — no declared body)`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (contract === "error-envelope") {
|
|
111
|
+
// An error body is the SAME envelope whatever route produced it, so it is
|
|
112
|
+
// checked without needing a binding — which is the point: the routes with
|
|
113
|
+
// no 2xx schema still have their failures covered.
|
|
114
|
+
if (errorEnvelope === null) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (check(errorEnvelope, "error-envelope", `${method} ${response.path}`, response)) {
|
|
118
|
+
errorsValidated.add(`${method} ${response.path} -> ${response.status}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
unschemad.add(`${method} ${response.path} -> ${response.status} (async schema — not validated)`);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const binding = bindings.find((candidate) => candidate.method === method && pathMatches(candidate.path, response.path));
|
|
126
|
+
if (!binding) {
|
|
127
|
+
unschemad.add(`${method} ${response.path}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!check(binding.schema, "response", binding.name, response)) {
|
|
131
|
+
unschemad.add(`${binding.name} (async schema — not validated)`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
validated.add(binding.name);
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
stop,
|
|
138
|
+
report: () => ({
|
|
139
|
+
originFilter,
|
|
140
|
+
validated: [...validated].sort(),
|
|
141
|
+
unexercised: bindings
|
|
142
|
+
.map((binding) => binding.name)
|
|
143
|
+
.filter((name) => !validated.has(name))
|
|
144
|
+
.sort(),
|
|
145
|
+
unschemad: [...unschemad].sort(),
|
|
146
|
+
errorsValidated: [...errorsValidated].sort(),
|
|
147
|
+
offPlane: [...offPlane].sort(),
|
|
148
|
+
violations,
|
|
149
|
+
observed
|
|
150
|
+
})
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Fold reports from several processes into one.
|
|
155
|
+
*
|
|
156
|
+
* The suites that matter drive the SDK out of process — one bun child per
|
|
157
|
+
* scenario, one test process per file — so no single process sees the whole
|
|
158
|
+
* surface, and a per-process report would understate coverage by construction.
|
|
159
|
+
*
|
|
160
|
+
* `unexercised` is INTERSECTED, not concatenated: a name is unexercised overall
|
|
161
|
+
* only when every fragment failed to exercise it. Concatenating would report a
|
|
162
|
+
* route as unexercised because some other process did not happen to call it,
|
|
163
|
+
* which is the mirror image of the false-green this harness exists to prevent.
|
|
164
|
+
* That identity only holds when every fragment was produced from the same
|
|
165
|
+
* binding table; {@link installWireConformance} is the only producer.
|
|
166
|
+
*/
|
|
167
|
+
export function mergeWireConformanceReports(reports) {
|
|
168
|
+
const validated = new Set();
|
|
169
|
+
const unschemad = new Set();
|
|
170
|
+
const errorsValidated = new Set();
|
|
171
|
+
const offPlane = new Set();
|
|
172
|
+
const violations = [];
|
|
173
|
+
const originFilters = new Set();
|
|
174
|
+
let unexercised;
|
|
175
|
+
let observed = 0;
|
|
176
|
+
let anyUnfiltered = false;
|
|
177
|
+
for (const report of reports) {
|
|
178
|
+
observed += report.observed;
|
|
179
|
+
for (const name of report.validated)
|
|
180
|
+
validated.add(name);
|
|
181
|
+
for (const route of report.unschemad)
|
|
182
|
+
unschemad.add(route);
|
|
183
|
+
for (const route of report.errorsValidated)
|
|
184
|
+
errorsValidated.add(route);
|
|
185
|
+
for (const route of report.offPlane)
|
|
186
|
+
offPlane.add(route);
|
|
187
|
+
violations.push(...report.violations);
|
|
188
|
+
if (report.originFilter === undefined)
|
|
189
|
+
anyUnfiltered = true;
|
|
190
|
+
else
|
|
191
|
+
originFilters.add(report.originFilter);
|
|
192
|
+
unexercised =
|
|
193
|
+
unexercised === undefined
|
|
194
|
+
? new Set(report.unexercised)
|
|
195
|
+
: new Set(report.unexercised.filter((name) => unexercised.has(name)));
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
// One filter across every fragment is the only case that can be stated
|
|
199
|
+
// simply. A mixed run is reported as unfiltered so the formatter's warning
|
|
200
|
+
// fires rather than a filter being claimed that not every fragment applied.
|
|
201
|
+
originFilter: !anyUnfiltered && originFilters.size === 1 ? [...originFilters][0] : undefined,
|
|
202
|
+
validated: [...validated].sort(),
|
|
203
|
+
unexercised: [...(unexercised ?? new Set())].filter((name) => !validated.has(name)).sort(),
|
|
204
|
+
unschemad: [...unschemad].sort(),
|
|
205
|
+
errorsValidated: [...errorsValidated].sort(),
|
|
206
|
+
offPlane: [...offPlane].sort(),
|
|
207
|
+
violations,
|
|
208
|
+
observed
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** Render a body for a violation without letting one huge response bury the report. */
|
|
212
|
+
function renderBody(body, limit = 2000) {
|
|
213
|
+
let text;
|
|
214
|
+
try {
|
|
215
|
+
text = JSON.stringify(body) ?? String(body);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
text = String(body);
|
|
219
|
+
}
|
|
220
|
+
return text.length > limit ? `${text.slice(0, limit)}… (${text.length} bytes total)` : text;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Render the report for a suite's output.
|
|
224
|
+
*
|
|
225
|
+
* Always prints the unexercised and unschemad lists, including when there are no
|
|
226
|
+
* violations — a run that reports only "0 violations" invites the reader to
|
|
227
|
+
* conclude the surface is verified. A violation always prints the BODY that
|
|
228
|
+
* caused it as well as the schema's complaint, because triage needs both.
|
|
229
|
+
*/
|
|
230
|
+
export function formatWireConformanceReport(report) {
|
|
231
|
+
const lines = [
|
|
232
|
+
`wire conformance: ${report.observed} response(s) observed, ` +
|
|
233
|
+
`${report.validated.length} operation(s) validated, ` +
|
|
234
|
+
`${report.errorsValidated.length} error response(s) checked, ` +
|
|
235
|
+
`${report.violations.length} violation(s)`
|
|
236
|
+
];
|
|
237
|
+
lines.push(report.originFilter === undefined
|
|
238
|
+
? ` ORIGIN FILTER: none — every observed response was matched by PATH alone. ` +
|
|
239
|
+
`Sound only for a process that drives ONE plane; the control plane serves ` +
|
|
240
|
+
`different bodies at GET /api/whoami and DELETE /api/workspaces/{id}.`
|
|
241
|
+
: ` ORIGIN FILTER: ${report.originFilter}`);
|
|
242
|
+
if (report.offPlane.length > 0) {
|
|
243
|
+
lines.push(` OFF-PLANE (other origin, not checked against these bindings):`);
|
|
244
|
+
for (const route of report.offPlane) {
|
|
245
|
+
lines.push(` - ${route}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (report.validated.length > 0) {
|
|
249
|
+
lines.push(` VALIDATED (${report.validated.length}): ${report.validated.join(", ")}`);
|
|
250
|
+
}
|
|
251
|
+
if (report.errorsValidated.length > 0) {
|
|
252
|
+
lines.push(` ERROR ENVELOPES CHECKED (${report.errorsValidated.length}):`);
|
|
253
|
+
for (const route of report.errorsValidated) {
|
|
254
|
+
lines.push(` - ${route}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (report.unexercised.length > 0) {
|
|
258
|
+
lines.push(` NOT EXERCISED (schema exists, no response seen) (${report.unexercised.length}): ` +
|
|
259
|
+
`${report.unexercised.join(", ")}`);
|
|
260
|
+
}
|
|
261
|
+
if (report.unschemad.length > 0) {
|
|
262
|
+
lines.push(` NO SCHEMA (response seen, nothing to check it against):`);
|
|
263
|
+
for (const route of report.unschemad) {
|
|
264
|
+
lines.push(` - ${route}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const violation of report.violations) {
|
|
268
|
+
lines.push(` VIOLATION [${violation.kind}] ${violation.name} ` +
|
|
269
|
+
`(${violation.method} ${violation.origin}${violation.path} -> ${violation.status}):`);
|
|
270
|
+
for (const issue of violation.issues) {
|
|
271
|
+
lines.push(` - ${issue}`);
|
|
272
|
+
}
|
|
273
|
+
lines.push(` body: ${renderBody(violation.body)}`);
|
|
274
|
+
}
|
|
275
|
+
return lines.join("\n");
|
|
276
|
+
}
|
package/dist/cli.mjs
CHANGED
|
@@ -1782,6 +1782,14 @@ function errorFromZod(error) {
|
|
|
1782
1782
|
return new Error(primaryIssue(error.issues)?.message ?? "invalid input");
|
|
1783
1783
|
}
|
|
1784
1784
|
function parseWire(schema, input) {
|
|
1785
|
+
const meta2 = wireObjectMeta.get(schema);
|
|
1786
|
+
if (meta2 && input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
1787
|
+
const unknownKey = Object.keys(input).find((key) => !meta2.permitted.includes(key));
|
|
1788
|
+
if (unknownKey !== void 0) {
|
|
1789
|
+
const diagnostic = meta2.unknownKey(meta2.resolvePath([]), unknownKey, meta2.permitted);
|
|
1790
|
+
throw typeof diagnostic === "string" ? new Error(diagnostic) : diagnostic;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1785
1793
|
const result = safeParse(schema, input);
|
|
1786
1794
|
if (!result.success) {
|
|
1787
1795
|
throw structuredError(schema, result.error) ?? errorFromZod(result.error);
|
package/dist/cli.mjs.sha256
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1f35333a7b3ceb4bbf8461214ae7a9b23cf985195b36e68b61e5e6a016f1f0c2 cli.mjs
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aexhq/sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7-canary",
|
|
4
4
|
"description": "TypeScript SDK for autonomous agent sessions with managed Vercel AI Gateway model access.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -55,6 +55,6 @@
|
|
|
55
55
|
"zod": "4.4.3"
|
|
56
56
|
},
|
|
57
57
|
"aexRelease": {
|
|
58
|
-
"sourceSha": "
|
|
58
|
+
"sourceSha": "f7c6d5902c4594d166b1a5878be05a064b20c1ea"
|
|
59
59
|
}
|
|
60
60
|
}
|