@routecraft/testing 0.4.0-canary.6 → 0.4.0-canary.8

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,6 +1,6 @@
1
1
  # @routecraft/testing
2
2
 
3
- Test utilities for RouteCraft capabilities. Use with [Vitest](https://vitest.dev) to run capability lifecycles and assert on output, logs, and errors.
3
+ Test utilities for Routecraft capabilities. Use with [Vitest](https://vitest.dev) to run capability lifecycles and assert on output, logs, and errors.
4
4
 
5
5
  ## Installation
6
6
 
@@ -58,15 +58,17 @@ Wrapper around `CraftContext` with:
58
58
  - **`logger`** -- A spy logger (Vitest `vi.fn()` methods) for asserting on log calls.
59
59
  - **`errors`** -- Collected capability errors.
60
60
  - **`test(options?)`** -- Runs start, waits for capabilities to be ready, optionally delays, drains, then stops. Assert after `await t.test()`.
61
- - **`startAndWaitReady()`** -- Starts the context and waits for all capabilities to be ready without draining. Use with `invoke()` to call a capability by ID, then call `stop()` when done.
61
+ - **`startAndWaitReady()`** -- Starts the context and waits for all capabilities to be ready without draining. Use with `t.send()` to send to a direct endpoint, then call `stop()` when done.
62
62
  - **`stop()`** / **`drain()`** -- Lifecycle helpers.
63
63
 
64
- ### `invoke(ctx, routeIdOrDestination, body, headers?)`
64
+ ### `t.send(endpoint, body, headers?)`
65
65
 
66
- Invoke a capability by ID or send to a `Destination` instance. Returns the result.
66
+ Send a message to a direct endpoint and return the result. Use with `startAndWaitReady()`.
67
67
 
68
68
  ```typescript
69
- const result = await invoke(t.ctx, 'send-email', { to: 'user@example.com' });
69
+ await t.startAndWaitReady();
70
+ const result = await t.send('send-email', { to: 'user@example.com' });
71
+ await t.stop();
70
72
  ```
71
73
 
72
74
  ### Options
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- 'use strict';var fs=require('fs'),vitest=require('vitest'),routecraft=require('@routecraft/routecraft');function h(){let r={info:vitest.vi.fn(),debug:vitest.vi.fn(),warn:vitest.vi.fn(),error:vitest.vi.fn(),trace:vitest.vi.fn(),fatal:vitest.vi.fn(),child:vitest.vi.fn()};return r.child.mockImplementation(()=>r),r}function R(){let r=vitest.vi.fn(),e=vitest.vi.fn(),t={info:r,debug:r,warn:r,error:r,trace:r,fatal:r,child:e};return e.mockImplementation(()=>t),t}var k=200,y=class{ctx;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;startedPromise;constructor(e,t){this.ctx=e,this.logger=t?.spyLogger??R(),t?.restoreLoggerChild&&(this.restoreLoggerChild=t.restoreLoggerChild),this.routesReadyTimeoutMs=t?.routesReadyTimeoutMs??k,e.on("error",o=>{let n=o.details.error;this.errors.push(routecraft.isRouteCraftError(n)?n:routecraft.rcError("RC9901",n));});}async startAndWaitReady(){let e=this.ctx,t=e.getRoutes().length,o=t===0?Promise.resolve():new Promise((n,i)=>{let a=0,s=false,d=setTimeout(()=>{s||(s=true,u(),f(),i(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),u=e.on("route:started",()=>{s||(a++,a>=t&&(s=true,clearTimeout(d),u(),f(),n()));}),f=e.on("error",l=>{s||(s=true,clearTimeout(d),u(),f(),i(l.details.error));});});this.startedPromise=e.start(),await Promise.all([this.startedPromise,o]);}async test(e){let t=this.ctx,o=t.getRoutes().length,n=o===0?Promise.resolve():new Promise((a,s)=>{let d=0,u=false,f=setTimeout(()=>{u||(m(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),l=t.on("route:started",()=>{u||(d++,d>=o&&(m(),a()));}),x=t.on("error",P=>{u||(m(),s(P.details.error));});function m(){u||(u=true,l(),x(),f!==void 0&&(clearTimeout(f),f=void 0));}}),i=t.start();try{await n;let a=e?.delayBeforeDrainMs??0;a>0&&await new Promise(s=>setTimeout(s,a)),await t.drain();}finally{try{await t.stop(),await i;}finally{this.restoreLoggerChild?.();}}}drain(){return this.ctx.drain()}async stop(){await this.ctx.stop(),this.startedPromise!==void 0&&await this.startedPromise;}},g=class{builder=new routecraft.ContextBuilder;routesReadyTimeoutMs;routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}with(e){return this.builder.with(e),this}on(e,t){return this.builder.on(e,t),this}once(e,t){return this.builder.once(e,t),this}store(e,t){return this.builder.store(e,t),this}routes(e){return this.builder.routes(e),this}async build(){let e=h(),t=routecraft.logger.child.bind(routecraft.logger);routecraft.logger.child=vitest.vi.fn(()=>e);let o=await this.builder.build(),n={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{routecraft.logger.child=t;}};return new y(o,n)}};function b(){return new g}function L(r){return typeof r=="object"&&r!==null&&typeof r.send=="function"}async function E(r,e,t,o){let n=new routecraft.DefaultExchange(r,{body:t,...o!==void 0&&{headers:o}}),i;if(typeof e=="string"){let s=r.getRouteById(e);if(!s)throw new Error(`No route with id "${e}". Did you start the context (e.g. await t.test())?`);let d=s.definition.source;if(!L(d))throw new Error(`Route "${e}" is not invokable: source must implement Destination (e.g. direct adapter).`);i=d;}else i=e;return await i.send(n)}function w(r,e){let t=()=>{throw new Error(`Pseudo adapter "${r}" is not implemented. Replace with a real adapter.`)},o=()=>Promise.resolve(void 0),n=a=>a,i=(a,s,d,u)=>(u?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${r}`,subscribe:e==="noop"?i:t,send:e==="noop"?o:t,process:e==="noop"?n:t}}function F(r="pseudo",e){let t=e?.runtime??"throw";return e&&"args"in e&&e.args==="keyed"?(n,i)=>w(r,t):n=>w(r,t)}function A(r){return JSON.parse(fs.readFileSync(r,"utf-8"))}function q(r,e){let t=A(r);if(!Array.isArray(t))throw new Error(`fixture.each: expected JSON array at "${r}", got ${typeof t}`);for(let o of t){if(typeof o?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(o)}`);vitest.test(o.name,()=>e(o));}}exports.TestContext=y;exports.TestContextBuilder=g;exports.createNoopSpyLogger=R;exports.createSpyLogger=h;exports.fixture=A;exports.fixtureEach=q;exports.invoke=E;exports.pseudo=F;exports.testContext=b;//# sourceMappingURL=index.cjs.map
1
+ 'use strict';var fs=require('fs'),vitest=require('vitest'),routecraft=require('@routecraft/routecraft');function m(){let t={info:vitest.vi.fn(),debug:vitest.vi.fn(),warn:vitest.vi.fn(),error:vitest.vi.fn(),trace:vitest.vi.fn(),fatal:vitest.vi.fn(),child:vitest.vi.fn()};return t.child.mockImplementation(()=>t),t}function R(){let t=vitest.vi.fn(),e=vitest.vi.fn(),r={info:t,debug:t,warn:t,error:t,trace:t,fatal:t,child:e};return e.mockImplementation(()=>r),r}var k=200,f=class{ctx;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;loggerChildRestored=false;startedPromise;constructor(e,r){this.ctx=e,this.logger=r?.spyLogger??R(),r?.restoreLoggerChild&&(this.restoreLoggerChild=r.restoreLoggerChild),this.routesReadyTimeoutMs=r?.routesReadyTimeoutMs??k,e.on("error",o=>{let n=o.details.error;this.errors.push(routecraft.isRoutecraftError(n)?n:routecraft.rcError("RC9901",n));});}async startAndWaitReady(){let e=this.ctx,r=e.getRoutes().length,o=r===0?Promise.resolve():new Promise((n,a)=>{let i=0,s=false,p=setTimeout(()=>{s||(s=true,d(),c(),a(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),d=e.on("route:started",()=>{s||(i++,i>=r&&(s=true,clearTimeout(p),d(),c(),n()));}),c=e.on("error",g=>{s||(s=true,clearTimeout(p),d(),c(),a(g.details.error));});});this.startedPromise=e.start(),await Promise.all([this.startedPromise,o]);}async test(e){let r=this.ctx,o=r.getRoutes().length,n=o===0?Promise.resolve():new Promise((i,s)=>{let p=0,d=false,c=setTimeout(()=>{d||(h(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),g=r.on("route:started",()=>{d||(p++,p>=o&&(h(),i()));}),v=r.on("error",w=>{d||(h(),s(w.details.error));});function h(){d||(d=true,g(),v(),c!==void 0&&(clearTimeout(c),c=void 0));}}),a=r.start();try{await n;let i=e?.delayBeforeDrainMs??0;i>0&&await new Promise(s=>setTimeout(s,i)),await r.drain();}finally{try{await r.stop(),await a;}finally{this.restoreLoggerChildOnce();}}}drain(){return this.ctx.drain()}async stop(){try{await this.ctx.stop(),this.startedPromise!==void 0&&await this.startedPromise;}finally{this.restoreLoggerChildOnce();}}restoreLoggerChildOnce(){this.loggerChildRestored||(this.restoreLoggerChild?.(),this.loggerChildRestored=true);}async send(e,r,o){let n=this.ctx.getStore(routecraft.ADAPTER_DIRECT_STORE),a=routecraft.sanitizeEndpoint(e),i=n?.get(a);if(!i)throw new Error(`No direct channel for endpoint "${e}". Did you call startAndWaitReady() first?`);let s=new routecraft.DefaultExchange(this.ctx,{body:r,...o!==void 0&&{headers:o}});return (await i.send(a,s)).body}},l=class{builder=new routecraft.ContextBuilder;routesReadyTimeoutMs;routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}with(e){return this.builder.with(e),this}on(e,r){return this.builder.on(e,r),this}once(e,r){return this.builder.once(e,r),this}store(e,r){return this.builder.store(e,r),this}routes(e){return this.builder.routes(e),this}async build(){let e=m(),r=routecraft.logger.child.bind(routecraft.logger);routecraft.logger.child=vitest.vi.fn(()=>e);let o=await this.builder.build(),n={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{routecraft.logger.child=r;}};return new f(o,n)}};function A(){return new l}function x(t,e){let r=()=>{throw new Error(`Pseudo adapter "${t}" is not implemented. Replace with a real adapter.`)},o=()=>Promise.resolve(void 0),n=i=>i,a=(i,s,p,d)=>(d?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${t}`,subscribe:e==="noop"?a:r,send:e==="noop"?o:r,process:e==="noop"?n:r}}function F(t="pseudo",e){let r=e?.runtime??"throw";return e&&"args"in e&&e.args==="keyed"?(n,a)=>x(t,r):n=>x(t,r)}function T(){return {received:[],calls:{send:0,process:0,enrich:0}}}function M(){let t=T();return {adapterId:"routecraft.adapter.spy",received:t.received,calls:t.calls,send(e){t.received.push(e),e.headers?.[routecraft.HeadersKeys.OPERATION]==="enrich"?t.calls.enrich++:t.calls.send++;},process(e){return t.received.push(e),t.calls.process++,e},reset(){t.received.length=0,t.calls.send=0,t.calls.process=0,t.calls.enrich=0;},lastReceived(){if(t.received.length===0)throw new Error("SpyAdapter: no exchanges recorded");return t.received[t.received.length-1]},receivedBodies(){return t.received.map(e=>e.body)}}}function I(t){return JSON.parse(fs.readFileSync(t,"utf-8"))}function Z(t,e){let r=I(t);if(!Array.isArray(r))throw new Error(`fixture.each: expected JSON array at "${t}", got ${typeof r}`);for(let o of r){if(typeof o?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(o)}`);vitest.test(o.name,()=>e(o));}}exports.TestContext=f;exports.TestContextBuilder=l;exports.createNoopSpyLogger=R;exports.createSpyLogger=m;exports.fixture=I;exports.fixtureEach=Z;exports.pseudo=F;exports.spy=M;exports.testContext=A;//# sourceMappingURL=index.cjs.map
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/spy-logger.ts","../src/test-context.ts","../src/invoke.ts","../src/adapters/pseudo/index.ts","../src/index.ts"],"names":["createSpyLogger","spy","vi","createNoopSpyLogger","noop","childFn","noopLogger","DEFAULT_ROUTES_READY_TIMEOUT_MS","TestContext","ctx","options","payload","err","isRouteCraftError","rcError","total","allReady","resolve","reject","ready","settled","timeoutId","offRouteStarted","offError","cleanup","started","delayMs","TestContextBuilder","ContextBuilder","ms","config","event","handler","key","value","routes","spyLogger","originalChild","logger","testContext","isDestination","obj","invoke","routeIdOrDestination","body","headers","exchange","DefaultExchange","dest","route","source","createAdapter","name","runtime","fail","noopSend","noopProcess","noopSubscribe","_context","_handler","_abortController","onReady","pseudo","opts","fixture","path","readFileSync","fixtureEach","run","entries","entry","test"],"mappings":"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","file":"index.cjs","sourcesContent":["import { vi } from \"vitest\";\n\n/**\n * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).\n */\nexport type SpyLogger = {\n info: ReturnType<typeof vi.fn>;\n debug: ReturnType<typeof vi.fn>;\n warn: ReturnType<typeof vi.fn>;\n error: ReturnType<typeof vi.fn>;\n trace: ReturnType<typeof vi.fn>;\n fatal: ReturnType<typeof vi.fn>;\n child: ReturnType<typeof vi.fn>;\n};\n\nexport function createSpyLogger(): SpyLogger {\n const spy: SpyLogger = {\n info: vi.fn(),\n debug: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n trace: vi.fn(),\n fatal: vi.fn(),\n child: vi.fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\nexport function createNoopSpyLogger(): SpyLogger {\n const noop = vi.fn();\n const childFn = vi.fn();\n const noopLogger: SpyLogger = {\n info: noop,\n debug: noop,\n warn: noop,\n error: noop,\n trace: noop,\n fatal: noop,\n child: childFn,\n };\n childFn.mockImplementation(() => noopLogger);\n return noopLogger;\n}\n","import { vi } from \"vitest\";\nimport type {\n CraftContext,\n CraftConfig,\n StoreRegistry,\n EventName,\n EventHandler,\n RouteDefinition,\n RouteBuilder,\n} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n isRouteCraftError,\n RouteCraftError,\n rcError,\n logger,\n} from \"@routecraft/routecraft\";\nimport type { SpyLogger } from \"./spy-logger\";\nimport { createSpyLogger, createNoopSpyLogger } from \"./spy-logger\";\n\nconst DEFAULT_ROUTES_READY_TIMEOUT_MS = 200;\n\nexport interface TestContextOptions {\n /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */\n routesReadyTimeoutMs?: number;\n}\n\n/** Options for TestContext.test(). */\nexport interface TestOptions {\n /**\n * Delay in ms after all routes are ready, before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n * E.g. `await t.test({ delayBeforeDrainMs: 50 })` for a timer with intervalMs >= 50.\n */\n delayBeforeDrainMs?: number;\n}\n\n/**\n * Test-friendly wrapper around CraftContext. Runs the real context but manages\n * lifecycle (start → wait routes ready → drain → stop) and collects errors.\n * t.logger is a spy logger (vi.fn() methods) for asserting on log calls.\n */\nexport class TestContext {\n readonly ctx: CraftContext;\n /** Spy logger; e.g. expect(t.logger.info).toHaveBeenCalledWith(...) */\n readonly logger: SpyLogger;\n readonly errors: RouteCraftError[] = [];\n private readonly routesReadyTimeoutMs: number;\n\n private restoreLoggerChild?: () => void;\n private startedPromise?: Promise<void>;\n\n constructor(\n ctx: CraftContext,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.logger = options?.spyLogger ?? createNoopSpyLogger();\n if (options?.restoreLoggerChild)\n this.restoreLoggerChild = options.restoreLoggerChild;\n this.routesReadyTimeoutMs =\n options?.routesReadyTimeoutMs ?? DEFAULT_ROUTES_READY_TIMEOUT_MS;\n ctx.on(\"error\", (payload) => {\n const err = payload.details.error;\n this.errors.push(\n isRouteCraftError(err)\n ? (err as RouteCraftError)\n : rcError(\"RC9901\", err),\n );\n });\n }\n\n /**\n * Start context and wait for all routes to be ready. Does not drain or stop.\n * Use with invoke() to send to a route by id, then call drain()/stop() when done.\n */\n async startAndWaitReady(): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n const timeoutId = setTimeout(() => {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"route:started\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n reject(payload.details.error);\n });\n });\n this.startedPromise = ctx.start();\n await Promise.all([this.startedPromise, allReady]);\n }\n\n /**\n * Start context, wait for all routes ready, optionally delay, drain in-flight, then stop.\n * Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).\n *\n * @param options.delayBeforeDrainMs — If set, wait this many ms after routes are ready before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n */\n async test(options?: TestOptions): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n let timeoutId: ReturnType<typeof setTimeout> | undefined =\n setTimeout(() => {\n if (settled) return;\n cleanup();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"route:started\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n cleanup();\n reject(payload.details.error);\n });\n\n function cleanup(): void {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n timeoutId = undefined;\n }\n }\n });\n const started = ctx.start();\n try {\n await allReady;\n const delayMs = options?.delayBeforeDrainMs ?? 0;\n if (delayMs > 0) {\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n await ctx.drain();\n } finally {\n try {\n await ctx.stop();\n await started;\n } finally {\n this.restoreLoggerChild?.();\n }\n }\n }\n\n drain(): Promise<void> {\n return this.ctx.drain();\n }\n\n async stop(): Promise<void> {\n await this.ctx.stop();\n if (this.startedPromise !== undefined) {\n await this.startedPromise;\n }\n }\n}\n\n/**\n * Builder that returns TestContext instead of CraftContext.\n * Same API as ContextBuilder (routes, on, with, store).\n */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n\n /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */\n routesReadyTimeout(ms: number): this {\n this.routesReadyTimeoutMs = ms;\n return this;\n }\n\n with(config: CraftConfig): this {\n this.builder.with(config);\n return this;\n }\n\n on<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.on(event, handler);\n return this;\n }\n\n once<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.once(event, handler);\n return this;\n }\n\n store<K extends keyof StoreRegistry>(key: K, value: StoreRegistry[K]): this {\n this.builder.store(key, value);\n return this;\n }\n\n routes(\n routes:\n | RouteDefinition[]\n | RouteBuilder<unknown>[]\n | RouteDefinition\n | RouteBuilder<unknown>,\n ): this {\n this.builder.routes(routes);\n return this;\n }\n\n async build(): Promise<TestContext> {\n const spyLogger = createSpyLogger();\n const originalChild = logger.child.bind(logger);\n logger.child = vi.fn(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n ) as typeof logger.child;\n const ctx = await this.builder.build();\n const options: TestContextOptions & {\n spyLogger: SpyLogger;\n restoreLoggerChild: () => void;\n } = {\n ...(this.routesReadyTimeoutMs !== undefined\n ? { routesReadyTimeoutMs: this.routesReadyTimeoutMs }\n : {}),\n spyLogger,\n restoreLoggerChild: () => {\n logger.child = originalChild;\n },\n };\n return new TestContext(ctx, options);\n }\n}\n\n/**\n * Create a test context builder. Use .routes(...).build(), await the result, then await t.test().\n *\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n */\nexport function testContext(): TestContextBuilder {\n return new TestContextBuilder();\n}\n","import type {\n CraftContext,\n Destination,\n ExchangeHeaders,\n} from \"@routecraft/routecraft\";\nimport { DefaultExchange } from \"@routecraft/routecraft\";\n\n/** Duck-type: object with send(exchange) returning Promise. */\nfunction isDestination(obj: unknown): obj is Destination<unknown, unknown> {\n return (\n typeof obj === \"object\" &&\n obj !== null &&\n typeof (obj as { send?: unknown }).send === \"function\"\n );\n}\n\n/**\n * Invoke a route by id or send to a destination and return the result.\n *\n * - **By route id:** Pass a string. The route is looked up with ctx.getRouteById(routeId).\n * The route's source must implement Destination (e.g. DirectAdapter). Works with multiple\n * routes in the default export — use the route's id.\n *\n * - **By destination:** Pass a Destination instance (e.g. direct(\"endpoint\")). Builds an\n * exchange and calls destination.send(exchange).\n *\n * @param ctx CraftContext (e.g. t.ctx from TestContext) with routes started\n * @param routeIdOrDestination Route id string or a Destination adapter instance\n * @param body Request body\n * @param headers Optional headers for the exchange\n * @returns The result from the route or destination (e.g. response body for DirectAdapter)\n */\nexport async function invoke<T = unknown, R = T>(\n ctx: CraftContext,\n routeIdOrDestination: string | Destination<T, R>,\n body: T,\n headers?: ExchangeHeaders,\n): Promise<R> {\n const exchange = new DefaultExchange(ctx, {\n body,\n ...(headers !== undefined && { headers }),\n });\n let dest: Destination<T, R>;\n\n if (typeof routeIdOrDestination === \"string\") {\n const route = ctx.getRouteById(routeIdOrDestination);\n if (!route) {\n throw new Error(\n `No route with id \"${routeIdOrDestination}\". Did you start the context (e.g. await t.test())?`,\n );\n }\n const source = route.definition.source;\n if (!isDestination(source)) {\n throw new Error(\n `Route \"${routeIdOrDestination}\" is not invokable: source must implement Destination (e.g. direct adapter).`,\n );\n }\n dest = source as Destination<T, R>;\n } else {\n dest = routeIdOrDestination;\n }\n\n const result = await dest.send(exchange as Parameters<typeof dest.send>[0]);\n return result as R;\n}\n","import type { Source, Destination, Processor } from \"@routecraft/routecraft\";\nimport type { PseudoOptions, PseudoKeyedOptions } from \"./shared\";\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- input position must accept any exchange for DSL assignability */\nexport type PseudoAdapter<R> = {\n adapterId: string;\n} & Source<R> &\n Destination<any, R> &\n Processor<any, R>;\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nexport type PseudoFactory<Opts> = <R = unknown>(opts: Opts) => PseudoAdapter<R>;\n\nexport type PseudoKeyedFactory<Opts> = <R = unknown>(\n key: string,\n opts?: Opts,\n) => PseudoAdapter<R>;\n\nfunction createAdapter<R>(\n name: string,\n runtime: \"throw\" | \"noop\",\n): PseudoAdapter<R> {\n const fail = (): never => {\n throw new Error(\n `Pseudo adapter \"${name}\" is not implemented. Replace with a real adapter.`,\n );\n };\n const noopSend = (): Promise<R> => Promise.resolve(undefined as unknown as R);\n const noopProcess = (exchange: unknown): unknown => exchange;\n const noopSubscribe = (\n _context: unknown,\n _handler: unknown,\n _abortController: unknown,\n onReady?: () => void,\n ): Promise<void> => {\n onReady?.();\n return Promise.resolve();\n };\n\n type SendFn = PseudoAdapter<R>[\"send\"];\n type ProcessFn = PseudoAdapter<R>[\"process\"];\n type SubscribeFn = Source<R>[\"subscribe\"];\n\n return {\n adapterId: `routecraft.adapter.pseudo.${name}`,\n subscribe:\n runtime === \"noop\"\n ? (noopSubscribe as SubscribeFn)\n : (fail as SubscribeFn),\n send: runtime === \"noop\" ? (noopSend as SendFn) : (fail as SendFn),\n process:\n runtime === \"noop\" ? (noopProcess as ProcessFn) : (fail as ProcessFn),\n };\n}\n\n// Overload: string-first (keyed) factory\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(name: string, options: PseudoKeyedOptions): PseudoKeyedFactory<Opts>;\n\n// Overload: object-only factory (default)\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(name?: string, options?: PseudoOptions): PseudoFactory<Opts>;\n\n// Implementation\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(\n name = \"pseudo\",\n options?: PseudoOptions | PseudoKeyedOptions,\n): PseudoFactory<Opts> | PseudoKeyedFactory<Opts> {\n const runtime = options?.runtime ?? \"throw\";\n const isKeyed = options && \"args\" in options && options.args === \"keyed\";\n\n if (isKeyed) {\n return <R = unknown>(key: string, opts?: Opts): PseudoAdapter<R> => {\n void key;\n void opts;\n return createAdapter<R>(name, runtime);\n };\n }\n return <R = unknown>(opts: Opts): PseudoAdapter<R> => {\n void opts;\n return createAdapter<R>(name, runtime);\n };\n}\n\n// Re-export types\nexport type { PseudoOptions, PseudoKeyedOptions } from \"./shared\";\n","import { readFileSync } from \"node:fs\";\nimport { test } from \"vitest\";\n\n// Re-export test context utilities\nexport {\n TestContext,\n TestContextBuilder,\n testContext,\n type TestContextOptions,\n type TestOptions,\n} from \"./test-context\";\n\n// Re-export spy logger utilities\nexport {\n createSpyLogger,\n createNoopSpyLogger,\n type SpyLogger,\n} from \"./spy-logger\";\n\n// Re-export invoke utility\nexport { invoke } from \"./invoke\";\n\n// Re-export pseudo adapter\nexport {\n pseudo,\n type PseudoAdapter,\n type PseudoFactory,\n type PseudoKeyedFactory,\n type PseudoOptions,\n type PseudoKeyedOptions,\n} from \"./adapters/pseudo\";\n\n/**\n * Load a JSON fixture file and return the parsed value.\n *\n * @param path Absolute or relative path to the JSON file\n * @returns Parsed JSON as T\n */\nexport function fixture<T = unknown>(path: string): T {\n return JSON.parse(readFileSync(path, \"utf-8\")) as T;\n}\n\n/** Fixture entry must have a `name` field used as the vitest test name. */\nexport interface FixtureWithName {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * Load a JSON array fixture and run one vitest test per entry. Each entry must have a `name` field (used as the test name).\n *\n * @param path Path to a JSON file that parses to an array\n * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.\n */\nexport function fixtureEach<T extends FixtureWithName>(\n path: string,\n run: (entry: T) => void | Promise<void>,\n): void {\n const entries = fixture<T[]>(path);\n if (!Array.isArray(entries)) {\n throw new Error(\n `fixture.each: expected JSON array at \"${path}\", got ${typeof entries}`,\n );\n }\n for (const entry of entries) {\n if (typeof entry?.name !== \"string\") {\n throw new Error(\n `fixture.each: each entry must have a \"name\" field (string). Got: ${JSON.stringify(entry)}`,\n );\n }\n test(entry.name, () => run(entry));\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/spy-logger.ts","../src/test-context.ts","../src/adapters/pseudo/index.ts","../src/adapters/spy/shared.ts","../src/adapters/spy/index.ts","../src/index.ts"],"names":["createSpyLogger","spy","vi","createNoopSpyLogger","noop","childFn","noopLogger","DEFAULT_ROUTES_READY_TIMEOUT_MS","TestContext","ctx","options","payload","err","isRoutecraftError","rcError","total","allReady","resolve","reject","ready","settled","timeoutId","offRouteStarted","offError","cleanup","started","delayMs","endpoint","body","headers","store","ADAPTER_DIRECT_STORE","sanitized","sanitizeEndpoint","channel","exchange","DefaultExchange","TestContextBuilder","ContextBuilder","ms","config","event","handler","key","value","routes","spyLogger","originalChild","logger","testContext","createAdapter","name","runtime","fail","noopSend","noopProcess","noopSubscribe","_context","_handler","_abortController","onReady","pseudo","opts","createSpyState","state","HeadersKeys","fixture","path","readFileSync","fixtureEach","run","entries","entry","test"],"mappings":"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","file":"index.cjs","sourcesContent":["import { vi } from \"vitest\";\n\n/**\n * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).\n *\n * @beta\n */\nexport type SpyLogger = {\n info: ReturnType<typeof vi.fn>;\n debug: ReturnType<typeof vi.fn>;\n warn: ReturnType<typeof vi.fn>;\n error: ReturnType<typeof vi.fn>;\n trace: ReturnType<typeof vi.fn>;\n fatal: ReturnType<typeof vi.fn>;\n child: ReturnType<typeof vi.fn>;\n};\n\n/** @beta */\nexport function createSpyLogger(): SpyLogger {\n const spy: SpyLogger = {\n info: vi.fn(),\n debug: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n trace: vi.fn(),\n fatal: vi.fn(),\n child: vi.fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\n/** @beta */\nexport function createNoopSpyLogger(): SpyLogger {\n const noop = vi.fn();\n const childFn = vi.fn();\n const noopLogger: SpyLogger = {\n info: noop,\n debug: noop,\n warn: noop,\n error: noop,\n trace: noop,\n fatal: noop,\n child: childFn,\n };\n childFn.mockImplementation(() => noopLogger);\n return noopLogger;\n}\n","import { vi } from \"vitest\";\nimport type {\n CraftContext,\n CraftConfig,\n StoreRegistry,\n EventName,\n EventHandler,\n RouteDefinition,\n RouteBuilder,\n Exchange,\n ExchangeHeaders,\n} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n DefaultExchange,\n ADAPTER_DIRECT_STORE,\n sanitizeEndpoint,\n isRoutecraftError,\n RoutecraftError,\n rcError,\n logger,\n} from \"@routecraft/routecraft\";\nimport type { SpyLogger } from \"./spy-logger\";\nimport { createSpyLogger, createNoopSpyLogger } from \"./spy-logger\";\n\nconst DEFAULT_ROUTES_READY_TIMEOUT_MS = 200;\n\nexport interface TestContextOptions {\n /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */\n routesReadyTimeoutMs?: number;\n}\n\n/** Options for TestContext.test(). */\nexport interface TestOptions {\n /**\n * Delay in ms after all routes are ready, before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n * E.g. `await t.test({ delayBeforeDrainMs: 50 })` for a timer with intervalMs >= 50.\n */\n delayBeforeDrainMs?: number;\n}\n\n/**\n * Test-friendly wrapper around CraftContext. Runs the real context but manages\n * lifecycle (start, wait routes ready, drain, stop) and collects errors.\n * t.logger is a spy logger (vi.fn() methods) for asserting on log calls.\n *\n * @beta\n */\nexport class TestContext {\n readonly ctx: CraftContext;\n /** Spy logger; e.g. expect(t.logger.info).toHaveBeenCalledWith(...) */\n readonly logger: SpyLogger;\n readonly errors: RoutecraftError[] = [];\n private readonly routesReadyTimeoutMs: number;\n\n private restoreLoggerChild?: () => void;\n private loggerChildRestored = false;\n private startedPromise?: Promise<void>;\n\n constructor(\n ctx: CraftContext,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.logger = options?.spyLogger ?? createNoopSpyLogger();\n if (options?.restoreLoggerChild)\n this.restoreLoggerChild = options.restoreLoggerChild;\n this.routesReadyTimeoutMs =\n options?.routesReadyTimeoutMs ?? DEFAULT_ROUTES_READY_TIMEOUT_MS;\n ctx.on(\"error\", (payload) => {\n const err = payload.details.error;\n this.errors.push(\n isRoutecraftError(err)\n ? (err as RoutecraftError)\n : rcError(\"RC9901\", err),\n );\n });\n }\n\n /**\n * Start context and wait for all routes to be ready. Does not drain or stop.\n * Use with invoke() to send to a route by id, then call drain()/stop() when done.\n */\n async startAndWaitReady(): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n const timeoutId = setTimeout(() => {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"route:started\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n reject(payload.details.error);\n });\n });\n this.startedPromise = ctx.start();\n await Promise.all([this.startedPromise, allReady]);\n }\n\n /**\n * Start context, wait for all routes ready, optionally delay, drain in-flight, then stop.\n * Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).\n *\n * @param options.delayBeforeDrainMs — If set, wait this many ms after routes are ready before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n */\n async test(options?: TestOptions): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n let timeoutId: ReturnType<typeof setTimeout> | undefined =\n setTimeout(() => {\n if (settled) return;\n cleanup();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"route:started\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n cleanup();\n reject(payload.details.error);\n });\n\n function cleanup(): void {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n timeoutId = undefined;\n }\n }\n });\n const started = ctx.start();\n try {\n await allReady;\n const delayMs = options?.delayBeforeDrainMs ?? 0;\n if (delayMs > 0) {\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n await ctx.drain();\n } finally {\n try {\n await ctx.stop();\n await started;\n } finally {\n this.restoreLoggerChildOnce();\n }\n }\n }\n\n drain(): Promise<void> {\n return this.ctx.drain();\n }\n\n async stop(): Promise<void> {\n try {\n await this.ctx.stop();\n if (this.startedPromise !== undefined) {\n await this.startedPromise;\n }\n } finally {\n this.restoreLoggerChildOnce();\n }\n }\n\n private restoreLoggerChildOnce(): void {\n if (this.loggerChildRestored) return;\n this.restoreLoggerChild?.();\n this.loggerChildRestored = true;\n }\n\n /**\n * Send a message to a direct endpoint and return the result.\n * Use after {@link startAndWaitReady} so the channel exists.\n *\n * @param endpoint Direct endpoint name (must match the endpoint string passed to `direct(endpoint, options)`)\n * @param body Request body\n * @param headers Optional exchange headers\n * @returns The response body from the route\n */\n async send<T = unknown, R = T>(\n endpoint: string,\n body: T,\n headers?: ExchangeHeaders,\n ): Promise<R> {\n const store = this.ctx.getStore(ADAPTER_DIRECT_STORE);\n const sanitized = sanitizeEndpoint(endpoint);\n const channel = store?.get(sanitized);\n if (!channel) {\n throw new Error(\n `No direct channel for endpoint \"${endpoint}\". Did you call startAndWaitReady() first?`,\n );\n }\n const exchange = new DefaultExchange(this.ctx, {\n body,\n ...(headers !== undefined && { headers }),\n });\n const result = await channel.send(sanitized, exchange);\n return (result as Exchange).body as R;\n }\n}\n\n/**\n * Builder that returns TestContext instead of CraftContext.\n * Same API as ContextBuilder (routes, on, with, store).\n *\n * @beta\n */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n\n /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */\n routesReadyTimeout(ms: number): this {\n this.routesReadyTimeoutMs = ms;\n return this;\n }\n\n with(config: CraftConfig): this {\n this.builder.with(config);\n return this;\n }\n\n on<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.on(event, handler);\n return this;\n }\n\n once<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.once(event, handler);\n return this;\n }\n\n store<K extends keyof StoreRegistry>(key: K, value: StoreRegistry[K]): this {\n this.builder.store(key, value);\n return this;\n }\n\n routes(\n routes:\n | RouteDefinition[]\n | RouteBuilder<unknown>[]\n | RouteDefinition\n | RouteBuilder<unknown>,\n ): this {\n this.builder.routes(routes);\n return this;\n }\n\n async build(): Promise<TestContext> {\n const spyLogger = createSpyLogger();\n const originalChild = logger.child.bind(logger);\n logger.child = vi.fn(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n ) as typeof logger.child;\n const ctx = await this.builder.build();\n const options: TestContextOptions & {\n spyLogger: SpyLogger;\n restoreLoggerChild: () => void;\n } = {\n ...(this.routesReadyTimeoutMs !== undefined\n ? { routesReadyTimeoutMs: this.routesReadyTimeoutMs }\n : {}),\n spyLogger,\n restoreLoggerChild: () => {\n logger.child = originalChild;\n },\n };\n return new TestContext(ctx, options);\n }\n}\n\n/**\n * Create a test context builder. Use .routes(...).build(), await the result, then await t.test().\n *\n * @beta\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n */\nexport function testContext(): TestContextBuilder {\n return new TestContextBuilder();\n}\n","import type { Source, Destination, Processor } from \"@routecraft/routecraft\";\nimport type { PseudoOptions, PseudoKeyedOptions } from \"./shared\";\n\n/**\n * @internal\n */\n/* eslint-disable @typescript-eslint/no-explicit-any -- input position must accept any exchange for DSL assignability */\nexport type PseudoAdapter<R> = {\n adapterId: string;\n} & Source<R> &\n Destination<any, R> &\n Processor<any, R>;\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n/** @internal */\nexport type PseudoFactory<Opts> = <R = unknown>(opts: Opts) => PseudoAdapter<R>;\n\n/** @internal */\nexport type PseudoKeyedFactory<Opts> = <R = unknown>(\n key: string,\n opts?: Opts,\n) => PseudoAdapter<R>;\n\nfunction createAdapter<R>(\n name: string,\n runtime: \"throw\" | \"noop\",\n): PseudoAdapter<R> {\n const fail = (): never => {\n throw new Error(\n `Pseudo adapter \"${name}\" is not implemented. Replace with a real adapter.`,\n );\n };\n const noopSend = (): Promise<R> => Promise.resolve(undefined as unknown as R);\n const noopProcess = (exchange: unknown): unknown => exchange;\n const noopSubscribe = (\n _context: unknown,\n _handler: unknown,\n _abortController: unknown,\n onReady?: () => void,\n ): Promise<void> => {\n onReady?.();\n return Promise.resolve();\n };\n\n type SendFn = PseudoAdapter<R>[\"send\"];\n type ProcessFn = PseudoAdapter<R>[\"process\"];\n type SubscribeFn = Source<R>[\"subscribe\"];\n\n return {\n adapterId: `routecraft.adapter.pseudo.${name}`,\n subscribe:\n runtime === \"noop\"\n ? (noopSubscribe as SubscribeFn)\n : (fail as SubscribeFn),\n send: runtime === \"noop\" ? (noopSend as SendFn) : (fail as SendFn),\n process:\n runtime === \"noop\" ? (noopProcess as ProcessFn) : (fail as ProcessFn),\n };\n}\n\n/**\n * Creates a pseudo (placeholder) adapter for use in tests or as a stub during development.\n *\n * @beta\n */\n// Overload: string-first (keyed) factory\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(name: string, options: PseudoKeyedOptions): PseudoKeyedFactory<Opts>;\n\n// Overload: object-only factory (default)\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(name?: string, options?: PseudoOptions): PseudoFactory<Opts>;\n\n// Implementation\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(\n name = \"pseudo\",\n options?: PseudoOptions | PseudoKeyedOptions,\n): PseudoFactory<Opts> | PseudoKeyedFactory<Opts> {\n const runtime = options?.runtime ?? \"throw\";\n const isKeyed = options && \"args\" in options && options.args === \"keyed\";\n\n if (isKeyed) {\n return <R = unknown>(key: string, opts?: Opts): PseudoAdapter<R> => {\n void key;\n void opts;\n return createAdapter<R>(name, runtime);\n };\n }\n return <R = unknown>(opts: Opts): PseudoAdapter<R> => {\n void opts;\n return createAdapter<R>(name, runtime);\n };\n}\n\n// Re-export types\nexport type { PseudoOptions, PseudoKeyedOptions } from \"./shared\";\n","import type { Exchange } from \"@routecraft/routecraft\";\n\n/**\n * Internal state container for the spy adapter.\n */\nexport interface SpyState<T> {\n received: Exchange<T>[];\n calls: { send: number; process: number; enrich: number };\n}\n\n/**\n * Creates fresh spy state with empty received array and zeroed counters.\n */\nexport function createSpyState<T>(): SpyState<T> {\n return {\n received: [],\n calls: { send: 0, process: 0, enrich: 0 },\n };\n}\n","import {\n HeadersKeys,\n type Destination,\n type Processor,\n type Exchange,\n} from \"@routecraft/routecraft\";\nimport { createSpyState } from \"./shared.ts\";\n\n/**\n * A spy adapter that records all exchanges passing through it.\n * Implements both {@link Destination} and {@link Processor} so it can be used\n * with `.to()`, `.enrich()`, `.tap()`, and `.process()`.\n */\nexport type SpyAdapter<T = unknown> = {\n /** Stable identifier for this adapter. */\n adapterId: string;\n\n /** All exchanges recorded, in order. */\n received: Exchange<T>[];\n\n /** Per-operation call counters. */\n calls: { send: number; process: number; enrich: number };\n\n /** Clear all recorded data and reset counters. */\n reset(): void;\n\n /** Most recent exchange. Throws if none recorded. */\n lastReceived(): Exchange<T>;\n\n /** Array of just the body values from received exchanges. */\n receivedBodies(): T[];\n /* eslint-disable @typescript-eslint/no-explicit-any -- both positions use any: Destination so the spy is assignable regardless of body type, Processor so spy<unknown>() is assignable in typed pipelines */\n} & Destination<any, void> &\n Processor<any, T>;\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n/**\n * Creates a spy adapter that records all exchanges for test assertions.\n *\n * Use as a destination (`.to()`, `.enrich()`, `.tap()`) or processor (`.process()`)\n * to capture pipeline output without side effects.\n *\n * @beta\n *\n * @returns A spy adapter that records exchanges and tracks call counts\n *\n * @example\n * ```ts\n * const s = spy();\n * const route = craft().id(\"test\").from(simple(\"hello\")).to(s);\n * const t = await testContext().routes(route).build();\n * await t.test();\n *\n * expect(s.received).toHaveLength(1);\n * expect(s.received[0].body).toBe(\"hello\");\n * expect(s.calls.send).toBe(1);\n * ```\n */\nexport function spy<T = unknown>(): SpyAdapter<T> {\n const state = createSpyState<T>();\n\n return {\n adapterId: \"routecraft.adapter.spy\",\n received: state.received,\n calls: state.calls,\n\n send(exchange: Exchange<T>): void {\n state.received.push(exchange);\n\n const operation = exchange.headers?.[HeadersKeys.OPERATION];\n if (operation === \"enrich\") {\n state.calls.enrich++;\n } else {\n state.calls.send++;\n }\n },\n\n process(exchange: Exchange<T>): Exchange<T> {\n state.received.push(exchange);\n state.calls.process++;\n return exchange;\n },\n\n reset(): void {\n state.received.length = 0;\n state.calls.send = 0;\n state.calls.process = 0;\n state.calls.enrich = 0;\n },\n\n lastReceived(): Exchange<T> {\n if (state.received.length === 0) {\n throw new Error(\"SpyAdapter: no exchanges recorded\");\n }\n return state.received[state.received.length - 1];\n },\n\n receivedBodies(): T[] {\n return state.received.map((e) => e.body);\n },\n };\n}\n","import { readFileSync } from \"node:fs\";\nimport { test } from \"vitest\";\n\n// Re-export test context utilities\nexport {\n TestContext,\n TestContextBuilder,\n testContext,\n type TestContextOptions,\n type TestOptions,\n} from \"./test-context\";\n\n// Re-export spy logger utilities\nexport {\n createSpyLogger,\n createNoopSpyLogger,\n type SpyLogger,\n} from \"./spy-logger\";\n\n// Re-export pseudo adapter\nexport {\n pseudo,\n type PseudoAdapter,\n type PseudoFactory,\n type PseudoKeyedFactory,\n type PseudoOptions,\n type PseudoKeyedOptions,\n} from \"./adapters/pseudo\";\n\n// Re-export spy adapter\nexport { spy, type SpyAdapter } from \"./adapters/spy\";\n\n/**\n * Load a JSON fixture file and return the parsed value.\n *\n * @beta\n * @param path Absolute or relative path to the JSON file\n * @returns Parsed JSON as T\n */\nexport function fixture<T = unknown>(path: string): T {\n return JSON.parse(readFileSync(path, \"utf-8\")) as T;\n}\n\n/** Fixture entry must have a `name` field used as the vitest test name. */\nexport interface FixtureWithName {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * Load a JSON array fixture and run one vitest test per entry. Each entry must have a `name` field (used as the test name).\n *\n * @beta\n * @param path Path to a JSON file that parses to an array\n * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.\n */\nexport function fixtureEach<T extends FixtureWithName>(\n path: string,\n run: (entry: T) => void | Promise<void>,\n): void {\n const entries = fixture<T[]>(path);\n if (!Array.isArray(entries)) {\n throw new Error(\n `fixture.each: expected JSON array at \"${path}\", got ${typeof entries}`,\n );\n }\n for (const entry of entries) {\n if (typeof entry?.name !== \"string\") {\n throw new Error(\n `fixture.each: each entry must have a \"name\" field (string). Got: ${JSON.stringify(entry)}`,\n );\n }\n test(entry.name, () => run(entry));\n }\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -1,8 +1,10 @@
1
- import { CraftContext, RouteCraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Destination, ExchangeHeaders, Source, Processor } from '@routecraft/routecraft';
1
+ import { CraftContext, RoutecraftError, ExchangeHeaders, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Source, Destination, Processor, Exchange } from '@routecraft/routecraft';
2
2
  import { vi } from 'vitest';
3
3
 
4
4
  /**
5
5
  * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).
6
+ *
7
+ * @beta
6
8
  */
7
9
  type SpyLogger = {
8
10
  info: ReturnType<typeof vi.fn>;
@@ -13,7 +15,9 @@ type SpyLogger = {
13
15
  fatal: ReturnType<typeof vi.fn>;
14
16
  child: ReturnType<typeof vi.fn>;
15
17
  };
18
+ /** @beta */
16
19
  declare function createSpyLogger(): SpyLogger;
20
+ /** @beta */
17
21
  declare function createNoopSpyLogger(): SpyLogger;
18
22
 
19
23
  interface TestContextOptions {
@@ -31,16 +35,19 @@ interface TestOptions {
31
35
  }
32
36
  /**
33
37
  * Test-friendly wrapper around CraftContext. Runs the real context but manages
34
- * lifecycle (start wait routes ready drain stop) and collects errors.
38
+ * lifecycle (start, wait routes ready, drain, stop) and collects errors.
35
39
  * t.logger is a spy logger (vi.fn() methods) for asserting on log calls.
40
+ *
41
+ * @beta
36
42
  */
37
43
  declare class TestContext {
38
44
  readonly ctx: CraftContext;
39
45
  /** Spy logger; e.g. expect(t.logger.info).toHaveBeenCalledWith(...) */
40
46
  readonly logger: SpyLogger;
41
- readonly errors: RouteCraftError[];
47
+ readonly errors: RoutecraftError[];
42
48
  private readonly routesReadyTimeoutMs;
43
49
  private restoreLoggerChild?;
50
+ private loggerChildRestored;
44
51
  private startedPromise?;
45
52
  constructor(ctx: CraftContext, options?: TestContextOptions & {
46
53
  spyLogger?: SpyLogger;
@@ -61,10 +68,23 @@ declare class TestContext {
61
68
  test(options?: TestOptions): Promise<void>;
62
69
  drain(): Promise<void>;
63
70
  stop(): Promise<void>;
71
+ private restoreLoggerChildOnce;
72
+ /**
73
+ * Send a message to a direct endpoint and return the result.
74
+ * Use after {@link startAndWaitReady} so the channel exists.
75
+ *
76
+ * @param endpoint Direct endpoint name (must match the endpoint string passed to `direct(endpoint, options)`)
77
+ * @param body Request body
78
+ * @param headers Optional exchange headers
79
+ * @returns The response body from the route
80
+ */
81
+ send<T = unknown, R = T>(endpoint: string, body: T, headers?: ExchangeHeaders): Promise<R>;
64
82
  }
65
83
  /**
66
84
  * Builder that returns TestContext instead of CraftContext.
67
85
  * Same API as ContextBuilder (routes, on, with, store).
86
+ *
87
+ * @beta
68
88
  */
69
89
  declare class TestContextBuilder {
70
90
  private builder;
@@ -81,6 +101,7 @@ declare class TestContextBuilder {
81
101
  /**
82
102
  * Create a test context builder. Use .routes(...).build(), await the result, then await t.test().
83
103
  *
104
+ * @beta
84
105
  * @example
85
106
  * const builder = testContext();
86
107
  * const t = await builder.routes(myRoutes).build();
@@ -88,24 +109,6 @@ declare class TestContextBuilder {
88
109
  */
89
110
  declare function testContext(): TestContextBuilder;
90
111
 
91
- /**
92
- * Invoke a route by id or send to a destination and return the result.
93
- *
94
- * - **By route id:** Pass a string. The route is looked up with ctx.getRouteById(routeId).
95
- * The route's source must implement Destination (e.g. DirectAdapter). Works with multiple
96
- * routes in the default export — use the route's id.
97
- *
98
- * - **By destination:** Pass a Destination instance (e.g. direct("endpoint")). Builds an
99
- * exchange and calls destination.send(exchange).
100
- *
101
- * @param ctx CraftContext (e.g. t.ctx from TestContext) with routes started
102
- * @param routeIdOrDestination Route id string or a Destination adapter instance
103
- * @param body Request body
104
- * @param headers Optional headers for the exchange
105
- * @returns The result from the route or destination (e.g. response body for DirectAdapter)
106
- */
107
- declare function invoke<T = unknown, R = T>(ctx: CraftContext, routeIdOrDestination: string | Destination<T, R>, body: T, headers?: ExchangeHeaders): Promise<R>;
108
-
109
112
  interface PseudoOptions {
110
113
  runtime?: "throw" | "noop";
111
114
  }
@@ -113,17 +116,75 @@ interface PseudoKeyedOptions extends PseudoOptions {
113
116
  args: "keyed";
114
117
  }
115
118
 
119
+ /**
120
+ * @internal
121
+ */
116
122
  type PseudoAdapter<R> = {
117
123
  adapterId: string;
118
124
  } & Source<R> & Destination<any, R> & Processor<any, R>;
125
+ /** @internal */
119
126
  type PseudoFactory<Opts> = <R = unknown>(opts: Opts) => PseudoAdapter<R>;
127
+ /** @internal */
120
128
  type PseudoKeyedFactory<Opts> = <R = unknown>(key: string, opts?: Opts) => PseudoAdapter<R>;
129
+ /**
130
+ * Creates a pseudo (placeholder) adapter for use in tests or as a stub during development.
131
+ *
132
+ * @beta
133
+ */
121
134
  declare function pseudo<Opts extends Record<string, unknown> = Record<string, unknown>>(name: string, options: PseudoKeyedOptions): PseudoKeyedFactory<Opts>;
122
135
  declare function pseudo<Opts extends Record<string, unknown> = Record<string, unknown>>(name?: string, options?: PseudoOptions): PseudoFactory<Opts>;
123
136
 
137
+ /**
138
+ * A spy adapter that records all exchanges passing through it.
139
+ * Implements both {@link Destination} and {@link Processor} so it can be used
140
+ * with `.to()`, `.enrich()`, `.tap()`, and `.process()`.
141
+ */
142
+ type SpyAdapter<T = unknown> = {
143
+ /** Stable identifier for this adapter. */
144
+ adapterId: string;
145
+ /** All exchanges recorded, in order. */
146
+ received: Exchange<T>[];
147
+ /** Per-operation call counters. */
148
+ calls: {
149
+ send: number;
150
+ process: number;
151
+ enrich: number;
152
+ };
153
+ /** Clear all recorded data and reset counters. */
154
+ reset(): void;
155
+ /** Most recent exchange. Throws if none recorded. */
156
+ lastReceived(): Exchange<T>;
157
+ /** Array of just the body values from received exchanges. */
158
+ receivedBodies(): T[];
159
+ } & Destination<any, void> & Processor<any, T>;
160
+ /**
161
+ * Creates a spy adapter that records all exchanges for test assertions.
162
+ *
163
+ * Use as a destination (`.to()`, `.enrich()`, `.tap()`) or processor (`.process()`)
164
+ * to capture pipeline output without side effects.
165
+ *
166
+ * @beta
167
+ *
168
+ * @returns A spy adapter that records exchanges and tracks call counts
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * const s = spy();
173
+ * const route = craft().id("test").from(simple("hello")).to(s);
174
+ * const t = await testContext().routes(route).build();
175
+ * await t.test();
176
+ *
177
+ * expect(s.received).toHaveLength(1);
178
+ * expect(s.received[0].body).toBe("hello");
179
+ * expect(s.calls.send).toBe(1);
180
+ * ```
181
+ */
182
+ declare function spy<T = unknown>(): SpyAdapter<T>;
183
+
124
184
  /**
125
185
  * Load a JSON fixture file and return the parsed value.
126
186
  *
187
+ * @beta
127
188
  * @param path Absolute or relative path to the JSON file
128
189
  * @returns Parsed JSON as T
129
190
  */
@@ -136,9 +197,10 @@ interface FixtureWithName {
136
197
  /**
137
198
  * Load a JSON array fixture and run one vitest test per entry. Each entry must have a `name` field (used as the test name).
138
199
  *
200
+ * @beta
139
201
  * @param path Path to a JSON file that parses to an array
140
202
  * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.
141
203
  */
142
204
  declare function fixtureEach<T extends FixtureWithName>(path: string, run: (entry: T) => void | Promise<void>): void;
143
205
 
144
- export { type FixtureWithName, type PseudoAdapter, type PseudoFactory, type PseudoKeyedFactory, type PseudoKeyedOptions, type PseudoOptions, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestOptions, createNoopSpyLogger, createSpyLogger, fixture, fixtureEach, invoke, pseudo, testContext };
206
+ export { type FixtureWithName, type PseudoAdapter, type PseudoFactory, type PseudoKeyedFactory, type PseudoKeyedOptions, type PseudoOptions, type SpyAdapter, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestOptions, createNoopSpyLogger, createSpyLogger, fixture, fixtureEach, pseudo, spy, testContext };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { CraftContext, RouteCraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Destination, ExchangeHeaders, Source, Processor } from '@routecraft/routecraft';
1
+ import { CraftContext, RoutecraftError, ExchangeHeaders, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Source, Destination, Processor, Exchange } from '@routecraft/routecraft';
2
2
  import { vi } from 'vitest';
3
3
 
4
4
  /**
5
5
  * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).
6
+ *
7
+ * @beta
6
8
  */
7
9
  type SpyLogger = {
8
10
  info: ReturnType<typeof vi.fn>;
@@ -13,7 +15,9 @@ type SpyLogger = {
13
15
  fatal: ReturnType<typeof vi.fn>;
14
16
  child: ReturnType<typeof vi.fn>;
15
17
  };
18
+ /** @beta */
16
19
  declare function createSpyLogger(): SpyLogger;
20
+ /** @beta */
17
21
  declare function createNoopSpyLogger(): SpyLogger;
18
22
 
19
23
  interface TestContextOptions {
@@ -31,16 +35,19 @@ interface TestOptions {
31
35
  }
32
36
  /**
33
37
  * Test-friendly wrapper around CraftContext. Runs the real context but manages
34
- * lifecycle (start wait routes ready drain stop) and collects errors.
38
+ * lifecycle (start, wait routes ready, drain, stop) and collects errors.
35
39
  * t.logger is a spy logger (vi.fn() methods) for asserting on log calls.
40
+ *
41
+ * @beta
36
42
  */
37
43
  declare class TestContext {
38
44
  readonly ctx: CraftContext;
39
45
  /** Spy logger; e.g. expect(t.logger.info).toHaveBeenCalledWith(...) */
40
46
  readonly logger: SpyLogger;
41
- readonly errors: RouteCraftError[];
47
+ readonly errors: RoutecraftError[];
42
48
  private readonly routesReadyTimeoutMs;
43
49
  private restoreLoggerChild?;
50
+ private loggerChildRestored;
44
51
  private startedPromise?;
45
52
  constructor(ctx: CraftContext, options?: TestContextOptions & {
46
53
  spyLogger?: SpyLogger;
@@ -61,10 +68,23 @@ declare class TestContext {
61
68
  test(options?: TestOptions): Promise<void>;
62
69
  drain(): Promise<void>;
63
70
  stop(): Promise<void>;
71
+ private restoreLoggerChildOnce;
72
+ /**
73
+ * Send a message to a direct endpoint and return the result.
74
+ * Use after {@link startAndWaitReady} so the channel exists.
75
+ *
76
+ * @param endpoint Direct endpoint name (must match the endpoint string passed to `direct(endpoint, options)`)
77
+ * @param body Request body
78
+ * @param headers Optional exchange headers
79
+ * @returns The response body from the route
80
+ */
81
+ send<T = unknown, R = T>(endpoint: string, body: T, headers?: ExchangeHeaders): Promise<R>;
64
82
  }
65
83
  /**
66
84
  * Builder that returns TestContext instead of CraftContext.
67
85
  * Same API as ContextBuilder (routes, on, with, store).
86
+ *
87
+ * @beta
68
88
  */
69
89
  declare class TestContextBuilder {
70
90
  private builder;
@@ -81,6 +101,7 @@ declare class TestContextBuilder {
81
101
  /**
82
102
  * Create a test context builder. Use .routes(...).build(), await the result, then await t.test().
83
103
  *
104
+ * @beta
84
105
  * @example
85
106
  * const builder = testContext();
86
107
  * const t = await builder.routes(myRoutes).build();
@@ -88,24 +109,6 @@ declare class TestContextBuilder {
88
109
  */
89
110
  declare function testContext(): TestContextBuilder;
90
111
 
91
- /**
92
- * Invoke a route by id or send to a destination and return the result.
93
- *
94
- * - **By route id:** Pass a string. The route is looked up with ctx.getRouteById(routeId).
95
- * The route's source must implement Destination (e.g. DirectAdapter). Works with multiple
96
- * routes in the default export — use the route's id.
97
- *
98
- * - **By destination:** Pass a Destination instance (e.g. direct("endpoint")). Builds an
99
- * exchange and calls destination.send(exchange).
100
- *
101
- * @param ctx CraftContext (e.g. t.ctx from TestContext) with routes started
102
- * @param routeIdOrDestination Route id string or a Destination adapter instance
103
- * @param body Request body
104
- * @param headers Optional headers for the exchange
105
- * @returns The result from the route or destination (e.g. response body for DirectAdapter)
106
- */
107
- declare function invoke<T = unknown, R = T>(ctx: CraftContext, routeIdOrDestination: string | Destination<T, R>, body: T, headers?: ExchangeHeaders): Promise<R>;
108
-
109
112
  interface PseudoOptions {
110
113
  runtime?: "throw" | "noop";
111
114
  }
@@ -113,17 +116,75 @@ interface PseudoKeyedOptions extends PseudoOptions {
113
116
  args: "keyed";
114
117
  }
115
118
 
119
+ /**
120
+ * @internal
121
+ */
116
122
  type PseudoAdapter<R> = {
117
123
  adapterId: string;
118
124
  } & Source<R> & Destination<any, R> & Processor<any, R>;
125
+ /** @internal */
119
126
  type PseudoFactory<Opts> = <R = unknown>(opts: Opts) => PseudoAdapter<R>;
127
+ /** @internal */
120
128
  type PseudoKeyedFactory<Opts> = <R = unknown>(key: string, opts?: Opts) => PseudoAdapter<R>;
129
+ /**
130
+ * Creates a pseudo (placeholder) adapter for use in tests or as a stub during development.
131
+ *
132
+ * @beta
133
+ */
121
134
  declare function pseudo<Opts extends Record<string, unknown> = Record<string, unknown>>(name: string, options: PseudoKeyedOptions): PseudoKeyedFactory<Opts>;
122
135
  declare function pseudo<Opts extends Record<string, unknown> = Record<string, unknown>>(name?: string, options?: PseudoOptions): PseudoFactory<Opts>;
123
136
 
137
+ /**
138
+ * A spy adapter that records all exchanges passing through it.
139
+ * Implements both {@link Destination} and {@link Processor} so it can be used
140
+ * with `.to()`, `.enrich()`, `.tap()`, and `.process()`.
141
+ */
142
+ type SpyAdapter<T = unknown> = {
143
+ /** Stable identifier for this adapter. */
144
+ adapterId: string;
145
+ /** All exchanges recorded, in order. */
146
+ received: Exchange<T>[];
147
+ /** Per-operation call counters. */
148
+ calls: {
149
+ send: number;
150
+ process: number;
151
+ enrich: number;
152
+ };
153
+ /** Clear all recorded data and reset counters. */
154
+ reset(): void;
155
+ /** Most recent exchange. Throws if none recorded. */
156
+ lastReceived(): Exchange<T>;
157
+ /** Array of just the body values from received exchanges. */
158
+ receivedBodies(): T[];
159
+ } & Destination<any, void> & Processor<any, T>;
160
+ /**
161
+ * Creates a spy adapter that records all exchanges for test assertions.
162
+ *
163
+ * Use as a destination (`.to()`, `.enrich()`, `.tap()`) or processor (`.process()`)
164
+ * to capture pipeline output without side effects.
165
+ *
166
+ * @beta
167
+ *
168
+ * @returns A spy adapter that records exchanges and tracks call counts
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * const s = spy();
173
+ * const route = craft().id("test").from(simple("hello")).to(s);
174
+ * const t = await testContext().routes(route).build();
175
+ * await t.test();
176
+ *
177
+ * expect(s.received).toHaveLength(1);
178
+ * expect(s.received[0].body).toBe("hello");
179
+ * expect(s.calls.send).toBe(1);
180
+ * ```
181
+ */
182
+ declare function spy<T = unknown>(): SpyAdapter<T>;
183
+
124
184
  /**
125
185
  * Load a JSON fixture file and return the parsed value.
126
186
  *
187
+ * @beta
127
188
  * @param path Absolute or relative path to the JSON file
128
189
  * @returns Parsed JSON as T
129
190
  */
@@ -136,9 +197,10 @@ interface FixtureWithName {
136
197
  /**
137
198
  * Load a JSON array fixture and run one vitest test per entry. Each entry must have a `name` field (used as the test name).
138
199
  *
200
+ * @beta
139
201
  * @param path Path to a JSON file that parses to an array
140
202
  * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.
141
203
  */
142
204
  declare function fixtureEach<T extends FixtureWithName>(path: string, run: (entry: T) => void | Promise<void>): void;
143
205
 
144
- export { type FixtureWithName, type PseudoAdapter, type PseudoFactory, type PseudoKeyedFactory, type PseudoKeyedOptions, type PseudoOptions, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestOptions, createNoopSpyLogger, createSpyLogger, fixture, fixtureEach, invoke, pseudo, testContext };
206
+ export { type FixtureWithName, type PseudoAdapter, type PseudoFactory, type PseudoKeyedFactory, type PseudoKeyedOptions, type PseudoOptions, type SpyAdapter, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestOptions, createNoopSpyLogger, createSpyLogger, fixture, fixtureEach, pseudo, spy, testContext };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import {readFileSync}from'fs';import {vi,test}from'vitest';import {isRouteCraftError,rcError,ContextBuilder,logger,DefaultExchange}from'@routecraft/routecraft';function h(){let r={info:vi.fn(),debug:vi.fn(),warn:vi.fn(),error:vi.fn(),trace:vi.fn(),fatal:vi.fn(),child:vi.fn()};return r.child.mockImplementation(()=>r),r}function R(){let r=vi.fn(),e=vi.fn(),t={info:r,debug:r,warn:r,error:r,trace:r,fatal:r,child:e};return e.mockImplementation(()=>t),t}var k=200,y=class{ctx;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;startedPromise;constructor(e,t){this.ctx=e,this.logger=t?.spyLogger??R(),t?.restoreLoggerChild&&(this.restoreLoggerChild=t.restoreLoggerChild),this.routesReadyTimeoutMs=t?.routesReadyTimeoutMs??k,e.on("error",o=>{let n=o.details.error;this.errors.push(isRouteCraftError(n)?n:rcError("RC9901",n));});}async startAndWaitReady(){let e=this.ctx,t=e.getRoutes().length,o=t===0?Promise.resolve():new Promise((n,i)=>{let a=0,s=false,d=setTimeout(()=>{s||(s=true,u(),f(),i(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),u=e.on("route:started",()=>{s||(a++,a>=t&&(s=true,clearTimeout(d),u(),f(),n()));}),f=e.on("error",l=>{s||(s=true,clearTimeout(d),u(),f(),i(l.details.error));});});this.startedPromise=e.start(),await Promise.all([this.startedPromise,o]);}async test(e){let t=this.ctx,o=t.getRoutes().length,n=o===0?Promise.resolve():new Promise((a,s)=>{let d=0,u=false,f=setTimeout(()=>{u||(m(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),l=t.on("route:started",()=>{u||(d++,d>=o&&(m(),a()));}),x=t.on("error",P=>{u||(m(),s(P.details.error));});function m(){u||(u=true,l(),x(),f!==void 0&&(clearTimeout(f),f=void 0));}}),i=t.start();try{await n;let a=e?.delayBeforeDrainMs??0;a>0&&await new Promise(s=>setTimeout(s,a)),await t.drain();}finally{try{await t.stop(),await i;}finally{this.restoreLoggerChild?.();}}}drain(){return this.ctx.drain()}async stop(){await this.ctx.stop(),this.startedPromise!==void 0&&await this.startedPromise;}},g=class{builder=new ContextBuilder;routesReadyTimeoutMs;routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}with(e){return this.builder.with(e),this}on(e,t){return this.builder.on(e,t),this}once(e,t){return this.builder.once(e,t),this}store(e,t){return this.builder.store(e,t),this}routes(e){return this.builder.routes(e),this}async build(){let e=h(),t=logger.child.bind(logger);logger.child=vi.fn(()=>e);let o=await this.builder.build(),n={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{logger.child=t;}};return new y(o,n)}};function b(){return new g}function L(r){return typeof r=="object"&&r!==null&&typeof r.send=="function"}async function E(r,e,t,o){let n=new DefaultExchange(r,{body:t,...o!==void 0&&{headers:o}}),i;if(typeof e=="string"){let s=r.getRouteById(e);if(!s)throw new Error(`No route with id "${e}". Did you start the context (e.g. await t.test())?`);let d=s.definition.source;if(!L(d))throw new Error(`Route "${e}" is not invokable: source must implement Destination (e.g. direct adapter).`);i=d;}else i=e;return await i.send(n)}function w(r,e){let t=()=>{throw new Error(`Pseudo adapter "${r}" is not implemented. Replace with a real adapter.`)},o=()=>Promise.resolve(void 0),n=a=>a,i=(a,s,d,u)=>(u?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${r}`,subscribe:e==="noop"?i:t,send:e==="noop"?o:t,process:e==="noop"?n:t}}function F(r="pseudo",e){let t=e?.runtime??"throw";return e&&"args"in e&&e.args==="keyed"?(n,i)=>w(r,t):n=>w(r,t)}function A(r){return JSON.parse(readFileSync(r,"utf-8"))}function q(r,e){let t=A(r);if(!Array.isArray(t))throw new Error(`fixture.each: expected JSON array at "${r}", got ${typeof t}`);for(let o of t){if(typeof o?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(o)}`);test(o.name,()=>e(o));}}export{y as TestContext,g as TestContextBuilder,R as createNoopSpyLogger,h as createSpyLogger,A as fixture,q as fixtureEach,E as invoke,F as pseudo,b as testContext};//# sourceMappingURL=index.js.map
1
+ import {readFileSync}from'fs';import {vi,test}from'vitest';import {isRoutecraftError,rcError,ADAPTER_DIRECT_STORE,sanitizeEndpoint,DefaultExchange,ContextBuilder,logger,HeadersKeys}from'@routecraft/routecraft';function m(){let t={info:vi.fn(),debug:vi.fn(),warn:vi.fn(),error:vi.fn(),trace:vi.fn(),fatal:vi.fn(),child:vi.fn()};return t.child.mockImplementation(()=>t),t}function R(){let t=vi.fn(),e=vi.fn(),r={info:t,debug:t,warn:t,error:t,trace:t,fatal:t,child:e};return e.mockImplementation(()=>r),r}var k=200,f=class{ctx;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;loggerChildRestored=false;startedPromise;constructor(e,r){this.ctx=e,this.logger=r?.spyLogger??R(),r?.restoreLoggerChild&&(this.restoreLoggerChild=r.restoreLoggerChild),this.routesReadyTimeoutMs=r?.routesReadyTimeoutMs??k,e.on("error",o=>{let n=o.details.error;this.errors.push(isRoutecraftError(n)?n:rcError("RC9901",n));});}async startAndWaitReady(){let e=this.ctx,r=e.getRoutes().length,o=r===0?Promise.resolve():new Promise((n,a)=>{let i=0,s=false,p=setTimeout(()=>{s||(s=true,d(),c(),a(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),d=e.on("route:started",()=>{s||(i++,i>=r&&(s=true,clearTimeout(p),d(),c(),n()));}),c=e.on("error",g=>{s||(s=true,clearTimeout(p),d(),c(),a(g.details.error));});});this.startedPromise=e.start(),await Promise.all([this.startedPromise,o]);}async test(e){let r=this.ctx,o=r.getRoutes().length,n=o===0?Promise.resolve():new Promise((i,s)=>{let p=0,d=false,c=setTimeout(()=>{d||(h(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),g=r.on("route:started",()=>{d||(p++,p>=o&&(h(),i()));}),v=r.on("error",w=>{d||(h(),s(w.details.error));});function h(){d||(d=true,g(),v(),c!==void 0&&(clearTimeout(c),c=void 0));}}),a=r.start();try{await n;let i=e?.delayBeforeDrainMs??0;i>0&&await new Promise(s=>setTimeout(s,i)),await r.drain();}finally{try{await r.stop(),await a;}finally{this.restoreLoggerChildOnce();}}}drain(){return this.ctx.drain()}async stop(){try{await this.ctx.stop(),this.startedPromise!==void 0&&await this.startedPromise;}finally{this.restoreLoggerChildOnce();}}restoreLoggerChildOnce(){this.loggerChildRestored||(this.restoreLoggerChild?.(),this.loggerChildRestored=true);}async send(e,r,o){let n=this.ctx.getStore(ADAPTER_DIRECT_STORE),a=sanitizeEndpoint(e),i=n?.get(a);if(!i)throw new Error(`No direct channel for endpoint "${e}". Did you call startAndWaitReady() first?`);let s=new DefaultExchange(this.ctx,{body:r,...o!==void 0&&{headers:o}});return (await i.send(a,s)).body}},l=class{builder=new ContextBuilder;routesReadyTimeoutMs;routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}with(e){return this.builder.with(e),this}on(e,r){return this.builder.on(e,r),this}once(e,r){return this.builder.once(e,r),this}store(e,r){return this.builder.store(e,r),this}routes(e){return this.builder.routes(e),this}async build(){let e=m(),r=logger.child.bind(logger);logger.child=vi.fn(()=>e);let o=await this.builder.build(),n={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{logger.child=r;}};return new f(o,n)}};function A(){return new l}function x(t,e){let r=()=>{throw new Error(`Pseudo adapter "${t}" is not implemented. Replace with a real adapter.`)},o=()=>Promise.resolve(void 0),n=i=>i,a=(i,s,p,d)=>(d?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${t}`,subscribe:e==="noop"?a:r,send:e==="noop"?o:r,process:e==="noop"?n:r}}function F(t="pseudo",e){let r=e?.runtime??"throw";return e&&"args"in e&&e.args==="keyed"?(n,a)=>x(t,r):n=>x(t,r)}function T(){return {received:[],calls:{send:0,process:0,enrich:0}}}function M(){let t=T();return {adapterId:"routecraft.adapter.spy",received:t.received,calls:t.calls,send(e){t.received.push(e),e.headers?.[HeadersKeys.OPERATION]==="enrich"?t.calls.enrich++:t.calls.send++;},process(e){return t.received.push(e),t.calls.process++,e},reset(){t.received.length=0,t.calls.send=0,t.calls.process=0,t.calls.enrich=0;},lastReceived(){if(t.received.length===0)throw new Error("SpyAdapter: no exchanges recorded");return t.received[t.received.length-1]},receivedBodies(){return t.received.map(e=>e.body)}}}function I(t){return JSON.parse(readFileSync(t,"utf-8"))}function Z(t,e){let r=I(t);if(!Array.isArray(r))throw new Error(`fixture.each: expected JSON array at "${t}", got ${typeof r}`);for(let o of r){if(typeof o?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(o)}`);test(o.name,()=>e(o));}}export{f as TestContext,l as TestContextBuilder,R as createNoopSpyLogger,m as createSpyLogger,I as fixture,Z as fixtureEach,F as pseudo,M as spy,A as testContext};//# sourceMappingURL=index.js.map
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/spy-logger.ts","../src/test-context.ts","../src/invoke.ts","../src/adapters/pseudo/index.ts","../src/index.ts"],"names":["createSpyLogger","spy","vi","createNoopSpyLogger","noop","childFn","noopLogger","DEFAULT_ROUTES_READY_TIMEOUT_MS","TestContext","ctx","options","payload","err","isRouteCraftError","rcError","total","allReady","resolve","reject","ready","settled","timeoutId","offRouteStarted","offError","cleanup","started","delayMs","TestContextBuilder","ContextBuilder","ms","config","event","handler","key","value","routes","spyLogger","originalChild","logger","testContext","isDestination","obj","invoke","routeIdOrDestination","body","headers","exchange","DefaultExchange","dest","route","source","createAdapter","name","runtime","fail","noopSend","noopProcess","noopSubscribe","_context","_handler","_abortController","onReady","pseudo","opts","fixture","path","readFileSync","fixtureEach","run","entries","entry","test"],"mappings":"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","file":"index.js","sourcesContent":["import { vi } from \"vitest\";\n\n/**\n * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).\n */\nexport type SpyLogger = {\n info: ReturnType<typeof vi.fn>;\n debug: ReturnType<typeof vi.fn>;\n warn: ReturnType<typeof vi.fn>;\n error: ReturnType<typeof vi.fn>;\n trace: ReturnType<typeof vi.fn>;\n fatal: ReturnType<typeof vi.fn>;\n child: ReturnType<typeof vi.fn>;\n};\n\nexport function createSpyLogger(): SpyLogger {\n const spy: SpyLogger = {\n info: vi.fn(),\n debug: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n trace: vi.fn(),\n fatal: vi.fn(),\n child: vi.fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\nexport function createNoopSpyLogger(): SpyLogger {\n const noop = vi.fn();\n const childFn = vi.fn();\n const noopLogger: SpyLogger = {\n info: noop,\n debug: noop,\n warn: noop,\n error: noop,\n trace: noop,\n fatal: noop,\n child: childFn,\n };\n childFn.mockImplementation(() => noopLogger);\n return noopLogger;\n}\n","import { vi } from \"vitest\";\nimport type {\n CraftContext,\n CraftConfig,\n StoreRegistry,\n EventName,\n EventHandler,\n RouteDefinition,\n RouteBuilder,\n} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n isRouteCraftError,\n RouteCraftError,\n rcError,\n logger,\n} from \"@routecraft/routecraft\";\nimport type { SpyLogger } from \"./spy-logger\";\nimport { createSpyLogger, createNoopSpyLogger } from \"./spy-logger\";\n\nconst DEFAULT_ROUTES_READY_TIMEOUT_MS = 200;\n\nexport interface TestContextOptions {\n /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */\n routesReadyTimeoutMs?: number;\n}\n\n/** Options for TestContext.test(). */\nexport interface TestOptions {\n /**\n * Delay in ms after all routes are ready, before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n * E.g. `await t.test({ delayBeforeDrainMs: 50 })` for a timer with intervalMs >= 50.\n */\n delayBeforeDrainMs?: number;\n}\n\n/**\n * Test-friendly wrapper around CraftContext. Runs the real context but manages\n * lifecycle (start → wait routes ready → drain → stop) and collects errors.\n * t.logger is a spy logger (vi.fn() methods) for asserting on log calls.\n */\nexport class TestContext {\n readonly ctx: CraftContext;\n /** Spy logger; e.g. expect(t.logger.info).toHaveBeenCalledWith(...) */\n readonly logger: SpyLogger;\n readonly errors: RouteCraftError[] = [];\n private readonly routesReadyTimeoutMs: number;\n\n private restoreLoggerChild?: () => void;\n private startedPromise?: Promise<void>;\n\n constructor(\n ctx: CraftContext,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.logger = options?.spyLogger ?? createNoopSpyLogger();\n if (options?.restoreLoggerChild)\n this.restoreLoggerChild = options.restoreLoggerChild;\n this.routesReadyTimeoutMs =\n options?.routesReadyTimeoutMs ?? DEFAULT_ROUTES_READY_TIMEOUT_MS;\n ctx.on(\"error\", (payload) => {\n const err = payload.details.error;\n this.errors.push(\n isRouteCraftError(err)\n ? (err as RouteCraftError)\n : rcError(\"RC9901\", err),\n );\n });\n }\n\n /**\n * Start context and wait for all routes to be ready. Does not drain or stop.\n * Use with invoke() to send to a route by id, then call drain()/stop() when done.\n */\n async startAndWaitReady(): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n const timeoutId = setTimeout(() => {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"route:started\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n reject(payload.details.error);\n });\n });\n this.startedPromise = ctx.start();\n await Promise.all([this.startedPromise, allReady]);\n }\n\n /**\n * Start context, wait for all routes ready, optionally delay, drain in-flight, then stop.\n * Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).\n *\n * @param options.delayBeforeDrainMs — If set, wait this many ms after routes are ready before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n */\n async test(options?: TestOptions): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n let timeoutId: ReturnType<typeof setTimeout> | undefined =\n setTimeout(() => {\n if (settled) return;\n cleanup();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"route:started\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n cleanup();\n reject(payload.details.error);\n });\n\n function cleanup(): void {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n timeoutId = undefined;\n }\n }\n });\n const started = ctx.start();\n try {\n await allReady;\n const delayMs = options?.delayBeforeDrainMs ?? 0;\n if (delayMs > 0) {\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n await ctx.drain();\n } finally {\n try {\n await ctx.stop();\n await started;\n } finally {\n this.restoreLoggerChild?.();\n }\n }\n }\n\n drain(): Promise<void> {\n return this.ctx.drain();\n }\n\n async stop(): Promise<void> {\n await this.ctx.stop();\n if (this.startedPromise !== undefined) {\n await this.startedPromise;\n }\n }\n}\n\n/**\n * Builder that returns TestContext instead of CraftContext.\n * Same API as ContextBuilder (routes, on, with, store).\n */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n\n /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */\n routesReadyTimeout(ms: number): this {\n this.routesReadyTimeoutMs = ms;\n return this;\n }\n\n with(config: CraftConfig): this {\n this.builder.with(config);\n return this;\n }\n\n on<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.on(event, handler);\n return this;\n }\n\n once<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.once(event, handler);\n return this;\n }\n\n store<K extends keyof StoreRegistry>(key: K, value: StoreRegistry[K]): this {\n this.builder.store(key, value);\n return this;\n }\n\n routes(\n routes:\n | RouteDefinition[]\n | RouteBuilder<unknown>[]\n | RouteDefinition\n | RouteBuilder<unknown>,\n ): this {\n this.builder.routes(routes);\n return this;\n }\n\n async build(): Promise<TestContext> {\n const spyLogger = createSpyLogger();\n const originalChild = logger.child.bind(logger);\n logger.child = vi.fn(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n ) as typeof logger.child;\n const ctx = await this.builder.build();\n const options: TestContextOptions & {\n spyLogger: SpyLogger;\n restoreLoggerChild: () => void;\n } = {\n ...(this.routesReadyTimeoutMs !== undefined\n ? { routesReadyTimeoutMs: this.routesReadyTimeoutMs }\n : {}),\n spyLogger,\n restoreLoggerChild: () => {\n logger.child = originalChild;\n },\n };\n return new TestContext(ctx, options);\n }\n}\n\n/**\n * Create a test context builder. Use .routes(...).build(), await the result, then await t.test().\n *\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n */\nexport function testContext(): TestContextBuilder {\n return new TestContextBuilder();\n}\n","import type {\n CraftContext,\n Destination,\n ExchangeHeaders,\n} from \"@routecraft/routecraft\";\nimport { DefaultExchange } from \"@routecraft/routecraft\";\n\n/** Duck-type: object with send(exchange) returning Promise. */\nfunction isDestination(obj: unknown): obj is Destination<unknown, unknown> {\n return (\n typeof obj === \"object\" &&\n obj !== null &&\n typeof (obj as { send?: unknown }).send === \"function\"\n );\n}\n\n/**\n * Invoke a route by id or send to a destination and return the result.\n *\n * - **By route id:** Pass a string. The route is looked up with ctx.getRouteById(routeId).\n * The route's source must implement Destination (e.g. DirectAdapter). Works with multiple\n * routes in the default export — use the route's id.\n *\n * - **By destination:** Pass a Destination instance (e.g. direct(\"endpoint\")). Builds an\n * exchange and calls destination.send(exchange).\n *\n * @param ctx CraftContext (e.g. t.ctx from TestContext) with routes started\n * @param routeIdOrDestination Route id string or a Destination adapter instance\n * @param body Request body\n * @param headers Optional headers for the exchange\n * @returns The result from the route or destination (e.g. response body for DirectAdapter)\n */\nexport async function invoke<T = unknown, R = T>(\n ctx: CraftContext,\n routeIdOrDestination: string | Destination<T, R>,\n body: T,\n headers?: ExchangeHeaders,\n): Promise<R> {\n const exchange = new DefaultExchange(ctx, {\n body,\n ...(headers !== undefined && { headers }),\n });\n let dest: Destination<T, R>;\n\n if (typeof routeIdOrDestination === \"string\") {\n const route = ctx.getRouteById(routeIdOrDestination);\n if (!route) {\n throw new Error(\n `No route with id \"${routeIdOrDestination}\". Did you start the context (e.g. await t.test())?`,\n );\n }\n const source = route.definition.source;\n if (!isDestination(source)) {\n throw new Error(\n `Route \"${routeIdOrDestination}\" is not invokable: source must implement Destination (e.g. direct adapter).`,\n );\n }\n dest = source as Destination<T, R>;\n } else {\n dest = routeIdOrDestination;\n }\n\n const result = await dest.send(exchange as Parameters<typeof dest.send>[0]);\n return result as R;\n}\n","import type { Source, Destination, Processor } from \"@routecraft/routecraft\";\nimport type { PseudoOptions, PseudoKeyedOptions } from \"./shared\";\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- input position must accept any exchange for DSL assignability */\nexport type PseudoAdapter<R> = {\n adapterId: string;\n} & Source<R> &\n Destination<any, R> &\n Processor<any, R>;\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nexport type PseudoFactory<Opts> = <R = unknown>(opts: Opts) => PseudoAdapter<R>;\n\nexport type PseudoKeyedFactory<Opts> = <R = unknown>(\n key: string,\n opts?: Opts,\n) => PseudoAdapter<R>;\n\nfunction createAdapter<R>(\n name: string,\n runtime: \"throw\" | \"noop\",\n): PseudoAdapter<R> {\n const fail = (): never => {\n throw new Error(\n `Pseudo adapter \"${name}\" is not implemented. Replace with a real adapter.`,\n );\n };\n const noopSend = (): Promise<R> => Promise.resolve(undefined as unknown as R);\n const noopProcess = (exchange: unknown): unknown => exchange;\n const noopSubscribe = (\n _context: unknown,\n _handler: unknown,\n _abortController: unknown,\n onReady?: () => void,\n ): Promise<void> => {\n onReady?.();\n return Promise.resolve();\n };\n\n type SendFn = PseudoAdapter<R>[\"send\"];\n type ProcessFn = PseudoAdapter<R>[\"process\"];\n type SubscribeFn = Source<R>[\"subscribe\"];\n\n return {\n adapterId: `routecraft.adapter.pseudo.${name}`,\n subscribe:\n runtime === \"noop\"\n ? (noopSubscribe as SubscribeFn)\n : (fail as SubscribeFn),\n send: runtime === \"noop\" ? (noopSend as SendFn) : (fail as SendFn),\n process:\n runtime === \"noop\" ? (noopProcess as ProcessFn) : (fail as ProcessFn),\n };\n}\n\n// Overload: string-first (keyed) factory\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(name: string, options: PseudoKeyedOptions): PseudoKeyedFactory<Opts>;\n\n// Overload: object-only factory (default)\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(name?: string, options?: PseudoOptions): PseudoFactory<Opts>;\n\n// Implementation\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(\n name = \"pseudo\",\n options?: PseudoOptions | PseudoKeyedOptions,\n): PseudoFactory<Opts> | PseudoKeyedFactory<Opts> {\n const runtime = options?.runtime ?? \"throw\";\n const isKeyed = options && \"args\" in options && options.args === \"keyed\";\n\n if (isKeyed) {\n return <R = unknown>(key: string, opts?: Opts): PseudoAdapter<R> => {\n void key;\n void opts;\n return createAdapter<R>(name, runtime);\n };\n }\n return <R = unknown>(opts: Opts): PseudoAdapter<R> => {\n void opts;\n return createAdapter<R>(name, runtime);\n };\n}\n\n// Re-export types\nexport type { PseudoOptions, PseudoKeyedOptions } from \"./shared\";\n","import { readFileSync } from \"node:fs\";\nimport { test } from \"vitest\";\n\n// Re-export test context utilities\nexport {\n TestContext,\n TestContextBuilder,\n testContext,\n type TestContextOptions,\n type TestOptions,\n} from \"./test-context\";\n\n// Re-export spy logger utilities\nexport {\n createSpyLogger,\n createNoopSpyLogger,\n type SpyLogger,\n} from \"./spy-logger\";\n\n// Re-export invoke utility\nexport { invoke } from \"./invoke\";\n\n// Re-export pseudo adapter\nexport {\n pseudo,\n type PseudoAdapter,\n type PseudoFactory,\n type PseudoKeyedFactory,\n type PseudoOptions,\n type PseudoKeyedOptions,\n} from \"./adapters/pseudo\";\n\n/**\n * Load a JSON fixture file and return the parsed value.\n *\n * @param path Absolute or relative path to the JSON file\n * @returns Parsed JSON as T\n */\nexport function fixture<T = unknown>(path: string): T {\n return JSON.parse(readFileSync(path, \"utf-8\")) as T;\n}\n\n/** Fixture entry must have a `name` field used as the vitest test name. */\nexport interface FixtureWithName {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * Load a JSON array fixture and run one vitest test per entry. Each entry must have a `name` field (used as the test name).\n *\n * @param path Path to a JSON file that parses to an array\n * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.\n */\nexport function fixtureEach<T extends FixtureWithName>(\n path: string,\n run: (entry: T) => void | Promise<void>,\n): void {\n const entries = fixture<T[]>(path);\n if (!Array.isArray(entries)) {\n throw new Error(\n `fixture.each: expected JSON array at \"${path}\", got ${typeof entries}`,\n );\n }\n for (const entry of entries) {\n if (typeof entry?.name !== \"string\") {\n throw new Error(\n `fixture.each: each entry must have a \"name\" field (string). Got: ${JSON.stringify(entry)}`,\n );\n }\n test(entry.name, () => run(entry));\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/spy-logger.ts","../src/test-context.ts","../src/adapters/pseudo/index.ts","../src/adapters/spy/shared.ts","../src/adapters/spy/index.ts","../src/index.ts"],"names":["createSpyLogger","spy","vi","createNoopSpyLogger","noop","childFn","noopLogger","DEFAULT_ROUTES_READY_TIMEOUT_MS","TestContext","ctx","options","payload","err","isRoutecraftError","rcError","total","allReady","resolve","reject","ready","settled","timeoutId","offRouteStarted","offError","cleanup","started","delayMs","endpoint","body","headers","store","ADAPTER_DIRECT_STORE","sanitized","sanitizeEndpoint","channel","exchange","DefaultExchange","TestContextBuilder","ContextBuilder","ms","config","event","handler","key","value","routes","spyLogger","originalChild","logger","testContext","createAdapter","name","runtime","fail","noopSend","noopProcess","noopSubscribe","_context","_handler","_abortController","onReady","pseudo","opts","createSpyState","state","HeadersKeys","fixture","path","readFileSync","fixtureEach","run","entries","entry","test"],"mappings":"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","file":"index.js","sourcesContent":["import { vi } from \"vitest\";\n\n/**\n * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).\n *\n * @beta\n */\nexport type SpyLogger = {\n info: ReturnType<typeof vi.fn>;\n debug: ReturnType<typeof vi.fn>;\n warn: ReturnType<typeof vi.fn>;\n error: ReturnType<typeof vi.fn>;\n trace: ReturnType<typeof vi.fn>;\n fatal: ReturnType<typeof vi.fn>;\n child: ReturnType<typeof vi.fn>;\n};\n\n/** @beta */\nexport function createSpyLogger(): SpyLogger {\n const spy: SpyLogger = {\n info: vi.fn(),\n debug: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n trace: vi.fn(),\n fatal: vi.fn(),\n child: vi.fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\n/** @beta */\nexport function createNoopSpyLogger(): SpyLogger {\n const noop = vi.fn();\n const childFn = vi.fn();\n const noopLogger: SpyLogger = {\n info: noop,\n debug: noop,\n warn: noop,\n error: noop,\n trace: noop,\n fatal: noop,\n child: childFn,\n };\n childFn.mockImplementation(() => noopLogger);\n return noopLogger;\n}\n","import { vi } from \"vitest\";\nimport type {\n CraftContext,\n CraftConfig,\n StoreRegistry,\n EventName,\n EventHandler,\n RouteDefinition,\n RouteBuilder,\n Exchange,\n ExchangeHeaders,\n} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n DefaultExchange,\n ADAPTER_DIRECT_STORE,\n sanitizeEndpoint,\n isRoutecraftError,\n RoutecraftError,\n rcError,\n logger,\n} from \"@routecraft/routecraft\";\nimport type { SpyLogger } from \"./spy-logger\";\nimport { createSpyLogger, createNoopSpyLogger } from \"./spy-logger\";\n\nconst DEFAULT_ROUTES_READY_TIMEOUT_MS = 200;\n\nexport interface TestContextOptions {\n /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */\n routesReadyTimeoutMs?: number;\n}\n\n/** Options for TestContext.test(). */\nexport interface TestOptions {\n /**\n * Delay in ms after all routes are ready, before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n * E.g. `await t.test({ delayBeforeDrainMs: 50 })` for a timer with intervalMs >= 50.\n */\n delayBeforeDrainMs?: number;\n}\n\n/**\n * Test-friendly wrapper around CraftContext. Runs the real context but manages\n * lifecycle (start, wait routes ready, drain, stop) and collects errors.\n * t.logger is a spy logger (vi.fn() methods) for asserting on log calls.\n *\n * @beta\n */\nexport class TestContext {\n readonly ctx: CraftContext;\n /** Spy logger; e.g. expect(t.logger.info).toHaveBeenCalledWith(...) */\n readonly logger: SpyLogger;\n readonly errors: RoutecraftError[] = [];\n private readonly routesReadyTimeoutMs: number;\n\n private restoreLoggerChild?: () => void;\n private loggerChildRestored = false;\n private startedPromise?: Promise<void>;\n\n constructor(\n ctx: CraftContext,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.logger = options?.spyLogger ?? createNoopSpyLogger();\n if (options?.restoreLoggerChild)\n this.restoreLoggerChild = options.restoreLoggerChild;\n this.routesReadyTimeoutMs =\n options?.routesReadyTimeoutMs ?? DEFAULT_ROUTES_READY_TIMEOUT_MS;\n ctx.on(\"error\", (payload) => {\n const err = payload.details.error;\n this.errors.push(\n isRoutecraftError(err)\n ? (err as RoutecraftError)\n : rcError(\"RC9901\", err),\n );\n });\n }\n\n /**\n * Start context and wait for all routes to be ready. Does not drain or stop.\n * Use with invoke() to send to a route by id, then call drain()/stop() when done.\n */\n async startAndWaitReady(): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n const timeoutId = setTimeout(() => {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"route:started\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n reject(payload.details.error);\n });\n });\n this.startedPromise = ctx.start();\n await Promise.all([this.startedPromise, allReady]);\n }\n\n /**\n * Start context, wait for all routes ready, optionally delay, drain in-flight, then stop.\n * Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).\n *\n * @param options.delayBeforeDrainMs — If set, wait this many ms after routes are ready before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n */\n async test(options?: TestOptions): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n let timeoutId: ReturnType<typeof setTimeout> | undefined =\n setTimeout(() => {\n if (settled) return;\n cleanup();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"route:started\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n cleanup();\n reject(payload.details.error);\n });\n\n function cleanup(): void {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n timeoutId = undefined;\n }\n }\n });\n const started = ctx.start();\n try {\n await allReady;\n const delayMs = options?.delayBeforeDrainMs ?? 0;\n if (delayMs > 0) {\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n await ctx.drain();\n } finally {\n try {\n await ctx.stop();\n await started;\n } finally {\n this.restoreLoggerChildOnce();\n }\n }\n }\n\n drain(): Promise<void> {\n return this.ctx.drain();\n }\n\n async stop(): Promise<void> {\n try {\n await this.ctx.stop();\n if (this.startedPromise !== undefined) {\n await this.startedPromise;\n }\n } finally {\n this.restoreLoggerChildOnce();\n }\n }\n\n private restoreLoggerChildOnce(): void {\n if (this.loggerChildRestored) return;\n this.restoreLoggerChild?.();\n this.loggerChildRestored = true;\n }\n\n /**\n * Send a message to a direct endpoint and return the result.\n * Use after {@link startAndWaitReady} so the channel exists.\n *\n * @param endpoint Direct endpoint name (must match the endpoint string passed to `direct(endpoint, options)`)\n * @param body Request body\n * @param headers Optional exchange headers\n * @returns The response body from the route\n */\n async send<T = unknown, R = T>(\n endpoint: string,\n body: T,\n headers?: ExchangeHeaders,\n ): Promise<R> {\n const store = this.ctx.getStore(ADAPTER_DIRECT_STORE);\n const sanitized = sanitizeEndpoint(endpoint);\n const channel = store?.get(sanitized);\n if (!channel) {\n throw new Error(\n `No direct channel for endpoint \"${endpoint}\". Did you call startAndWaitReady() first?`,\n );\n }\n const exchange = new DefaultExchange(this.ctx, {\n body,\n ...(headers !== undefined && { headers }),\n });\n const result = await channel.send(sanitized, exchange);\n return (result as Exchange).body as R;\n }\n}\n\n/**\n * Builder that returns TestContext instead of CraftContext.\n * Same API as ContextBuilder (routes, on, with, store).\n *\n * @beta\n */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n\n /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */\n routesReadyTimeout(ms: number): this {\n this.routesReadyTimeoutMs = ms;\n return this;\n }\n\n with(config: CraftConfig): this {\n this.builder.with(config);\n return this;\n }\n\n on<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.on(event, handler);\n return this;\n }\n\n once<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.once(event, handler);\n return this;\n }\n\n store<K extends keyof StoreRegistry>(key: K, value: StoreRegistry[K]): this {\n this.builder.store(key, value);\n return this;\n }\n\n routes(\n routes:\n | RouteDefinition[]\n | RouteBuilder<unknown>[]\n | RouteDefinition\n | RouteBuilder<unknown>,\n ): this {\n this.builder.routes(routes);\n return this;\n }\n\n async build(): Promise<TestContext> {\n const spyLogger = createSpyLogger();\n const originalChild = logger.child.bind(logger);\n logger.child = vi.fn(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n ) as typeof logger.child;\n const ctx = await this.builder.build();\n const options: TestContextOptions & {\n spyLogger: SpyLogger;\n restoreLoggerChild: () => void;\n } = {\n ...(this.routesReadyTimeoutMs !== undefined\n ? { routesReadyTimeoutMs: this.routesReadyTimeoutMs }\n : {}),\n spyLogger,\n restoreLoggerChild: () => {\n logger.child = originalChild;\n },\n };\n return new TestContext(ctx, options);\n }\n}\n\n/**\n * Create a test context builder. Use .routes(...).build(), await the result, then await t.test().\n *\n * @beta\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n */\nexport function testContext(): TestContextBuilder {\n return new TestContextBuilder();\n}\n","import type { Source, Destination, Processor } from \"@routecraft/routecraft\";\nimport type { PseudoOptions, PseudoKeyedOptions } from \"./shared\";\n\n/**\n * @internal\n */\n/* eslint-disable @typescript-eslint/no-explicit-any -- input position must accept any exchange for DSL assignability */\nexport type PseudoAdapter<R> = {\n adapterId: string;\n} & Source<R> &\n Destination<any, R> &\n Processor<any, R>;\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n/** @internal */\nexport type PseudoFactory<Opts> = <R = unknown>(opts: Opts) => PseudoAdapter<R>;\n\n/** @internal */\nexport type PseudoKeyedFactory<Opts> = <R = unknown>(\n key: string,\n opts?: Opts,\n) => PseudoAdapter<R>;\n\nfunction createAdapter<R>(\n name: string,\n runtime: \"throw\" | \"noop\",\n): PseudoAdapter<R> {\n const fail = (): never => {\n throw new Error(\n `Pseudo adapter \"${name}\" is not implemented. Replace with a real adapter.`,\n );\n };\n const noopSend = (): Promise<R> => Promise.resolve(undefined as unknown as R);\n const noopProcess = (exchange: unknown): unknown => exchange;\n const noopSubscribe = (\n _context: unknown,\n _handler: unknown,\n _abortController: unknown,\n onReady?: () => void,\n ): Promise<void> => {\n onReady?.();\n return Promise.resolve();\n };\n\n type SendFn = PseudoAdapter<R>[\"send\"];\n type ProcessFn = PseudoAdapter<R>[\"process\"];\n type SubscribeFn = Source<R>[\"subscribe\"];\n\n return {\n adapterId: `routecraft.adapter.pseudo.${name}`,\n subscribe:\n runtime === \"noop\"\n ? (noopSubscribe as SubscribeFn)\n : (fail as SubscribeFn),\n send: runtime === \"noop\" ? (noopSend as SendFn) : (fail as SendFn),\n process:\n runtime === \"noop\" ? (noopProcess as ProcessFn) : (fail as ProcessFn),\n };\n}\n\n/**\n * Creates a pseudo (placeholder) adapter for use in tests or as a stub during development.\n *\n * @beta\n */\n// Overload: string-first (keyed) factory\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(name: string, options: PseudoKeyedOptions): PseudoKeyedFactory<Opts>;\n\n// Overload: object-only factory (default)\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(name?: string, options?: PseudoOptions): PseudoFactory<Opts>;\n\n// Implementation\nexport function pseudo<\n Opts extends Record<string, unknown> = Record<string, unknown>,\n>(\n name = \"pseudo\",\n options?: PseudoOptions | PseudoKeyedOptions,\n): PseudoFactory<Opts> | PseudoKeyedFactory<Opts> {\n const runtime = options?.runtime ?? \"throw\";\n const isKeyed = options && \"args\" in options && options.args === \"keyed\";\n\n if (isKeyed) {\n return <R = unknown>(key: string, opts?: Opts): PseudoAdapter<R> => {\n void key;\n void opts;\n return createAdapter<R>(name, runtime);\n };\n }\n return <R = unknown>(opts: Opts): PseudoAdapter<R> => {\n void opts;\n return createAdapter<R>(name, runtime);\n };\n}\n\n// Re-export types\nexport type { PseudoOptions, PseudoKeyedOptions } from \"./shared\";\n","import type { Exchange } from \"@routecraft/routecraft\";\n\n/**\n * Internal state container for the spy adapter.\n */\nexport interface SpyState<T> {\n received: Exchange<T>[];\n calls: { send: number; process: number; enrich: number };\n}\n\n/**\n * Creates fresh spy state with empty received array and zeroed counters.\n */\nexport function createSpyState<T>(): SpyState<T> {\n return {\n received: [],\n calls: { send: 0, process: 0, enrich: 0 },\n };\n}\n","import {\n HeadersKeys,\n type Destination,\n type Processor,\n type Exchange,\n} from \"@routecraft/routecraft\";\nimport { createSpyState } from \"./shared.ts\";\n\n/**\n * A spy adapter that records all exchanges passing through it.\n * Implements both {@link Destination} and {@link Processor} so it can be used\n * with `.to()`, `.enrich()`, `.tap()`, and `.process()`.\n */\nexport type SpyAdapter<T = unknown> = {\n /** Stable identifier for this adapter. */\n adapterId: string;\n\n /** All exchanges recorded, in order. */\n received: Exchange<T>[];\n\n /** Per-operation call counters. */\n calls: { send: number; process: number; enrich: number };\n\n /** Clear all recorded data and reset counters. */\n reset(): void;\n\n /** Most recent exchange. Throws if none recorded. */\n lastReceived(): Exchange<T>;\n\n /** Array of just the body values from received exchanges. */\n receivedBodies(): T[];\n /* eslint-disable @typescript-eslint/no-explicit-any -- both positions use any: Destination so the spy is assignable regardless of body type, Processor so spy<unknown>() is assignable in typed pipelines */\n} & Destination<any, void> &\n Processor<any, T>;\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n/**\n * Creates a spy adapter that records all exchanges for test assertions.\n *\n * Use as a destination (`.to()`, `.enrich()`, `.tap()`) or processor (`.process()`)\n * to capture pipeline output without side effects.\n *\n * @beta\n *\n * @returns A spy adapter that records exchanges and tracks call counts\n *\n * @example\n * ```ts\n * const s = spy();\n * const route = craft().id(\"test\").from(simple(\"hello\")).to(s);\n * const t = await testContext().routes(route).build();\n * await t.test();\n *\n * expect(s.received).toHaveLength(1);\n * expect(s.received[0].body).toBe(\"hello\");\n * expect(s.calls.send).toBe(1);\n * ```\n */\nexport function spy<T = unknown>(): SpyAdapter<T> {\n const state = createSpyState<T>();\n\n return {\n adapterId: \"routecraft.adapter.spy\",\n received: state.received,\n calls: state.calls,\n\n send(exchange: Exchange<T>): void {\n state.received.push(exchange);\n\n const operation = exchange.headers?.[HeadersKeys.OPERATION];\n if (operation === \"enrich\") {\n state.calls.enrich++;\n } else {\n state.calls.send++;\n }\n },\n\n process(exchange: Exchange<T>): Exchange<T> {\n state.received.push(exchange);\n state.calls.process++;\n return exchange;\n },\n\n reset(): void {\n state.received.length = 0;\n state.calls.send = 0;\n state.calls.process = 0;\n state.calls.enrich = 0;\n },\n\n lastReceived(): Exchange<T> {\n if (state.received.length === 0) {\n throw new Error(\"SpyAdapter: no exchanges recorded\");\n }\n return state.received[state.received.length - 1];\n },\n\n receivedBodies(): T[] {\n return state.received.map((e) => e.body);\n },\n };\n}\n","import { readFileSync } from \"node:fs\";\nimport { test } from \"vitest\";\n\n// Re-export test context utilities\nexport {\n TestContext,\n TestContextBuilder,\n testContext,\n type TestContextOptions,\n type TestOptions,\n} from \"./test-context\";\n\n// Re-export spy logger utilities\nexport {\n createSpyLogger,\n createNoopSpyLogger,\n type SpyLogger,\n} from \"./spy-logger\";\n\n// Re-export pseudo adapter\nexport {\n pseudo,\n type PseudoAdapter,\n type PseudoFactory,\n type PseudoKeyedFactory,\n type PseudoOptions,\n type PseudoKeyedOptions,\n} from \"./adapters/pseudo\";\n\n// Re-export spy adapter\nexport { spy, type SpyAdapter } from \"./adapters/spy\";\n\n/**\n * Load a JSON fixture file and return the parsed value.\n *\n * @beta\n * @param path Absolute or relative path to the JSON file\n * @returns Parsed JSON as T\n */\nexport function fixture<T = unknown>(path: string): T {\n return JSON.parse(readFileSync(path, \"utf-8\")) as T;\n}\n\n/** Fixture entry must have a `name` field used as the vitest test name. */\nexport interface FixtureWithName {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * Load a JSON array fixture and run one vitest test per entry. Each entry must have a `name` field (used as the test name).\n *\n * @beta\n * @param path Path to a JSON file that parses to an array\n * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.\n */\nexport function fixtureEach<T extends FixtureWithName>(\n path: string,\n run: (entry: T) => void | Promise<void>,\n): void {\n const entries = fixture<T[]>(path);\n if (!Array.isArray(entries)) {\n throw new Error(\n `fixture.each: expected JSON array at \"${path}\", got ${typeof entries}`,\n );\n }\n for (const entry of entries) {\n if (typeof entry?.name !== \"string\") {\n throw new Error(\n `fixture.each: each entry must have a \"name\" field (string). Got: ${JSON.stringify(entry)}`,\n );\n }\n test(entry.name, () => run(entry));\n }\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@routecraft/testing",
3
- "version": "0.4.0-canary.6",
4
- "description": "Test utilities for RouteCraft routes",
3
+ "version": "0.4.0-canary.8",
4
+ "description": "Test utilities for Routecraft routes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "prepublishOnly": "pnpm run build"
24
24
  },
25
25
  "dependencies": {
26
- "@routecraft/routecraft": "^0.4.0-canary.6"
26
+ "@routecraft/routecraft": "^0.4.0-canary.8"
27
27
  },
28
28
  "devDependencies": {
29
29
  "vitest": "^4.0.18"