@routecraft/testing 0.5.0-canary.5 → 0.5.0-canary.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,13 +5,13 @@ Test utilities for Routecraft capabilities. Use with [Vitest](https://vitest.dev
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install -D @routecraft/testing
9
- ```
10
-
11
- or
8
+ # Bun (recommended)
9
+ bun add -D @routecraft/testing
12
10
 
13
- ```bash
11
+ # npm / pnpm / yarn
12
+ npm install -D @routecraft/testing
14
13
  pnpm add -D @routecraft/testing
14
+ yarn add -D @routecraft/testing
15
15
  ```
16
16
 
17
17
  Install as a devDependency. Requires `vitest` (>=4.0.0) and `@routecraft/routecraft`.
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 y(){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 g(){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 m=Symbol.for("routecraft.testing.adapter-mock");function v(t){return typeof t=="object"&&t!==null&&t[m]===true}function O(t,e){let r={target:t,calls:{source:[],send:[]}};return e.source!==void 0&&(r.source=e.source),e.send!==void 0&&(r.send=e.send),{[m]:true,override:r,get calls(){return {source:[...r.calls.source],send:[...r.calls.send]}}}}var b=200;function E(t){return typeof t=="function"&&typeof t.name=="string"?`${/^[A-Z]/.test(t.name)&&t.prototype!==void 0?"class":"factory"} ${t.name||"<anonymous>"}`:"target"}var p=class{ctx;client;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;loggerChildRestored=false;startedPromise;constructor(e,r,n){this.ctx=e,this.client=r,this.logger=n?.spyLogger??g(),n?.restoreLoggerChild&&(this.restoreLoggerChild=n.restoreLoggerChild),this.routesReadyTimeoutMs=n?.routesReadyTimeoutMs??b;let s=o=>{this.errors.push(routecraft.isRoutecraftError(o)?o:routecraft.rcError("RC9901",o));};e.on("context:error",o=>{s(o.details.error);});}awaitRoutesReady(){let e=this.ctx,r=e.getRoutes().length;return r===0?Promise.resolve():new Promise((n,s)=>{let o=0,i=false,d=setTimeout(()=>{i||(f(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),h=e.on("route:*:started",(()=>{i||(o++,o>=r&&(f(),n()));})),l=e.on("context:error",S=>{i||(f(),s(S.details.error));});function f(){i||(i=true,h(),l(),d!==void 0&&(clearTimeout(d),d=void 0));}})}async startAndWaitReady(){let e=this.awaitRoutesReady();this.startedPromise=this.ctx.start(),this.startedPromise.catch(()=>{}),await e;}async test(e){let r=this.ctx,n=this.awaitRoutesReady(),s=r.start();s.catch(()=>{});try{await n;let o=e?.delayBeforeDrainMs??0;o>0&&await new Promise(i=>setTimeout(i,o)),await r.drain();}finally{try{await r.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);}},c=class{builder=new routecraft.ContextBuilder;routesReadyTimeoutMs;adapterOverrides=[];routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}override(e){let r=v(e)?e.override:e;if(this.adapterOverrides.find(s=>s.target===r.target)!==void 0){let s=E(r.target);throw new Error(`testContext().override(): duplicate override for ${s}. Each target may only be registered once; remove the redundant override() call.`)}return this.adapterOverrides.push(r),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=y(),r=routecraft.logger.child.bind(routecraft.logger);routecraft.logger.child=vitest.vi.fn(()=>e);let{context:n,client:s}=await this.builder.build();if(this.adapterOverrides.length>0){let i=n.getStore(routecraft.RC_ADAPTER_OVERRIDES)??[];n.setStore(routecraft.RC_ADAPTER_OVERRIDES,[...i,...this.adapterOverrides]);}let o={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{routecraft.logger.child=r;}};return new p(n,s,o)}};function M(){return new c}function x(t,e){let r=()=>{throw new Error(`Pseudo adapter "${t}" is not implemented. Replace with a real adapter.`)},n=()=>Promise.resolve(void 0),s=i=>i,o=(i,d,h,l)=>(l?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${t}`,subscribe:e==="noop"?o:r,send:e==="noop"?n:r,process:e==="noop"?s:r}}function F(t="pseudo",e){let r=e?.runtime??"throw";return e&&"args"in e&&e.args==="keyed"?(s,o)=>x(t,r):s=>x(t,r)}function R(){return {received:[],calls:{send:0,process:0,enrich:0}}}function K(){let t=R();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)}}}async function B(t,e,r={}){let n=t.input["~standard"];if(typeof n?.validate!="function")throw routecraft.rcError("RC5003",void 0,{message:"testFn: spec.input must be a Standard Schema with a callable validate."});let s=n.validate,o=s(e);if(o instanceof Promise&&(o=await o),o.issues!==void 0&&o.issues!==null)throw routecraft.rcError("RC5002",void 0,{message:`testFn: input validation failed: ${routecraft.formatSchemaIssues(o.issues)}`});let i={logger:r.logger??routecraft.logger.child({test:"fn"}),abortSignal:r.signal??new AbortController().signal},d="value"in o?o.value:e;return await t.handler(d,i)}function H(t){return JSON.parse(fs.readFileSync(t,"utf-8"))}function ie(t,e){let r=H(t);if(!Array.isArray(r))throw new Error(`fixture.each: expected JSON array at "${t}", got ${typeof r}`);for(let n of r){if(typeof n?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(n)}`);vitest.test(n.name,()=>e(n));}}exports.TestContext=p;exports.TestContextBuilder=c;exports.createNoopSpyLogger=g;exports.createSpyLogger=y;exports.fixture=H;exports.fixtureEach=ie;exports.mockAdapter=O;exports.pseudo=F;exports.spy=K;exports.testContext=M;exports.testFn=B;//# 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/test-fn.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","resolve","reject","ready","settled","timeoutId","cleanup","offRouteStarted","offError","allReady","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","testFn","spec","input","standard","validate","result","formatSchemaIssues","defaultLogger","validated","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 * Build a promise that resolves once every route has emitted\n * `route:*:started`, or rejects on `context:error` or the configured\n * routes-ready timeout. Shared by {@link startAndWaitReady} and {@link test}.\n */\n private awaitRoutesReady(): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n if (total === 0) return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n let timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout(\n () => {\n if (settled) return;\n cleanup();\n reject(new Error(\"Timeout waiting for routes to start\"));\n },\n this.routesReadyTimeoutMs,\n );\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 }\n\n /**\n * Start context and resolve once every route has emitted `route:*:started`.\n * Does not drain or stop. Does not await `ctx.start()` completion, which\n * lets this method work with long-running sources (direct, mcp, HTTP, etc.)\n * whose subscribe blocks until the route is aborted. The start promise is\n * stored internally and awaited by {@link stop} for clean shutdown.\n *\n * Use with {@link CraftClient.send} (via `t.client`) for direct endpoints,\n * or drive sources directly via the context store, then call `drain()` /\n * `stop()` when done.\n *\n * If `ctx.start()` rejects (synchronously or before any route emits\n * `route:*:started`), the rejection surfaces here via the\n * `context:error` listener installed by `awaitRoutesReady`. A no-op\n * catch is attached to `startedPromise` as a safety net so that a\n * slow rejection does not trigger an `unhandledRejection` before\n * `stop()` awaits the promise for teardown.\n */\n async startAndWaitReady(): Promise<void> {\n const allReady = this.awaitRoutesReady();\n this.startedPromise = this.ctx.start();\n // Attach a no-op handler so Node does not report the rejection as\n // unhandled before `stop()` re-awaits the promise.\n this.startedPromise.catch(() => {});\n await 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 allReady = this.awaitRoutesReady();\n const started = ctx.start();\n // Shield a synchronous rejection of `started` from becoming an\n // unhandled rejection before the `finally` block re-awaits it.\n started.catch(() => {});\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 type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport {\n formatSchemaIssues,\n logger as defaultLogger,\n rcError,\n} from \"@routecraft/routecraft\";\n\n/**\n * Structural shape of a fn-like spec for testing. Does not import\n * `FnOptions` from `@routecraft/ai` so this package stays free of\n * a reverse dependency. Real `FnOptions` values are structurally\n * assignable here -- the extra `description` field is ignored.\n *\n * @beta\n */\nexport interface TestFnSpec<TIn, TOut> {\n /** Schema whose validated/coerced output is passed to `handler`. */\n input: StandardSchemaV1<unknown, TIn>;\n handler: (input: TIn, ctx: TestFnHandlerContext) => Promise<TOut> | TOut;\n}\n\n/**\n * Synthetic context handed to a fn handler under `testFn`. Mirrors the\n * minimum shape `agentPlugin` provides at production dispatch time\n * (without coupling to that implementation). Extra fields a handler may\n * read at runtime can be added here in follow-ups without breaking the\n * structural contract.\n *\n * @beta\n */\nexport interface TestFnHandlerContext {\n logger: ReturnType<typeof defaultLogger.child>;\n abortSignal: AbortSignal;\n}\n\n/**\n * Options for {@link testFn}.\n *\n * @beta\n */\nexport interface TestFnOptions {\n /** Caller-supplied abort signal. Defaults to a never-firing signal. */\n signal?: AbortSignal;\n /** Caller-supplied logger. Defaults to a child of the framework logger bound to `{ test: \"fn\" }`. */\n logger?: ReturnType<typeof defaultLogger.child>;\n}\n\n/**\n * Run a fn-like spec end-to-end in tests. Validates `input` against the\n * spec's Standard Schema, then calls the handler with a synthetic\n * context. Designed to mirror what `agentPlugin` does internally at\n * production dispatch time, without exposing or depending on that\n * dispatcher.\n *\n * Throws `RC5002` (Validation failed) if the input does not pass the\n * schema. Errors thrown from the handler propagate as-is.\n *\n * @beta\n *\n * @example\n * ```typescript\n * import { testFn } from \"@routecraft/testing\";\n * import { z } from \"zod\";\n *\n * const greet = {\n * description: \"...\",\n * input: z.object({ name: z.string() }),\n * handler: async (input, ctx) => `hello ${input.name}`,\n * };\n *\n * const out = await testFn(greet, { name: \"alice\" });\n * expect(out).toBe(\"hello alice\");\n * ```\n */\nexport async function testFn<TIn, TOut>(\n spec: TestFnSpec<TIn, TOut>,\n input: unknown,\n options: TestFnOptions = {},\n): Promise<TOut> {\n const standard = (spec.input as { [\"~standard\"]?: { validate?: unknown } })[\n \"~standard\"\n ];\n if (typeof standard?.validate !== \"function\") {\n throw rcError(\"RC5003\", undefined, {\n message: `testFn: spec.input must be a Standard Schema with a callable validate.`,\n });\n }\n\n const validate = standard.validate as (\n value: unknown,\n ) =>\n | { value?: unknown; issues?: unknown }\n | Promise<{ value?: unknown; issues?: unknown }>;\n let result = validate(input);\n if (result instanceof Promise) result = await result;\n if (result.issues !== undefined && result.issues !== null) {\n throw rcError(\"RC5002\", undefined, {\n message: `testFn: input validation failed: ${formatSchemaIssues(result.issues)}`,\n });\n }\n\n const ctx: TestFnHandlerContext = {\n logger: options.logger ?? defaultLogger.child({ test: \"fn\" }),\n abortSignal: options.signal ?? new AbortController().signal,\n };\n\n const validated = \"value\" in result ? (result.value as TIn) : (input as TIn);\n return (await spec.handler(validated, ctx)) as TOut;\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// Test helper for fn-like specs (schema + handler). Used to exercise\n// fns registered in `@routecraft/ai`'s agentPlugin without depending on\n// any non-public dispatcher.\nexport {\n testFn,\n type TestFnHandlerContext,\n type TestFnOptions,\n type TestFnSpec,\n} from \"./test-fn\";\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,5 +1,6 @@
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, logger } from '@routecraft/routecraft';
2
2
  import { vi } from 'vitest';
3
+ import { StandardSchemaV1 } from '@standard-schema/spec';
3
4
 
4
5
  /**
5
6
  * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).
@@ -20,6 +21,119 @@ declare function createSpyLogger(): SpyLogger;
20
21
  /** @beta */
21
22
  declare function createNoopSpyLogger(): SpyLogger;
22
23
 
24
+ /**
25
+ * Brand symbol stamped on the handles returned by `mockAdapter()`.
26
+ *
27
+ * `testContext().override()` uses this symbol to distinguish a mock handle
28
+ * from a raw `AdapterOverride` (both objects carry a `calls` field, so a
29
+ * structural check on a non-branded key would be fragile).
30
+ *
31
+ * Use `Symbol.for` so cross-realm / multi-package-load equality holds.
32
+ *
33
+ * @internal
34
+ */
35
+ declare const ADAPTER_MOCK_BRAND: unique symbol;
36
+ /**
37
+ * Extract the message type `M` from an adapter factory or adapter class,
38
+ * so `mockAdapter(target, { source: [...] })` can check fixtures against
39
+ * the real adapter shape. Falls back to `unknown` when `target` has no
40
+ * inferable Source role (e.g. destination-only factories, or overloaded
41
+ * factories where TypeScript cannot pick the source overload).
42
+ */
43
+ 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;
44
+ /**
45
+ * Behaviour description for a mock adapter. A mock may stub the source side,
46
+ * the destination side, or both. The framework picks the matching behaviour
47
+ * based on the call site's role in the route.
48
+ *
49
+ * @experimental
50
+ */
51
+ interface MockAdapterBehavior<M = unknown> {
52
+ /**
53
+ * Source-role behaviour. Used when the adapter is the `.from()` of a route.
54
+ * Pass an array of fixtures, an async iterable, or a callable that receives
55
+ * the construction args and returns the stream to emit.
56
+ */
57
+ source?: SourceOverrideBehavior<M>;
58
+ /**
59
+ * Destination-role behaviour. Used when the adapter is passed to `.to()`,
60
+ * `.enrich()`, or `.tap()`. Receives the exchange and a meta object with
61
+ * the construction args; returning a value replaces the body upstream.
62
+ */
63
+ send?: SendOverrideHandler;
64
+ }
65
+ /**
66
+ * Handle returned by `mockAdapter(factory, behaviour)`. Carries the resolved
67
+ * override the framework should install on the context, plus `calls` for
68
+ * assertions.
69
+ *
70
+ * @experimental
71
+ */
72
+ interface AdapterMock {
73
+ /** Brand used by `testContext().override()` to discriminate handles from raw overrides. */
74
+ readonly [ADAPTER_MOCK_BRAND]: true;
75
+ readonly override: AdapterOverride;
76
+ /**
77
+ * Recorded calls, populated as the route runs. Assert on these after
78
+ * awaiting `t.test()`.
79
+ */
80
+ readonly calls: {
81
+ source: readonly AdapterSourceCall[];
82
+ send: readonly AdapterSendCall[];
83
+ };
84
+ }
85
+ /**
86
+ * Create a mock for an adapter. The `target` may be either:
87
+ *
88
+ * - An adapter factory (e.g. `mail`, `http`, `mcp`). The mock matches every
89
+ * adapter instance produced by that factory. Requires the factory to stamp
90
+ * its adapters via `tagAdapter()`.
91
+ * - An adapter class (e.g. `MailSourceAdapter`, `HttpDestinationAdapter`).
92
+ * The mock matches any adapter whose `constructor === target`. Works for
93
+ * every adapter without opt-in tagging, including third-party ones.
94
+ *
95
+ * Pass the result to `testContext().override(mock)` and run the route
96
+ * under test as-is; the framework invokes the mock's `source` / `send`
97
+ * handlers in place of the real adapter at every matching call site.
98
+ *
99
+ * @experimental
100
+ * @param target - The adapter factory or adapter class to intercept
101
+ * @param behavior - Source and/or destination-role handlers
102
+ * @returns A handle with `calls` for assertions and an internal `override`
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * // Factory form (preferred for single-role factories)
107
+ * import { http, mail } from "@routecraft/routecraft";
108
+ * import { mockAdapter, testContext } from "@routecraft/testing";
109
+ *
110
+ * const httpMock = mockAdapter(http, {
111
+ * send: async () => ({ status: 200, body: { ok: true } }),
112
+ * });
113
+ *
114
+ * const mailMock = mockAdapter(mail, {
115
+ * source: [{ uid: 1, from: "a@b", subject: "hi", ... }],
116
+ * send: async () => ({ messageId: "<fake>" }),
117
+ * });
118
+ *
119
+ * // Class form (works for any adapter, including third-party ones)
120
+ * import { SomeAdapterClass } from "third-party-adapter";
121
+ *
122
+ * const thirdPartyMock = mockAdapter(SomeAdapterClass, {
123
+ * send: async () => ({ ok: true }),
124
+ * });
125
+ *
126
+ * const t = await testContext()
127
+ * .override(httpMock)
128
+ * .override(mailMock)
129
+ * .override(thirdPartyMock)
130
+ * .routes(route)
131
+ * .build();
132
+ * await t.test();
133
+ * ```
134
+ */
135
+ declare function mockAdapter<T extends ((...args: never[]) => unknown) | (new (...args: never[]) => unknown), M = InferAdapterMessage<T>>(target: T, behavior: MockAdapterBehavior<M>): AdapterMock;
136
+
23
137
  interface TestContextOptions {
24
138
  /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */
25
139
  routesReadyTimeoutMs?: number;
@@ -56,15 +170,35 @@ declare class TestContext {
56
170
  restoreLoggerChild?: () => void;
57
171
  });
58
172
  /**
59
- * Start context and wait for all routes to be ready. Does not drain or stop.
60
- * Use with invoke() to send to a route by id, then call drain()/stop() when done.
173
+ * Build a promise that resolves once every route has emitted
174
+ * `route:*:started`, or rejects on `context:error` or the configured
175
+ * routes-ready timeout. Shared by {@link startAndWaitReady} and {@link test}.
176
+ */
177
+ private awaitRoutesReady;
178
+ /**
179
+ * Start context and resolve once every route has emitted `route:*:started`.
180
+ * Does not drain or stop. Does not await `ctx.start()` completion, which
181
+ * lets this method work with long-running sources (direct, mcp, HTTP, etc.)
182
+ * whose subscribe blocks until the route is aborted. The start promise is
183
+ * stored internally and awaited by {@link stop} for clean shutdown.
184
+ *
185
+ * Use with {@link CraftClient.send} (via `t.client`) for direct endpoints,
186
+ * or drive sources directly via the context store, then call `drain()` /
187
+ * `stop()` when done.
188
+ *
189
+ * If `ctx.start()` rejects (synchronously or before any route emits
190
+ * `route:*:started`), the rejection surfaces here via the
191
+ * `context:error` listener installed by `awaitRoutesReady`. A no-op
192
+ * catch is attached to `startedPromise` as a safety net so that a
193
+ * slow rejection does not trigger an `unhandledRejection` before
194
+ * `stop()` awaits the promise for teardown.
61
195
  */
62
196
  startAndWaitReady(): Promise<void>;
63
197
  /**
64
198
  * Start context, wait for all routes ready, optionally delay, drain in-flight, then stop.
65
199
  * Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).
66
200
  *
67
- * @param options.delayBeforeDrainMs If set, wait this many ms after routes are ready before draining.
201
+ * @param options.delayBeforeDrainMs If set, wait this many ms after routes are ready before draining.
68
202
  * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.
69
203
  */
70
204
  test(options?: TestOptions): Promise<void>;
@@ -81,8 +215,18 @@ declare class TestContext {
81
215
  declare class TestContextBuilder {
82
216
  private builder;
83
217
  private routesReadyTimeoutMs;
218
+ private adapterOverrides;
84
219
  /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */
85
220
  routesReadyTimeout(ms: number): this;
221
+ /**
222
+ * Register an adapter mock. At route execution time, calls to adapters
223
+ * produced by the same factory are routed through the mock's handlers
224
+ * instead of invoking the real adapter. Accepts either the handle returned
225
+ * by `mockAdapter()` or a raw `AdapterOverride`.
226
+ *
227
+ * @experimental
228
+ */
229
+ override(mock: AdapterMock | AdapterOverride): this;
86
230
  with(config: CraftConfig): this;
87
231
  on<K extends EventName>(event: K, handler: EventHandler<K>): this;
88
232
  once<K extends EventName>(event: K, handler: EventHandler<K>): this;
@@ -173,6 +317,72 @@ type SpyAdapter<T = unknown> = {
173
317
  */
174
318
  declare function spy<T = unknown>(): SpyAdapter<T>;
175
319
 
320
+ /**
321
+ * Structural shape of a fn-like spec for testing. Does not import
322
+ * `FnOptions` from `@routecraft/ai` so this package stays free of
323
+ * a reverse dependency. Real `FnOptions` values are structurally
324
+ * assignable here -- the extra `description` field is ignored.
325
+ *
326
+ * @beta
327
+ */
328
+ interface TestFnSpec<TIn, TOut> {
329
+ /** Schema whose validated/coerced output is passed to `handler`. */
330
+ input: StandardSchemaV1<unknown, TIn>;
331
+ handler: (input: TIn, ctx: TestFnHandlerContext) => Promise<TOut> | TOut;
332
+ }
333
+ /**
334
+ * Synthetic context handed to a fn handler under `testFn`. Mirrors the
335
+ * minimum shape `agentPlugin` provides at production dispatch time
336
+ * (without coupling to that implementation). Extra fields a handler may
337
+ * read at runtime can be added here in follow-ups without breaking the
338
+ * structural contract.
339
+ *
340
+ * @beta
341
+ */
342
+ interface TestFnHandlerContext {
343
+ logger: ReturnType<typeof logger.child>;
344
+ abortSignal: AbortSignal;
345
+ }
346
+ /**
347
+ * Options for {@link testFn}.
348
+ *
349
+ * @beta
350
+ */
351
+ interface TestFnOptions {
352
+ /** Caller-supplied abort signal. Defaults to a never-firing signal. */
353
+ signal?: AbortSignal;
354
+ /** Caller-supplied logger. Defaults to a child of the framework logger bound to `{ test: "fn" }`. */
355
+ logger?: ReturnType<typeof logger.child>;
356
+ }
357
+ /**
358
+ * Run a fn-like spec end-to-end in tests. Validates `input` against the
359
+ * spec's Standard Schema, then calls the handler with a synthetic
360
+ * context. Designed to mirror what `agentPlugin` does internally at
361
+ * production dispatch time, without exposing or depending on that
362
+ * dispatcher.
363
+ *
364
+ * Throws `RC5002` (Validation failed) if the input does not pass the
365
+ * schema. Errors thrown from the handler propagate as-is.
366
+ *
367
+ * @beta
368
+ *
369
+ * @example
370
+ * ```typescript
371
+ * import { testFn } from "@routecraft/testing";
372
+ * import { z } from "zod";
373
+ *
374
+ * const greet = {
375
+ * description: "...",
376
+ * input: z.object({ name: z.string() }),
377
+ * handler: async (input, ctx) => `hello ${input.name}`,
378
+ * };
379
+ *
380
+ * const out = await testFn(greet, { name: "alice" });
381
+ * expect(out).toBe("hello alice");
382
+ * ```
383
+ */
384
+ declare function testFn<TIn, TOut>(spec: TestFnSpec<TIn, TOut>, input: unknown, options?: TestFnOptions): Promise<TOut>;
385
+
176
386
  /**
177
387
  * Load a JSON fixture file and return the parsed value.
178
388
  *
@@ -195,4 +405,4 @@ interface FixtureWithName {
195
405
  */
196
406
  declare function fixtureEach<T extends FixtureWithName>(path: string, run: (entry: T) => void | Promise<void>): void;
197
407
 
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 };
408
+ 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 TestFnHandlerContext, type TestFnOptions, type TestFnSpec, type TestOptions, createNoopSpyLogger, createSpyLogger, fixture, fixtureEach, mockAdapter, pseudo, spy, testContext, testFn };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
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, logger } from '@routecraft/routecraft';
2
2
  import { vi } from 'vitest';
3
+ import { StandardSchemaV1 } from '@standard-schema/spec';
3
4
 
4
5
  /**
5
6
  * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).
@@ -20,6 +21,119 @@ declare function createSpyLogger(): SpyLogger;
20
21
  /** @beta */
21
22
  declare function createNoopSpyLogger(): SpyLogger;
22
23
 
24
+ /**
25
+ * Brand symbol stamped on the handles returned by `mockAdapter()`.
26
+ *
27
+ * `testContext().override()` uses this symbol to distinguish a mock handle
28
+ * from a raw `AdapterOverride` (both objects carry a `calls` field, so a
29
+ * structural check on a non-branded key would be fragile).
30
+ *
31
+ * Use `Symbol.for` so cross-realm / multi-package-load equality holds.
32
+ *
33
+ * @internal
34
+ */
35
+ declare const ADAPTER_MOCK_BRAND: unique symbol;
36
+ /**
37
+ * Extract the message type `M` from an adapter factory or adapter class,
38
+ * so `mockAdapter(target, { source: [...] })` can check fixtures against
39
+ * the real adapter shape. Falls back to `unknown` when `target` has no
40
+ * inferable Source role (e.g. destination-only factories, or overloaded
41
+ * factories where TypeScript cannot pick the source overload).
42
+ */
43
+ 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;
44
+ /**
45
+ * Behaviour description for a mock adapter. A mock may stub the source side,
46
+ * the destination side, or both. The framework picks the matching behaviour
47
+ * based on the call site's role in the route.
48
+ *
49
+ * @experimental
50
+ */
51
+ interface MockAdapterBehavior<M = unknown> {
52
+ /**
53
+ * Source-role behaviour. Used when the adapter is the `.from()` of a route.
54
+ * Pass an array of fixtures, an async iterable, or a callable that receives
55
+ * the construction args and returns the stream to emit.
56
+ */
57
+ source?: SourceOverrideBehavior<M>;
58
+ /**
59
+ * Destination-role behaviour. Used when the adapter is passed to `.to()`,
60
+ * `.enrich()`, or `.tap()`. Receives the exchange and a meta object with
61
+ * the construction args; returning a value replaces the body upstream.
62
+ */
63
+ send?: SendOverrideHandler;
64
+ }
65
+ /**
66
+ * Handle returned by `mockAdapter(factory, behaviour)`. Carries the resolved
67
+ * override the framework should install on the context, plus `calls` for
68
+ * assertions.
69
+ *
70
+ * @experimental
71
+ */
72
+ interface AdapterMock {
73
+ /** Brand used by `testContext().override()` to discriminate handles from raw overrides. */
74
+ readonly [ADAPTER_MOCK_BRAND]: true;
75
+ readonly override: AdapterOverride;
76
+ /**
77
+ * Recorded calls, populated as the route runs. Assert on these after
78
+ * awaiting `t.test()`.
79
+ */
80
+ readonly calls: {
81
+ source: readonly AdapterSourceCall[];
82
+ send: readonly AdapterSendCall[];
83
+ };
84
+ }
85
+ /**
86
+ * Create a mock for an adapter. The `target` may be either:
87
+ *
88
+ * - An adapter factory (e.g. `mail`, `http`, `mcp`). The mock matches every
89
+ * adapter instance produced by that factory. Requires the factory to stamp
90
+ * its adapters via `tagAdapter()`.
91
+ * - An adapter class (e.g. `MailSourceAdapter`, `HttpDestinationAdapter`).
92
+ * The mock matches any adapter whose `constructor === target`. Works for
93
+ * every adapter without opt-in tagging, including third-party ones.
94
+ *
95
+ * Pass the result to `testContext().override(mock)` and run the route
96
+ * under test as-is; the framework invokes the mock's `source` / `send`
97
+ * handlers in place of the real adapter at every matching call site.
98
+ *
99
+ * @experimental
100
+ * @param target - The adapter factory or adapter class to intercept
101
+ * @param behavior - Source and/or destination-role handlers
102
+ * @returns A handle with `calls` for assertions and an internal `override`
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * // Factory form (preferred for single-role factories)
107
+ * import { http, mail } from "@routecraft/routecraft";
108
+ * import { mockAdapter, testContext } from "@routecraft/testing";
109
+ *
110
+ * const httpMock = mockAdapter(http, {
111
+ * send: async () => ({ status: 200, body: { ok: true } }),
112
+ * });
113
+ *
114
+ * const mailMock = mockAdapter(mail, {
115
+ * source: [{ uid: 1, from: "a@b", subject: "hi", ... }],
116
+ * send: async () => ({ messageId: "<fake>" }),
117
+ * });
118
+ *
119
+ * // Class form (works for any adapter, including third-party ones)
120
+ * import { SomeAdapterClass } from "third-party-adapter";
121
+ *
122
+ * const thirdPartyMock = mockAdapter(SomeAdapterClass, {
123
+ * send: async () => ({ ok: true }),
124
+ * });
125
+ *
126
+ * const t = await testContext()
127
+ * .override(httpMock)
128
+ * .override(mailMock)
129
+ * .override(thirdPartyMock)
130
+ * .routes(route)
131
+ * .build();
132
+ * await t.test();
133
+ * ```
134
+ */
135
+ declare function mockAdapter<T extends ((...args: never[]) => unknown) | (new (...args: never[]) => unknown), M = InferAdapterMessage<T>>(target: T, behavior: MockAdapterBehavior<M>): AdapterMock;
136
+
23
137
  interface TestContextOptions {
24
138
  /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */
25
139
  routesReadyTimeoutMs?: number;
@@ -56,15 +170,35 @@ declare class TestContext {
56
170
  restoreLoggerChild?: () => void;
57
171
  });
58
172
  /**
59
- * Start context and wait for all routes to be ready. Does not drain or stop.
60
- * Use with invoke() to send to a route by id, then call drain()/stop() when done.
173
+ * Build a promise that resolves once every route has emitted
174
+ * `route:*:started`, or rejects on `context:error` or the configured
175
+ * routes-ready timeout. Shared by {@link startAndWaitReady} and {@link test}.
176
+ */
177
+ private awaitRoutesReady;
178
+ /**
179
+ * Start context and resolve once every route has emitted `route:*:started`.
180
+ * Does not drain or stop. Does not await `ctx.start()` completion, which
181
+ * lets this method work with long-running sources (direct, mcp, HTTP, etc.)
182
+ * whose subscribe blocks until the route is aborted. The start promise is
183
+ * stored internally and awaited by {@link stop} for clean shutdown.
184
+ *
185
+ * Use with {@link CraftClient.send} (via `t.client`) for direct endpoints,
186
+ * or drive sources directly via the context store, then call `drain()` /
187
+ * `stop()` when done.
188
+ *
189
+ * If `ctx.start()` rejects (synchronously or before any route emits
190
+ * `route:*:started`), the rejection surfaces here via the
191
+ * `context:error` listener installed by `awaitRoutesReady`. A no-op
192
+ * catch is attached to `startedPromise` as a safety net so that a
193
+ * slow rejection does not trigger an `unhandledRejection` before
194
+ * `stop()` awaits the promise for teardown.
61
195
  */
62
196
  startAndWaitReady(): Promise<void>;
63
197
  /**
64
198
  * Start context, wait for all routes ready, optionally delay, drain in-flight, then stop.
65
199
  * Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).
66
200
  *
67
- * @param options.delayBeforeDrainMs If set, wait this many ms after routes are ready before draining.
201
+ * @param options.delayBeforeDrainMs If set, wait this many ms after routes are ready before draining.
68
202
  * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.
69
203
  */
70
204
  test(options?: TestOptions): Promise<void>;
@@ -81,8 +215,18 @@ declare class TestContext {
81
215
  declare class TestContextBuilder {
82
216
  private builder;
83
217
  private routesReadyTimeoutMs;
218
+ private adapterOverrides;
84
219
  /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */
85
220
  routesReadyTimeout(ms: number): this;
221
+ /**
222
+ * Register an adapter mock. At route execution time, calls to adapters
223
+ * produced by the same factory are routed through the mock's handlers
224
+ * instead of invoking the real adapter. Accepts either the handle returned
225
+ * by `mockAdapter()` or a raw `AdapterOverride`.
226
+ *
227
+ * @experimental
228
+ */
229
+ override(mock: AdapterMock | AdapterOverride): this;
86
230
  with(config: CraftConfig): this;
87
231
  on<K extends EventName>(event: K, handler: EventHandler<K>): this;
88
232
  once<K extends EventName>(event: K, handler: EventHandler<K>): this;
@@ -173,6 +317,72 @@ type SpyAdapter<T = unknown> = {
173
317
  */
174
318
  declare function spy<T = unknown>(): SpyAdapter<T>;
175
319
 
320
+ /**
321
+ * Structural shape of a fn-like spec for testing. Does not import
322
+ * `FnOptions` from `@routecraft/ai` so this package stays free of
323
+ * a reverse dependency. Real `FnOptions` values are structurally
324
+ * assignable here -- the extra `description` field is ignored.
325
+ *
326
+ * @beta
327
+ */
328
+ interface TestFnSpec<TIn, TOut> {
329
+ /** Schema whose validated/coerced output is passed to `handler`. */
330
+ input: StandardSchemaV1<unknown, TIn>;
331
+ handler: (input: TIn, ctx: TestFnHandlerContext) => Promise<TOut> | TOut;
332
+ }
333
+ /**
334
+ * Synthetic context handed to a fn handler under `testFn`. Mirrors the
335
+ * minimum shape `agentPlugin` provides at production dispatch time
336
+ * (without coupling to that implementation). Extra fields a handler may
337
+ * read at runtime can be added here in follow-ups without breaking the
338
+ * structural contract.
339
+ *
340
+ * @beta
341
+ */
342
+ interface TestFnHandlerContext {
343
+ logger: ReturnType<typeof logger.child>;
344
+ abortSignal: AbortSignal;
345
+ }
346
+ /**
347
+ * Options for {@link testFn}.
348
+ *
349
+ * @beta
350
+ */
351
+ interface TestFnOptions {
352
+ /** Caller-supplied abort signal. Defaults to a never-firing signal. */
353
+ signal?: AbortSignal;
354
+ /** Caller-supplied logger. Defaults to a child of the framework logger bound to `{ test: "fn" }`. */
355
+ logger?: ReturnType<typeof logger.child>;
356
+ }
357
+ /**
358
+ * Run a fn-like spec end-to-end in tests. Validates `input` against the
359
+ * spec's Standard Schema, then calls the handler with a synthetic
360
+ * context. Designed to mirror what `agentPlugin` does internally at
361
+ * production dispatch time, without exposing or depending on that
362
+ * dispatcher.
363
+ *
364
+ * Throws `RC5002` (Validation failed) if the input does not pass the
365
+ * schema. Errors thrown from the handler propagate as-is.
366
+ *
367
+ * @beta
368
+ *
369
+ * @example
370
+ * ```typescript
371
+ * import { testFn } from "@routecraft/testing";
372
+ * import { z } from "zod";
373
+ *
374
+ * const greet = {
375
+ * description: "...",
376
+ * input: z.object({ name: z.string() }),
377
+ * handler: async (input, ctx) => `hello ${input.name}`,
378
+ * };
379
+ *
380
+ * const out = await testFn(greet, { name: "alice" });
381
+ * expect(out).toBe("hello alice");
382
+ * ```
383
+ */
384
+ declare function testFn<TIn, TOut>(spec: TestFnSpec<TIn, TOut>, input: unknown, options?: TestFnOptions): Promise<TOut>;
385
+
176
386
  /**
177
387
  * Load a JSON fixture file and return the parsed value.
178
388
  *
@@ -195,4 +405,4 @@ interface FixtureWithName {
195
405
  */
196
406
  declare function fixtureEach<T extends FixtureWithName>(path: string, run: (entry: T) => void | Promise<void>): void;
197
407
 
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 };
408
+ 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 TestFnHandlerContext, type TestFnOptions, type TestFnSpec, type TestOptions, createNoopSpyLogger, createSpyLogger, fixture, fixtureEach, mockAdapter, pseudo, spy, testContext, testFn };
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,rcError,formatSchemaIssues,isRoutecraftError}from'@routecraft/routecraft';function y(){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 g(){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 m=Symbol.for("routecraft.testing.adapter-mock");function v(t){return typeof t=="object"&&t!==null&&t[m]===true}function O(t,e){let r={target:t,calls:{source:[],send:[]}};return e.source!==void 0&&(r.source=e.source),e.send!==void 0&&(r.send=e.send),{[m]:true,override:r,get calls(){return {source:[...r.calls.source],send:[...r.calls.send]}}}}var b=200;function E(t){return typeof t=="function"&&typeof t.name=="string"?`${/^[A-Z]/.test(t.name)&&t.prototype!==void 0?"class":"factory"} ${t.name||"<anonymous>"}`:"target"}var p=class{ctx;client;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;loggerChildRestored=false;startedPromise;constructor(e,r,n){this.ctx=e,this.client=r,this.logger=n?.spyLogger??g(),n?.restoreLoggerChild&&(this.restoreLoggerChild=n.restoreLoggerChild),this.routesReadyTimeoutMs=n?.routesReadyTimeoutMs??b;let s=o=>{this.errors.push(isRoutecraftError(o)?o:rcError("RC9901",o));};e.on("context:error",o=>{s(o.details.error);});}awaitRoutesReady(){let e=this.ctx,r=e.getRoutes().length;return r===0?Promise.resolve():new Promise((n,s)=>{let o=0,i=false,d=setTimeout(()=>{i||(f(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),h=e.on("route:*:started",(()=>{i||(o++,o>=r&&(f(),n()));})),l=e.on("context:error",S=>{i||(f(),s(S.details.error));});function f(){i||(i=true,h(),l(),d!==void 0&&(clearTimeout(d),d=void 0));}})}async startAndWaitReady(){let e=this.awaitRoutesReady();this.startedPromise=this.ctx.start(),this.startedPromise.catch(()=>{}),await e;}async test(e){let r=this.ctx,n=this.awaitRoutesReady(),s=r.start();s.catch(()=>{});try{await n;let o=e?.delayBeforeDrainMs??0;o>0&&await new Promise(i=>setTimeout(i,o)),await r.drain();}finally{try{await r.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);}},c=class{builder=new ContextBuilder;routesReadyTimeoutMs;adapterOverrides=[];routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}override(e){let r=v(e)?e.override:e;if(this.adapterOverrides.find(s=>s.target===r.target)!==void 0){let s=E(r.target);throw new Error(`testContext().override(): duplicate override for ${s}. Each target may only be registered once; remove the redundant override() call.`)}return this.adapterOverrides.push(r),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=y(),r=logger.child.bind(logger);logger.child=vi.fn(()=>e);let{context:n,client:s}=await this.builder.build();if(this.adapterOverrides.length>0){let i=n.getStore(RC_ADAPTER_OVERRIDES)??[];n.setStore(RC_ADAPTER_OVERRIDES,[...i,...this.adapterOverrides]);}let o={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{logger.child=r;}};return new p(n,s,o)}};function M(){return new c}function x(t,e){let r=()=>{throw new Error(`Pseudo adapter "${t}" is not implemented. Replace with a real adapter.`)},n=()=>Promise.resolve(void 0),s=i=>i,o=(i,d,h,l)=>(l?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${t}`,subscribe:e==="noop"?o:r,send:e==="noop"?n:r,process:e==="noop"?s:r}}function F(t="pseudo",e){let r=e?.runtime??"throw";return e&&"args"in e&&e.args==="keyed"?(s,o)=>x(t,r):s=>x(t,r)}function R(){return {received:[],calls:{send:0,process:0,enrich:0}}}function K(){let t=R();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)}}}async function B(t,e,r={}){let n=t.input["~standard"];if(typeof n?.validate!="function")throw rcError("RC5003",void 0,{message:"testFn: spec.input must be a Standard Schema with a callable validate."});let s=n.validate,o=s(e);if(o instanceof Promise&&(o=await o),o.issues!==void 0&&o.issues!==null)throw rcError("RC5002",void 0,{message:`testFn: input validation failed: ${formatSchemaIssues(o.issues)}`});let i={logger:r.logger??logger.child({test:"fn"}),abortSignal:r.signal??new AbortController().signal},d="value"in o?o.value:e;return await t.handler(d,i)}function H(t){return JSON.parse(readFileSync(t,"utf-8"))}function ie(t,e){let r=H(t);if(!Array.isArray(r))throw new Error(`fixture.each: expected JSON array at "${t}", got ${typeof r}`);for(let n of r){if(typeof n?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(n)}`);test(n.name,()=>e(n));}}export{p as TestContext,c as TestContextBuilder,g as createNoopSpyLogger,y as createSpyLogger,H as fixture,ie as fixtureEach,O as mockAdapter,F as pseudo,K as spy,M as testContext,B as testFn};//# 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/test-fn.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","resolve","reject","ready","settled","timeoutId","cleanup","offRouteStarted","offError","allReady","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","testFn","spec","input","standard","validate","result","formatSchemaIssues","defaultLogger","validated","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 * Build a promise that resolves once every route has emitted\n * `route:*:started`, or rejects on `context:error` or the configured\n * routes-ready timeout. Shared by {@link startAndWaitReady} and {@link test}.\n */\n private awaitRoutesReady(): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n if (total === 0) return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n let timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout(\n () => {\n if (settled) return;\n cleanup();\n reject(new Error(\"Timeout waiting for routes to start\"));\n },\n this.routesReadyTimeoutMs,\n );\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 }\n\n /**\n * Start context and resolve once every route has emitted `route:*:started`.\n * Does not drain or stop. Does not await `ctx.start()` completion, which\n * lets this method work with long-running sources (direct, mcp, HTTP, etc.)\n * whose subscribe blocks until the route is aborted. The start promise is\n * stored internally and awaited by {@link stop} for clean shutdown.\n *\n * Use with {@link CraftClient.send} (via `t.client`) for direct endpoints,\n * or drive sources directly via the context store, then call `drain()` /\n * `stop()` when done.\n *\n * If `ctx.start()` rejects (synchronously or before any route emits\n * `route:*:started`), the rejection surfaces here via the\n * `context:error` listener installed by `awaitRoutesReady`. A no-op\n * catch is attached to `startedPromise` as a safety net so that a\n * slow rejection does not trigger an `unhandledRejection` before\n * `stop()` awaits the promise for teardown.\n */\n async startAndWaitReady(): Promise<void> {\n const allReady = this.awaitRoutesReady();\n this.startedPromise = this.ctx.start();\n // Attach a no-op handler so Node does not report the rejection as\n // unhandled before `stop()` re-awaits the promise.\n this.startedPromise.catch(() => {});\n await 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 allReady = this.awaitRoutesReady();\n const started = ctx.start();\n // Shield a synchronous rejection of `started` from becoming an\n // unhandled rejection before the `finally` block re-awaits it.\n started.catch(() => {});\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 type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport {\n formatSchemaIssues,\n logger as defaultLogger,\n rcError,\n} from \"@routecraft/routecraft\";\n\n/**\n * Structural shape of a fn-like spec for testing. Does not import\n * `FnOptions` from `@routecraft/ai` so this package stays free of\n * a reverse dependency. Real `FnOptions` values are structurally\n * assignable here -- the extra `description` field is ignored.\n *\n * @beta\n */\nexport interface TestFnSpec<TIn, TOut> {\n /** Schema whose validated/coerced output is passed to `handler`. */\n input: StandardSchemaV1<unknown, TIn>;\n handler: (input: TIn, ctx: TestFnHandlerContext) => Promise<TOut> | TOut;\n}\n\n/**\n * Synthetic context handed to a fn handler under `testFn`. Mirrors the\n * minimum shape `agentPlugin` provides at production dispatch time\n * (without coupling to that implementation). Extra fields a handler may\n * read at runtime can be added here in follow-ups without breaking the\n * structural contract.\n *\n * @beta\n */\nexport interface TestFnHandlerContext {\n logger: ReturnType<typeof defaultLogger.child>;\n abortSignal: AbortSignal;\n}\n\n/**\n * Options for {@link testFn}.\n *\n * @beta\n */\nexport interface TestFnOptions {\n /** Caller-supplied abort signal. Defaults to a never-firing signal. */\n signal?: AbortSignal;\n /** Caller-supplied logger. Defaults to a child of the framework logger bound to `{ test: \"fn\" }`. */\n logger?: ReturnType<typeof defaultLogger.child>;\n}\n\n/**\n * Run a fn-like spec end-to-end in tests. Validates `input` against the\n * spec's Standard Schema, then calls the handler with a synthetic\n * context. Designed to mirror what `agentPlugin` does internally at\n * production dispatch time, without exposing or depending on that\n * dispatcher.\n *\n * Throws `RC5002` (Validation failed) if the input does not pass the\n * schema. Errors thrown from the handler propagate as-is.\n *\n * @beta\n *\n * @example\n * ```typescript\n * import { testFn } from \"@routecraft/testing\";\n * import { z } from \"zod\";\n *\n * const greet = {\n * description: \"...\",\n * input: z.object({ name: z.string() }),\n * handler: async (input, ctx) => `hello ${input.name}`,\n * };\n *\n * const out = await testFn(greet, { name: \"alice\" });\n * expect(out).toBe(\"hello alice\");\n * ```\n */\nexport async function testFn<TIn, TOut>(\n spec: TestFnSpec<TIn, TOut>,\n input: unknown,\n options: TestFnOptions = {},\n): Promise<TOut> {\n const standard = (spec.input as { [\"~standard\"]?: { validate?: unknown } })[\n \"~standard\"\n ];\n if (typeof standard?.validate !== \"function\") {\n throw rcError(\"RC5003\", undefined, {\n message: `testFn: spec.input must be a Standard Schema with a callable validate.`,\n });\n }\n\n const validate = standard.validate as (\n value: unknown,\n ) =>\n | { value?: unknown; issues?: unknown }\n | Promise<{ value?: unknown; issues?: unknown }>;\n let result = validate(input);\n if (result instanceof Promise) result = await result;\n if (result.issues !== undefined && result.issues !== null) {\n throw rcError(\"RC5002\", undefined, {\n message: `testFn: input validation failed: ${formatSchemaIssues(result.issues)}`,\n });\n }\n\n const ctx: TestFnHandlerContext = {\n logger: options.logger ?? defaultLogger.child({ test: \"fn\" }),\n abortSignal: options.signal ?? new AbortController().signal,\n };\n\n const validated = \"value\" in result ? (result.value as TIn) : (input as TIn);\n return (await spec.handler(validated, ctx)) as TOut;\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// Test helper for fn-like specs (schema + handler). Used to exercise\n// fns registered in `@routecraft/ai`'s agentPlugin without depending on\n// any non-public dispatcher.\nexport {\n testFn,\n type TestFnHandlerContext,\n type TestFnOptions,\n type TestFnSpec,\n} from \"./test-fn\";\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.5",
3
+ "version": "0.5.0-canary.50",
4
4
  "description": "Test utilities for Routecraft routes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -20,13 +20,14 @@
20
20
  "build": "tsup src/index.ts --format esm,cjs --dts",
21
21
  "test": "vitest",
22
22
  "test:coverage": "vitest --coverage",
23
- "prepublishOnly": "pnpm run build"
23
+ "prepublishOnly": "bun run build"
24
24
  },
25
25
  "dependencies": {
26
- "@routecraft/routecraft": "^0.5.0-canary.5"
26
+ "@routecraft/routecraft": "^0.5.0-canary.50",
27
+ "@standard-schema/spec": "^1.1.0"
27
28
  },
28
29
  "devDependencies": {
29
- "vitest": "^4.1.4"
30
+ "vitest": "^4.1.5"
30
31
  },
31
32
  "peerDependencies": {
32
33
  "@routecraft/routecraft": "*",