@danypops/vehicle-conformance 0.4.1 → 0.4.3

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
@@ -1,9 +1,10 @@
1
1
  # @danypops/vehicle-conformance
2
2
 
3
- Host-neutral `bun:test` conformance suite for any `VehicleClient`
3
+ Compiled runner-neutral conformance suite for any `VehicleClient`
4
4
  implementation -- one shared assertion set that a `LocalVehicleClient`, a
5
- `RemoteVehicleClient`, or any future transport must satisfy identically.
6
- Ships raw TypeScript; a test-time devDependency, not a runtime library.
5
+ `RemoteVehicleClient`, or any future transport must satisfy identically. The
6
+ root export registers through Bun; `./core` exposes the runner-neutral matrix,
7
+ and `./vitest` adapts a Vitest-compatible API.
7
8
 
8
9
  ```bash
9
10
  bun add -d @danypops/vehicle-conformance
@@ -17,6 +18,15 @@ import {
17
18
  } from "@danypops/vehicle-conformance";
18
19
  ```
19
20
 
21
+ Vitest consumers provide their installed runner API rather than loading Bun:
22
+
23
+ ```ts
24
+ import { describe, expect, it } from "vitest";
25
+ import { runVehicleClientConformanceWithVitest } from "@danypops/vehicle-conformance/vitest";
26
+
27
+ runVehicleClientConformanceWithVitest({ describe, expect, it }, fixture);
28
+ ```
29
+
20
30
  `runToolShellDualChannelConformance(fixture)` is the host-neutral Tool Shell
21
31
  matrix. A fixture adapts one provider's real projection/rendering boundary via
22
32
  `execute`, `render`, `replay`, `renderCall`, and `invalidProjection`; the shared
