@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 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/daemon-kit#readme) for
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.1.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": "workspace:*",
18
- "@danypops/vehicle-server": "workspace:*"
17
+ "@danypops/vehicle-core": "^0.10.0",
18
+ "@danypops/vehicle-server": "^0.11.0"
19
19
  },
20
20
  "devDependencies": {
21
- "@danypops/vehicle-client": "workspace:*",
21
+ "@danypops/vehicle-client": "^0.5.0",
22
22
  "@types/node": "^22.0.0",
23
- "typescript": "latest"
23
+ "typescript": "^5.9.2"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "git+https://github.com/DanyPops/daemon-kit.git",
27
+ "url": "git+https://github.com/DanyPops/vehicle.git",
28
28
  "directory": "packages/vehicle-conformance"
29
29
  },
30
- "keywords": ["vehicle", "agent-tools", "testing"],
31
- "files": ["src", "README.md"]
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 }>("conformance.echo", 1, { value: "hi" }, { permissions: ["conformance:echo"] });
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(client.invoke("conformance.echo", 1, { value: oversized }, { permissions: ["conformance:echo"] })).rejects.toMatchObject({
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 }>("conformance.progress", 1, { value: "hi" }, {
261
- onProgress: (p) => {
262
- expect(resolved).toBe(false);
263
- progress.push(p);
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(client.invoke("conformance.echo", 1, { value: "hi" }, { permissions: ["conformance:echo"], deadline: Date.now() - 1 })).rejects.toMatchObject({
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
  }