@danypops/vehicle-conformance 0.1.0 → 0.2.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 +1 -1
- package/package.json +15 -8
- package/src/vehicle-conformance.ts +121 -9
package/README.md
CHANGED
|
@@ -13,5 +13,5 @@ bun add -d @danypops/vehicle-conformance
|
|
|
13
13
|
import { registerConformanceOperations, runVehicleClientConformance } from "@danypops/vehicle-conformance";
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
See the [workspace README](https://github.com/DanyPops/
|
|
16
|
+
See the [workspace README](https://github.com/DanyPops/vehicle#readme) for
|
|
17
17
|
the full Vehicle package layout.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/vehicle-conformance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Host-neutral conformance suite for any VehicleClient implementation: one shared bun:test assertion set that LocalVehicleClient, RemoteVehicleClient, and any future transport must satisfy identically. A Bun-only devDependency for testing, not a runtime library -- ships raw TypeScript, never precompiled.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,19 +14,26 @@
|
|
|
14
14
|
"typecheck": "tsc --noEmit"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@danypops/vehicle-core": "
|
|
18
|
-
"@danypops/vehicle-server": "
|
|
17
|
+
"@danypops/vehicle-core": "^0.10.0",
|
|
18
|
+
"@danypops/vehicle-server": "^0.11.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@danypops/vehicle-client": "
|
|
21
|
+
"@danypops/vehicle-client": "^0.5.0",
|
|
22
22
|
"@types/node": "^22.0.0",
|
|
23
|
-
"typescript": "
|
|
23
|
+
"typescript": "^5.9.2"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/DanyPops/
|
|
27
|
+
"url": "git+https://github.com/DanyPops/vehicle.git",
|
|
28
28
|
"directory": "packages/vehicle-conformance"
|
|
29
29
|
},
|
|
30
|
-
"keywords": [
|
|
31
|
-
|
|
30
|
+
"keywords": [
|
|
31
|
+
"vehicle",
|
|
32
|
+
"agent-tools",
|
|
33
|
+
"testing"
|
|
34
|
+
],
|
|
35
|
+
"files": [
|
|
36
|
+
"src",
|
|
37
|
+
"README.md"
|
|
38
|
+
]
|
|
32
39
|
}
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* scope this generalizes.
|
|
20
20
|
*/
|
|
21
21
|
import { describe, expect, it } from "bun:test";
|
|
22
|
-
import { bindVehicleOperation, defineVehicleOperation, defineVehicleSchema, VehicleError } from "@danypops/vehicle-core";
|
|
23
22
|
import type { VehicleClient } from "@danypops/vehicle-core";
|
|
23
|
+
import { bindVehicleOperation, defineVehicleOperation, defineVehicleSchema, VehicleError } from "@danypops/vehicle-core";
|
|
24
24
|
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
25
25
|
|
|
26
26
|
const passthroughSchema = defineVehicleSchema<{ value: string }>({
|
|
@@ -105,6 +105,22 @@ const ConformanceNever = defineVehicleOperation({
|
|
|
105
105
|
limits: LIMITS,
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
/** Genuinely slow (unlike ConformanceProgress, which resolves near-instantly) -- the streaming-progress-required check needs real elapsed time to exceed its threshold before the "did it report progress" assertion means anything. */
|
|
109
|
+
const SLOW_PROGRESS_DELAY_MS = 60;
|
|
110
|
+
const ConformanceSlowProgress = defineVehicleOperation({
|
|
111
|
+
name: "conformance.slow-progress",
|
|
112
|
+
version: 1,
|
|
113
|
+
description:
|
|
114
|
+
"Reports one progress event partway through a real delay, then resolves -- streaming: true declares it must never silently block.",
|
|
115
|
+
input: passthroughSchema,
|
|
116
|
+
output: outputSchema,
|
|
117
|
+
permissions: [],
|
|
118
|
+
effect: "read",
|
|
119
|
+
idempotency: { mode: "safe" },
|
|
120
|
+
streaming: true,
|
|
121
|
+
limits: LIMITS,
|
|
122
|
+
});
|
|
123
|
+
|
|
108
124
|
/** Registers the fixed conformance operation set onto `registry`. Every fixture must call this before handing back its client. */
|
|
109
125
|
export function registerConformanceOperations(registry: VehicleRegistry): void {
|
|
110
126
|
registry.register(
|
|
@@ -137,8 +153,19 @@ export function registerConformanceOperations(registry: VehicleRegistry): void {
|
|
|
137
153
|
});
|
|
138
154
|
}),
|
|
139
155
|
);
|
|
156
|
+
registry.register(
|
|
157
|
+
"conformance",
|
|
158
|
+
bindVehicleOperation(ConformanceSlowProgress, () => async (context) => {
|
|
159
|
+
context.reportProgress({ step: 1 });
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, SLOW_PROGRESS_DELAY_MS));
|
|
161
|
+
return { echoed: context.input.value };
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
140
164
|
}
|
|
141
165
|
|
|
166
|
+
/** Every operation this suite declares with streaming: true -- the streaming-progress-required check generates one named it() per entry, per this project's own "per-check test isolation" requirement. */
|
|
167
|
+
const STREAMING_OPERATIONS = [{ descriptor: ConformanceSlowProgress.descriptor, thresholdMs: SLOW_PROGRESS_DELAY_MS / 2 }] as const;
|
|
168
|
+
|
|
142
169
|
export interface VehicleConformanceFixture {
|
|
143
170
|
/** Used in describe() block titles, e.g. "LocalVehicleClient" or "RemoteVehicleClient (HTTP)". */
|
|
144
171
|
label: string;
|
|
@@ -159,6 +186,7 @@ export function runVehicleClientConformance(fixture: VehicleConformanceFixture):
|
|
|
159
186
|
"conformance.keyed@1",
|
|
160
187
|
"conformance.never@1",
|
|
161
188
|
"conformance.progress@1",
|
|
189
|
+
"conformance.slow-progress@1",
|
|
162
190
|
]);
|
|
163
191
|
const echo = manifest.operations.find((op) => op.name === "conformance.echo");
|
|
164
192
|
expect(echo?.permissions).toEqual(["conformance:echo"]);
|
|
@@ -176,7 +204,12 @@ export function runVehicleClientConformance(fixture: VehicleConformanceFixture):
|
|
|
176
204
|
it("invoke() returns the real handler output on success", async () => {
|
|
177
205
|
const { client, cleanup } = await fixture.create();
|
|
178
206
|
try {
|
|
179
|
-
const result = await client.invoke<{ echoed: string }>(
|
|
207
|
+
const result = await client.invoke<{ echoed: string }>(
|
|
208
|
+
"conformance.echo",
|
|
209
|
+
1,
|
|
210
|
+
{ value: "hi" },
|
|
211
|
+
{ permissions: ["conformance:echo"] },
|
|
212
|
+
);
|
|
180
213
|
expect(result).toEqual({ echoed: "hi" });
|
|
181
214
|
} finally {
|
|
182
215
|
await cleanup();
|
|
@@ -235,7 +268,9 @@ export function runVehicleClientConformance(fixture: VehicleConformanceFixture):
|
|
|
235
268
|
const { client, cleanup } = await fixture.create();
|
|
236
269
|
try {
|
|
237
270
|
const oversized = "x".repeat(1024);
|
|
238
|
-
await expect(
|
|
271
|
+
await expect(
|
|
272
|
+
client.invoke("conformance.echo", 1, { value: oversized }, { permissions: ["conformance:echo"] }),
|
|
273
|
+
).rejects.toMatchObject({
|
|
239
274
|
code: "request-too-large",
|
|
240
275
|
});
|
|
241
276
|
} finally {
|
|
@@ -257,12 +292,17 @@ export function runVehicleClientConformance(fixture: VehicleConformanceFixture):
|
|
|
257
292
|
try {
|
|
258
293
|
const progress: unknown[] = [];
|
|
259
294
|
let resolved = false;
|
|
260
|
-
const result = await client.invoke<{ echoed: string }>(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
295
|
+
const result = await client.invoke<{ echoed: string }>(
|
|
296
|
+
"conformance.progress",
|
|
297
|
+
1,
|
|
298
|
+
{ value: "hi" },
|
|
299
|
+
{
|
|
300
|
+
onProgress: (p) => {
|
|
301
|
+
expect(resolved).toBe(false);
|
|
302
|
+
progress.push(p);
|
|
303
|
+
},
|
|
264
304
|
},
|
|
265
|
-
|
|
305
|
+
);
|
|
266
306
|
resolved = true;
|
|
267
307
|
expect(progress).toEqual([{ step: 1 }, { step: 2 }]);
|
|
268
308
|
expect(result).toEqual({ echoed: "hi" });
|
|
@@ -287,7 +327,9 @@ export function runVehicleClientConformance(fixture: VehicleConformanceFixture):
|
|
|
287
327
|
it("invoke() respects an explicit deadline that has already elapsed", async () => {
|
|
288
328
|
const { client, cleanup } = await fixture.create();
|
|
289
329
|
try {
|
|
290
|
-
await expect(
|
|
330
|
+
await expect(
|
|
331
|
+
client.invoke("conformance.echo", 1, { value: "hi" }, { permissions: ["conformance:echo"], deadline: Date.now() - 1 }),
|
|
332
|
+
).rejects.toMatchObject({
|
|
291
333
|
code: "deadline-exceeded",
|
|
292
334
|
});
|
|
293
335
|
} finally {
|
|
@@ -304,5 +346,75 @@ export function runVehicleClientConformance(fixture: VehicleConformanceFixture):
|
|
|
304
346
|
await cleanup();
|
|
305
347
|
}
|
|
306
348
|
});
|
|
349
|
+
|
|
350
|
+
// Schema-rejection timing: an invalid-input invocation must resolve (with a
|
|
351
|
+
// validation error) within a small bound, never falling through to a general
|
|
352
|
+
// timeout -- catches a handler whose validation path accidentally does real
|
|
353
|
+
// I/O before checking input shape. Ported from Alef's own adapter-contract.ts
|
|
354
|
+
// runSchemaContract (200ms bound), a separate named check from the existing
|
|
355
|
+
// "rejects invalid input" test above per this suite's own per-check isolation.
|
|
356
|
+
it("invoke() rejects invalid input within a bounded time, never falling through to a general timeout", async () => {
|
|
357
|
+
const { client, cleanup } = await fixture.create();
|
|
358
|
+
try {
|
|
359
|
+
const start = Date.now();
|
|
360
|
+
await expect(client.invoke("conformance.echo", 1, { value: 123 }, { permissions: ["conformance:echo"] })).rejects.toMatchObject({
|
|
361
|
+
code: "invalid-input",
|
|
362
|
+
});
|
|
363
|
+
const elapsed = Date.now() - start;
|
|
364
|
+
expect(elapsed, `schema rejection took ${elapsed}ms -- should be immediate (<200ms)`).toBeLessThan(200);
|
|
365
|
+
} finally {
|
|
366
|
+
await cleanup();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Human-readable error messages: a validation failure's own .message must
|
|
371
|
+
// never leak an internal validation-library-specific type name or a bare
|
|
372
|
+
// stringified object -- a real bug class this ports from Alef's own
|
|
373
|
+
// adapter-contract.ts (a live zod "[InputValidation]" prefix leak there).
|
|
374
|
+
it("invoke() rejects invalid input with a human-readable message, never a raw validation-library leak", async () => {
|
|
375
|
+
const { client, cleanup } = await fixture.create();
|
|
376
|
+
try {
|
|
377
|
+
const error = await client.invoke("conformance.echo", 1, { value: 123 }, { permissions: ["conformance:echo"] }).catch((e) => e);
|
|
378
|
+
const message = (error as { message?: unknown }).message;
|
|
379
|
+
expect(typeof message).toBe("string");
|
|
380
|
+
expect(message).not.toBe("[object Object]");
|
|
381
|
+
expect(message as string).not.toMatch(/ValueError|TypeBoxError|\[InputValidation\]|ZodError/);
|
|
382
|
+
// Genuinely readable: names which operation failed, not just "invalid".
|
|
383
|
+
expect(message as string).toContain("conformance.echo");
|
|
384
|
+
} finally {
|
|
385
|
+
await cleanup();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Streaming-progress-required: any operation declared streaming: true must
|
|
390
|
+
// emit at least one progress event before resolving, once its real
|
|
391
|
+
// execution exceeds a threshold duration -- catches a handler that
|
|
392
|
+
// silently blocks the caller instead of reporting progress despite
|
|
393
|
+
// declaring progress support. One named it() per discovered
|
|
394
|
+
// streaming-capable operation (this suite currently declares exactly one),
|
|
395
|
+
// per this project's own per-check test isolation.
|
|
396
|
+
describe("streaming-progress-required (operations declared streaming: true)", () => {
|
|
397
|
+
for (const { descriptor, thresholdMs } of STREAMING_OPERATIONS) {
|
|
398
|
+
it(`${descriptor.name}@${descriptor.version} emits progress before resolving, once it runs past ${thresholdMs}ms`, async () => {
|
|
399
|
+
const { client, cleanup } = await fixture.create();
|
|
400
|
+
try {
|
|
401
|
+
const progress: unknown[] = [];
|
|
402
|
+
const start = Date.now();
|
|
403
|
+
await client.invoke(descriptor.name, descriptor.version, { value: "hi" }, { onProgress: (p) => progress.push(p) });
|
|
404
|
+
const elapsed = Date.now() - start;
|
|
405
|
+
expect(
|
|
406
|
+
elapsed,
|
|
407
|
+
`test fixture ran in ${elapsed}ms, below its own ${thresholdMs}ms threshold -- this check can't prove anything`,
|
|
408
|
+
).toBeGreaterThan(thresholdMs);
|
|
409
|
+
expect(
|
|
410
|
+
progress.length,
|
|
411
|
+
`${descriptor.name} ran for ${elapsed}ms but emitted zero progress events -- a streaming: true operation must never silently block`,
|
|
412
|
+
).toBeGreaterThan(0);
|
|
413
|
+
} finally {
|
|
414
|
+
await cleanup();
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
});
|
|
307
419
|
});
|
|
308
420
|
}
|