package/dist/bun.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { type ToolShellDualChannelFixture, type VehicleConformanceFixture } from "./vehicle-conformance.js";
2
+ /** Registers the shared Vehicle client matrix with Bun's test runner. */
3
+ export declare function runVehicleClientConformance(fixture: VehicleConformanceFixture): void;
4
+ /** Registers the shared Tool Shell matrix with Bun's test runner. */
5
+ export declare function runToolShellDualChannelConformance(fixture: ToolShellDualChannelFixture): void;
6
+ export * from "./vehicle-conformance.js";
package/dist/bun.js ADDED
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { registerToolShellDualChannelConformance, registerVehicleClientConformance, } from "./vehicle-conformance.js";
3
+ const bunRunner = {
4
+ describe: (name, body) => describe(name, body),
5
+ it: (name, body) => it(name, body),
6
+ expect: (actual, message) => expect(actual, message),
7
+ };
8
+ /** Registers the shared Vehicle client matrix with Bun's test runner. */
9
+ export function runVehicleClientConformance(fixture) {
10
+ registerVehicleClientConformance(bunRunner, fixture);
11
+ }
12
+ /** Registers the shared Tool Shell matrix with Bun's test runner. */
13
+ export function runToolShellDualChannelConformance(fixture) {
14
+ registerToolShellDualChannelConformance(bunRunner, fixture);
15
+ }
16
+ export * from "./vehicle-conformance.js";
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Host-neutral conformance suite: one shared set of assertions that any
3
+ * VehicleClient implementation (LocalVehicleClient, RemoteVehicleClient,
4
+ * and any future MCP/CLI projection) must satisfy identically. Registers
5
+ * its own fixed set of test operations onto whatever registry the fixture
6
+ * hands back, so the *same* operation definitions exercise every
7
+ * implementation -- two independently hand-written test files could drift
8
+ * apart without either one noticing; a shared suite can't.
9
+ *
10
+ * Deliberately built on bun:test directly (not a framework-agnostic DSL) --
11
+ * every consumer of this package that would run it is already a Bun
12
+ * project, and inventing a test-runner abstraction for a single-runtime
13
+ * ecosystem would be pure ceremony.
14
+ *
15
+ * A fixture only supplies a fresh, isolated registry + a client bound to
16
+ * it + cleanup -- it does not define operations or assertions itself, so
17
+ * host-specific concerns (Alef's bus/context/display assertions, a CLI's
18
+ * argument parsing) stay out of this module entirely, per the extraction
19
+ * scope this generalizes.
20
+ */
21
+ import type { VehicleClient } from "@danypops/vehicle-core";
22
+ export interface VehicleConformanceMatchers {
23
+ toEqual(expected: unknown): unknown;
24
+ toMatchObject(expected: unknown): unknown;
25
+ toBe(expected: unknown): unknown;
26
+ toBeTruthy(): unknown;
27
+ toBeUndefined(): unknown;
28
+ toContain(expected: unknown): unknown;
29
+ toMatch(expected: RegExp | string): unknown;
30
+ toBeGreaterThan(expected: number): unknown;
31
+ toBeGreaterThanOrEqual(expected: number): unknown;
32
+ toBeLessThan(expected: number): unknown;
33
+ toBeLessThanOrEqual(expected: number): unknown;
34
+ readonly not: VehicleConformanceMatchers;
35
+ readonly rejects: VehicleConformanceMatchers;
36
+ readonly resolves: VehicleConformanceMatchers;
37
+ }
38
+ export interface VehicleConformanceRunner {
39
+ describe(name: string, body: () => void): unknown;
40
+ it(name: string, body: () => void | Promise<void>): unknown;
41
+ expect(actual: unknown, message?: string): VehicleConformanceMatchers;
42
+ }
43
+ import type { VehicleRegistry } from "@danypops/vehicle-server";
44
+ /** Registers the fixed conformance operation set onto `registry`. Every fixture must call this before handing back its client. */
45
+ export declare function registerConformanceOperations(registry: VehicleRegistry): void;
46
+ export interface VehicleConformanceFixture {
47
+ /** Used in describe() block titles, e.g. "LocalVehicleClient" or "RemoteVehicleClient (HTTP)". */
48
+ label: string;
49
+ /** Builds a fresh, isolated registry (with registerConformanceOperations already applied) plus a client bound to it. Must not share state across calls -- each test gets its own. */
50
+ create(): Promise<{
51
+ client: VehicleClient;
52
+ cleanup: () => Promise<void>;
53
+ }>;
54
+ }
55
+ export declare function registerVehicleClientConformance(runner: VehicleConformanceRunner, fixture: VehicleConformanceFixture): void;
56
+ export interface ToolShellConformanceSnapshot {
57
+ readonly content: string;
58
+ readonly details: unknown;
59
+ }
60
+ export interface ToolShellRenderOptions {
61
+ readonly width: 40 | 80 | 120;
62
+ readonly expanded: boolean;
63
+ readonly partial?: boolean;
64
+ }
65
+ /**
66
+ * Host adapter for the Tool Shell's two independent persisted channels. The
67
+ * conformance package stays Pi-free: a Pi adapter supplies component output,
68
+ * while a CLI or another host can supply its own renderer through this same API.
69
+ */
70
+ export interface ToolShellDualChannelSubject {
71
+ readonly bounds: {
72
+ readonly modelContentBytes: number;
73
+ readonly presentationDetailsBytes: number;
74
+ };
75
+ execute(): Promise<ToolShellConformanceSnapshot>;
76
+ render(snapshot: ToolShellConformanceSnapshot, options: ToolShellRenderOptions): readonly string[];
77
+ replay(details: unknown, fallbackContent: string, options: ToolShellRenderOptions): readonly string[];
78
+ renderCall(args: unknown, width: 40 | 80 | 120): readonly string[];
79
+ invalidProjection(): Promise<unknown>;
80
+ /**
81
+ * Optional -- the discriminator values (a `format`/`kind`/`action`/... field) this provider's
82
+ * own presentation-details schema declares, each paired with a representative raw application
83
+ * payload for that value. Supplying this (together with renderDeclaredValue) enables the
84
+ * declared-value coverage check below, which generically catches the pi-web-spider bug class
85
+ * (see doc 4e9e08c1, Finding 1/4): most declared values falling through to an undifferentiated
86
+ * JSON.stringify dump of their own payload instead of a real projected view. Omit entirely for
87
+ * a subject with no such discriminator -- the check then no-ops.
88
+ */
89
+ readonly declaredValueCases?: readonly ToolShellDeclaredValueCase[];
90
+ /** Required alongside declaredValueCases: renders the expanded view for one declared value's
91
+ * own raw payload, through exactly the same projection+render pipeline the real handler uses. */
92
+ renderDeclaredValue?(value: string, rawPayload: unknown, options: ToolShellRenderOptions): readonly string[];
93
+ }
94
+ export interface ToolShellDeclaredValueCase {
95
+ /** e.g. a WebFormat value ('search'/'lean'/...), a PackageToolDetails['kind'], a tickets action name. */
96
+ readonly value: string;
97
+ /** The real, untransformed application output this declared value would carry. */
98
+ readonly rawPayload: unknown;
99
+ }
100
+ export interface DeclaredValueCoverageResult {
101
+ /** Declared values whose rendered output is NOT indistinguishable from a raw JSON.stringify dump of their own payload. */
102
+ readonly nonRawValues: readonly string[];
103
+ /** Declared values whose rendered output IS indistinguishable from a raw JSON.stringify dump of their own payload. */
104
+ readonly rawValues: readonly string[];
105
+ }
106
+ /**
107
+ * Pure, independently unit-testable core of the declared-value coverage check -- separated from
108
+ * the bun:test `it()` wiring below so a fixture reproducing a known-bad shape (e.g.
109
+ * pi-web-spider's own pre-fix behavior) can be asserted against directly, proving the classifier
110
+ * itself actually detects that bug class rather than trusting the wrapping `it()` alone.
111
+ */
112
+ export declare function evaluateDeclaredValueCoverage(cases: readonly ToolShellDeclaredValueCase[], renderDeclaredValue: (value: string, rawPayload: unknown, options: ToolShellRenderOptions) => readonly string[], options?: ToolShellRenderOptions): DeclaredValueCoverageResult;
113
+ export interface ToolShellDualChannelFixture {
114
+ readonly label: string;
115
+ create(): Promise<{
116
+ readonly subject: ToolShellDualChannelSubject;
117
+ readonly cleanup: () => Promise<void>;
118
+ }>;
119
+ }
120
+ /** Reusable provider-facing dual-channel contract matrix. */
121
+ export declare function registerToolShellDualChannelConformance(runner: VehicleConformanceRunner, fixture: ToolShellDualChannelFixture): void;
@@ -0,0 +1,522 @@
1
+ import { bindVehicleOperation, defineVehicleOperation, defineVehicleSchema, VehicleError } from "@danypops/vehicle-core";
2
+ const passthroughSchema = defineVehicleSchema({
3
+ jsonSchema: { type: "object", properties: { value: { type: "string" } }, additionalProperties: false },
4
+ safeParse(value) {
5
+ if (typeof value === "object" && value !== null && typeof value.value === "string") {
6
+ return { success: true, value: value };
7
+ }
8
+ return { success: false, issues: [{ path: ["value"], message: "value must be a string" }] };
9
+ },
10
+ });
11
+ const outputSchema = defineVehicleSchema({
12
+ jsonSchema: { type: "object", properties: { echoed: { type: "string" } }, additionalProperties: false },
13
+ safeParse(value) {
14
+ if (typeof value === "object" && value !== null && typeof value.echoed === "string") {
15
+ return { success: true, value: value };
16
+ }
17
+ return { success: false, issues: [{ path: ["echoed"], message: "echoed must be a string" }] };
18
+ },
19
+ });
20
+ const LIMITS = { defaultTimeoutMs: 200, maxTimeoutMs: 2_000, maxRequestBytes: 256, maxResponseBytes: 256 };
21
+ const ConformanceEcho = defineVehicleOperation({
22
+ name: "conformance.echo",
23
+ version: 1,
24
+ description: "Echoes its input.",
25
+ input: passthroughSchema,
26
+ output: outputSchema,
27
+ permissions: ["conformance:echo"],
28
+ effect: "read",
29
+ idempotency: { mode: "safe" },
30
+ limits: LIMITS,
31
+ });
32
+ const ConformanceBoom = defineVehicleOperation({
33
+ name: "conformance.boom",
34
+ version: 1,
35
+ description: "Always throws a real VehicleError from its handler.",
36
+ input: passthroughSchema,
37
+ output: outputSchema,
38
+ permissions: [],
39
+ effect: "read",
40
+ idempotency: { mode: "safe" },
41
+ limits: LIMITS,
42
+ });
43
+ const ConformanceKeyed = defineVehicleOperation({
44
+ name: "conformance.keyed",
45
+ version: 1,
46
+ description: "Requires a keyed idempotency key.",
47
+ input: passthroughSchema,
48
+ output: outputSchema,
49
+ permissions: [],
50
+ effect: "external-write",
51
+ requiresApproval: false,
52
+ idempotency: { mode: "keyed", retentionMs: 60_000 },
53
+ limits: LIMITS,
54
+ });
55
+ const ConformanceUnconfiguredRisk = defineVehicleOperation({
56
+ name: "conformance.unconfigured-risk",
57
+ version: 1,
58
+ description: "Proves that risky operations require an explicit registry approval-policy decision.",
59
+ input: passthroughSchema,
60
+ output: outputSchema,
61
+ permissions: [],
62
+ effect: "external-write",
63
+ idempotency: { mode: "keyed", retentionMs: 60_000 },
64
+ limits: LIMITS,
65
+ });
66
+ const ConformanceProgress = defineVehicleOperation({
67
+ name: "conformance.progress",
68
+ version: 1,
69
+ description: "Reports two progress events, then resolves.",
70
+ input: passthroughSchema,
71
+ output: outputSchema,
72
+ permissions: [],
73
+ effect: "read",
74
+ idempotency: { mode: "safe" },
75
+ limits: LIMITS,
76
+ });
77
+ const ConformanceNever = defineVehicleOperation({
78
+ name: "conformance.never",
79
+ version: 1,
80
+ description: "Never resolves on its own -- only via cancellation or deadline.",
81
+ input: passthroughSchema,
82
+ output: outputSchema,
83
+ permissions: [],
84
+ effect: "read",
85
+ idempotency: { mode: "safe" },
86
+ limits: LIMITS,
87
+ });
88
+ /** 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. */
89
+ const SLOW_PROGRESS_DELAY_MS = 60;
90
+ const ConformanceSlowProgress = defineVehicleOperation({
91
+ name: "conformance.slow-progress",
92
+ version: 1,
93
+ description: "Reports one progress event partway through a real delay, then resolves -- streaming: true declares it must never silently block.",
94
+ input: passthroughSchema,
95
+ output: outputSchema,
96
+ permissions: [],
97
+ effect: "read",
98
+ idempotency: { mode: "safe" },
99
+ streaming: true,
100
+ limits: LIMITS,
101
+ });
102
+ /** Registers the fixed conformance operation set onto `registry`. Every fixture must call this before handing back its client. */
103
+ export function registerConformanceOperations(registry) {
104
+ registry.register("conformance", bindVehicleOperation(ConformanceEcho, () => async (context) => ({ echoed: context.input.value })));
105
+ registry.register("conformance", bindVehicleOperation(ConformanceBoom, () => async () => {
106
+ throw new VehicleError("conformance-boom", "conformance.boom always fails", { category: "internal" });
107
+ }));
108
+ registry.register("conformance", bindVehicleOperation(ConformanceKeyed, () => async (context) => ({ echoed: context.input.value })));
109
+ registry.register("conformance", bindVehicleOperation(ConformanceUnconfiguredRisk, () => async (context) => ({ echoed: context.input.value })));
110
+ registry.register("conformance", bindVehicleOperation(ConformanceProgress, () => async (context) => {
111
+ context.reportProgress({ step: 1 });
112
+ context.reportProgress({ step: 2 });
113
+ return { echoed: context.input.value };
114
+ }));
115
+ registry.register("conformance", bindVehicleOperation(ConformanceNever, () => (context) => {
116
+ return new Promise((_resolve, reject) => {
117
+ context.signal.addEventListener("abort", () => reject(new Error("conformance.never aborted")), { once: true });
118
+ });
119
+ }));
120
+ registry.register("conformance", bindVehicleOperation(ConformanceSlowProgress, () => async (context) => {
121
+ context.reportProgress({ step: 1 });
122
+ await new Promise((resolve) => setTimeout(resolve, SLOW_PROGRESS_DELAY_MS));
123
+ return { echoed: context.input.value };
124
+ }));
125
+ }
126
+ /** 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. */
127
+ const STREAMING_OPERATIONS = [{ descriptor: ConformanceSlowProgress.descriptor, thresholdMs: SLOW_PROGRESS_DELAY_MS / 2 }];
128
+ export function registerVehicleClientConformance(runner, fixture) {
129
+ const { describe, expect, it } = runner;
130
+ describe(`Vehicle client conformance: ${fixture.label}`, () => {
131
+ it("manifest() lists every registered operation with its real descriptor fields", async () => {
132
+ const { client, cleanup } = await fixture.create();
133
+ try {
134
+ const manifest = await client.manifest();
135
+ const names = manifest.operations.map((op) => `${op.name}@${op.version}`).sort();
136
+ expect(names).toEqual([
137
+ "conformance.boom@1",
138
+ "conformance.echo@1",
139
+ "conformance.keyed@1",
140
+ "conformance.never@1",
141
+ "conformance.progress@1",
142
+ "conformance.slow-progress@1",
143
+ "conformance.unconfigured-risk@1",
144
+ ]);
145
+ const echo = manifest.operations.find((op) => op.name === "conformance.echo");
146
+ expect(echo?.permissions).toEqual(["conformance:echo"]);
147
+ expect(echo?.idempotency).toEqual({ mode: "safe" });
148
+ // available defaults to true for every operation, and must survive
149
+ // the wire round trip identically for a remote (HTTP/JSON) client,
150
+ // not just the in-process local one.
151
+ expect(manifest.operations.every((op) => op.available === true)).toBe(true);
152
+ expect(echo?.unavailableReason).toBeUndefined();
153
+ }
154
+ finally {
155
+ await cleanup();
156
+ }
157
+ });
158
+ it("negotiate() agrees on the shared protocol and rejects an incompatible range", async () => {
159
+ const { client, cleanup } = await fixture.create();
160
+ try {
161
+ if (!client.negotiate)
162
+ throw new Error("Vehicle client does not implement protocol negotiation");
163
+ await expect(client.negotiate({ minimumVersion: 1, maximumVersion: 2, requiredCapabilities: [], optionalCapabilities: ["future"] })).resolves.toEqual({ version: 1, capabilities: [] });
164
+ await expect(client.negotiate({ minimumVersion: 2, maximumVersion: 3, requiredCapabilities: [], optionalCapabilities: [] })).rejects.toMatchObject({ code: "protocol-version-incompatible" });
165
+ }
166
+ finally {
167
+ await cleanup();
168
+ }
169
+ });
170
+ it("invoke() returns the real handler output on success", async () => {
171
+ const { client, cleanup } = await fixture.create();
172
+ try {
173
+ const result = await client.invoke("conformance.echo", 1, { value: "hi" }, { permissions: ["conformance:echo"] });
174
+ expect(result).toEqual({ echoed: "hi" });
175
+ }
176
+ finally {
177
+ await cleanup();
178
+ }
179
+ });
180
+ it("invoke() rejects invalid input before the handler ever runs", async () => {
181
+ const { client, cleanup } = await fixture.create();
182
+ try {
183
+ await expect(client.invoke("conformance.echo", 1, { value: 123 }, { permissions: ["conformance:echo"] })).rejects.toMatchObject({
184
+ code: "invalid-input",
185
+ });
186
+ }
187
+ finally {
188
+ await cleanup();
189
+ }
190
+ });
191
+ it("invoke() enforces required permissions with permission-denied/authorization", async () => {
192
+ const { client, cleanup } = await fixture.create();
193
+ try {
194
+ await expect(client.invoke("conformance.echo", 1, { value: "hi" }, {})).rejects.toMatchObject({
195
+ code: "permission-denied",
196
+ category: "authorization",
197
+ });
198
+ }
199
+ finally {
200
+ await cleanup();
201
+ }
202
+ });
203
+ it("invoke() surfaces a real handler failure's own code/category/message, not a generic wrapper", async () => {
204
+ const { client, cleanup } = await fixture.create();
205
+ try {
206
+ await expect(client.invoke("conformance.boom", 1, { value: "x" }, {})).rejects.toMatchObject({
207
+ code: "conformance-boom",
208
+ message: "conformance.boom always fails",
209
+ });
210
+ }
211
+ finally {
212
+ await cleanup();
213
+ }
214
+ });
215
+ it("invoke() requires an idempotency key for a keyed operation", async () => {
216
+ const { client, cleanup } = await fixture.create();
217
+ try {
218
+ await expect(client.invoke("conformance.keyed", 1, { value: "x" }, {})).rejects.toMatchObject({
219
+ code: "idempotency-key-required",
220
+ });
221
+ const result = await client.invoke("conformance.keyed", 1, { value: "x" }, { idempotencyKey: "k-1" });
222
+ expect(result).toEqual({ echoed: "x" });
223
+ }
224
+ finally {
225
+ await cleanup();
226
+ }
227
+ });
228
+ it("reports an unconfigured risky operation as a hard failure", async () => {
229
+ const { client, cleanup } = await fixture.create();
230
+ try {
231
+ const manifest = await client.manifest();
232
+ expect(manifest.approvalPolicy).toMatchObject({
233
+ status: "unconfigured",
234
+ unconfiguredRiskyOperations: ["conformance.unconfigured-risk@1"],
235
+ });
236
+ await expect(client.invoke("conformance.unconfigured-risk", 1, { value: "x" }, { idempotencyKey: "risk-1" })).rejects.toMatchObject({ code: "approval-policy-unconfigured", category: "authorization" });
237
+ }
238
+ finally {
239
+ await cleanup();
240
+ }
241
+ });
242
+ it("invoke() rejects a request exceeding its declared byte bound", async () => {
243
+ const { client, cleanup } = await fixture.create();
244
+ try {
245
+ const oversized = "x".repeat(1024);
246
+ await expect(client.invoke("conformance.echo", 1, { value: oversized }, { permissions: ["conformance:echo"] })).rejects.toMatchObject({
247
+ code: "request-too-large",
248
+ });
249
+ }
250
+ finally {
251
+ await cleanup();
252
+ }
253
+ });
254
+ it("invoke() rejects an operation for a name/version pair that was never registered", async () => {
255
+ const { client, cleanup } = await fixture.create();
256
+ try {
257
+ await expect(client.invoke("conformance.nonexistent", 1, {}, {})).rejects.toMatchObject({ code: "not-found" });
258
+ }
259
+ finally {
260
+ await cleanup();
261
+ }
262
+ });
263
+ it("invoke() delivers every progress event before resolving with the final result, never after", async () => {
264
+ const { client, cleanup } = await fixture.create();
265
+ try {
266
+ const progress = [];
267
+ let resolved = false;
268
+ const result = await client.invoke("conformance.progress", 1, { value: "hi" }, {
269
+ onProgress: (p) => {
270
+ expect(resolved).toBe(false);
271
+ progress.push(p);
272
+ },
273
+ });
274
+ resolved = true;
275
+ expect(progress).toEqual([{ step: 1 }, { step: 2 }]);
276
+ expect(result).toEqual({ echoed: "hi" });
277
+ }
278
+ finally {
279
+ await cleanup();
280
+ }
281
+ });
282
+ it("invoke() propagates cancellation via AbortSignal to the operation itself", async () => {
283
+ const { client, cleanup } = await fixture.create();
284
+ try {
285
+ const controller = new AbortController();
286
+ const invocation = client.invoke("conformance.never", 1, { value: "x" }, { signal: controller.signal });
287
+ await new Promise((resolve) => setTimeout(resolve, 15));
288
+ controller.abort();
289
+ await expect(invocation).rejects.toBeTruthy();
290
+ }
291
+ finally {
292
+ await cleanup();
293
+ }
294
+ });
295
+ it("invoke() respects an explicit deadline that has already elapsed", async () => {
296
+ const { client, cleanup } = await fixture.create();
297
+ try {
298
+ await expect(client.invoke("conformance.echo", 1, { value: "hi" }, { permissions: ["conformance:echo"], deadline: Date.now() - 1 })).rejects.toMatchObject({
299
+ code: "deadline-exceeded",
300
+ });
301
+ }
302
+ finally {
303
+ await cleanup();
304
+ }
305
+ });
306
+ it("close() prevents further invoke()/manifest() calls on this client instance", async () => {
307
+ const { client, cleanup } = await fixture.create();
308
+ try {
309
+ await client.close();
310
+ await expect(client.manifest()).rejects.toBeTruthy();
311
+ }
312
+ finally {
313
+ await cleanup();
314
+ }
315
+ });
316
+ // Schema-rejection timing: an invalid-input invocation must resolve (with a
317
+ // validation error) within a small bound, never falling through to a general
318
+ // timeout -- catches a handler whose validation path accidentally does real
319
+ // I/O before checking input shape. Ported from Alef's own adapter-contract.ts
320
+ // runSchemaContract (200ms bound), a separate named check from the existing
321
+ // "rejects invalid input" test above per this suite's own per-check isolation.
322
+ it("invoke() rejects invalid input within a bounded time, never falling through to a general timeout", async () => {
323
+ const { client, cleanup } = await fixture.create();
324
+ try {
325
+ const start = Date.now();
326
+ await expect(client.invoke("conformance.echo", 1, { value: 123 }, { permissions: ["conformance:echo"] })).rejects.toMatchObject({
327
+ code: "invalid-input",
328
+ });
329
+ const elapsed = Date.now() - start;
330
+ expect(elapsed, `schema rejection took ${elapsed}ms -- should be immediate (<200ms)`).toBeLessThan(200);
331
+ }
332
+ finally {
333
+ await cleanup();
334
+ }
335
+ });
336
+ // Human-readable error messages: a validation failure's own .message must
337
+ // never leak an internal validation-library-specific type name or a bare
338
+ // stringified object -- a real bug class this ports from Alef's own
339
+ // adapter-contract.ts (a live zod "[InputValidation]" prefix leak there).
340
+ it("invoke() rejects invalid input with a human-readable message, never a raw validation-library leak", async () => {
341
+ const { client, cleanup } = await fixture.create();
342
+ try {
343
+ const error = await client.invoke("conformance.echo", 1, { value: 123 }, { permissions: ["conformance:echo"] }).catch((e) => e);
344
+ const message = error.message;
345
+ expect(typeof message).toBe("string");
346
+ expect(message).not.toBe("[object Object]");
347
+ expect(message).not.toMatch(/ValueError|TypeBoxError|\[InputValidation\]|ZodError/);
348
+ // Genuinely readable: names which operation failed, not just "invalid".
349
+ expect(message).toContain("conformance.echo");
350
+ }
351
+ finally {
352
+ await cleanup();
353
+ }
354
+ });
355
+ // Streaming-progress-required: any operation declared streaming: true must
356
+ // emit at least one progress event before resolving, once its real
357
+ // execution exceeds a threshold duration -- catches a handler that
358
+ // silently blocks the caller instead of reporting progress despite
359
+ // declaring progress support. One named it() per discovered
360
+ // streaming-capable operation (this suite currently declares exactly one),
361
+ // per this project's own per-check test isolation.
362
+ describe("streaming-progress-required (operations declared streaming: true)", () => {
363
+ for (const { descriptor, thresholdMs } of STREAMING_OPERATIONS) {
364
+ it(`${descriptor.name}@${descriptor.version} emits progress before resolving, once it runs past ${thresholdMs}ms`, async () => {
365
+ const { client, cleanup } = await fixture.create();
366
+ try {
367
+ const progress = [];
368
+ const start = Date.now();
369
+ await client.invoke(descriptor.name, descriptor.version, { value: "hi" }, { onProgress: (p) => progress.push(p) });
370
+ const elapsed = Date.now() - start;
371
+ expect(elapsed, `test fixture ran in ${elapsed}ms, below its own ${thresholdMs}ms threshold -- this check can't prove anything`).toBeGreaterThan(thresholdMs);
372
+ expect(progress.length, `${descriptor.name} ran for ${elapsed}ms but emitted zero progress events -- a streaming: true operation must never silently block`).toBeGreaterThan(0);
373
+ }
374
+ finally {
375
+ await cleanup();
376
+ }
377
+ });
378
+ }
379
+ });
380
+ });
381
+ }
382
+ function normalizeForComparison(text) {
383
+ return text.replace(/\s+/g, "");
384
+ }
385
+ /**
386
+ * True when `renderedLines` is textually indistinguishable (ignoring ANSI styling and whitespace)
387
+ * from `JSON.stringify(rawPayload, null, 2)` -- the exact shape pi-web-spider's `primaryLines()`
388
+ * fell back to for every non-"markdown" format. Whitespace-insensitive so a renderer that reflows
389
+ * the same JSON text to a narrower width still counts as "raw", matching the real bug (a Text
390
+ * component wrapping the identical JSON.stringify output).
391
+ */
392
+ function looksLikeRawJsonDump(renderedLines, rawPayload) {
393
+ let rawJson;
394
+ try {
395
+ rawJson = JSON.stringify(rawPayload, null, 2) ?? "";
396
+ }
397
+ catch {
398
+ return false;
399
+ }
400
+ if (rawJson.length === 0)
401
+ return false;
402
+ const renderedText = renderedLines.join("\n").replace(ANSI_CSI_PATTERN, "");
403
+ return normalizeForComparison(renderedText) === normalizeForComparison(rawJson);
404
+ }
405
+ /**
406
+ * Pure, independently unit-testable core of the declared-value coverage check -- separated from
407
+ * the bun:test `it()` wiring below so a fixture reproducing a known-bad shape (e.g.
408
+ * pi-web-spider's own pre-fix behavior) can be asserted against directly, proving the classifier
409
+ * itself actually detects that bug class rather than trusting the wrapping `it()` alone.
410
+ */
411
+ export function evaluateDeclaredValueCoverage(cases, renderDeclaredValue, options = { width: 80, expanded: true }) {
412
+ const nonRawValues = [];
413
+ const rawValues = [];
414
+ for (const { value, rawPayload } of cases) {
415
+ const lines = renderDeclaredValue(value, rawPayload, options);
416
+ (looksLikeRawJsonDump(lines, rawPayload) ? rawValues : nonRawValues).push(value);
417
+ }
418
+ return { nonRawValues, rawValues };
419
+ }
420
+ function utf8Length(value) {
421
+ return new TextEncoder().encode(value).byteLength;
422
+ }
423
+ // biome-ignore lint/complexity/useRegexLiterals: a constructor avoids control-character lint on the equivalent literal.
424
+ const ANSI_CSI_PATTERN = new RegExp("\\u001B\\[[0-?]*[ -/]*[@-~]", "g");
425
+ function assertPhysicalLines(expect, lines, width) {
426
+ expect(lines.length).toBeGreaterThan(0);
427
+ for (const line of lines) {
428
+ expect(line).not.toContain("\n");
429
+ // ANSI is forbidden in model content, but permitted in host rendering.
430
+ const visible = line.replace(ANSI_CSI_PATTERN, "");
431
+ expect([...visible].length).toBeLessThanOrEqual(width);
432
+ }
433
+ }
434
+ /** Reusable provider-facing dual-channel contract matrix. */
435
+ export function registerToolShellDualChannelConformance(runner, fixture) {
436
+ const { describe, expect, it } = runner;
437
+ describe(`Vehicle Tool Shell dual-channel conformance: ${fixture.label}`, () => {
438
+ it("keeps model and persisted-presentation sentinels isolated under independent named bounds", async () => {
439
+ const { subject, cleanup } = await fixture.create();
440
+ try {
441
+ const snapshot = await subject.execute();
442
+ expect(snapshot.content).toContain("MODEL_ONLY");
443
+ expect(snapshot.content).not.toContain("PRESENTATION_ONLY");
444
+ const details = JSON.stringify(snapshot.details);
445
+ expect(details).toContain("PRESENTATION_ONLY");
446
+ expect(details).not.toContain("MODEL_ONLY");
447
+ expect(details).not.toContain("RAW_SECRET");
448
+ expect(utf8Length(snapshot.content)).toBeLessThanOrEqual(subject.bounds.modelContentBytes);
449
+ expect(utf8Length(details)).toBeLessThanOrEqual(subject.bounds.presentationDetailsBytes);
450
+ }
451
+ finally {
452
+ await cleanup();
453
+ }
454
+ });
455
+ it("keeps model content semantic, ANSI-free, and useful when replay details reject", async () => {
456
+ const { subject, cleanup } = await fixture.create();
457
+ try {
458
+ const snapshot = await subject.execute();
459
+ expect(snapshot.content).not.toContain("\u001b[");
460
+ for (const details of [{ schema: "unknown/v99" }, { malformed: true }, { output: { legacy: true } }, undefined]) {
461
+ const lines = subject.replay(details, snapshot.content, { width: 80, expanded: false });
462
+ expect(lines.join("\n")).toContain("MODEL_ONLY");
463
+ }
464
+ }
465
+ finally {
466
+ await cleanup();
467
+ }
468
+ });
469
+ it("changes only human rendering across collapsed/expanded and 40/80/120 layouts", async () => {
470
+ const { subject, cleanup } = await fixture.create();
471
+ try {
472
+ const snapshot = await subject.execute();
473
+ const before = JSON.stringify(snapshot);
474
+ for (const width of [40, 80, 120]) {
475
+ assertPhysicalLines(expect, subject.render(snapshot, { width, expanded: false }), width);
476
+ assertPhysicalLines(expect, subject.render(snapshot, { width, expanded: true }), width);
477
+ assertPhysicalLines(expect, subject.render(snapshot, { width, expanded: false, partial: true }), width);
478
+ }
479
+ expect(JSON.stringify(snapshot)).toBe(before);
480
+ }
481
+ finally {
482
+ await cleanup();
483
+ }
484
+ });
485
+ it("never echoes schema-sensitive call input and follows the documented projector exception policy", async () => {
486
+ const { subject, cleanup } = await fixture.create();
487
+ try {
488
+ for (const width of [40, 80, 120]) {
489
+ const call = subject.renderCall({ name: "safe-task", token: "RAW_SECRET" }, width).join("\n");
490
+ expect(call).toContain("safe-task");
491
+ expect(call).not.toContain("RAW_SECRET");
492
+ }
493
+ await expect(subject.invalidProjection()).rejects.toBeTruthy();
494
+ }
495
+ finally {
496
+ await cleanup();
497
+ }
498
+ });
499
+ it("renders most of its own declared discriminator values as more than a raw JSON dump of their own payload", async () => {
500
+ const { subject, cleanup } = await fixture.create();
501
+ try {
502
+ const cases = subject.declaredValueCases;
503
+ if (!cases || cases.length === 0)
504
+ return; // opt-in: no discriminator declared, nothing to check
505
+ if (!subject.renderDeclaredValue) {
506
+ throw new Error("declaredValueCases supplied without a matching renderDeclaredValue implementation");
507
+ }
508
+ const renderDeclaredValue = subject.renderDeclaredValue.bind(subject);
509
+ const options = { width: 80, expanded: true };
510
+ for (const { value, rawPayload } of cases) {
511
+ assertPhysicalLines(expect, renderDeclaredValue(value, rawPayload, options), options.width);
512
+ }
513
+ const { nonRawValues, rawValues } = evaluateDeclaredValueCoverage(cases, renderDeclaredValue, options);
514
+ expect(nonRawValues.length, `declared values [${cases.map((c) => c.value).join(", ")}] mostly render as an undifferentiated JSON.stringify dump of ` +
515
+ `their own payload -- only [${nonRawValues.join(", ") || "none"}] escape it, [${rawValues.join(", ")}] don't`).toBeGreaterThanOrEqual(Math.min(2, cases.length));
516
+ }
517
+ finally {
518
+ await cleanup();
519
+ }
520
+ });
521
+ });
522
+ }
@@ -0,0 +1,11 @@
1
+ import { type ToolShellDualChannelFixture, type VehicleConformanceFixture } from "./vehicle-conformance.js";
2
+ export interface VitestConformanceApi {
3
+ describe(name: string, body: () => void): unknown;
4
+ it(name: string, body: () => void | Promise<void>): unknown;
5
+ expect(actual: unknown, message?: string): unknown;
6
+ }
7
+ /** Registers the shared Vehicle client matrix through a Vitest-compatible API. */
8
+ export declare function runVehicleClientConformanceWithVitest(api: VitestConformanceApi, fixture: VehicleConformanceFixture): void;
9
+ /** Registers the shared Tool Shell matrix through a Vitest-compatible API. */
10
+ export declare function runToolShellDualChannelConformanceWithVitest(api: VitestConformanceApi, fixture: ToolShellDualChannelFixture): void;
11
+ export * from "./vehicle-conformance.js";
package/dist/vitest.js ADDED
@@ -0,0 +1,17 @@
1
+ import { registerToolShellDualChannelConformance, registerVehicleClientConformance, } from "./vehicle-conformance.js";
2
+ function runner(api) {
3
+ return {
4
+ describe: (name, body) => api.describe(name, body),
5
+ it: (name, body) => api.it(name, body),
6
+ expect: (actual, message) => api.expect(actual, message),
7
+ };
8
+ }
9
+ /** Registers the shared Vehicle client matrix through a Vitest-compatible API. */
10
+ export function runVehicleClientConformanceWithVitest(api, fixture) {
11
+ registerVehicleClientConformance(runner(api), fixture);
12
+ }
13
+ /** Registers the shared Tool Shell matrix through a Vitest-compatible API. */
14
+ export function runToolShellDualChannelConformanceWithVitest(api, fixture) {
15
+ registerToolShellDualChannelConformance(runner(api), fixture);
16
+ }
17
+ export * from "./vehicle-conformance.js";
package/package.json CHANGED
@@ -1,24 +1,36 @@
1
1
  {
2
2
  "name": "@danypops/vehicle-conformance",
3
- "version": "0.4.1",
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.",
3
+ "version": "0.4.3",
4
+ "description": "Runner-neutral conformance suite for VehicleClient implementations, with Bun and Vitest adapters over one shared assertion matrix.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "main": "./src/vehicle-conformance.ts",
8
- "types": "./src/vehicle-conformance.ts",
7
+ "main": "./dist/bun.js",
8
+ "types": "./dist/bun.d.ts",
9
9
  "exports": {
10
- ".": "./src/vehicle-conformance.ts"
10
+ ".": {
11
+ "types": "./dist/bun.d.ts",
12
+ "default": "./dist/bun.js"
13
+ },
14
+ "./core": {
15
+ "types": "./dist/vehicle-conformance.d.ts",
16
+ "default": "./dist/vehicle-conformance.js"
17
+ },
18
+ "./vitest": {
19
+ "types": "./dist/vitest.d.ts",
20
+ "default": "./dist/vitest.js"
21
+ }
11
22
  },
12
23
  "scripts": {
13
- "test": "bun test test",
24
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
25
+ "test": "bun run build && bun test test",
14
26
  "typecheck": "tsc --noEmit"
15
27
  },
16
28
  "dependencies": {
17
- "@danypops/vehicle-core": "^0.19.0",
18
- "@danypops/vehicle-server": "^0.27.0"
29
+ "@danypops/vehicle-core": "^0.19.2",
30
+ "@danypops/vehicle-server": "^0.27.3"
19
31
  },
20
32
  "devDependencies": {
21
- "@danypops/vehicle-client": "^0.10.6",
33
+ "@danypops/vehicle-client": "^0.10.8",
22
34
  "@types/node": "^22.0.0",
23
35
  "typescript": "^5.9.3"
24
36
  },
@@ -34,6 +46,7 @@
34
46
  ],
