@routecraft/testing 0.6.0-canary.8 → 0.6.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 +14 -10
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +171 -33
- package/dist/index.d.ts +171 -33
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -7
package/dist/index.d.cts
CHANGED
|
@@ -1,21 +1,53 @@
|
|
|
1
|
-
import { AdapterOverride, AdapterSourceCall, AdapterSendCall, SourceOverrideBehavior, SendOverrideHandler, Source, CraftContext, CraftClient, RoutecraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition,
|
|
2
|
-
import { vi } from 'vitest';
|
|
1
|
+
import { AdapterOverride, AdapterSourceCall, AdapterSendCall, SourceOverrideBehavior, SendOverrideHandler, Source, CraftContext, CraftClient, RoutecraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, AnyRouteBuilder, Destination, Enricher, Processor, Exchange, ExchangeHeaders, SourceFixture, logger, OnParseError, SourceMeta, Subscription } from '@routecraft/routecraft';
|
|
3
2
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
5
|
+
* Minimal runner-agnostic spy function. Records calls in the jest-compatible
|
|
6
|
+
* `mock.calls` shape so assertions like
|
|
7
|
+
* `expect(t.logger.warn.mock.calls.some(...))` work under bun:test, Vitest,
|
|
8
|
+
* and node:test without this package importing any runner.
|
|
9
|
+
*
|
|
10
|
+
* Runner mocks (`vi.fn` from Vitest, `mock` from bun:test) are structurally
|
|
11
|
+
* assignable to this interface, so they can be injected via {@link SpyFactory}
|
|
12
|
+
* when full matcher support (`expect(fn).toHaveBeenCalledWith(...)`) is
|
|
13
|
+
* wanted.
|
|
14
|
+
*/
|
|
15
|
+
interface SpyFn {
|
|
16
|
+
(...args: unknown[]): unknown;
|
|
17
|
+
mock: {
|
|
18
|
+
calls: unknown[][];
|
|
19
|
+
};
|
|
20
|
+
mockImplementation(impl: (...args: unknown[]) => unknown): SpyFn;
|
|
21
|
+
mockClear(): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Factory producing spy functions. Defaults to the built-in {@link createSpyFn};
|
|
25
|
+
* pass your runner's mock factory (`vi.fn`, or `mock` from bun:test) to get
|
|
26
|
+
* native mocks that work with the runner's `expect` matchers.
|
|
27
|
+
*/
|
|
28
|
+
type SpyFactory = () => SpyFn;
|
|
29
|
+
/**
|
|
30
|
+
* Create a built-in spy function. Dependency-free; records calls in
|
|
31
|
+
* `fn.mock.calls` and supports `mockImplementation` / `mockClear`.
|
|
32
|
+
*/
|
|
33
|
+
declare function createSpyFn(): SpyFn;
|
|
34
|
+
/**
|
|
35
|
+
* Spy logger with spy methods for assertions (e.g.
|
|
36
|
+
* `t.logger.info.mock.calls` under any runner, or
|
|
37
|
+
* `expect(t.logger.info).toHaveBeenCalledWith(...)` when built from an
|
|
38
|
+
* injected runner mock factory).
|
|
7
39
|
*/
|
|
8
40
|
type SpyLogger = {
|
|
9
|
-
info:
|
|
10
|
-
debug:
|
|
11
|
-
warn:
|
|
12
|
-
error:
|
|
13
|
-
trace:
|
|
14
|
-
fatal:
|
|
15
|
-
child:
|
|
41
|
+
info: SpyFn;
|
|
42
|
+
debug: SpyFn;
|
|
43
|
+
warn: SpyFn;
|
|
44
|
+
error: SpyFn;
|
|
45
|
+
trace: SpyFn;
|
|
46
|
+
fatal: SpyFn;
|
|
47
|
+
child: SpyFn;
|
|
16
48
|
};
|
|
17
|
-
declare function createSpyLogger(): SpyLogger;
|
|
18
|
-
declare function createNoopSpyLogger(): SpyLogger;
|
|
49
|
+
declare function createSpyLogger(fn?: SpyFactory): SpyLogger;
|
|
50
|
+
declare function createNoopSpyLogger(fn?: SpyFactory): SpyLogger;
|
|
19
51
|
|
|
20
52
|
/**
|
|
21
53
|
* Brand symbol stamped on the handles returned by `mockAdapter()`.
|
|
@@ -50,9 +82,13 @@ interface MockAdapterBehavior<M = unknown> {
|
|
|
50
82
|
*/
|
|
51
83
|
source?: SourceOverrideBehavior<M>;
|
|
52
84
|
/**
|
|
53
|
-
* Destination-role behaviour. Used when the adapter is passed to
|
|
54
|
-
* `.enrich()`, or `.tap()`. Receives the exchange and a meta
|
|
55
|
-
* the construction args
|
|
85
|
+
* Destination/enricher-role behaviour. Used when the adapter is passed to
|
|
86
|
+
* `.to()`, `.enrich()`, or `.tap()`. Receives the exchange and a meta
|
|
87
|
+
* object with the construction args. The handler's return value follows
|
|
88
|
+
* the step's slot resolution: a fetch-resolved step (`.enrich()`, or a
|
|
89
|
+
* fetch-only adapter in `.to()`) uses it as the fetched value (replacing
|
|
90
|
+
* the body by default), while a send-resolved `.to()` discards it (send
|
|
91
|
+
* is void) and `.tap()` always discards.
|
|
56
92
|
*/
|
|
57
93
|
send?: SendOverrideHandler;
|
|
58
94
|
}
|
|
@@ -80,7 +116,7 @@ interface AdapterMock {
|
|
|
80
116
|
* - An adapter factory (e.g. `mail`, `http`, `mcp`). The mock matches every
|
|
81
117
|
* adapter instance produced by that factory. Requires the factory to stamp
|
|
82
118
|
* its adapters via `tagAdapter()`.
|
|
83
|
-
* - An adapter class (e.g. `MailSourceAdapter`, `
|
|
119
|
+
* - An adapter class (e.g. `MailSourceAdapter`, `HttpEnricherAdapter`).
|
|
84
120
|
* The mock matches any adapter whose `constructor === target`. Works for
|
|
85
121
|
* every adapter without opt-in tagging, including third-party ones.
|
|
86
122
|
*
|
|
@@ -104,7 +140,7 @@ interface AdapterMock {
|
|
|
104
140
|
*
|
|
105
141
|
* const mailMock = mockAdapter(mail, {
|
|
106
142
|
* source: [{ uid: 1, from: "a@b", subject: "hi", ... }],
|
|
107
|
-
* send: async () =>
|
|
143
|
+
* send: async () => undefined,
|
|
108
144
|
* });
|
|
109
145
|
*
|
|
110
146
|
* // Class form (works for any adapter, including third-party ones)
|
|
@@ -128,6 +164,13 @@ declare function mockAdapter<T extends ((...args: never[]) => unknown) | (new (.
|
|
|
128
164
|
interface TestContextOptions {
|
|
129
165
|
/** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */
|
|
130
166
|
routesReadyTimeoutMs?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Mock factory used to build the spy logger. Defaults to the built-in
|
|
169
|
+
* runner-agnostic spy. Pass your runner's factory (`vi.fn` from Vitest, or
|
|
170
|
+
* `mock` from bun:test) to get native mocks that work with the runner's
|
|
171
|
+
* `expect` matchers, e.g. `testContext({ fn: vi.fn })`.
|
|
172
|
+
*/
|
|
173
|
+
fn?: SpyFactory;
|
|
131
174
|
}
|
|
132
175
|
/**
|
|
133
176
|
* Options for TestContext.test().
|
|
@@ -143,13 +186,13 @@ interface TestOptions {
|
|
|
143
186
|
/**
|
|
144
187
|
* Test-friendly wrapper around CraftContext. Runs the real context but manages
|
|
145
188
|
* lifecycle (start, wait routes ready, drain, stop) and collects errors.
|
|
146
|
-
* t.logger is a spy logger
|
|
189
|
+
* t.logger is a spy logger for asserting on log calls.
|
|
147
190
|
*/
|
|
148
191
|
declare class TestContext {
|
|
149
192
|
readonly ctx: CraftContext;
|
|
150
193
|
/** Client for dispatching messages to direct endpoints in tests. */
|
|
151
194
|
readonly client: CraftClient;
|
|
152
|
-
/** Spy logger; e.g. expect(t.logger.info).toHaveBeenCalledWith(...) */
|
|
195
|
+
/** Spy logger; e.g. t.logger.info.mock.calls, or expect(t.logger.info).toHaveBeenCalledWith(...) with an injected runner mock factory */
|
|
153
196
|
readonly logger: SpyLogger;
|
|
154
197
|
readonly errors: RoutecraftError[];
|
|
155
198
|
private readonly routesReadyTimeoutMs;
|
|
@@ -162,23 +205,23 @@ declare class TestContext {
|
|
|
162
205
|
});
|
|
163
206
|
/**
|
|
164
207
|
* Build a promise that resolves once every route has emitted
|
|
165
|
-
* `route
|
|
208
|
+
* `route:started`, or rejects on `context:error` or the configured
|
|
166
209
|
* routes-ready timeout. Shared by {@link startAndWaitReady} and {@link test}.
|
|
167
210
|
*/
|
|
168
211
|
private awaitRoutesReady;
|
|
169
212
|
/**
|
|
170
|
-
* Start context and resolve once every route has emitted `route
|
|
213
|
+
* Start context and resolve once every route has emitted `route:started`.
|
|
171
214
|
* Does not drain or stop. Does not await `ctx.start()` completion, which
|
|
172
215
|
* lets this method work with long-running sources (direct, mcp, HTTP, etc.)
|
|
173
216
|
* whose subscribe blocks until the route is aborted. The start promise is
|
|
174
217
|
* stored internally and awaited by {@link stop} for clean shutdown.
|
|
175
218
|
*
|
|
176
|
-
* Use with {@link CraftClient.
|
|
219
|
+
* Use with {@link CraftClient.sendDirect} (via `t.client`) for direct endpoints,
|
|
177
220
|
* or drive sources directly via the context store, then call `drain()` /
|
|
178
221
|
* `stop()` when done.
|
|
179
222
|
*
|
|
180
223
|
* If `ctx.start()` rejects (synchronously or before any route emits
|
|
181
|
-
* `route
|
|
224
|
+
* `route:started`), the rejection surfaces here via the
|
|
182
225
|
* `context:error` listener installed by `awaitRoutesReady`. A no-op
|
|
183
226
|
* catch is attached to `startedPromise` as a safety net so that a
|
|
184
227
|
* slow rejection does not trigger an `unhandledRejection` before
|
|
@@ -205,6 +248,8 @@ declare class TestContextBuilder {
|
|
|
205
248
|
private builder;
|
|
206
249
|
private routesReadyTimeoutMs;
|
|
207
250
|
private adapterOverrides;
|
|
251
|
+
private readonly spyFactory;
|
|
252
|
+
constructor(options?: TestContextOptions);
|
|
208
253
|
/** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */
|
|
209
254
|
routesReadyTimeout(ms: number): this;
|
|
210
255
|
/**
|
|
@@ -218,18 +263,27 @@ declare class TestContextBuilder {
|
|
|
218
263
|
on<K extends EventName>(event: K, handler: EventHandler<K>): this;
|
|
219
264
|
once<K extends EventName>(event: K, handler: EventHandler<K>): this;
|
|
220
265
|
store<K extends keyof StoreRegistry>(key: K, value: StoreRegistry[K]): this;
|
|
221
|
-
routes(routes: RouteDefinition[] |
|
|
266
|
+
routes(routes: RouteDefinition[] | AnyRouteBuilder[] | RouteDefinition | AnyRouteBuilder): this;
|
|
222
267
|
build(): Promise<TestContext>;
|
|
223
268
|
}
|
|
224
269
|
/**
|
|
225
270
|
* Create a test context builder. Use .routes(...).build(), await the result, then await t.test().
|
|
226
271
|
*
|
|
272
|
+
* Runner-agnostic by default: the spy logger uses a built-in spy that records
|
|
273
|
+
* calls in the jest-compatible `mock.calls` shape. Pass `{ fn }` with your
|
|
274
|
+
* runner's mock factory (`vi.fn` from Vitest, `mock` from bun:test) when you
|
|
275
|
+
* want `expect(t.logger.info).toHaveBeenCalledWith(...)` matcher support.
|
|
276
|
+
*
|
|
227
277
|
* @example
|
|
228
278
|
* const builder = testContext();
|
|
229
279
|
* const t = await builder.routes(myRoutes).build();
|
|
230
280
|
* await t.test();
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* // Vitest, with native matcher support:
|
|
284
|
+
* const t = await testContext({ fn: vi.fn }).routes(myRoutes).build();
|
|
231
285
|
*/
|
|
232
|
-
declare function testContext(): TestContextBuilder;
|
|
286
|
+
declare function testContext(options?: TestContextOptions): TestContextBuilder;
|
|
233
287
|
|
|
234
288
|
interface PseudoOptions {
|
|
235
289
|
runtime?: "throw" | "noop";
|
|
@@ -243,7 +297,7 @@ interface PseudoKeyedOptions extends PseudoOptions {
|
|
|
243
297
|
*/
|
|
244
298
|
type PseudoAdapter<R> = {
|
|
245
299
|
adapterId: string;
|
|
246
|
-
} & Source<R> & Destination<any, R> & Processor<any, R>;
|
|
300
|
+
} & Source<R> & Destination<any> & Enricher<any, R> & Processor<any, R>;
|
|
247
301
|
/** @internal */
|
|
248
302
|
type PseudoFactory<Opts> = <R = unknown>(opts: Opts) => PseudoAdapter<R>;
|
|
249
303
|
/** @internal */
|
|
@@ -256,8 +310,10 @@ declare function pseudo<Opts extends Record<string, unknown> = Record<string, un
|
|
|
256
310
|
|
|
257
311
|
/**
|
|
258
312
|
* A spy adapter that records all exchanges passing through it.
|
|
259
|
-
* Implements
|
|
260
|
-
* with `.to()`, `.
|
|
313
|
+
* Implements {@link Destination} (send), {@link Enricher} (fetch), and
|
|
314
|
+
* {@link Processor} so it can be used with `.to()`, `.tap()`, `.enrich()`,
|
|
315
|
+
* and `.process()`. The fetch face returns the current body, so a bare
|
|
316
|
+
* `.enrich(spy())` observes without changing the body.
|
|
261
317
|
*/
|
|
262
318
|
type SpyAdapter<T = unknown> = {
|
|
263
319
|
/** Stable identifier for this adapter. */
|
|
@@ -276,7 +332,7 @@ type SpyAdapter<T = unknown> = {
|
|
|
276
332
|
lastReceived(): Exchange<T>;
|
|
277
333
|
/** Array of just the body values from received exchanges. */
|
|
278
334
|
receivedBodies(): T[];
|
|
279
|
-
} & Destination<any,
|
|
335
|
+
} & Destination<any> & Enricher<any, T> & Processor<any, T>;
|
|
280
336
|
/**
|
|
281
337
|
* Creates a spy adapter that records all exchanges for test assertions.
|
|
282
338
|
*
|
|
@@ -299,6 +355,37 @@ type SpyAdapter<T = unknown> = {
|
|
|
299
355
|
*/
|
|
300
356
|
declare function spy<T = unknown>(): SpyAdapter<T>;
|
|
301
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Wrap a message body with the headers a real source would have attached, for
|
|
360
|
+
* use as a `mockAdapter(..., { source: [...] })` fixture.
|
|
361
|
+
*
|
|
362
|
+
* Envelope-carrying sources (mail, http) split each incoming message into a
|
|
363
|
+
* payload on `exchange.body` and metadata on `routecraft.<adapter>.*` headers.
|
|
364
|
+
* A bare fixture array only sets the body, so a route that reads those headers
|
|
365
|
+
* cannot be exercised through the mock. `sourceMessage(body, headers)` lets the
|
|
366
|
+
* mock reproduce that split.
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```typescript
|
|
370
|
+
* const mailMock = mockAdapter(mail, {
|
|
371
|
+
* source: [
|
|
372
|
+
* sourceMessage(
|
|
373
|
+
* { text: "Tracking: ABC123" },
|
|
374
|
+
* {
|
|
375
|
+
* "routecraft.mail.from": "noreply@acme.test",
|
|
376
|
+
* "routecraft.mail.subject": "Your order has shipped",
|
|
377
|
+
* },
|
|
378
|
+
* ),
|
|
379
|
+
* ],
|
|
380
|
+
* });
|
|
381
|
+
* ```
|
|
382
|
+
*
|
|
383
|
+
* @param body - The body the source would deliver on the exchange
|
|
384
|
+
* @param headers - Headers the source would attach (omit for a body-only fixture)
|
|
385
|
+
* @returns A branded fixture recognised by the source-mock dispatcher
|
|
386
|
+
*/
|
|
387
|
+
declare function sourceMessage<M>(body: M, headers?: ExchangeHeaders): SourceFixture<M>;
|
|
388
|
+
|
|
302
389
|
/**
|
|
303
390
|
* Structural shape of a fn-like spec for testing. Does not import
|
|
304
391
|
* `FnOptions` from `@routecraft/ai` so this package stays free of
|
|
@@ -357,6 +444,43 @@ interface TestFnOptions {
|
|
|
357
444
|
*/
|
|
358
445
|
declare function testFn<TIn, TOut>(spec: TestFnSpec<TIn, TOut>, input: unknown, options?: TestFnOptions): Promise<TOut>;
|
|
359
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Options for {@link testSubscription}. The `handler` mirrors the message
|
|
449
|
+
* fields the engine receives, flattened into positional arguments so test
|
|
450
|
+
* assertions read naturally.
|
|
451
|
+
*/
|
|
452
|
+
interface TestSubscriptionOptions<T = unknown> {
|
|
453
|
+
/** Context handed to the source (store access, logger). */
|
|
454
|
+
context: CraftContext;
|
|
455
|
+
/** Receives each emitted message; its return value resolves `emit`. */
|
|
456
|
+
handler: (message: T, headers?: ExchangeHeaders, parse?: (raw: unknown) => unknown | Promise<unknown>, parseFailureMode?: OnParseError) => Promise<unknown> | unknown;
|
|
457
|
+
/** Abort to stop the source; `complete()` calls its `abort`. */
|
|
458
|
+
abortController?: AbortController;
|
|
459
|
+
/** Called when the source signals readiness. */
|
|
460
|
+
onReady?: () => void;
|
|
461
|
+
/** Source meta; defaults to `{ routeId: "test" }`. */
|
|
462
|
+
meta?: SourceMeta;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Build a {@link Subscription} for driving a source adapter directly in a
|
|
466
|
+
* unit test, without a running route. Wire-up mirrors the engine: `emit`
|
|
467
|
+
* forwards to `handler`, `ready` to `onReady`, `complete` aborts the
|
|
468
|
+
* controller, and `signal` observes it.
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* ```typescript
|
|
472
|
+
* const received: unknown[] = [];
|
|
473
|
+
* await adapter.subscribe(
|
|
474
|
+
* testSubscription({
|
|
475
|
+
* context: t.ctx,
|
|
476
|
+
* handler: (message) => void received.push(message),
|
|
477
|
+
* abortController,
|
|
478
|
+
* }),
|
|
479
|
+
* );
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
declare function testSubscription<T = unknown>(options: TestSubscriptionOptions<T>): Subscription<T>;
|
|
483
|
+
|
|
360
484
|
/**
|
|
361
485
|
* Load a JSON fixture file and return the parsed value.
|
|
362
486
|
*
|
|
@@ -365,18 +489,32 @@ declare function testFn<TIn, TOut>(spec: TestFnSpec<TIn, TOut>, input: unknown,
|
|
|
365
489
|
*/
|
|
366
490
|
declare function fixture<T = unknown>(path: string): T;
|
|
367
491
|
/**
|
|
368
|
-
* Fixture entry must have a `name` field used as the
|
|
492
|
+
* Fixture entry must have a `name` field used as the test name.
|
|
369
493
|
*/
|
|
370
494
|
interface FixtureWithName {
|
|
371
495
|
name: string;
|
|
372
496
|
[key: string]: unknown;
|
|
373
497
|
}
|
|
374
498
|
/**
|
|
375
|
-
*
|
|
499
|
+
* A test runner's `test` function, as accepted by {@link fixtureEach}.
|
|
500
|
+
* Both `test` from bun:test and `test` from Vitest satisfy this shape.
|
|
501
|
+
*/
|
|
502
|
+
type FixtureTestFn = (name: string, fn: () => void | Promise<void>) => unknown;
|
|
503
|
+
/**
|
|
504
|
+
* Load a JSON array fixture and run one test per entry. Each entry must have
|
|
505
|
+
* a `name` field (used as the test name). Runner-agnostic: pass your runner's
|
|
506
|
+
* `test` function (bun:test, Vitest, node:test).
|
|
376
507
|
*
|
|
377
508
|
* @param path Path to a JSON file that parses to an array
|
|
509
|
+
* @param test The runner's `test` function used to register each entry
|
|
378
510
|
* @param run Callback invoked per entry; use for assertions. Receives the fixture entry.
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* import { test } from "bun:test";
|
|
514
|
+
* fixtureEach("./cases.json", test, (entry) => {
|
|
515
|
+
* expect(run(entry.input)).toEqual(entry.expected);
|
|
516
|
+
* });
|
|
379
517
|
*/
|
|
380
|
-
declare function fixtureEach<T extends FixtureWithName>(path: string, run: (entry: T) => void | Promise<void>): void;
|
|
518
|
+
declare function fixtureEach<T extends FixtureWithName>(path: string, test: FixtureTestFn, run: (entry: T) => void | Promise<void>): void;
|
|
381
519
|
|
|
382
|
-
export { type AdapterMock, type FixtureWithName, type MockAdapterBehavior, type PseudoKeyedOptions, type PseudoOptions, type SpyAdapter, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestFnHandlerContext, type TestFnOptions, type TestFnSpec, type TestOptions, createNoopSpyLogger, createSpyLogger, fixture, fixtureEach, mockAdapter, pseudo, spy, testContext, testFn };
|
|
520
|
+
export { type AdapterMock, type FixtureTestFn, type FixtureWithName, type MockAdapterBehavior, type PseudoKeyedOptions, type PseudoOptions, type SpyAdapter, type SpyFactory, type SpyFn, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestFnHandlerContext, type TestFnOptions, type TestFnSpec, type TestOptions, type TestSubscriptionOptions, createNoopSpyLogger, createSpyFn, createSpyLogger, fixture, fixtureEach, mockAdapter, pseudo, sourceMessage, spy, testContext, testFn, testSubscription };
|