@routecraft/testing 0.5.0-canary.8 → 0.5.0-canary.9

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/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 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 O=200,f=class{ctx;client;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;loggerChildRestored=false;startedPromise;constructor(e,r,o){this.ctx=e,this.client=r,this.logger=o?.spyLogger??R(),o?.restoreLoggerChild&&(this.restoreLoggerChild=o.restoreLoggerChild),this.routesReadyTimeoutMs=o?.routesReadyTimeoutMs??O;let s=n=>{this.errors.push(routecraft.isRoutecraftError(n)?n:routecraft.rcError("RC9901",n));};e.on("context:error",n=>{s(n.details.error);});}async startAndWaitReady(){let e=this.ctx,r=e.getRoutes().length,o=r===0?Promise.resolve():new Promise((s,n)=>{let d=0,i=false,c=setTimeout(()=>{i||(i=true,a(),p(),n(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),a=e.on("route:*:started",(()=>{i||(d++,d>=r&&(i=true,clearTimeout(c),a(),p(),s()));})),p=e.on("context:error",g=>{i||(i=true,clearTimeout(c),a(),p(),n(g.details.error));});});this.startedPromise=e.start(),await Promise.all([this.startedPromise,o]);}async test(e){let r=this.ctx,o=r.getRoutes().length,s=o===0?Promise.resolve():new Promise((d,i)=>{let c=0,a=false,p=setTimeout(()=>{a||(m(),i(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),g=r.on("route:*:started",(()=>{a||(c++,c>=o&&(m(),d()));})),T=r.on("context:error",w=>{a||(m(),i(w.details.error));});function m(){a||(a=true,g(),T(),p!==void 0&&(clearTimeout(p),p=void 0));}}),n=r.start();try{await s;let d=e?.delayBeforeDrainMs??0;d>0&&await new Promise(i=>setTimeout(i,d)),await r.drain();}finally{try{await r.stop(),await n;}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);}},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=h(),r=routecraft.logger.child.bind(routecraft.logger);routecraft.logger.child=vitest.vi.fn(()=>e);let{context:o,client:s}=await this.builder.build(),n={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{routecraft.logger.child=r;}};return new f(o,s,n)}};function b(){return new l}function v(t,e){let r=()=>{throw new Error(`Pseudo adapter "${t}" is not implemented. Replace with a real adapter.`)},o=()=>Promise.resolve(void 0),s=d=>d,n=(d,i,c,a)=>(a?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${t}`,subscribe:e==="noop"?n:r,send:e==="noop"?o:r,process:e==="noop"?s:r}}function L(t="pseudo",e){let r=e?.runtime??"throw";return e&&"args"in e&&e.args==="keyed"?(s,n)=>v(t,r):s=>v(t,r)}function x(){return {received:[],calls:{send:0,process:0,enrich:0}}}function F(){let t=x();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 M(t){return JSON.parse(fs.readFileSync(t,"utf-8"))}function V(t,e){let r=M(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=h;exports.fixture=M;exports.fixtureEach=V;exports.pseudo=L;exports.spy=F;exports.testContext=b;//# sourceMappingURL=index.cjs.map
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 v(){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 R=Symbol.for("routecraft.testing.adapter-mock");function x(r){return typeof r=="object"&&r!==null&&r[R]===true}function A(r,e){let t={target:r,calls:{source:[],send:[]}};return e.source!==void 0&&(t.source=e.source),e.send!==void 0&&(t.send=e.send),{[R]:true,override:t,get calls(){return {source:[...t.calls.source],send:[...t.calls.send]}}}}var b=200;function L(r){return typeof r=="function"&&typeof r.name=="string"?`${/^[A-Z]/.test(r.name)&&r.prototype!==void 0?"class":"factory"} ${r.name||"<anonymous>"}`:"target"}var f=class{ctx;client;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;loggerChildRestored=false;startedPromise;constructor(e,t,o){this.ctx=e,this.client=t,this.logger=o?.spyLogger??v(),o?.restoreLoggerChild&&(this.restoreLoggerChild=o.restoreLoggerChild),this.routesReadyTimeoutMs=o?.routesReadyTimeoutMs??b;let n=s=>{this.errors.push(routecraft.isRoutecraftError(s)?s:routecraft.rcError("RC9901",s));};e.on("context:error",s=>{n(s.details.error);});}async startAndWaitReady(){let e=this.ctx,t=e.getRoutes().length,o=t===0?Promise.resolve():new Promise((n,s)=>{let i=0,a=false,c=setTimeout(()=>{a||(a=true,d(),p(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),d=e.on("route:*:started",(()=>{a||(i++,i>=t&&(a=true,clearTimeout(c),d(),p(),n()));})),p=e.on("context:error",g=>{a||(a=true,clearTimeout(c),d(),p(),s(g.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((i,a)=>{let c=0,d=false,p=setTimeout(()=>{d||(m(),a(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),g=t.on("route:*:started",(()=>{d||(c++,c>=o&&(m(),i()));})),O=t.on("context:error",P=>{d||(m(),a(P.details.error));});function m(){d||(d=true,g(),O(),p!==void 0&&(clearTimeout(p),p=void 0));}}),s=t.start();try{await n;let i=e?.delayBeforeDrainMs??0;i>0&&await new Promise(a=>setTimeout(a,i)),await t.drain();}finally{try{await t.stop(),await s;}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);}},y=class{builder=new routecraft.ContextBuilder;routesReadyTimeoutMs;adapterOverrides=[];routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}override(e){let t=x(e)?e.override:e;if(this.adapterOverrides.find(n=>n.target===t.target)!==void 0){let n=L(t.target);throw new Error(`testContext().override(): duplicate override for ${n}. Each target may only be registered once; remove the redundant override() call.`)}return this.adapterOverrides.push(t),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{context:o,client:n}=await this.builder.build();if(this.adapterOverrides.length>0){let i=o.getStore(routecraft.RC_ADAPTER_OVERRIDES)??[];o.setStore(routecraft.RC_ADAPTER_OVERRIDES,[...i,...this.adapterOverrides]);}let s={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{routecraft.logger.child=t;}};return new f(o,n,s)}};function K(){return new y}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=i=>i,s=(i,a,c,d)=>(d?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${r}`,subscribe:e==="noop"?s: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,s)=>w(r,t):n=>w(r,t)}function S(){return {received:[],calls:{send:0,process:0,enrich:0}}}function N(){let r=S();return {adapterId:"routecraft.adapter.spy",received:r.received,calls:r.calls,send(e){r.received.push(e),e.headers?.[routecraft.HeadersKeys.OPERATION]==="enrich"?r.calls.enrich++:r.calls.send++;},process(e){return r.received.push(e),r.calls.process++,e},reset(){r.received.length=0,r.calls.send=0,r.calls.process=0,r.calls.enrich=0;},lastReceived(){if(r.received.length===0)throw new Error("SpyAdapter: no exchanges recorded");return r.received[r.received.length-1]},receivedBodies(){return r.received.map(e=>e.body)}}}function _(r){return JSON.parse(fs.readFileSync(r,"utf-8"))}function oe(r,e){let t=_(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=f;exports.TestContextBuilder=y;exports.createNoopSpyLogger=v;exports.createSpyLogger=h;exports.fixture=_;exports.fixtureEach=oe;exports.mockAdapter=A;exports.pseudo=F;exports.spy=N;exports.testContext=K;//# 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/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","client","options","pushError","err","isRoutecraftError","rcError","payload","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","createAdapter","name","runtime","fail","noopSend","noopProcess","exchange","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} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n CraftClient,\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 /** Client for dispatching messages to direct endpoints in tests. */\n readonly client: CraftClient;\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 client: CraftClient,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.client = client;\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 const pushError = (err: unknown) => {\n this.errors.push(\n isRoutecraftError(err)\n ? (err as RoutecraftError)\n : rcError(\"RC9901\", err),\n );\n };\n ctx.on(\"context:error\", (payload) => {\n pushError(payload.details.error);\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(\n \"route:*:started\" as EventName,\n (() => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n resolve();\n }\n }) as EventHandler<EventName>,\n );\n const offError = ctx.on(\"context: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(\n \"route:*:started\" as EventName,\n (() => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n }) as EventHandler<EventName>,\n );\n const offError = ctx.on(\"context: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/**\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 { context: ctx, client } = 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, client, 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 * @experimental\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 * @experimental\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"]}
1
+ {"version":3,"sources":["../src/spy-logger.ts","../src/mock-adapter.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","ADAPTER_MOCK_BRAND","isAdapterMock","value","mockAdapter","target","behavior","override","DEFAULT_ROUTES_READY_TIMEOUT_MS","describeOverrideTarget","TestContext","ctx","client","options","pushError","err","isRoutecraftError","rcError","payload","total","allReady","resolve","reject","ready","settled","timeoutId","offRouteStarted","offError","cleanup","started","delayMs","TestContextBuilder","ContextBuilder","ms","mock","entry","o","name","config","event","handler","key","routes","spyLogger","originalChild","logger","existing","RC_ADAPTER_OVERRIDES","testContext","createAdapter","runtime","fail","noopSend","noopProcess","exchange","noopSubscribe","_context","_handler","_abortController","onReady","pseudo","opts","createSpyState","state","HeadersKeys","fixture","path","readFileSync","fixtureEach","run","entries","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 type {\n AdapterOverride,\n AdapterSendCall,\n AdapterSourceCall,\n SendOverrideHandler,\n Source,\n SourceOverrideBehavior,\n} from \"@routecraft/routecraft\";\n\n/**\n * Brand symbol stamped on the handles returned by `mockAdapter()`.\n *\n * `testContext().override()` uses this symbol to distinguish a mock handle\n * from a raw `AdapterOverride` (both objects carry a `calls` field, so a\n * structural check on a non-branded key would be fragile).\n *\n * Use `Symbol.for` so cross-realm / multi-package-load equality holds.\n *\n * @internal\n */\nexport const ADAPTER_MOCK_BRAND: unique symbol = Symbol.for(\n \"routecraft.testing.adapter-mock\",\n);\n\n/**\n * Extract the message type `M` from an adapter factory or adapter class,\n * so `mockAdapter(target, { source: [...] })` can check fixtures against\n * the real adapter shape. Falls back to `unknown` when `target` has no\n * inferable Source role (e.g. destination-only factories, or overloaded\n * factories where TypeScript cannot pick the source overload).\n */\ntype InferAdapterMessage<T> = T extends new (...args: never[]) => infer I\n ? I extends Source<infer M>\n ? M\n : unknown\n : T extends (...args: never[]) => infer R\n ? R extends Source<infer M>\n ? M\n : unknown\n : unknown;\n\n/**\n * Behaviour description for a mock adapter. A mock may stub the source side,\n * the destination side, or both. The framework picks the matching behaviour\n * based on the call site's role in the route.\n *\n * @experimental\n */\nexport interface MockAdapterBehavior<M = unknown> {\n /**\n * Source-role behaviour. Used when the adapter is the `.from()` of a route.\n * Pass an array of fixtures, an async iterable, or a callable that receives\n * the construction args and returns the stream to emit.\n */\n source?: SourceOverrideBehavior<M>;\n /**\n * Destination-role behaviour. Used when the adapter is passed to `.to()`,\n * `.enrich()`, or `.tap()`. Receives the exchange and a meta object with\n * the construction args; returning a value replaces the body upstream.\n */\n send?: SendOverrideHandler;\n}\n\n/**\n * Handle returned by `mockAdapter(factory, behaviour)`. Carries the resolved\n * override the framework should install on the context, plus `calls` for\n * assertions.\n *\n * @experimental\n */\nexport interface AdapterMock {\n /** Brand used by `testContext().override()` to discriminate handles from raw overrides. */\n readonly [ADAPTER_MOCK_BRAND]: true;\n readonly override: AdapterOverride;\n /**\n * Recorded calls, populated as the route runs. Assert on these after\n * awaiting `t.test()`.\n */\n readonly calls: {\n source: readonly AdapterSourceCall[];\n send: readonly AdapterSendCall[];\n };\n}\n\n/**\n * Type guard that distinguishes an `AdapterMock` (handle returned by\n * `mockAdapter()`) from a raw `AdapterOverride` value.\n *\n * @internal\n */\nexport function isAdapterMock(\n value: AdapterMock | AdapterOverride,\n): value is AdapterMock {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { [ADAPTER_MOCK_BRAND]?: unknown })[ADAPTER_MOCK_BRAND] === true\n );\n}\n\n/**\n * Create a mock for an adapter. The `target` may be either:\n *\n * - An adapter factory (e.g. `mail`, `http`, `mcp`). The mock matches every\n * adapter instance produced by that factory. Requires the factory to stamp\n * its adapters via `tagAdapter()`.\n * - An adapter class (e.g. `MailSourceAdapter`, `HttpDestinationAdapter`).\n * The mock matches any adapter whose `constructor === target`. Works for\n * every adapter without opt-in tagging, including third-party ones.\n *\n * Pass the result to `testContext().override(mock)` and run the route\n * under test as-is; the framework invokes the mock's `source` / `send`\n * handlers in place of the real adapter at every matching call site.\n *\n * @experimental\n * @param target - The adapter factory or adapter class to intercept\n * @param behavior - Source and/or destination-role handlers\n * @returns A handle with `calls` for assertions and an internal `override`\n *\n * @example\n * ```ts\n * // Factory form (preferred for single-role factories)\n * import { http, mail } from \"@routecraft/routecraft\";\n * import { mockAdapter, testContext } from \"@routecraft/testing\";\n *\n * const httpMock = mockAdapter(http, {\n * send: async () => ({ status: 200, body: { ok: true } }),\n * });\n *\n * const mailMock = mockAdapter(mail, {\n * source: [{ uid: 1, from: \"a@b\", subject: \"hi\", ... }],\n * send: async () => ({ messageId: \"<fake>\" }),\n * });\n *\n * // Class form (works for any adapter, including third-party ones)\n * import { SomeAdapterClass } from \"third-party-adapter\";\n *\n * const thirdPartyMock = mockAdapter(SomeAdapterClass, {\n * send: async () => ({ ok: true }),\n * });\n *\n * const t = await testContext()\n * .override(httpMock)\n * .override(mailMock)\n * .override(thirdPartyMock)\n * .routes(route)\n * .build();\n * await t.test();\n * ```\n */\nexport function mockAdapter<\n T extends\n | ((...args: never[]) => unknown)\n | (new (...args: never[]) => unknown),\n M = InferAdapterMessage<T>,\n>(target: T, behavior: MockAdapterBehavior<M>): AdapterMock {\n const override: AdapterOverride = {\n target,\n calls: { source: [], send: [] },\n };\n if (behavior.source !== undefined) {\n override.source = behavior.source as SourceOverrideBehavior;\n }\n if (behavior.send !== undefined) {\n override.send = behavior.send;\n }\n return {\n [ADAPTER_MOCK_BRAND]: true,\n override,\n get calls() {\n // Snapshot the live arrays so the `readonly` contract on AdapterMock.calls\n // is honoured at runtime (users cannot mutate the recorded calls via\n // the returned reference).\n return {\n source: [...override.calls.source],\n send: [...override.calls.send],\n };\n },\n };\n}\n","import { vi } from \"vitest\";\nimport type {\n CraftContext,\n CraftConfig,\n StoreRegistry,\n EventName,\n EventHandler,\n RouteDefinition,\n RouteBuilder,\n AdapterOverride,\n} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n CraftClient,\n isRoutecraftError,\n RoutecraftError,\n rcError,\n logger,\n RC_ADAPTER_OVERRIDES,\n} from \"@routecraft/routecraft\";\nimport type { SpyLogger } from \"./spy-logger\";\nimport { createSpyLogger, createNoopSpyLogger } from \"./spy-logger\";\nimport { isAdapterMock, type AdapterMock } from \"./mock-adapter\";\n\nconst DEFAULT_ROUTES_READY_TIMEOUT_MS = 200;\n\nfunction describeOverrideTarget(target: unknown): string {\n if (typeof target === \"function\" && typeof target.name === \"string\") {\n const kind =\n /^[A-Z]/.test(target.name) && target.prototype !== undefined\n ? \"class\"\n : \"factory\";\n return `${kind} ${target.name || \"<anonymous>\"}`;\n }\n return \"target\";\n}\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 /** Client for dispatching messages to direct endpoints in tests. */\n readonly client: CraftClient;\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 client: CraftClient,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.client = client;\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 const pushError = (err: unknown) => {\n this.errors.push(\n isRoutecraftError(err)\n ? (err as RoutecraftError)\n : rcError(\"RC9901\", err),\n );\n };\n ctx.on(\"context:error\", (payload) => {\n pushError(payload.details.error);\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(\n \"route:*:started\" as EventName,\n (() => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n resolve();\n }\n }) as EventHandler<EventName>,\n );\n const offError = ctx.on(\"context: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(\n \"route:*:started\" as EventName,\n (() => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n }) as EventHandler<EventName>,\n );\n const offError = ctx.on(\"context: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/**\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 private adapterOverrides: AdapterOverride[] = [];\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 /**\n * Register an adapter mock. At route execution time, calls to adapters\n * produced by the same factory are routed through the mock's handlers\n * instead of invoking the real adapter. Accepts either the handle returned\n * by `mockAdapter()` or a raw `AdapterOverride`.\n *\n * @experimental\n */\n override(mock: AdapterMock | AdapterOverride): this {\n const entry: AdapterOverride = isAdapterMock(mock) ? mock.override : mock;\n // Fail fast if two overrides target the same factory/class. The framework\n // uses first-match semantics at execution time, so silently accepting a\n // duplicate would mean the second mock's assertions always see zero calls\n // and the user has no signal that their new override is being shadowed.\n const duplicate = this.adapterOverrides.find(\n (o) => o.target === entry.target,\n );\n if (duplicate !== undefined) {\n const name = describeOverrideTarget(entry.target);\n throw new Error(\n `testContext().override(): duplicate override for ${name}. Each target may only be registered once; remove the redundant override() call.`,\n );\n }\n this.adapterOverrides.push(entry);\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 { context: ctx, client } = await this.builder.build();\n\n // Install registered adapter overrides onto the context store so that\n // ToStep / EnrichStep / Route source can resolve them at execution time.\n if (this.adapterOverrides.length > 0) {\n const existing = ctx.getStore(RC_ADAPTER_OVERRIDES) ?? [];\n ctx.setStore(RC_ADAPTER_OVERRIDES, [\n ...existing,\n ...this.adapterOverrides,\n ]);\n }\n\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, client, 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 * @experimental\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 * @experimental\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// Adapter mocking API\nexport {\n mockAdapter,\n type AdapterMock,\n type MockAdapterBehavior,\n} from \"./mock-adapter\";\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,4 +1,4 @@
1
- import { CraftContext, CraftClient, RoutecraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Source, Destination, Processor, Exchange } from '@routecraft/routecraft';
1
+ import { AdapterOverride, AdapterSourceCall, AdapterSendCall, SourceOverrideBehavior, SendOverrideHandler, Source, CraftContext, CraftClient, RoutecraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Destination, Processor, Exchange } from '@routecraft/routecraft';
2
2
  import { vi } from 'vitest';
3
3
 
4
4
  /**
@@ -20,6 +20,119 @@ declare function createSpyLogger(): SpyLogger;
20
20
  /** @beta */
21
21
  declare function createNoopSpyLogger(): SpyLogger;
22
22
 
23
+ /**
24
+ * Brand symbol stamped on the handles returned by `mockAdapter()`.
25
+ *
26
+ * `testContext().override()` uses this symbol to distinguish a mock handle
27
+ * from a raw `AdapterOverride` (both objects carry a `calls` field, so a
28
+ * structural check on a non-branded key would be fragile).
29
+ *
30
+ * Use `Symbol.for` so cross-realm / multi-package-load equality holds.
31
+ *
32
+ * @internal
33
+ */
34
+ declare const ADAPTER_MOCK_BRAND: unique symbol;
35
+ /**
36
+ * Extract the message type `M` from an adapter factory or adapter class,
37
+ * so `mockAdapter(target, { source: [...] })` can check fixtures against
38
+ * the real adapter shape. Falls back to `unknown` when `target` has no
39
+ * inferable Source role (e.g. destination-only factories, or overloaded
40
+ * factories where TypeScript cannot pick the source overload).
41
+ */
42
+ type InferAdapterMessage<T> = T extends new (...args: never[]) => infer I ? I extends Source<infer M> ? M : unknown : T extends (...args: never[]) => infer R ? R extends Source<infer M> ? M : unknown : unknown;
43
+ /**
44
+ * Behaviour description for a mock adapter. A mock may stub the source side,
45
+ * the destination side, or both. The framework picks the matching behaviour
46
+ * based on the call site's role in the route.
47
+ *
48
+ * @experimental
49
+ */
50
+ interface MockAdapterBehavior<M = unknown> {
51
+ /**
52
+ * Source-role behaviour. Used when the adapter is the `.from()` of a route.
53
+ * Pass an array of fixtures, an async iterable, or a callable that receives
54
+ * the construction args and returns the stream to emit.
55
+ */
56
+ source?: SourceOverrideBehavior<M>;
57
+ /**
58
+ * Destination-role behaviour. Used when the adapter is passed to `.to()`,
59
+ * `.enrich()`, or `.tap()`. Receives the exchange and a meta object with
60
+ * the construction args; returning a value replaces the body upstream.
61
+ */
62
+ send?: SendOverrideHandler;
63
+ }
64
+ /**
65
+ * Handle returned by `mockAdapter(factory, behaviour)`. Carries the resolved
66
+ * override the framework should install on the context, plus `calls` for
67
+ * assertions.
68
+ *
69
+ * @experimental
70
+ */
71
+ interface AdapterMock {
72
+ /** Brand used by `testContext().override()` to discriminate handles from raw overrides. */
73
+ readonly [ADAPTER_MOCK_BRAND]: true;
74
+ readonly override: AdapterOverride;
75
+ /**
76
+ * Recorded calls, populated as the route runs. Assert on these after
77
+ * awaiting `t.test()`.
78
+ */
79
+ readonly calls: {
80
+ source: readonly AdapterSourceCall[];
81
+ send: readonly AdapterSendCall[];
82
+ };
83
+ }
84
+ /**
85
+ * Create a mock for an adapter. The `target` may be either:
86
+ *
87
+ * - An adapter factory (e.g. `mail`, `http`, `mcp`). The mock matches every
88
+ * adapter instance produced by that factory. Requires the factory to stamp
89
+ * its adapters via `tagAdapter()`.
90
+ * - An adapter class (e.g. `MailSourceAdapter`, `HttpDestinationAdapter`).
91
+ * The mock matches any adapter whose `constructor === target`. Works for
92
+ * every adapter without opt-in tagging, including third-party ones.
93
+ *
94
+ * Pass the result to `testContext().override(mock)` and run the route
95
+ * under test as-is; the framework invokes the mock's `source` / `send`
96
+ * handlers in place of the real adapter at every matching call site.
97
+ *
98
+ * @experimental
99
+ * @param target - The adapter factory or adapter class to intercept
100
+ * @param behavior - Source and/or destination-role handlers
101
+ * @returns A handle with `calls` for assertions and an internal `override`
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * // Factory form (preferred for single-role factories)
106
+ * import { http, mail } from "@routecraft/routecraft";
107
+ * import { mockAdapter, testContext } from "@routecraft/testing";
108
+ *
109
+ * const httpMock = mockAdapter(http, {
110
+ * send: async () => ({ status: 200, body: { ok: true } }),
111
+ * });
112
+ *
113
+ * const mailMock = mockAdapter(mail, {
114
+ * source: [{ uid: 1, from: "a@b", subject: "hi", ... }],
115
+ * send: async () => ({ messageId: "<fake>" }),
116
+ * });
117
+ *
118
+ * // Class form (works for any adapter, including third-party ones)
119
+ * import { SomeAdapterClass } from "third-party-adapter";
120
+ *
121
+ * const thirdPartyMock = mockAdapter(SomeAdapterClass, {
122
+ * send: async () => ({ ok: true }),
123
+ * });
124
+ *
125
+ * const t = await testContext()
126
+ * .override(httpMock)
127
+ * .override(mailMock)
128
+ * .override(thirdPartyMock)
129
+ * .routes(route)
130
+ * .build();
131
+ * await t.test();
132
+ * ```
133
+ */
134
+ declare function mockAdapter<T extends ((...args: never[]) => unknown) | (new (...args: never[]) => unknown), M = InferAdapterMessage<T>>(target: T, behavior: MockAdapterBehavior<M>): AdapterMock;
135
+
23
136
  interface TestContextOptions {
24
137
  /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */
25
138
  routesReadyTimeoutMs?: number;
@@ -81,8 +194,18 @@ declare class TestContext {
81
194
  declare class TestContextBuilder {
82
195
  private builder;
83
196
  private routesReadyTimeoutMs;
197
+ private adapterOverrides;
84
198
  /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */
85
199
  routesReadyTimeout(ms: number): this;
200
+ /**
201
+ * Register an adapter mock. At route execution time, calls to adapters
202
+ * produced by the same factory are routed through the mock's handlers
203
+ * instead of invoking the real adapter. Accepts either the handle returned
204
+ * by `mockAdapter()` or a raw `AdapterOverride`.
205
+ *
206
+ * @experimental
207
+ */
208
+ override(mock: AdapterMock | AdapterOverride): this;
86
209
  with(config: CraftConfig): this;
87
210
  on<K extends EventName>(event: K, handler: EventHandler<K>): this;
88
211
  once<K extends EventName>(event: K, handler: EventHandler<K>): this;
@@ -195,4 +318,4 @@ interface FixtureWithName {
195
318
  */
196
319
  declare function fixtureEach<T extends FixtureWithName>(path: string, run: (entry: T) => void | Promise<void>): void;
197
320
 
198
- 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 };
321
+ export { type AdapterMock, type FixtureWithName, type MockAdapterBehavior, type PseudoAdapter, type PseudoFactory, type PseudoKeyedFactory, type PseudoKeyedOptions, type PseudoOptions, type SpyAdapter, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestOptions, createNoopSpyLogger, createSpyLogger, fixture, fixtureEach, mockAdapter, pseudo, spy, testContext };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CraftContext, CraftClient, RoutecraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Source, Destination, Processor, Exchange } from '@routecraft/routecraft';
1
+ import { AdapterOverride, AdapterSourceCall, AdapterSendCall, SourceOverrideBehavior, SendOverrideHandler, Source, CraftContext, CraftClient, RoutecraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Destination, Processor, Exchange } from '@routecraft/routecraft';
2
2
  import { vi } from 'vitest';
3
3
 
4
4
  /**
@@ -20,6 +20,119 @@ declare function createSpyLogger(): SpyLogger;
20
20
  /** @beta */
21
21
  declare function createNoopSpyLogger(): SpyLogger;
22
22
 
23
+ /**
24
+ * Brand symbol stamped on the handles returned by `mockAdapter()`.
25
+ *
26
+ * `testContext().override()` uses this symbol to distinguish a mock handle
27
+ * from a raw `AdapterOverride` (both objects carry a `calls` field, so a
28
+ * structural check on a non-branded key would be fragile).
29
+ *
30
+ * Use `Symbol.for` so cross-realm / multi-package-load equality holds.
31
+ *
32
+ * @internal
33
+ */
34
+ declare const ADAPTER_MOCK_BRAND: unique symbol;
35
+ /**
36
+ * Extract the message type `M` from an adapter factory or adapter class,
37
+ * so `mockAdapter(target, { source: [...] })` can check fixtures against
38
+ * the real adapter shape. Falls back to `unknown` when `target` has no
39
+ * inferable Source role (e.g. destination-only factories, or overloaded
40
+ * factories where TypeScript cannot pick the source overload).
41
+ */
42
+ type InferAdapterMessage<T> = T extends new (...args: never[]) => infer I ? I extends Source<infer M> ? M : unknown : T extends (...args: never[]) => infer R ? R extends Source<infer M> ? M : unknown : unknown;
43
+ /**
44
+ * Behaviour description for a mock adapter. A mock may stub the source side,
45
+ * the destination side, or both. The framework picks the matching behaviour
46
+ * based on the call site's role in the route.
47
+ *
48
+ * @experimental
49
+ */
50
+ interface MockAdapterBehavior<M = unknown> {
51
+ /**
52
+ * Source-role behaviour. Used when the adapter is the `.from()` of a route.
53
+ * Pass an array of fixtures, an async iterable, or a callable that receives
54
+ * the construction args and returns the stream to emit.
55
+ */
56
+ source?: SourceOverrideBehavior<M>;
57
+ /**
58
+ * Destination-role behaviour. Used when the adapter is passed to `.to()`,
59
+ * `.enrich()`, or `.tap()`. Receives the exchange and a meta object with
60
+ * the construction args; returning a value replaces the body upstream.
61
+ */
62
+ send?: SendOverrideHandler;
63
+ }
64
+ /**
65
+ * Handle returned by `mockAdapter(factory, behaviour)`. Carries the resolved
66
+ * override the framework should install on the context, plus `calls` for
67
+ * assertions.
68
+ *
69
+ * @experimental
70
+ */
71
+ interface AdapterMock {
72
+ /** Brand used by `testContext().override()` to discriminate handles from raw overrides. */
73
+ readonly [ADAPTER_MOCK_BRAND]: true;
74
+ readonly override: AdapterOverride;
75
+ /**
76
+ * Recorded calls, populated as the route runs. Assert on these after
77
+ * awaiting `t.test()`.
78
+ */
79
+ readonly calls: {
80
+ source: readonly AdapterSourceCall[];
81
+ send: readonly AdapterSendCall[];
82
+ };
83
+ }
84
+ /**
85
+ * Create a mock for an adapter. The `target` may be either:
86
+ *
87
+ * - An adapter factory (e.g. `mail`, `http`, `mcp`). The mock matches every
88
+ * adapter instance produced by that factory. Requires the factory to stamp
89
+ * its adapters via `tagAdapter()`.
90
+ * - An adapter class (e.g. `MailSourceAdapter`, `HttpDestinationAdapter`).
91
+ * The mock matches any adapter whose `constructor === target`. Works for
92
+ * every adapter without opt-in tagging, including third-party ones.
93
+ *
94
+ * Pass the result to `testContext().override(mock)` and run the route
95
+ * under test as-is; the framework invokes the mock's `source` / `send`
96
+ * handlers in place of the real adapter at every matching call site.
97
+ *
98
+ * @experimental
99
+ * @param target - The adapter factory or adapter class to intercept
100
+ * @param behavior - Source and/or destination-role handlers
101
+ * @returns A handle with `calls` for assertions and an internal `override`
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * // Factory form (preferred for single-role factories)
106
+ * import { http, mail } from "@routecraft/routecraft";
107
+ * import { mockAdapter, testContext } from "@routecraft/testing";
108
+ *
109
+ * const httpMock = mockAdapter(http, {
110
+ * send: async () => ({ status: 200, body: { ok: true } }),
111
+ * });
112
+ *
113
+ * const mailMock = mockAdapter(mail, {
114
+ * source: [{ uid: 1, from: "a@b", subject: "hi", ... }],
115
+ * send: async () => ({ messageId: "<fake>" }),
116
+ * });
117
+ *
118
+ * // Class form (works for any adapter, including third-party ones)
119
+ * import { SomeAdapterClass } from "third-party-adapter";
120
+ *
121
+ * const thirdPartyMock = mockAdapter(SomeAdapterClass, {
122
+ * send: async () => ({ ok: true }),
123
+ * });
124
+ *
125
+ * const t = await testContext()
126
+ * .override(httpMock)
127
+ * .override(mailMock)
128
+ * .override(thirdPartyMock)
129
+ * .routes(route)
130
+ * .build();
131
+ * await t.test();
132
+ * ```
133
+ */
134
+ declare function mockAdapter<T extends ((...args: never[]) => unknown) | (new (...args: never[]) => unknown), M = InferAdapterMessage<T>>(target: T, behavior: MockAdapterBehavior<M>): AdapterMock;
135
+
23
136
  interface TestContextOptions {
24
137
  /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */
25
138
  routesReadyTimeoutMs?: number;
@@ -81,8 +194,18 @@ declare class TestContext {
81
194
  declare class TestContextBuilder {
82
195
  private builder;
83
196
  private routesReadyTimeoutMs;
197
+ private adapterOverrides;
84
198
  /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */
85
199
  routesReadyTimeout(ms: number): this;
200
+ /**
201
+ * Register an adapter mock. At route execution time, calls to adapters
202
+ * produced by the same factory are routed through the mock's handlers
203
+ * instead of invoking the real adapter. Accepts either the handle returned
204
+ * by `mockAdapter()` or a raw `AdapterOverride`.
205
+ *
206
+ * @experimental
207
+ */
208
+ override(mock: AdapterMock | AdapterOverride): this;
86
209
  with(config: CraftConfig): this;
87
210
  on<K extends EventName>(event: K, handler: EventHandler<K>): this;
88
211
  once<K extends EventName>(event: K, handler: EventHandler<K>): this;
@@ -195,4 +318,4 @@ interface FixtureWithName {
195
318
  */
196
319
  declare function fixtureEach<T extends FixtureWithName>(path: string, run: (entry: T) => void | Promise<void>): void;
197
320
 
198
- 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 };
321
+ export { type AdapterMock, type FixtureWithName, type MockAdapterBehavior, type PseudoAdapter, type PseudoFactory, type PseudoKeyedFactory, type PseudoKeyedOptions, type PseudoOptions, type SpyAdapter, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestOptions, createNoopSpyLogger, createSpyLogger, fixture, fixtureEach, mockAdapter, pseudo, spy, testContext };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import {readFileSync}from'fs';import {vi,test}from'vitest';import {ContextBuilder,logger,HeadersKeys,isRoutecraftError,rcError}from'@routecraft/routecraft';function h(){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 O=200,f=class{ctx;client;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;loggerChildRestored=false;startedPromise;constructor(e,r,o){this.ctx=e,this.client=r,this.logger=o?.spyLogger??R(),o?.restoreLoggerChild&&(this.restoreLoggerChild=o.restoreLoggerChild),this.routesReadyTimeoutMs=o?.routesReadyTimeoutMs??O;let s=n=>{this.errors.push(isRoutecraftError(n)?n:rcError("RC9901",n));};e.on("context:error",n=>{s(n.details.error);});}async startAndWaitReady(){let e=this.ctx,r=e.getRoutes().length,o=r===0?Promise.resolve():new Promise((s,n)=>{let d=0,i=false,c=setTimeout(()=>{i||(i=true,a(),p(),n(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),a=e.on("route:*:started",(()=>{i||(d++,d>=r&&(i=true,clearTimeout(c),a(),p(),s()));})),p=e.on("context:error",g=>{i||(i=true,clearTimeout(c),a(),p(),n(g.details.error));});});this.startedPromise=e.start(),await Promise.all([this.startedPromise,o]);}async test(e){let r=this.ctx,o=r.getRoutes().length,s=o===0?Promise.resolve():new Promise((d,i)=>{let c=0,a=false,p=setTimeout(()=>{a||(m(),i(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),g=r.on("route:*:started",(()=>{a||(c++,c>=o&&(m(),d()));})),T=r.on("context:error",w=>{a||(m(),i(w.details.error));});function m(){a||(a=true,g(),T(),p!==void 0&&(clearTimeout(p),p=void 0));}}),n=r.start();try{await s;let d=e?.delayBeforeDrainMs??0;d>0&&await new Promise(i=>setTimeout(i,d)),await r.drain();}finally{try{await r.stop(),await n;}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);}},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=h(),r=logger.child.bind(logger);logger.child=vi.fn(()=>e);let{context:o,client:s}=await this.builder.build(),n={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{logger.child=r;}};return new f(o,s,n)}};function b(){return new l}function v(t,e){let r=()=>{throw new Error(`Pseudo adapter "${t}" is not implemented. Replace with a real adapter.`)},o=()=>Promise.resolve(void 0),s=d=>d,n=(d,i,c,a)=>(a?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${t}`,subscribe:e==="noop"?n:r,send:e==="noop"?o:r,process:e==="noop"?s:r}}function L(t="pseudo",e){let r=e?.runtime??"throw";return e&&"args"in e&&e.args==="keyed"?(s,n)=>v(t,r):s=>v(t,r)}function x(){return {received:[],calls:{send:0,process:0,enrich:0}}}function F(){let t=x();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 M(t){return JSON.parse(readFileSync(t,"utf-8"))}function V(t,e){let r=M(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,h as createSpyLogger,M as fixture,V as fixtureEach,L as pseudo,F as spy,b as testContext};//# sourceMappingURL=index.js.map
1
+ import {readFileSync}from'fs';import {vi,test}from'vitest';import {ContextBuilder,logger,RC_ADAPTER_OVERRIDES,HeadersKeys,isRoutecraftError,rcError}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 v(){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 R=Symbol.for("routecraft.testing.adapter-mock");function x(r){return typeof r=="object"&&r!==null&&r[R]===true}function A(r,e){let t={target:r,calls:{source:[],send:[]}};return e.source!==void 0&&(t.source=e.source),e.send!==void 0&&(t.send=e.send),{[R]:true,override:t,get calls(){return {source:[...t.calls.source],send:[...t.calls.send]}}}}var b=200;function L(r){return typeof r=="function"&&typeof r.name=="string"?`${/^[A-Z]/.test(r.name)&&r.prototype!==void 0?"class":"factory"} ${r.name||"<anonymous>"}`:"target"}var f=class{ctx;client;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;loggerChildRestored=false;startedPromise;constructor(e,t,o){this.ctx=e,this.client=t,this.logger=o?.spyLogger??v(),o?.restoreLoggerChild&&(this.restoreLoggerChild=o.restoreLoggerChild),this.routesReadyTimeoutMs=o?.routesReadyTimeoutMs??b;let n=s=>{this.errors.push(isRoutecraftError(s)?s:rcError("RC9901",s));};e.on("context:error",s=>{n(s.details.error);});}async startAndWaitReady(){let e=this.ctx,t=e.getRoutes().length,o=t===0?Promise.resolve():new Promise((n,s)=>{let i=0,a=false,c=setTimeout(()=>{a||(a=true,d(),p(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),d=e.on("route:*:started",(()=>{a||(i++,i>=t&&(a=true,clearTimeout(c),d(),p(),n()));})),p=e.on("context:error",g=>{a||(a=true,clearTimeout(c),d(),p(),s(g.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((i,a)=>{let c=0,d=false,p=setTimeout(()=>{d||(m(),a(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),g=t.on("route:*:started",(()=>{d||(c++,c>=o&&(m(),i()));})),O=t.on("context:error",P=>{d||(m(),a(P.details.error));});function m(){d||(d=true,g(),O(),p!==void 0&&(clearTimeout(p),p=void 0));}}),s=t.start();try{await n;let i=e?.delayBeforeDrainMs??0;i>0&&await new Promise(a=>setTimeout(a,i)),await t.drain();}finally{try{await t.stop(),await s;}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);}},y=class{builder=new ContextBuilder;routesReadyTimeoutMs;adapterOverrides=[];routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}override(e){let t=x(e)?e.override:e;if(this.adapterOverrides.find(n=>n.target===t.target)!==void 0){let n=L(t.target);throw new Error(`testContext().override(): duplicate override for ${n}. Each target may only be registered once; remove the redundant override() call.`)}return this.adapterOverrides.push(t),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{context:o,client:n}=await this.builder.build();if(this.adapterOverrides.length>0){let i=o.getStore(RC_ADAPTER_OVERRIDES)??[];o.setStore(RC_ADAPTER_OVERRIDES,[...i,...this.adapterOverrides]);}let s={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{logger.child=t;}};return new f(o,n,s)}};function K(){return new y}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=i=>i,s=(i,a,c,d)=>(d?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${r}`,subscribe:e==="noop"?s: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,s)=>w(r,t):n=>w(r,t)}function S(){return {received:[],calls:{send:0,process:0,enrich:0}}}function N(){let r=S();return {adapterId:"routecraft.adapter.spy",received:r.received,calls:r.calls,send(e){r.received.push(e),e.headers?.[HeadersKeys.OPERATION]==="enrich"?r.calls.enrich++:r.calls.send++;},process(e){return r.received.push(e),r.calls.process++,e},reset(){r.received.length=0,r.calls.send=0,r.calls.process=0,r.calls.enrich=0;},lastReceived(){if(r.received.length===0)throw new Error("SpyAdapter: no exchanges recorded");return r.received[r.received.length-1]},receivedBodies(){return r.received.map(e=>e.body)}}}function _(r){return JSON.parse(readFileSync(r,"utf-8"))}function oe(r,e){let t=_(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{f as TestContext,y as TestContextBuilder,v as createNoopSpyLogger,h as createSpyLogger,_ as fixture,oe as fixtureEach,A as mockAdapter,F as pseudo,N as spy,K 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/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","client","options","pushError","err","isRoutecraftError","rcError","payload","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","createAdapter","name","runtime","fail","noopSend","noopProcess","exchange","noopSubscribe","_context","_handler","_abortController","onReady","pseudo","opts","createSpyState","state","HeadersKeys","fixture","path","readFileSync","fixtureEach","run","entries","entry","test"],"mappings":"4JAkBO,SAASA,GAA6B,CAC3C,IAAMC,CAAAA,CAAiB,CACrB,IAAA,CAAMC,EAAAA,CAAG,IAAG,CACZ,KAAA,CAAOA,EAAAA,CAAG,EAAA,EAAG,CACb,IAAA,CAAMA,GAAG,EAAA,EAAG,CACZ,KAAA,CAAOA,EAAAA,CAAG,EAAA,EAAG,CACb,KAAA,CAAOA,EAAAA,CAAG,EAAA,EAAG,CACb,KAAA,CAAOA,EAAAA,CAAG,EAAA,EAAG,CACb,MAAOA,EAAAA,CAAG,EAAA,EACZ,CAAA,CACA,OAAAD,CAAAA,CAAI,MAAM,kBAAA,CAAmB,IAAMA,CAAG,CAAA,CAC/BA,CACT,CAGO,SAASE,CAAAA,EAAiC,CAC/C,IAAMC,CAAAA,CAAOF,EAAAA,CAAG,EAAA,GACVG,CAAAA,CAAUH,EAAAA,CAAG,EAAA,EAAG,CAChBI,CAAAA,CAAwB,CAC5B,KAAMF,CAAAA,CACN,KAAA,CAAOA,CAAAA,CACP,IAAA,CAAMA,CAAAA,CACN,KAAA,CAAOA,EACP,KAAA,CAAOA,CAAAA,CACP,KAAA,CAAOA,CAAAA,CACP,KAAA,CAAOC,CACT,EACA,OAAAA,CAAAA,CAAQ,kBAAA,CAAmB,IAAMC,CAAU,CAAA,CACpCA,CACT,CC1BA,IAAMC,CAAAA,CAAkC,GAAA,CAwB3BC,CAAAA,CAAN,KAAkB,CACd,IAEA,MAAA,CAEA,MAAA,CACA,MAAA,CAA4B,EAAC,CACrB,oBAAA,CAET,mBACA,mBAAA,CAAsB,KAAA,CACtB,cAAA,CAER,WAAA,CACEC,CAAAA,CACAC,CAAAA,CACAC,EAIA,CACA,IAAA,CAAK,GAAA,CAAMF,CAAAA,CACX,IAAA,CAAK,MAAA,CAASC,EACd,IAAA,CAAK,MAAA,CAASC,CAAAA,EAAS,SAAA,EAAaR,CAAAA,EAAoB,CACpDQ,GAAS,kBAAA,GACX,IAAA,CAAK,kBAAA,CAAqBA,CAAAA,CAAQ,kBAAA,CAAA,CACpC,IAAA,CAAK,qBACHA,CAAAA,EAAS,oBAAA,EAAwBJ,CAAAA,CACnC,IAAMK,CAAAA,CAAaC,CAAAA,EAAiB,CAClC,IAAA,CAAK,MAAA,CAAO,IAAA,CACVC,iBAAAA,CAAkBD,CAAG,CAAA,CAChBA,CAAAA,CACDE,OAAAA,CAAQ,QAAA,CAAUF,CAAG,CAC3B,EACF,CAAA,CACAJ,CAAAA,CAAI,GAAG,eAAA,CAAkBO,CAAAA,EAAY,CACnCJ,CAAAA,CAAUI,CAAAA,CAAQ,OAAA,CAAQ,KAAK,EACjC,CAAC,EACH,CAMA,MAAM,iBAAA,EAAmC,CACvC,IAAMP,CAAAA,CAAM,IAAA,CAAK,GAAA,CACXQ,CAAAA,CAAQR,CAAAA,CAAI,WAAU,CAAE,MAAA,CACxBS,CAAAA,CACJD,CAAAA,GAAU,CAAA,CACN,OAAA,CAAQ,SAAQ,CAChB,IAAI,OAAA,CAAc,CAACE,CAAAA,CAASC,CAAAA,GAAW,CACrC,IAAIC,CAAAA,CAAQ,CAAA,CACRC,CAAAA,CAAU,KAAA,CACRC,CAAAA,CAAY,WAAW,IAAM,CAC7BD,CAAAA,GACJA,CAAAA,CAAU,IAAA,CACVE,CAAAA,EAAgB,CAChBC,CAAAA,EAAS,CACTL,CAAAA,CAAO,IAAI,KAAA,CAAM,qCAAqC,CAAC,GACzD,CAAA,CAAG,IAAA,CAAK,oBAAoB,CAAA,CAEtBI,CAAAA,CAAkBf,CAAAA,CAAI,GAC1B,iBAAA,EACC,IAAM,CACDa,CAAAA,GACJD,CAAAA,EAAAA,CACIA,CAAAA,EAASJ,IACXK,CAAAA,CAAU,IAAA,CACV,YAAA,CAAaC,CAAS,CAAA,CACtBC,CAAAA,GACAC,CAAAA,EAAS,CACTN,CAAAA,EAAQ,CAAA,EAEZ,CAAA,EACF,CACMM,EAAWhB,CAAAA,CAAI,EAAA,CAAG,eAAA,CAAkBO,CAAAA,EAAY,CAChDM,CAAAA,GACJA,EAAU,IAAA,CACV,YAAA,CAAaC,CAAS,CAAA,CACtBC,CAAAA,EAAgB,CAChBC,GAAS,CACTL,CAAAA,CAAOJ,CAAAA,CAAQ,OAAA,CAAQ,KAAK,CAAA,EAC9B,CAAC,EACH,CAAC,CAAA,CACP,IAAA,CAAK,cAAA,CAAiBP,CAAAA,CAAI,KAAA,GAC1B,MAAM,OAAA,CAAQ,GAAA,CAAI,CAAC,IAAA,CAAK,cAAA,CAAgBS,CAAQ,CAAC,EACnD,CASA,MAAM,IAAA,CAAKP,CAAAA,CAAsC,CAC/C,IAAMF,CAAAA,CAAM,IAAA,CAAK,GAAA,CACXQ,CAAAA,CAAQR,CAAAA,CAAI,WAAU,CAAE,MAAA,CACxBS,CAAAA,CACJD,CAAAA,GAAU,CAAA,CACN,OAAA,CAAQ,SAAQ,CAChB,IAAI,OAAA,CAAc,CAACE,CAAAA,CAASC,CAAAA,GAAW,CACrC,IAAIC,CAAAA,CAAQ,CAAA,CACRC,CAAAA,CAAU,KAAA,CACVC,CAAAA,CACF,WAAW,IAAM,CACXD,CAAAA,GACJI,CAAAA,EAAQ,CACRN,CAAAA,CAAO,IAAI,KAAA,CAAM,qCAAqC,CAAC,CAAA,EACzD,CAAA,CAAG,IAAA,CAAK,oBAAoB,EAExBI,CAAAA,CAAkBf,CAAAA,CAAI,EAAA,CAC1B,iBAAA,EACC,IAAM,CACDa,IACJD,CAAAA,EAAAA,CACIA,CAAAA,EAASJ,CAAAA,GACXS,CAAAA,EAAQ,CACRP,CAAAA,KAEJ,CAAA,EACF,CACMM,CAAAA,CAAWhB,CAAAA,CAAI,EAAA,CAAG,eAAA,CAAkBO,GAAY,CAChDM,CAAAA,GACJI,CAAAA,EAAQ,CACRN,CAAAA,CAAOJ,CAAAA,CAAQ,QAAQ,KAAK,CAAA,EAC9B,CAAC,CAAA,CAED,SAASU,CAAAA,EAAgB,CACnBJ,CAAAA,GACJA,CAAAA,CAAU,IAAA,CACVE,CAAAA,EAAgB,CAChBC,CAAAA,GACIF,CAAAA,GAAc,MAAA,GAChB,YAAA,CAAaA,CAAS,CAAA,CACtBA,CAAAA,CAAY,MAAA,CAAA,EAEhB,CACF,CAAC,CAAA,CACDI,CAAAA,CAAUlB,CAAAA,CAAI,KAAA,EAAM,CAC1B,GAAI,CACF,MAAMS,CAAAA,CACN,IAAMU,CAAAA,CAAUjB,CAAAA,EAAS,oBAAsB,CAAA,CAC3CiB,CAAAA,CAAU,CAAA,EACZ,MAAM,IAAI,OAAA,CAAST,GAAY,UAAA,CAAWA,CAAAA,CAASS,CAAO,CAAC,CAAA,CAE7D,MAAMnB,EAAI,KAAA,GACZ,CAAA,OAAE,CACA,GAAI,CACF,MAAMA,CAAAA,CAAI,IAAA,EAAK,CACf,MAAMkB,EACR,CAAA,OAAE,CACA,IAAA,CAAK,sBAAA,GACP,CACF,CACF,CAEA,OAAuB,CACrB,OAAO,IAAA,CAAK,GAAA,CAAI,KAAA,EAClB,CAEA,MAAM,IAAA,EAAsB,CAC1B,GAAI,CACF,MAAM,IAAA,CAAK,IAAI,IAAA,EAAK,CAChB,IAAA,CAAK,cAAA,GAAmB,KAAA,CAAA,EAC1B,MAAM,KAAK,eAEf,CAAA,OAAE,CACA,IAAA,CAAK,sBAAA,GACP,CACF,CAEQ,sBAAA,EAA+B,CACjC,IAAA,CAAK,mBAAA,GACT,IAAA,CAAK,sBAAqB,CAC1B,IAAA,CAAK,mBAAA,CAAsB,IAAA,EAC7B,CACF,CAAA,CAQaE,EAAN,KAAyB,CACtB,OAAA,CAAU,IAAIC,cAAAA,CACd,oBAAA,CAGR,mBAAmBC,CAAAA,CAAkB,CACnC,OAAA,IAAA,CAAK,oBAAA,CAAuBA,CAAAA,CACrB,IACT,CAEA,IAAA,CAAKC,CAAAA,CAA2B,CAC9B,OAAA,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAKA,CAAM,CAAA,CACjB,IACT,CAEA,EAAA,CAAwBC,CAAAA,CAAUC,CAAAA,CAAgC,CAChE,YAAK,OAAA,CAAQ,EAAA,CAAGD,CAAAA,CAAOC,CAAO,CAAA,CACvB,IACT,CAEA,IAAA,CAA0BD,CAAAA,CAAUC,CAAAA,CAAgC,CAClE,OAAA,IAAA,CAAK,OAAA,CAAQ,KAAKD,CAAAA,CAAOC,CAAO,CAAA,CACzB,IACT,CAEA,KAAA,CAAqCC,EAAQC,CAAAA,CAA+B,CAC1E,OAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAMD,CAAAA,CAAKC,CAAK,CAAA,CACtB,IACT,CAEA,MAAA,CACEC,CAAAA,CAKM,CACN,YAAK,OAAA,CAAQ,MAAA,CAAOA,CAAM,CAAA,CACnB,IACT,CAEA,MAAM,KAAA,EAA8B,CAClC,IAAMC,CAAAA,CAAYtC,CAAAA,EAAgB,CAC5BuC,CAAAA,CAAgBC,MAAAA,CAAO,KAAA,CAAM,IAAA,CAAKA,MAAM,CAAA,CAC9CA,MAAAA,CAAO,KAAA,CAAQtC,GAAG,EAAA,CAChB,IAAMoC,CACR,CAAA,CACA,GAAM,CAAE,QAAS7B,CAAAA,CAAK,MAAA,CAAAC,CAAO,CAAA,CAAI,MAAM,IAAA,CAAK,QAAQ,KAAA,EAAM,CACpDC,CAAAA,CAGF,CACF,GAAI,IAAA,CAAK,uBAAyB,MAAA,CAC9B,CAAE,oBAAA,CAAsB,IAAA,CAAK,oBAAqB,CAAA,CAClD,EAAC,CACL,SAAA,CAAA2B,CAAAA,CACA,kBAAA,CAAoB,IAAM,CACxBE,OAAO,KAAA,CAAQD,EACjB,CACF,CAAA,CACA,OAAO,IAAI/B,EAAYC,CAAAA,CAAKC,CAAAA,CAAQC,CAAO,CAC7C,CACF,EAWO,SAAS8B,CAAAA,EAAkC,CAChD,OAAO,IAAIZ,CACb,CC1RA,SAASa,EACPC,CAAAA,CACAC,CAAAA,CACkB,CAClB,IAAMC,CAAAA,CAAO,IAAa,CACxB,MAAM,IAAI,KAAA,CACR,CAAA,gBAAA,EAAmBF,CAAI,CAAA,kDAAA,CACzB,CACF,CAAA,CACMG,CAAAA,CAAW,IAAkB,OAAA,CAAQ,OAAA,CAAQ,MAAyB,EACtEC,CAAAA,CAAeC,CAAAA,EAA+BA,CAAAA,CAC9CC,CAAAA,CAAgB,CACpBC,CAAAA,CACAC,EACAC,CAAAA,CACAC,CAAAA,IAEAA,CAAAA,IAAU,CACH,OAAA,CAAQ,OAAA,IAOjB,OAAO,CACL,SAAA,CAAW,CAAA,0BAAA,EAA6BV,CAAI,CAAA,CAAA,CAC5C,UACEC,CAAAA,GAAY,MAAA,CACPK,CAAAA,CACAJ,CAAAA,CACP,IAAA,CAAMD,CAAAA,GAAY,MAAA,CAAUE,CAAAA,CAAuBD,CAAAA,CACnD,OAAA,CACED,CAAAA,GAAY,MAAA,CAAUG,CAAAA,CAA6BF,CACvD,CACF,CAkBO,SAASS,CAAAA,CAGdX,CAAAA,CAAO,QAAA,CACPhC,CAAAA,CACgD,CAChD,IAAMiC,CAAAA,CAAUjC,CAAAA,EAAS,OAAA,EAAW,OAAA,CAGpC,OAFgBA,GAAW,MAAA,GAAUA,CAAAA,EAAWA,CAAAA,CAAQ,IAAA,GAAS,OAAA,CAGxD,CAAcwB,EAAaoB,CAAAA,GAGzBb,CAAAA,CAAiBC,CAAAA,CAAMC,CAAO,CAAA,CAGpBW,CAAAA,EAEZb,EAAiBC,CAAAA,CAAMC,CAAO,CAEzC,CCnFO,SAASY,CAAAA,EAAiC,CAC/C,OAAO,CACL,SAAU,EAAC,CACX,KAAA,CAAO,CAAE,IAAA,CAAM,CAAA,CAAG,OAAA,CAAS,CAAA,CAAG,MAAA,CAAQ,CAAE,CAC1C,CACF,CCwCO,SAASvD,GAAkC,CAChD,IAAMwD,CAAAA,CAAQD,CAAAA,EAAkB,CAEhC,OAAO,CACL,SAAA,CAAW,wBAAA,CACX,QAAA,CAAUC,CAAAA,CAAM,QAAA,CAChB,KAAA,CAAOA,EAAM,KAAA,CAEb,IAAA,CAAKT,CAAAA,CAA6B,CAChCS,CAAAA,CAAM,QAAA,CAAS,KAAKT,CAAQ,CAAA,CAEVA,CAAAA,CAAS,OAAA,GAAUU,WAAAA,CAAY,SAAS,IACxC,QAAA,CAChBD,CAAAA,CAAM,KAAA,CAAM,MAAA,EAAA,CAEZA,CAAAA,CAAM,KAAA,CAAM,OAEhB,CAAA,CAEA,OAAA,CAAQT,CAAAA,CAAoC,CAC1C,OAAAS,CAAAA,CAAM,SAAS,IAAA,CAAKT,CAAQ,CAAA,CAC5BS,CAAAA,CAAM,KAAA,CAAM,OAAA,EAAA,CACLT,CACT,CAAA,CAEA,KAAA,EAAc,CACZS,CAAAA,CAAM,QAAA,CAAS,MAAA,CAAS,CAAA,CACxBA,EAAM,KAAA,CAAM,IAAA,CAAO,CAAA,CACnBA,CAAAA,CAAM,KAAA,CAAM,OAAA,CAAU,EACtBA,CAAAA,CAAM,KAAA,CAAM,MAAA,CAAS,EACvB,CAAA,CAEA,YAAA,EAA4B,CAC1B,GAAIA,CAAAA,CAAM,QAAA,CAAS,MAAA,GAAW,CAAA,CAC5B,MAAM,IAAI,KAAA,CAAM,mCAAmC,CAAA,CAErD,OAAOA,CAAAA,CAAM,QAAA,CAASA,EAAM,QAAA,CAAS,MAAA,CAAS,CAAC,CACjD,CAAA,CAEA,cAAA,EAAsB,CACpB,OAAOA,CAAAA,CAAM,QAAA,CAAS,GAAA,CAAK,CAAA,EAAM,CAAA,CAAE,IAAI,CACzC,CACF,CACF,CC9DO,SAASE,CAAAA,CAAqBC,CAAAA,CAAiB,CACpD,OAAO,IAAA,CAAK,KAAA,CAAMC,YAAAA,CAAaD,CAAAA,CAAM,OAAO,CAAC,CAC/C,CAeO,SAASE,CAAAA,CACdF,CAAAA,CACAG,CAAAA,CACM,CACN,IAAMC,CAAAA,CAAUL,CAAAA,CAAaC,CAAI,CAAA,CACjC,GAAI,CAAC,KAAA,CAAM,OAAA,CAAQI,CAAO,CAAA,CACxB,MAAM,IAAI,KAAA,CACR,CAAA,sCAAA,EAAyCJ,CAAI,CAAA,OAAA,EAAU,OAAOI,CAAO,CAAA,CACvE,CAAA,CAEF,QAAWC,CAAAA,IAASD,CAAAA,CAAS,CAC3B,GAAI,OAAOC,CAAAA,EAAO,MAAS,QAAA,CACzB,MAAM,IAAI,KAAA,CACR,CAAA,iEAAA,EAAoE,IAAA,CAAK,UAAUA,CAAK,CAAC,CAAA,CAC3F,CAAA,CAEFC,IAAAA,CAAKD,CAAAA,CAAM,IAAA,CAAM,IAAMF,CAAAA,CAAIE,CAAK,CAAC,EACnC,CACF","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} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n CraftClient,\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 /** Client for dispatching messages to direct endpoints in tests. */\n readonly client: CraftClient;\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 client: CraftClient,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.client = client;\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 const pushError = (err: unknown) => {\n this.errors.push(\n isRoutecraftError(err)\n ? (err as RoutecraftError)\n : rcError(\"RC9901\", err),\n );\n };\n ctx.on(\"context:error\", (payload) => {\n pushError(payload.details.error);\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(\n \"route:*:started\" as EventName,\n (() => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n resolve();\n }\n }) as EventHandler<EventName>,\n );\n const offError = ctx.on(\"context: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(\n \"route:*:started\" as EventName,\n (() => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n }) as EventHandler<EventName>,\n );\n const offError = ctx.on(\"context: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/**\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 { context: ctx, client } = 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, client, 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 * @experimental\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 * @experimental\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"]}
1
+ {"version":3,"sources":["../src/spy-logger.ts","../src/mock-adapter.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","ADAPTER_MOCK_BRAND","isAdapterMock","value","mockAdapter","target","behavior","override","DEFAULT_ROUTES_READY_TIMEOUT_MS","describeOverrideTarget","TestContext","ctx","client","options","pushError","err","isRoutecraftError","rcError","payload","total","allReady","resolve","reject","ready","settled","timeoutId","offRouteStarted","offError","cleanup","started","delayMs","TestContextBuilder","ContextBuilder","ms","mock","entry","o","name","config","event","handler","key","routes","spyLogger","originalChild","logger","existing","RC_ADAPTER_OVERRIDES","testContext","createAdapter","runtime","fail","noopSend","noopProcess","exchange","noopSubscribe","_context","_handler","_abortController","onReady","pseudo","opts","createSpyState","state","HeadersKeys","fixture","path","readFileSync","fixtureEach","run","entries","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 type {\n AdapterOverride,\n AdapterSendCall,\n AdapterSourceCall,\n SendOverrideHandler,\n Source,\n SourceOverrideBehavior,\n} from \"@routecraft/routecraft\";\n\n/**\n * Brand symbol stamped on the handles returned by `mockAdapter()`.\n *\n * `testContext().override()` uses this symbol to distinguish a mock handle\n * from a raw `AdapterOverride` (both objects carry a `calls` field, so a\n * structural check on a non-branded key would be fragile).\n *\n * Use `Symbol.for` so cross-realm / multi-package-load equality holds.\n *\n * @internal\n */\nexport const ADAPTER_MOCK_BRAND: unique symbol = Symbol.for(\n \"routecraft.testing.adapter-mock\",\n);\n\n/**\n * Extract the message type `M` from an adapter factory or adapter class,\n * so `mockAdapter(target, { source: [...] })` can check fixtures against\n * the real adapter shape. Falls back to `unknown` when `target` has no\n * inferable Source role (e.g. destination-only factories, or overloaded\n * factories where TypeScript cannot pick the source overload).\n */\ntype InferAdapterMessage<T> = T extends new (...args: never[]) => infer I\n ? I extends Source<infer M>\n ? M\n : unknown\n : T extends (...args: never[]) => infer R\n ? R extends Source<infer M>\n ? M\n : unknown\n : unknown;\n\n/**\n * Behaviour description for a mock adapter. A mock may stub the source side,\n * the destination side, or both. The framework picks the matching behaviour\n * based on the call site's role in the route.\n *\n * @experimental\n */\nexport interface MockAdapterBehavior<M = unknown> {\n /**\n * Source-role behaviour. Used when the adapter is the `.from()` of a route.\n * Pass an array of fixtures, an async iterable, or a callable that receives\n * the construction args and returns the stream to emit.\n */\n source?: SourceOverrideBehavior<M>;\n /**\n * Destination-role behaviour. Used when the adapter is passed to `.to()`,\n * `.enrich()`, or `.tap()`. Receives the exchange and a meta object with\n * the construction args; returning a value replaces the body upstream.\n */\n send?: SendOverrideHandler;\n}\n\n/**\n * Handle returned by `mockAdapter(factory, behaviour)`. Carries the resolved\n * override the framework should install on the context, plus `calls` for\n * assertions.\n *\n * @experimental\n */\nexport interface AdapterMock {\n /** Brand used by `testContext().override()` to discriminate handles from raw overrides. */\n readonly [ADAPTER_MOCK_BRAND]: true;\n readonly override: AdapterOverride;\n /**\n * Recorded calls, populated as the route runs. Assert on these after\n * awaiting `t.test()`.\n */\n readonly calls: {\n source: readonly AdapterSourceCall[];\n send: readonly AdapterSendCall[];\n };\n}\n\n/**\n * Type guard that distinguishes an `AdapterMock` (handle returned by\n * `mockAdapter()`) from a raw `AdapterOverride` value.\n *\n * @internal\n */\nexport function isAdapterMock(\n value: AdapterMock | AdapterOverride,\n): value is AdapterMock {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { [ADAPTER_MOCK_BRAND]?: unknown })[ADAPTER_MOCK_BRAND] === true\n );\n}\n\n/**\n * Create a mock for an adapter. The `target` may be either:\n *\n * - An adapter factory (e.g. `mail`, `http`, `mcp`). The mock matches every\n * adapter instance produced by that factory. Requires the factory to stamp\n * its adapters via `tagAdapter()`.\n * - An adapter class (e.g. `MailSourceAdapter`, `HttpDestinationAdapter`).\n * The mock matches any adapter whose `constructor === target`. Works for\n * every adapter without opt-in tagging, including third-party ones.\n *\n * Pass the result to `testContext().override(mock)` and run the route\n * under test as-is; the framework invokes the mock's `source` / `send`\n * handlers in place of the real adapter at every matching call site.\n *\n * @experimental\n * @param target - The adapter factory or adapter class to intercept\n * @param behavior - Source and/or destination-role handlers\n * @returns A handle with `calls` for assertions and an internal `override`\n *\n * @example\n * ```ts\n * // Factory form (preferred for single-role factories)\n * import { http, mail } from \"@routecraft/routecraft\";\n * import { mockAdapter, testContext } from \"@routecraft/testing\";\n *\n * const httpMock = mockAdapter(http, {\n * send: async () => ({ status: 200, body: { ok: true } }),\n * });\n *\n * const mailMock = mockAdapter(mail, {\n * source: [{ uid: 1, from: \"a@b\", subject: \"hi\", ... }],\n * send: async () => ({ messageId: \"<fake>\" }),\n * });\n *\n * // Class form (works for any adapter, including third-party ones)\n * import { SomeAdapterClass } from \"third-party-adapter\";\n *\n * const thirdPartyMock = mockAdapter(SomeAdapterClass, {\n * send: async () => ({ ok: true }),\n * });\n *\n * const t = await testContext()\n * .override(httpMock)\n * .override(mailMock)\n * .override(thirdPartyMock)\n * .routes(route)\n * .build();\n * await t.test();\n * ```\n */\nexport function mockAdapter<\n T extends\n | ((...args: never[]) => unknown)\n | (new (...args: never[]) => unknown),\n M = InferAdapterMessage<T>,\n>(target: T, behavior: MockAdapterBehavior<M>): AdapterMock {\n const override: AdapterOverride = {\n target,\n calls: { source: [], send: [] },\n };\n if (behavior.source !== undefined) {\n override.source = behavior.source as SourceOverrideBehavior;\n }\n if (behavior.send !== undefined) {\n override.send = behavior.send;\n }\n return {\n [ADAPTER_MOCK_BRAND]: true,\n override,\n get calls() {\n // Snapshot the live arrays so the `readonly` contract on AdapterMock.calls\n // is honoured at runtime (users cannot mutate the recorded calls via\n // the returned reference).\n return {\n source: [...override.calls.source],\n send: [...override.calls.send],\n };\n },\n };\n}\n","import { vi } from \"vitest\";\nimport type {\n CraftContext,\n CraftConfig,\n StoreRegistry,\n EventName,\n EventHandler,\n RouteDefinition,\n RouteBuilder,\n AdapterOverride,\n} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n CraftClient,\n isRoutecraftError,\n RoutecraftError,\n rcError,\n logger,\n RC_ADAPTER_OVERRIDES,\n} from \"@routecraft/routecraft\";\nimport type { SpyLogger } from \"./spy-logger\";\nimport { createSpyLogger, createNoopSpyLogger } from \"./spy-logger\";\nimport { isAdapterMock, type AdapterMock } from \"./mock-adapter\";\n\nconst DEFAULT_ROUTES_READY_TIMEOUT_MS = 200;\n\nfunction describeOverrideTarget(target: unknown): string {\n if (typeof target === \"function\" && typeof target.name === \"string\") {\n const kind =\n /^[A-Z]/.test(target.name) && target.prototype !== undefined\n ? \"class\"\n : \"factory\";\n return `${kind} ${target.name || \"<anonymous>\"}`;\n }\n return \"target\";\n}\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 /** Client for dispatching messages to direct endpoints in tests. */\n readonly client: CraftClient;\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 client: CraftClient,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.client = client;\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 const pushError = (err: unknown) => {\n this.errors.push(\n isRoutecraftError(err)\n ? (err as RoutecraftError)\n : rcError(\"RC9901\", err),\n );\n };\n ctx.on(\"context:error\", (payload) => {\n pushError(payload.details.error);\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(\n \"route:*:started\" as EventName,\n (() => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n resolve();\n }\n }) as EventHandler<EventName>,\n );\n const offError = ctx.on(\"context: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(\n \"route:*:started\" as EventName,\n (() => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n }) as EventHandler<EventName>,\n );\n const offError = ctx.on(\"context: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/**\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 private adapterOverrides: AdapterOverride[] = [];\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 /**\n * Register an adapter mock. At route execution time, calls to adapters\n * produced by the same factory are routed through the mock's handlers\n * instead of invoking the real adapter. Accepts either the handle returned\n * by `mockAdapter()` or a raw `AdapterOverride`.\n *\n * @experimental\n */\n override(mock: AdapterMock | AdapterOverride): this {\n const entry: AdapterOverride = isAdapterMock(mock) ? mock.override : mock;\n // Fail fast if two overrides target the same factory/class. The framework\n // uses first-match semantics at execution time, so silently accepting a\n // duplicate would mean the second mock's assertions always see zero calls\n // and the user has no signal that their new override is being shadowed.\n const duplicate = this.adapterOverrides.find(\n (o) => o.target === entry.target,\n );\n if (duplicate !== undefined) {\n const name = describeOverrideTarget(entry.target);\n throw new Error(\n `testContext().override(): duplicate override for ${name}. Each target may only be registered once; remove the redundant override() call.`,\n );\n }\n this.adapterOverrides.push(entry);\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 { context: ctx, client } = await this.builder.build();\n\n // Install registered adapter overrides onto the context store so that\n // ToStep / EnrichStep / Route source can resolve them at execution time.\n if (this.adapterOverrides.length > 0) {\n const existing = ctx.getStore(RC_ADAPTER_OVERRIDES) ?? [];\n ctx.setStore(RC_ADAPTER_OVERRIDES, [\n ...existing,\n ...this.adapterOverrides,\n ]);\n }\n\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, client, 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 * @experimental\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 * @experimental\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// Adapter mocking API\nexport {\n mockAdapter,\n type AdapterMock,\n type MockAdapterBehavior,\n} from \"./mock-adapter\";\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,6 +1,6 @@
1
1
  {
2
2
  "name": "@routecraft/testing",
3
- "version": "0.5.0-canary.8",
3
+ "version": "0.5.0-canary.9",
4
4
  "description": "Test utilities for Routecraft routes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -23,7 +23,7 @@
23
23
  "prepublishOnly": "pnpm run build"
24
24
  },
25
25
  "dependencies": {
26
- "@routecraft/routecraft": "^0.5.0-canary.8"
26
+ "@routecraft/routecraft": "^0.5.0-canary.9"
27
27
  },
28
28
  "devDependencies": {
29
29
  "vitest": "^4.1.4"