35
47
  "files": [
36
48
  "src",
49
+ "dist",
37
50
  "README.md"
38
51
  ]
39
52
  }
package/src/bun.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ registerToolShellDualChannelConformance,
4
+ registerVehicleClientConformance,
5
+ type ToolShellDualChannelFixture,
6
+ type VehicleConformanceFixture,
7
+ type VehicleConformanceMatchers,
8
+ type VehicleConformanceRunner,
9
+ } from "./vehicle-conformance.js";
10
+
11
+ const bunRunner: VehicleConformanceRunner = {
12
+ describe: (name, body) => describe(name, body),
13
+ it: (name, body) => it(name, body),
14
+ expect: (actual, message) => expect(actual, message) as unknown as VehicleConformanceMatchers,
15
+ };
16
+
17
+ /** Registers the shared Vehicle client matrix with Bun's test runner. */
18
+ export function runVehicleClientConformance(fixture: VehicleConformanceFixture): void {
19
+ registerVehicleClientConformance(bunRunner, fixture);
20
+ }
21
+
22
+ /** Registers the shared Tool Shell matrix with Bun's test runner. */
23
+ export function runToolShellDualChannelConformance(fixture: ToolShellDualChannelFixture): void {
24
+ registerToolShellDualChannelConformance(bunRunner, fixture);
25
+ }
26
+
27
+ export * from "./vehicle-conformance.js";
@@ -18,8 +18,31 @@
18
18
  * argument parsing) stay out of this module entirely, per the extraction
19
19
  * scope this generalizes.
20
20
  */
21
- import { describe, expect, it } from "bun:test";
22
21
  import type { VehicleClient } from "@danypops/vehicle-core";
22
+
23
+ export interface VehicleConformanceMatchers {
24
+ toEqual(expected: unknown): unknown;
25
+ toMatchObject(expected: unknown): unknown;
26
+ toBe(expected: unknown): unknown;
27
+ toBeTruthy(): unknown;
28
+ toBeUndefined(): unknown;
29
+ toContain(expected: unknown): unknown;
30
+ toMatch(expected: RegExp | string): unknown;
31
+ toBeGreaterThan(expected: number): unknown;
32
+ toBeGreaterThanOrEqual(expected: number): unknown;
33
+ toBeLessThan(expected: number): unknown;
34
+ toBeLessThanOrEqual(expected: number): unknown;
35
+ readonly not: VehicleConformanceMatchers;
36
+ readonly rejects: VehicleConformanceMatchers;
37
+ readonly resolves: VehicleConformanceMatchers;
38
+ }
39
+
40
+ export interface VehicleConformanceRunner {
41
+ describe(name: string, body: () => void): unknown;
42
+ it(name: string, body: () => void | Promise<void>): unknown;
43
+ expect(actual: unknown, message?: string): VehicleConformanceMatchers;
44
+ }
45
+
23
46
  import { bindVehicleOperation, defineVehicleOperation, defineVehicleSchema, VehicleError } from "@danypops/vehicle-core";
24
47
  import type { VehicleRegistry } from "@danypops/vehicle-server";
25
48
 
@@ -77,6 +100,19 @@ const ConformanceKeyed = defineVehicleOperation({
77
100
  output: outputSchema,
78
101
  permissions: [],
79
102
  effect: "external-write",
103
+ requiresApproval: false,
104
+ idempotency: { mode: "keyed", retentionMs: 60_000 },
105
+ limits: LIMITS,
106
+ });
107
+
108
+ const ConformanceUnconfiguredRisk = defineVehicleOperation({
109
+ name: "conformance.unconfigured-risk",
110
+ version: 1,
111
+ description: "Proves that risky operations require an explicit registry approval-policy decision.",
112
+ input: passthroughSchema,
113
+ output: outputSchema,
114
+ permissions: [],
115
+ effect: "external-write",
80
116
  idempotency: { mode: "keyed", retentionMs: 60_000 },
81
117
  limits: LIMITS,
82
118
  });
@@ -137,6 +173,10 @@ export function registerConformanceOperations(registry: VehicleRegistry): void {
137
173
  "conformance",
138
174
  bindVehicleOperation(ConformanceKeyed, () => async (context) => ({ echoed: context.input.value })),
139
175
  );
176
+ registry.register(
177
+ "conformance",
178
+ bindVehicleOperation(ConformanceUnconfiguredRisk, () => async (context) => ({ echoed: context.input.value })),
179
+ );
140
180
  registry.register(
141
181
  "conformance",
142
182
  bindVehicleOperation(ConformanceProgress, () => async (context) => {
@@ -173,7 +213,8 @@ export interface VehicleConformanceFixture {
173
213
  create(): Promise<{ client: VehicleClient; cleanup: () => Promise<void> }>;
174
214
  }
175
215
 
176
- export function runVehicleClientConformance(fixture: VehicleConformanceFixture): void {
216
+ export function registerVehicleClientConformance(runner: VehicleConformanceRunner, fixture: VehicleConformanceFixture): void {
217
+ const { describe, expect, it } = runner;
177
218
  describe(`Vehicle client conformance: ${fixture.label}`, () => {
178
219
  it("manifest() lists every registered operation with its real descriptor fields", async () => {
179
220
  const { client, cleanup } = await fixture.create();
@@ -187,6 +228,7 @@ export function runVehicleClientConformance(fixture: VehicleConformanceFixture):
187
228
  "conformance.never@1",
188
229
  "conformance.progress@1",
189
230
  "conformance.slow-progress@1",
231
+ "conformance.unconfigured-risk@1",
190
232
  ]);
191
233
  const echo = manifest.operations.find((op) => op.name === "conformance.echo");
192
234
  expect(echo?.permissions).toEqual(["conformance:echo"]);
@@ -201,6 +243,21 @@ export function runVehicleClientConformance(fixture: VehicleConformanceFixture):
201
243
  }
202
244
  });
203
245
 
246
+ it("negotiate() agrees on the shared protocol and rejects an incompatible range", async () => {
247
+ const { client, cleanup } = await fixture.create();
248
+ try {
249
+ if (!client.negotiate) throw new Error("Vehicle client does not implement protocol negotiation");
250
+ await expect(
251
+ client.negotiate({ minimumVersion: 1, maximumVersion: 2, requiredCapabilities: [], optionalCapabilities: ["future"] }),
252
+ ).resolves.toEqual({ version: 1, capabilities: [] });
253
+ await expect(
254
+ client.negotiate({ minimumVersion: 2, maximumVersion: 3, requiredCapabilities: [], optionalCapabilities: [] }),
255
+ ).rejects.toMatchObject({ code: "protocol-version-incompatible" });
256
+ } finally {
257
+ await cleanup();
258
+ }
259
+ });
260
+
204
261
  it("invoke() returns the real handler output on success", async () => {
205
262
  const { client, cleanup } = await fixture.create();
206
263
  try {
@@ -264,6 +321,22 @@ export function runVehicleClientConformance(fixture: VehicleConformanceFixture):
264
321
  }
265
322
  });
266
323
 
324
+ it("reports an unconfigured risky operation as a hard failure", async () => {
325
+ const { client, cleanup } = await fixture.create();
326
+ try {
327
+ const manifest = await client.manifest();
328
+ expect(manifest.approvalPolicy).toMatchObject({
329
+ status: "unconfigured",
330
+ unconfiguredRiskyOperations: ["conformance.unconfigured-risk@1"],
331
+ });
332
+ await expect(client.invoke("conformance.unconfigured-risk", 1, { value: "x" }, { idempotencyKey: "risk-1" })).rejects.toMatchObject(
333
+ { code: "approval-policy-unconfigured", category: "authorization" },
334
+ );
335
+ } finally {
336
+ await cleanup();
337
+ }
338
+ });
339
+
267
340
  it("invoke() rejects a request exceeding its declared byte bound", async () => {
268
341
  const { client, cleanup } = await fixture.create();
269
342
  try {
@@ -526,7 +599,7 @@ function utf8Length(value: string): number {
526
599
  // biome-ignore lint/complexity/useRegexLiterals: a constructor avoids control-character lint on the equivalent literal.
527
600
  const ANSI_CSI_PATTERN = new RegExp("\\u001B\\[[0-?]*[ -/]*[@-~]", "g");
528
601
 
529
- function assertPhysicalLines(lines: readonly string[], width: number): void {
602
+ function assertPhysicalLines(expect: VehicleConformanceRunner["expect"], lines: readonly string[], width: number): void {
530
603
  expect(lines.length).toBeGreaterThan(0);
531
604
  for (const line of lines) {
532
605
  expect(line).not.toContain("\n");
@@ -537,7 +610,8 @@ function assertPhysicalLines(lines: readonly string[], width: number): void {
537
610
  }
538
611
 
539
612
  /** Reusable provider-facing dual-channel contract matrix. */
540
- export function runToolShellDualChannelConformance(fixture: ToolShellDualChannelFixture): void {
613
+ export function registerToolShellDualChannelConformance(runner: VehicleConformanceRunner, fixture: ToolShellDualChannelFixture): void {
614
+ const { describe, expect, it } = runner;
541
615
  describe(`Vehicle Tool Shell dual-channel conformance: ${fixture.label}`, () => {
542
616
  it("keeps model and persisted-presentation sentinels isolated under independent named bounds", async () => {
543
617
  const { subject, cleanup } = await fixture.create();
@@ -576,9 +650,9 @@ export function runToolShellDualChannelConformance(fixture: ToolShellDualChannel
576
650
  const snapshot = await subject.execute();
577
651
  const before = JSON.stringify(snapshot);
578
652
  for (const width of [40, 80, 120] as const) {
579
- assertPhysicalLines(subject.render(snapshot, { width, expanded: false }), width);
580
- assertPhysicalLines(subject.render(snapshot, { width, expanded: true }), width);
581
- assertPhysicalLines(subject.render(snapshot, { width, expanded: false, partial: true }), width);
653
+ assertPhysicalLines(expect, subject.render(snapshot, { width, expanded: false }), width);
654
+ assertPhysicalLines(expect, subject.render(snapshot, { width, expanded: true }), width);
655
+ assertPhysicalLines(expect, subject.render(snapshot, { width, expanded: false, partial: true }), width);
582
656
  }
583
657
  expect(JSON.stringify(snapshot)).toBe(before);
584
658
  } finally {
@@ -611,7 +685,7 @@ export function runToolShellDualChannelConformance(fixture: ToolShellDualChannel
611
685
  const renderDeclaredValue = subject.renderDeclaredValue.bind(subject);
612
686
  const options: ToolShellRenderOptions = { width: 80, expanded: true };
613
687
  for (const { value, rawPayload } of cases) {
614
- assertPhysicalLines(renderDeclaredValue(value, rawPayload, options), options.width);
688
+ assertPhysicalLines(expect, renderDeclaredValue(value, rawPayload, options), options.width);
615
689
  }
616
690
  const { nonRawValues, rawValues } = evaluateDeclaredValueCoverage(cases, renderDeclaredValue, options);
617
691
  expect(
package/src/vitest.ts ADDED
@@ -0,0 +1,34 @@
1
+ import {
2
+ registerToolShellDualChannelConformance,
3
+ registerVehicleClientConformance,
4
+ type ToolShellDualChannelFixture,
5
+ type VehicleConformanceFixture,
6
+ type VehicleConformanceMatchers,
7
+ type VehicleConformanceRunner,
8
+ } from "./vehicle-conformance.js";
9
+
10
+ export interface VitestConformanceApi {
11
+ describe(name: string, body: () => void): unknown;
12
+ it(name: string, body: () => void | Promise<void>): unknown;
13
+ expect(actual: unknown, message?: string): unknown;
14
+ }
15
+
16
+ function runner(api: VitestConformanceApi): VehicleConformanceRunner {
17
+ return {
18
+ describe: (name, body) => api.describe(name, body),
19
+ it: (name, body) => api.it(name, body),
20
+ expect: (actual, message) => api.expect(actual, message) as VehicleConformanceMatchers,
21
+ };
22
+ }
23
+
24
+ /** Registers the shared Vehicle client matrix through a Vitest-compatible API. */
25
+ export function runVehicleClientConformanceWithVitest(api: VitestConformanceApi, fixture: VehicleConformanceFixture): void {
26
+ registerVehicleClientConformance(runner(api), fixture);
27
+ }
28
+
29
+ /** Registers the shared Tool Shell matrix through a Vitest-compatible API. */
30
+ export function runToolShellDualChannelConformanceWithVitest(api: VitestConformanceApi, fixture: ToolShellDualChannelFixture): void {
31
+ registerToolShellDualChannelConformance(runner(api), fixture);
32
+ }
33
+
34
+ export * from "./vehicle-conformance.js";