@routecraft/testing 0.6.0-canary.18 → 0.6.0-canary.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- 'use strict';var fs=require('fs'),routecraft=require('@routecraft/routecraft');function u(){let t,e=[],r=((...o)=>(e.push(o),t?.(...o)));return r.mock={calls:e},r.mockImplementation=o=>(t=o,r),r.mockClear=()=>{e.length=0;},r}function f(t=u){let e={info:t(),debug:t(),warn:t(),error:t(),trace:t(),fatal:t(),child:t()};return e.child.mockImplementation(()=>e),e}function h(t=u){let e=t(),r=t(),o={info:e,debug:e,warn:e,error:e,trace:e,fatal:e,child:r};return r.mockImplementation(()=>o),o}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 P=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 c=class{ctx;client;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;loggerChildRestored=false;startedPromise;constructor(e,r,o){this.ctx=e,this.client=r,this.logger=o?.spyLogger??h(),o?.restoreLoggerChild&&(this.restoreLoggerChild=o.restoreLoggerChild),this.routesReadyTimeoutMs=o?.routesReadyTimeoutMs??P;let s=n=>{this.errors.push(routecraft.isRoutecraftError(n)?n:routecraft.rcError("RC9901",n));};e.on("context:error",n=>{s(n.details.error);});}awaitRoutesReady(){let e=this.ctx,r=e.getRoutes().length;return r===0?Promise.resolve():new Promise((o,s)=>{let n=0,i=false,a=setTimeout(()=>{i||(g(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),d=e.on("route:*:started",(()=>{i||(n++,n>=r&&(g(),o()));})),y=e.on("context:error",k=>{i||(g(),s(k.details.error));});function g(){i||(i=true,d(),y(),a!==void 0&&(clearTimeout(a),a=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,o=this.awaitRoutesReady(),s=r.start();s.catch(()=>{});try{await o;let n=e?.delayBeforeDrainMs??0;n>0&&await new Promise(i=>setTimeout(i,n)),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);}},l=class{builder=new routecraft.ContextBuilder;routesReadyTimeoutMs;adapterOverrides=[];spyFactory;constructor(e){this.spyFactory=e?.fn??u,this.routesReadyTimeoutMs=e?.routesReadyTimeoutMs;}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=f(this.spyFactory),r=routecraft.logger.child.bind(routecraft.logger),o=this.spyFactory();o.mockImplementation(()=>e),routecraft.logger.child=o;let s;try{s=await this.builder.build();}catch(d){throw routecraft.logger.child=r,d}let{context:n,client:i}=s;if(this.adapterOverrides.length>0){let d=n.getStore(routecraft.RC_ADAPTER_OVERRIDES)??[];n.setStore(routecraft.RC_ADAPTER_OVERRIDES,[...d,...this.adapterOverrides]);}let a={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{routecraft.logger.child=r;}};return new c(n,i,a)}};function b(t){return new l(t)}function S(t,e){let r=()=>{throw new Error(`Pseudo adapter "${t}" is not implemented. Replace with a real adapter.`)},o=()=>Promise.resolve(void 0),s=i=>i,n=(i,a,d,y)=>(y?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${t}`,subscribe:e==="noop"?n:r,send:e==="noop"?o:r,process:e==="noop"?s:r}}function M(t="pseudo",e){let r=e?.runtime??"throw";return e&&"args"in e&&e.args==="keyed"?(s,n)=>S(t,r):s=>S(t,r)}function T(){return {received:[],calls:{send:0,process:0,enrich:0}}}function I(){let t=T();return {adapterId:"routecraft.adapter.spy",received:t.received,calls:t.calls,send(e){t.received.push(e),e.headers?.[routecraft.HeadersKeys.OPERATION]==="enrich"?t.calls.enrich++:t.calls.send++;},process(e){return t.received.push(e),t.calls.process++,e},reset(){t.received.length=0,t.calls.send=0,t.calls.process=0,t.calls.enrich=0;},lastReceived(){if(t.received.length===0)throw new Error("SpyAdapter: no exchanges recorded");return t.received[t.received.length-1]},receivedBodies(){return t.received.map(e=>e.body)}}}function K(t,e){return e===void 0?{[routecraft.SOURCE_FIXTURE]:true,body:t}:{[routecraft.SOURCE_FIXTURE]:true,body:t,headers:e}}async function N(t,e,r={}){let o=t.input["~standard"];if(typeof o?.validate!="function")throw routecraft.rcError("RC5003",void 0,{message:"testFn: spec.input must be a Standard Schema with a callable validate."});let s=o.validate,n=s(e);if(n instanceof Promise&&(n=await n),n.issues!==void 0&&n.issues!==null)throw routecraft.rcError("RC5002",void 0,{message:`testFn: input validation failed: ${routecraft.formatSchemaIssues(n.issues)}`});let i={logger:r.logger??routecraft.logger.child({test:"fn"}),abortSignal:r.signal??new AbortController().signal},a="value"in n?n.value:e;return await t.handler(a,i)}function H(t){return JSON.parse(fs.readFileSync(t,"utf-8"))}function se(t,e,r){let o=H(t);if(!Array.isArray(o))throw new Error(`fixture.each: expected JSON array at "${t}", got ${typeof o}`);for(let s of o){if(typeof s?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(s)}`);e(s.name,()=>r(s));}}exports.TestContext=c;exports.TestContextBuilder=l;exports.createNoopSpyLogger=h;exports.createSpyFn=u;exports.createSpyLogger=f;exports.fixture=H;exports.fixtureEach=se;exports.mockAdapter=O;exports.pseudo=M;exports.sourceMessage=K;exports.spy=I;exports.testContext=b;exports.testFn=N;//# sourceMappingURL=index.cjs.map
1
+ 'use strict';var fs=require('fs'),routecraft=require('@routecraft/routecraft');function u(){let t,e=[],r=((...o)=>(e.push(o),t?.(...o)));return r.mock={calls:e},r.mockImplementation=o=>(t=o,r),r.mockClear=()=>{e.length=0;},r}function f(t=u){let e={info:t(),debug:t(),warn:t(),error:t(),trace:t(),fatal:t(),child:t()};return e.child.mockImplementation(()=>e),e}function g(t=u){let e=t(),r=t(),o={info:e,debug:e,warn:e,error:e,trace:e,fatal:e,child:r};return r.mockImplementation(()=>o),o}var m=Symbol.for("routecraft.testing.adapter-mock");function h(t){return typeof t=="object"&&t!==null&&t[m]===true}function P(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 A=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,o){this.ctx=e,this.client=r,this.logger=o?.spyLogger??g(),o?.restoreLoggerChild&&(this.restoreLoggerChild=o.restoreLoggerChild),this.routesReadyTimeoutMs=o?.routesReadyTimeoutMs??A;let s=n=>{this.errors.push(routecraft.isRoutecraftError(n)?n:routecraft.rcError("RC9901",n));};e.on("context:error",n=>{s(n.details.error);});}awaitRoutesReady(){let e=this.ctx,r=e.getRoutes().length;return r===0?Promise.resolve():new Promise((o,s)=>{let n=0,i=false,a=setTimeout(()=>{i||(y(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),d=e.on("route:started",(()=>{i||(n++,n>=r&&(y(),o()));})),k=e.on("context:error",O=>{i||(y(),s(O.details.error));});function y(){i||(i=true,d(),k(),a!==void 0&&(clearTimeout(a),a=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,o=this.awaitRoutesReady(),s=r.start();s.catch(()=>{});try{await o;let n=e?.delayBeforeDrainMs??0;n>0&&await new Promise(i=>setTimeout(i,n)),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);}},l=class{builder=new routecraft.ContextBuilder;routesReadyTimeoutMs;adapterOverrides=[];spyFactory;constructor(e){this.spyFactory=e?.fn??u,this.routesReadyTimeoutMs=e?.routesReadyTimeoutMs;}routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}override(e){let r=h(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=f(this.spyFactory),r=routecraft.logger.child.bind(routecraft.logger),o=this.spyFactory();o.mockImplementation(()=>e),routecraft.logger.child=o;let s;try{s=await this.builder.build();}catch(d){throw routecraft.logger.child=r,d}let{context:n,client:i}=s;if(this.adapterOverrides.length>0){let d=n.getStore(routecraft.RC_ADAPTER_OVERRIDES)??[];n.setStore(routecraft.RC_ADAPTER_OVERRIDES,[...d,...this.adapterOverrides]);}let a={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{routecraft.logger.child=r;}};return new p(n,i,a)}};function M(t){return new l(t)}function S(){return t=>(t.ready(),Promise.resolve())}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=i=>i,n=S();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 w(){return {received:[],calls:{send:0,process:0,enrich:0}}}function K(){let t=w();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 N(t,e){return e===void 0?{[routecraft.SOURCE_FIXTURE]:true,body:t}:{[routecraft.SOURCE_FIXTURE]:true,body:t,headers:e}}async function H(t,e,r={}){let o=t.input["~standard"];if(typeof o?.validate!="function")throw routecraft.rcError("RC5003",void 0,{message:"testFn: spec.input must be a Standard Schema with a callable validate."});let s=o.validate,n=s(e);if(n instanceof Promise&&(n=await n),n.issues!==void 0&&n.issues!==null)throw routecraft.rcError("RC5002",void 0,{message:`testFn: input validation failed: ${routecraft.formatSchemaIssues(n.issues)}`});let i={logger:r.logger??routecraft.logger.child({test:"fn"}),abortSignal:r.signal??new AbortController().signal},a="value"in n?n.value:e;return await t.handler(a,i)}function _(t){let e=t.abortController??new AbortController;return {context:t.context,signal:e.signal,meta:t.meta??{routeId:"test"},ready:t.onReady??(()=>{}),complete:r=>e.abort(r),emit:async r=>await t.handler(r.message,r.headers,r.parse,r.parseFailureMode)}}function U(t){return JSON.parse(fs.readFileSync(t,"utf-8"))}function ce(t,e,r){let o=U(t);if(!Array.isArray(o))throw new Error(`fixture.each: expected JSON array at "${t}", got ${typeof o}`);for(let s of o){if(typeof s?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(s)}`);e(s.name,()=>r(s));}}exports.TestContext=p;exports.TestContextBuilder=l;exports.createNoopSpyLogger=g;exports.createSpyFn=u;exports.createSpyLogger=f;exports.fixture=U;exports.fixtureEach=ce;exports.mockAdapter=P;exports.pseudo=L;exports.sourceMessage=N;exports.spy=K;exports.testContext=M;exports.testFn=H;exports.testSubscription=_;//# sourceMappingURL=index.cjs.map
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
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/source-message.ts","../src/test-fn.ts","../src/index.ts"],"names":["createSpyFn","impl","calls","fn","args","next","createSpyLogger","spy","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","childSpy","built","error","existing","RC_ADAPTER_OVERRIDES","testContext","createAdapter","runtime","fail","noopSend","noopProcess","exchange","noopSubscribe","_context","_handler","_abortController","onReady","pseudo","opts","createSpyState","state","HeadersKeys","sourceMessage","body","headers","SOURCE_FIXTURE","testFn","spec","input","standard","validate","result","formatSchemaIssues","defaultLogger","validated","fixture","path","readFileSync","fixtureEach","test","run","entries"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Minimal runner-agnostic spy function. Records calls in the jest-compatible\n * `mock.calls` shape so assertions like\n * `expect(t.logger.warn.mock.calls.some(...))` work under bun:test, Vitest,\n * and node:test without this package importing any runner.\n *\n * Runner mocks (`vi.fn` from Vitest, `mock` from bun:test) are structurally\n * assignable to this interface, so they can be injected via {@link SpyFactory}\n * when full matcher support (`expect(fn).toHaveBeenCalledWith(...)`) is\n * wanted.\n */\nexport interface SpyFn {\n (...args: unknown[]): unknown;\n mock: { calls: unknown[][] };\n mockImplementation(impl: (...args: unknown[]) => unknown): SpyFn;\n mockClear(): void;\n}\n\n/**\n * Factory producing spy functions. Defaults to the built-in {@link createSpyFn};\n * pass your runner's mock factory (`vi.fn`, or `mock` from bun:test) to get\n * native mocks that work with the runner's `expect` matchers.\n */\nexport type SpyFactory = () => SpyFn;\n\n/**\n * Create a built-in spy function. Dependency-free; records calls in\n * `fn.mock.calls` and supports `mockImplementation` / `mockClear`.\n */\nexport function createSpyFn(): SpyFn {\n let impl: ((...args: unknown[]) => unknown) | undefined;\n const calls: unknown[][] = [];\n const fn = ((...args: unknown[]): unknown => {\n calls.push(args);\n return impl?.(...args);\n }) as SpyFn;\n fn.mock = { calls };\n fn.mockImplementation = (next) => {\n impl = next;\n return fn;\n };\n fn.mockClear = () => {\n calls.length = 0;\n };\n return fn;\n}\n\n/**\n * Spy logger with spy methods for assertions (e.g.\n * `t.logger.info.mock.calls` under any runner, or\n * `expect(t.logger.info).toHaveBeenCalledWith(...)` when built from an\n * injected runner mock factory).\n */\nexport type SpyLogger = {\n info: SpyFn;\n debug: SpyFn;\n warn: SpyFn;\n error: SpyFn;\n trace: SpyFn;\n fatal: SpyFn;\n child: SpyFn;\n};\n\nexport function createSpyLogger(fn: SpyFactory = createSpyFn): SpyLogger {\n const spy: SpyLogger = {\n info: fn(),\n debug: fn(),\n warn: fn(),\n error: fn(),\n trace: fn(),\n fatal: fn(),\n child: fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\nexport function createNoopSpyLogger(fn: SpyFactory = createSpyFn): SpyLogger {\n const noop = fn();\n const childFn = 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 */\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 */\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 * @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 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 { SpyFactory, SpyLogger } from \"./spy-logger\";\nimport {\n createSpyFn,\n createSpyLogger,\n createNoopSpyLogger,\n} 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 * Mock factory used to build the spy logger. Defaults to the built-in\n * runner-agnostic spy. Pass your runner's factory (`vi.fn` from Vitest, or\n * `mock` from bun:test) to get native mocks that work with the runner's\n * `expect` matchers, e.g. `testContext({ fn: vi.fn })`.\n */\n fn?: SpyFactory;\n}\n\n/**\n * Options for TestContext.test().\n */\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 for asserting on log calls.\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. t.logger.info.mock.calls, or expect(t.logger.info).toHaveBeenCalledWith(...) with an injected runner mock factory */\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 */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n private adapterOverrides: AdapterOverride[] = [];\n private readonly spyFactory: SpyFactory;\n\n constructor(options?: TestContextOptions) {\n this.spyFactory = options?.fn ?? createSpyFn;\n this.routesReadyTimeoutMs = options?.routesReadyTimeoutMs;\n }\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 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(this.spyFactory);\n const originalChild = logger.child.bind(logger);\n const childSpy = this.spyFactory();\n childSpy.mockImplementation(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n );\n logger.child = childSpy as unknown as typeof logger.child;\n // Restore the patched child on build failure: the restore hook is only\n // handed to TestContext after a successful build, so without this a\n // rejected build (e.g. an invalid route) would leak the stub into every\n // later test in the process.\n let built: Awaited<ReturnType<ContextBuilder[\"build\"]>>;\n try {\n built = await this.builder.build();\n } catch (error) {\n logger.child = originalChild;\n throw error;\n }\n const { context: ctx, client } = built;\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 * Runner-agnostic by default: the spy logger uses a built-in spy that records\n * calls in the jest-compatible `mock.calls` shape. Pass `{ fn }` with your\n * runner's mock factory (`vi.fn` from Vitest, `mock` from bun:test) when you\n * want `expect(t.logger.info).toHaveBeenCalledWith(...)` matcher support.\n *\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n *\n * @example\n * // Vitest, with native matcher support:\n * const t = await testContext({ fn: vi.fn }).routes(myRoutes).build();\n */\nexport function testContext(options?: TestContextOptions): TestContextBuilder {\n return new TestContextBuilder(options);\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// 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 * @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 {\n SOURCE_FIXTURE,\n type ExchangeHeaders,\n type SourceFixture,\n} from \"@routecraft/routecraft\";\n\n/**\n * Wrap a message body with the headers a real source would have attached, for\n * use as a `mockAdapter(..., { source: [...] })` fixture.\n *\n * Envelope-carrying sources (mail, http) split each incoming message into a\n * payload on `exchange.body` and metadata on `routecraft.<adapter>.*` headers.\n * A bare fixture array only sets the body, so a route that reads those headers\n * cannot be exercised through the mock. `sourceMessage(body, headers)` lets the\n * mock reproduce that split.\n *\n * @example\n * ```typescript\n * const mailMock = mockAdapter(mail, {\n * source: [\n * sourceMessage(\n * { text: \"Tracking: ABC123\" },\n * {\n * \"routecraft.mail.from\": \"noreply@acme.test\",\n * \"routecraft.mail.subject\": \"Your order has shipped\",\n * },\n * ),\n * ],\n * });\n * ```\n *\n * @param body - The body the source would deliver on the exchange\n * @param headers - Headers the source would attach (omit for a body-only fixture)\n * @returns A branded fixture recognised by the source-mock dispatcher\n */\nexport function sourceMessage<M>(\n body: M,\n headers?: ExchangeHeaders,\n): SourceFixture<M> {\n return headers === undefined\n ? { [SOURCE_FIXTURE]: true, body }\n : { [SOURCE_FIXTURE]: true, body, headers };\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 */\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 */\nexport interface TestFnHandlerContext {\n logger: ReturnType<typeof defaultLogger.child>;\n abortSignal: AbortSignal;\n}\n\n/**\n * Options for {@link testFn}.\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 * @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\";\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 createSpyFn,\n createSpyLogger,\n createNoopSpyLogger,\n type SpyFn,\n type SpyFactory,\n type SpyLogger,\n} from \"./spy-logger\";\n\n// Re-export pseudo adapter\nexport {\n pseudo,\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// Source fixture helper: attach headers to a source-role mock fixture so\n// routes that read `routecraft.<adapter>.*` headers can be exercised.\nexport { sourceMessage } from \"./source-message\";\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 * @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/**\n * Fixture entry must have a `name` field used as the test name.\n */\nexport interface FixtureWithName {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * A test runner's `test` function, as accepted by {@link fixtureEach}.\n * Both `test` from bun:test and `test` from Vitest satisfy this shape.\n */\nexport type FixtureTestFn = (\n name: string,\n fn: () => void | Promise<void>,\n) => unknown;\n\n/**\n * Load a JSON array fixture and run one test per entry. Each entry must have\n * a `name` field (used as the test name). Runner-agnostic: pass your runner's\n * `test` function (bun:test, Vitest, node:test).\n *\n * @param path Path to a JSON file that parses to an array\n * @param test The runner's `test` function used to register each entry\n * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.\n *\n * @example\n * import { test } from \"bun:test\";\n * fixtureEach(\"./cases.json\", test, (entry) => {\n * expect(run(entry.input)).toEqual(entry.expected);\n * });\n */\nexport function fixtureEach<T extends FixtureWithName>(\n path: string,\n test: FixtureTestFn,\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/shared.ts","../src/adapters/pseudo/index.ts","../src/adapters/spy/shared.ts","../src/adapters/spy/index.ts","../src/source-message.ts","../src/test-fn.ts","../src/test-subscription.ts","../src/index.ts"],"names":["createSpyFn","impl","calls","fn","args","next","createSpyLogger","spy","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","childSpy","built","error","existing","RC_ADAPTER_OVERRIDES","testContext","createNoopSubscribe","sub","createAdapter","runtime","fail","noopSend","noopProcess","exchange","noopSubscribe","pseudo","opts","createSpyState","state","HeadersKeys","sourceMessage","body","headers","SOURCE_FIXTURE","testFn","spec","input","standard","validate","result","formatSchemaIssues","defaultLogger","validated","testSubscription","controller","reason","msg","fixture","path","readFileSync","fixtureEach","test","run","entries"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Minimal runner-agnostic spy function. Records calls in the jest-compatible\n * `mock.calls` shape so assertions like\n * `expect(t.logger.warn.mock.calls.some(...))` work under bun:test, Vitest,\n * and node:test without this package importing any runner.\n *\n * Runner mocks (`vi.fn` from Vitest, `mock` from bun:test) are structurally\n * assignable to this interface, so they can be injected via {@link SpyFactory}\n * when full matcher support (`expect(fn).toHaveBeenCalledWith(...)`) is\n * wanted.\n */\nexport interface SpyFn {\n (...args: unknown[]): unknown;\n mock: { calls: unknown[][] };\n mockImplementation(impl: (...args: unknown[]) => unknown): SpyFn;\n mockClear(): void;\n}\n\n/**\n * Factory producing spy functions. Defaults to the built-in {@link createSpyFn};\n * pass your runner's mock factory (`vi.fn`, or `mock` from bun:test) to get\n * native mocks that work with the runner's `expect` matchers.\n */\nexport type SpyFactory = () => SpyFn;\n\n/**\n * Create a built-in spy function. Dependency-free; records calls in\n * `fn.mock.calls` and supports `mockImplementation` / `mockClear`.\n */\nexport function createSpyFn(): SpyFn {\n let impl: ((...args: unknown[]) => unknown) | undefined;\n const calls: unknown[][] = [];\n const fn = ((...args: unknown[]): unknown => {\n calls.push(args);\n return impl?.(...args);\n }) as SpyFn;\n fn.mock = { calls };\n fn.mockImplementation = (next) => {\n impl = next;\n return fn;\n };\n fn.mockClear = () => {\n calls.length = 0;\n };\n return fn;\n}\n\n/**\n * Spy logger with spy methods for assertions (e.g.\n * `t.logger.info.mock.calls` under any runner, or\n * `expect(t.logger.info).toHaveBeenCalledWith(...)` when built from an\n * injected runner mock factory).\n */\nexport type SpyLogger = {\n info: SpyFn;\n debug: SpyFn;\n warn: SpyFn;\n error: SpyFn;\n trace: SpyFn;\n fatal: SpyFn;\n child: SpyFn;\n};\n\nexport function createSpyLogger(fn: SpyFactory = createSpyFn): SpyLogger {\n const spy: SpyLogger = {\n info: fn(),\n debug: fn(),\n warn: fn(),\n error: fn(),\n trace: fn(),\n fatal: fn(),\n child: fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\nexport function createNoopSpyLogger(fn: SpyFactory = createSpyFn): SpyLogger {\n const noop = fn();\n const childFn = 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 */\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 */\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 * @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 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 { SpyFactory, SpyLogger } from \"./spy-logger\";\nimport {\n createSpyFn,\n createSpyLogger,\n createNoopSpyLogger,\n} 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 * Mock factory used to build the spy logger. Defaults to the built-in\n * runner-agnostic spy. Pass your runner's factory (`vi.fn` from Vitest, or\n * `mock` from bun:test) to get native mocks that work with the runner's\n * `expect` matchers, e.g. `testContext({ fn: vi.fn })`.\n */\n fn?: SpyFactory;\n}\n\n/**\n * Options for TestContext.test().\n */\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 for asserting on log calls.\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. t.logger.info.mock.calls, or expect(t.logger.info).toHaveBeenCalledWith(...) with an injected runner mock factory */\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(\"route:started\", (() => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n }) as EventHandler<EventName>);\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 */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n private adapterOverrides: AdapterOverride[] = [];\n private readonly spyFactory: SpyFactory;\n\n constructor(options?: TestContextOptions) {\n this.spyFactory = options?.fn ?? createSpyFn;\n this.routesReadyTimeoutMs = options?.routesReadyTimeoutMs;\n }\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 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(this.spyFactory);\n const originalChild = logger.child.bind(logger);\n const childSpy = this.spyFactory();\n childSpy.mockImplementation(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n );\n logger.child = childSpy as unknown as typeof logger.child;\n // Restore the patched child on build failure: the restore hook is only\n // handed to TestContext after a successful build, so without this a\n // rejected build (e.g. an invalid route) would leak the stub into every\n // later test in the process.\n let built: Awaited<ReturnType<ContextBuilder[\"build\"]>>;\n try {\n built = await this.builder.build();\n } catch (error) {\n logger.child = originalChild;\n throw error;\n }\n const { context: ctx, client } = built;\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 * Runner-agnostic by default: the spy logger uses a built-in spy that records\n * calls in the jest-compatible `mock.calls` shape. Pass `{ fn }` with your\n * runner's mock factory (`vi.fn` from Vitest, `mock` from bun:test) when you\n * want `expect(t.logger.info).toHaveBeenCalledWith(...)` matcher support.\n *\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n *\n * @example\n * // Vitest, with native matcher support:\n * const t = await testContext({ fn: vi.fn }).routes(myRoutes).build();\n */\nexport function testContext(options?: TestContextOptions): TestContextBuilder {\n return new TestContextBuilder(options);\n}\n","import type { Exchange } from \"@routecraft/routecraft\";\n\nexport interface PseudoOptions {\n runtime?: \"throw\" | \"noop\";\n}\n\nexport interface PseudoKeyedOptions extends PseudoOptions {\n args: \"keyed\";\n}\n\n/**\n * Creates a noop send function that resolves to undefined.\n */\nexport function createNoopSend<R>(): () => Promise<R> {\n return (): Promise<R> => Promise.resolve(undefined as unknown as R);\n}\n\n/**\n * Creates a noop process function that returns the exchange as-is.\n */\nexport function createNoopProcess<R>(): <T>(\n exchange: Exchange<T>,\n) => Exchange<R> {\n return <T>(exchange: Exchange<T>): Exchange<R> =>\n exchange as unknown as Exchange<R>;\n}\n\n/**\n * Creates a noop subscribe function that signals readiness immediately.\n */\nexport function createNoopSubscribe(): (\n sub: import(\"@routecraft/routecraft\").Subscription,\n) => Promise<void> {\n return (sub): Promise<void> => {\n sub.ready();\n return Promise.resolve();\n };\n}\n\n/**\n * Creates a function that throws an error indicating the adapter is not implemented.\n */\nexport function createFailFunction(name: string): () => never {\n return (): never => {\n throw new Error(\n `Pseudo adapter \"${name}\" is not implemented. Replace with a real adapter.`,\n );\n };\n}\n","import type { Source, Destination, Processor } from \"@routecraft/routecraft\";\nimport {\n createNoopSubscribe,\n type PseudoOptions,\n type PseudoKeyedOptions,\n} 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 = createNoopSubscribe();\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// 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 * @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 {\n SOURCE_FIXTURE,\n type ExchangeHeaders,\n type SourceFixture,\n} from \"@routecraft/routecraft\";\n\n/**\n * Wrap a message body with the headers a real source would have attached, for\n * use as a `mockAdapter(..., { source: [...] })` fixture.\n *\n * Envelope-carrying sources (mail, http) split each incoming message into a\n * payload on `exchange.body` and metadata on `routecraft.<adapter>.*` headers.\n * A bare fixture array only sets the body, so a route that reads those headers\n * cannot be exercised through the mock. `sourceMessage(body, headers)` lets the\n * mock reproduce that split.\n *\n * @example\n * ```typescript\n * const mailMock = mockAdapter(mail, {\n * source: [\n * sourceMessage(\n * { text: \"Tracking: ABC123\" },\n * {\n * \"routecraft.mail.from\": \"noreply@acme.test\",\n * \"routecraft.mail.subject\": \"Your order has shipped\",\n * },\n * ),\n * ],\n * });\n * ```\n *\n * @param body - The body the source would deliver on the exchange\n * @param headers - Headers the source would attach (omit for a body-only fixture)\n * @returns A branded fixture recognised by the source-mock dispatcher\n */\nexport function sourceMessage<M>(\n body: M,\n headers?: ExchangeHeaders,\n): SourceFixture<M> {\n return headers === undefined\n ? { [SOURCE_FIXTURE]: true, body }\n : { [SOURCE_FIXTURE]: true, body, headers };\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 */\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 */\nexport interface TestFnHandlerContext {\n logger: ReturnType<typeof defaultLogger.child>;\n abortSignal: AbortSignal;\n}\n\n/**\n * Options for {@link testFn}.\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 * @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 type {\n CraftContext,\n Exchange,\n ExchangeHeaders,\n OnParseError,\n SourceMeta,\n Subscription,\n} from \"@routecraft/routecraft\";\n\n/**\n * Options for {@link testSubscription}. The `handler` mirrors the message\n * fields the engine receives, flattened into positional arguments so test\n * assertions read naturally.\n */\nexport interface TestSubscriptionOptions<T = unknown> {\n /** Context handed to the source (store access, logger). */\n context: CraftContext;\n /** Receives each emitted message; its return value resolves `emit`. */\n handler: (\n message: T,\n headers?: ExchangeHeaders,\n parse?: (raw: unknown) => unknown | Promise<unknown>,\n parseFailureMode?: OnParseError,\n ) => Promise<unknown> | unknown;\n /** Abort to stop the source; `complete()` calls its `abort`. */\n abortController?: AbortController;\n /** Called when the source signals readiness. */\n onReady?: () => void;\n /** Source meta; defaults to `{ routeId: \"test\" }`. */\n meta?: SourceMeta;\n}\n\n/**\n * Build a {@link Subscription} for driving a source adapter directly in a\n * unit test, without a running route. Wire-up mirrors the engine: `emit`\n * forwards to `handler`, `ready` to `onReady`, `complete` aborts the\n * controller, and `signal` observes it.\n *\n * @example\n * ```typescript\n * const received: unknown[] = [];\n * await adapter.subscribe(\n * testSubscription({\n * context: t.ctx,\n * handler: (message) => void received.push(message),\n * abortController,\n * }),\n * );\n * ```\n */\nexport function testSubscription<T = unknown>(\n options: TestSubscriptionOptions<T>,\n): Subscription<T> {\n const controller = options.abortController ?? new AbortController();\n return {\n context: options.context,\n signal: controller.signal,\n meta: options.meta ?? { routeId: \"test\" },\n ready: options.onReady ?? (() => {}),\n complete: (reason?: unknown) => controller.abort(reason),\n emit: async (msg) =>\n (await options.handler(\n msg.message,\n msg.headers,\n msg.parse,\n msg.parseFailureMode,\n )) as Exchange,\n };\n}\n","import { readFileSync } from \"node:fs\";\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 createSpyFn,\n createSpyLogger,\n createNoopSpyLogger,\n type SpyFn,\n type SpyFactory,\n type SpyLogger,\n} from \"./spy-logger\";\n\n// Re-export pseudo adapter\nexport {\n pseudo,\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// Source fixture helper: attach headers to a source-role mock fixture so\n// routes that read `routecraft.<adapter>.*` headers can be exercised.\nexport { sourceMessage } from \"./source-message\";\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 * @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/**\n * Fixture entry must have a `name` field used as the test name.\n */\nexport interface FixtureWithName {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * A test runner's `test` function, as accepted by {@link fixtureEach}.\n * Both `test` from bun:test and `test` from Vitest satisfy this shape.\n */\nexport type FixtureTestFn = (\n name: string,\n fn: () => void | Promise<void>,\n) => unknown;\n\n/**\n * Load a JSON array fixture and run one test per entry. Each entry must have\n * a `name` field (used as the test name). Runner-agnostic: pass your runner's\n * `test` function (bun:test, Vitest, node:test).\n *\n * @param path Path to a JSON file that parses to an array\n * @param test The runner's `test` function used to register each entry\n * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.\n *\n * @example\n * import { test } from \"bun:test\";\n * fixtureEach(\"./cases.json\", test, (entry) => {\n * expect(run(entry.input)).toEqual(entry.expected);\n * });\n */\nexport function fixtureEach<T extends FixtureWithName>(\n path: string,\n test: FixtureTestFn,\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\n// Subscription builder for driving source adapters directly in unit tests\nexport {\n testSubscription,\n type TestSubscriptionOptions,\n} from \"./test-subscription\";\n"]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { AdapterOverride, AdapterSourceCall, AdapterSendCall, SourceOverrideBehavior, SendOverrideHandler, Source, CraftContext, CraftClient, RoutecraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Destination, Processor, Exchange, ExchangeHeaders, SourceFixture, logger } from '@routecraft/routecraft';
1
+ import { AdapterOverride, AdapterSourceCall, AdapterSendCall, SourceOverrideBehavior, SendOverrideHandler, Source, CraftContext, CraftClient, RoutecraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Destination, Processor, Exchange, ExchangeHeaders, SourceFixture, logger, OnParseError, SourceMeta, Subscription } from '@routecraft/routecraft';
2
2
  import { StandardSchemaV1 } from '@standard-schema/spec';
3
3
 
4
4
  /**
@@ -201,12 +201,12 @@ declare class TestContext {
201
201
  });
202
202
  /**
203
203
  * Build a promise that resolves once every route has emitted
204
- * `route:*:started`, or rejects on `context:error` or the configured
204
+ * `route:started`, or rejects on `context:error` or the configured
205
205
  * routes-ready timeout. Shared by {@link startAndWaitReady} and {@link test}.
206
206
  */
207
207
  private awaitRoutesReady;
208
208
  /**
209
- * Start context and resolve once every route has emitted `route:*:started`.
209
+ * Start context and resolve once every route has emitted `route:started`.
210
210
  * Does not drain or stop. Does not await `ctx.start()` completion, which
211
211
  * lets this method work with long-running sources (direct, mcp, HTTP, etc.)
212
212
  * whose subscribe blocks until the route is aborted. The start promise is
@@ -217,7 +217,7 @@ declare class TestContext {
217
217
  * `stop()` when done.
218
218
  *
219
219
  * If `ctx.start()` rejects (synchronously or before any route emits
220
- * `route:*:started`), the rejection surfaces here via the
220
+ * `route:started`), the rejection surfaces here via the
221
221
  * `context:error` listener installed by `awaitRoutesReady`. A no-op
222
222
  * catch is attached to `startedPromise` as a safety net so that a
223
223
  * slow rejection does not trigger an `unhandledRejection` before
@@ -438,6 +438,43 @@ interface TestFnOptions {
438
438
  */
439
439
  declare function testFn<TIn, TOut>(spec: TestFnSpec<TIn, TOut>, input: unknown, options?: TestFnOptions): Promise<TOut>;
440
440
 
441
+ /**
442
+ * Options for {@link testSubscription}. The `handler` mirrors the message
443
+ * fields the engine receives, flattened into positional arguments so test
444
+ * assertions read naturally.
445
+ */
446
+ interface TestSubscriptionOptions<T = unknown> {
447
+ /** Context handed to the source (store access, logger). */
448
+ context: CraftContext;
449
+ /** Receives each emitted message; its return value resolves `emit`. */
450
+ handler: (message: T, headers?: ExchangeHeaders, parse?: (raw: unknown) => unknown | Promise<unknown>, parseFailureMode?: OnParseError) => Promise<unknown> | unknown;
451
+ /** Abort to stop the source; `complete()` calls its `abort`. */
452
+ abortController?: AbortController;
453
+ /** Called when the source signals readiness. */
454
+ onReady?: () => void;
455
+ /** Source meta; defaults to `{ routeId: "test" }`. */
456
+ meta?: SourceMeta;
457
+ }
458
+ /**
459
+ * Build a {@link Subscription} for driving a source adapter directly in a
460
+ * unit test, without a running route. Wire-up mirrors the engine: `emit`
461
+ * forwards to `handler`, `ready` to `onReady`, `complete` aborts the
462
+ * controller, and `signal` observes it.
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * const received: unknown[] = [];
467
+ * await adapter.subscribe(
468
+ * testSubscription({
469
+ * context: t.ctx,
470
+ * handler: (message) => void received.push(message),
471
+ * abortController,
472
+ * }),
473
+ * );
474
+ * ```
475
+ */
476
+ declare function testSubscription<T = unknown>(options: TestSubscriptionOptions<T>): Subscription<T>;
477
+
441
478
  /**
442
479
  * Load a JSON fixture file and return the parsed value.
443
480
  *
@@ -474,4 +511,4 @@ type FixtureTestFn = (name: string, fn: () => void | Promise<void>) => unknown;
474
511
  */
475
512
  declare function fixtureEach<T extends FixtureWithName>(path: string, test: FixtureTestFn, run: (entry: T) => void | Promise<void>): void;
476
513
 
477
- export { type AdapterMock, type FixtureTestFn, type FixtureWithName, type MockAdapterBehavior, type PseudoKeyedOptions, type PseudoOptions, type SpyAdapter, type SpyFactory, type SpyFn, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestFnHandlerContext, type TestFnOptions, type TestFnSpec, type TestOptions, createNoopSpyLogger, createSpyFn, createSpyLogger, fixture, fixtureEach, mockAdapter, pseudo, sourceMessage, spy, testContext, testFn };
514
+ export { type AdapterMock, type FixtureTestFn, type FixtureWithName, type MockAdapterBehavior, type PseudoKeyedOptions, type PseudoOptions, type SpyAdapter, type SpyFactory, type SpyFn, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestFnHandlerContext, type TestFnOptions, type TestFnSpec, type TestOptions, type TestSubscriptionOptions, createNoopSpyLogger, createSpyFn, createSpyLogger, fixture, fixtureEach, mockAdapter, pseudo, sourceMessage, spy, testContext, testFn, testSubscription };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AdapterOverride, AdapterSourceCall, AdapterSendCall, SourceOverrideBehavior, SendOverrideHandler, Source, CraftContext, CraftClient, RoutecraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Destination, Processor, Exchange, ExchangeHeaders, SourceFixture, logger } from '@routecraft/routecraft';
1
+ import { AdapterOverride, AdapterSourceCall, AdapterSendCall, SourceOverrideBehavior, SendOverrideHandler, Source, CraftContext, CraftClient, RoutecraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Destination, Processor, Exchange, ExchangeHeaders, SourceFixture, logger, OnParseError, SourceMeta, Subscription } from '@routecraft/routecraft';
2
2
  import { StandardSchemaV1 } from '@standard-schema/spec';
3
3
 
4
4
  /**
@@ -201,12 +201,12 @@ declare class TestContext {
201
201
  });
202
202
  /**
203
203
  * Build a promise that resolves once every route has emitted
204
- * `route:*:started`, or rejects on `context:error` or the configured
204
+ * `route:started`, or rejects on `context:error` or the configured
205
205
  * routes-ready timeout. Shared by {@link startAndWaitReady} and {@link test}.
206
206
  */
207
207
  private awaitRoutesReady;
208
208
  /**
209
- * Start context and resolve once every route has emitted `route:*:started`.
209
+ * Start context and resolve once every route has emitted `route:started`.
210
210
  * Does not drain or stop. Does not await `ctx.start()` completion, which
211
211
  * lets this method work with long-running sources (direct, mcp, HTTP, etc.)
212
212
  * whose subscribe blocks until the route is aborted. The start promise is
@@ -217,7 +217,7 @@ declare class TestContext {
217
217
  * `stop()` when done.
218
218
  *
219
219
  * If `ctx.start()` rejects (synchronously or before any route emits
220
- * `route:*:started`), the rejection surfaces here via the
220
+ * `route:started`), the rejection surfaces here via the
221
221
  * `context:error` listener installed by `awaitRoutesReady`. A no-op
222
222
  * catch is attached to `startedPromise` as a safety net so that a
223
223
  * slow rejection does not trigger an `unhandledRejection` before
@@ -438,6 +438,43 @@ interface TestFnOptions {
438
438
  */
439
439
  declare function testFn<TIn, TOut>(spec: TestFnSpec<TIn, TOut>, input: unknown, options?: TestFnOptions): Promise<TOut>;
440
440
 
441
+ /**
442
+ * Options for {@link testSubscription}. The `handler` mirrors the message
443
+ * fields the engine receives, flattened into positional arguments so test
444
+ * assertions read naturally.
445
+ */
446
+ interface TestSubscriptionOptions<T = unknown> {
447
+ /** Context handed to the source (store access, logger). */
448
+ context: CraftContext;
449
+ /** Receives each emitted message; its return value resolves `emit`. */
450
+ handler: (message: T, headers?: ExchangeHeaders, parse?: (raw: unknown) => unknown | Promise<unknown>, parseFailureMode?: OnParseError) => Promise<unknown> | unknown;
451
+ /** Abort to stop the source; `complete()` calls its `abort`. */
452
+ abortController?: AbortController;
453
+ /** Called when the source signals readiness. */
454
+ onReady?: () => void;
455
+ /** Source meta; defaults to `{ routeId: "test" }`. */
456
+ meta?: SourceMeta;
457
+ }
458
+ /**
459
+ * Build a {@link Subscription} for driving a source adapter directly in a
460
+ * unit test, without a running route. Wire-up mirrors the engine: `emit`
461
+ * forwards to `handler`, `ready` to `onReady`, `complete` aborts the
462
+ * controller, and `signal` observes it.
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * const received: unknown[] = [];
467
+ * await adapter.subscribe(
468
+ * testSubscription({
469
+ * context: t.ctx,
470
+ * handler: (message) => void received.push(message),
471
+ * abortController,
472
+ * }),
473
+ * );
474
+ * ```
475
+ */
476
+ declare function testSubscription<T = unknown>(options: TestSubscriptionOptions<T>): Subscription<T>;
477
+
441
478
  /**
442
479
  * Load a JSON fixture file and return the parsed value.
443
480
  *
@@ -474,4 +511,4 @@ type FixtureTestFn = (name: string, fn: () => void | Promise<void>) => unknown;
474
511
  */
475
512
  declare function fixtureEach<T extends FixtureWithName>(path: string, test: FixtureTestFn, run: (entry: T) => void | Promise<void>): void;
476
513
 
477
- export { type AdapterMock, type FixtureTestFn, type FixtureWithName, type MockAdapterBehavior, type PseudoKeyedOptions, type PseudoOptions, type SpyAdapter, type SpyFactory, type SpyFn, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestFnHandlerContext, type TestFnOptions, type TestFnSpec, type TestOptions, createNoopSpyLogger, createSpyFn, createSpyLogger, fixture, fixtureEach, mockAdapter, pseudo, sourceMessage, spy, testContext, testFn };
514
+ export { type AdapterMock, type FixtureTestFn, type FixtureWithName, type MockAdapterBehavior, type PseudoKeyedOptions, type PseudoOptions, type SpyAdapter, type SpyFactory, type SpyFn, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestFnHandlerContext, type TestFnOptions, type TestFnSpec, type TestOptions, type TestSubscriptionOptions, createNoopSpyLogger, createSpyFn, createSpyLogger, fixture, fixtureEach, mockAdapter, pseudo, sourceMessage, spy, testContext, testFn, testSubscription };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import {readFileSync}from'fs';import {ContextBuilder,logger,RC_ADAPTER_OVERRIDES,HeadersKeys,SOURCE_FIXTURE,rcError,formatSchemaIssues,isRoutecraftError}from'@routecraft/routecraft';function u(){let t,e=[],r=((...o)=>(e.push(o),t?.(...o)));return r.mock={calls:e},r.mockImplementation=o=>(t=o,r),r.mockClear=()=>{e.length=0;},r}function f(t=u){let e={info:t(),debug:t(),warn:t(),error:t(),trace:t(),fatal:t(),child:t()};return e.child.mockImplementation(()=>e),e}function h(t=u){let e=t(),r=t(),o={info:e,debug:e,warn:e,error:e,trace:e,fatal:e,child:r};return r.mockImplementation(()=>o),o}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 P=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 c=class{ctx;client;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;loggerChildRestored=false;startedPromise;constructor(e,r,o){this.ctx=e,this.client=r,this.logger=o?.spyLogger??h(),o?.restoreLoggerChild&&(this.restoreLoggerChild=o.restoreLoggerChild),this.routesReadyTimeoutMs=o?.routesReadyTimeoutMs??P;let s=n=>{this.errors.push(isRoutecraftError(n)?n:rcError("RC9901",n));};e.on("context:error",n=>{s(n.details.error);});}awaitRoutesReady(){let e=this.ctx,r=e.getRoutes().length;return r===0?Promise.resolve():new Promise((o,s)=>{let n=0,i=false,a=setTimeout(()=>{i||(g(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),d=e.on("route:*:started",(()=>{i||(n++,n>=r&&(g(),o()));})),y=e.on("context:error",k=>{i||(g(),s(k.details.error));});function g(){i||(i=true,d(),y(),a!==void 0&&(clearTimeout(a),a=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,o=this.awaitRoutesReady(),s=r.start();s.catch(()=>{});try{await o;let n=e?.delayBeforeDrainMs??0;n>0&&await new Promise(i=>setTimeout(i,n)),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);}},l=class{builder=new ContextBuilder;routesReadyTimeoutMs;adapterOverrides=[];spyFactory;constructor(e){this.spyFactory=e?.fn??u,this.routesReadyTimeoutMs=e?.routesReadyTimeoutMs;}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=f(this.spyFactory),r=logger.child.bind(logger),o=this.spyFactory();o.mockImplementation(()=>e),logger.child=o;let s;try{s=await this.builder.build();}catch(d){throw logger.child=r,d}let{context:n,client:i}=s;if(this.adapterOverrides.length>0){let d=n.getStore(RC_ADAPTER_OVERRIDES)??[];n.setStore(RC_ADAPTER_OVERRIDES,[...d,...this.adapterOverrides]);}let a={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{logger.child=r;}};return new c(n,i,a)}};function b(t){return new l(t)}function S(t,e){let r=()=>{throw new Error(`Pseudo adapter "${t}" is not implemented. Replace with a real adapter.`)},o=()=>Promise.resolve(void 0),s=i=>i,n=(i,a,d,y)=>(y?.(),Promise.resolve());return {adapterId:`routecraft.adapter.pseudo.${t}`,subscribe:e==="noop"?n:r,send:e==="noop"?o:r,process:e==="noop"?s:r}}function M(t="pseudo",e){let r=e?.runtime??"throw";return e&&"args"in e&&e.args==="keyed"?(s,n)=>S(t,r):s=>S(t,r)}function T(){return {received:[],calls:{send:0,process:0,enrich:0}}}function I(){let t=T();return {adapterId:"routecraft.adapter.spy",received:t.received,calls:t.calls,send(e){t.received.push(e),e.headers?.[HeadersKeys.OPERATION]==="enrich"?t.calls.enrich++:t.calls.send++;},process(e){return t.received.push(e),t.calls.process++,e},reset(){t.received.length=0,t.calls.send=0,t.calls.process=0,t.calls.enrich=0;},lastReceived(){if(t.received.length===0)throw new Error("SpyAdapter: no exchanges recorded");return t.received[t.received.length-1]},receivedBodies(){return t.received.map(e=>e.body)}}}function K(t,e){return e===void 0?{[SOURCE_FIXTURE]:true,body:t}:{[SOURCE_FIXTURE]:true,body:t,headers:e}}async function N(t,e,r={}){let o=t.input["~standard"];if(typeof o?.validate!="function")throw rcError("RC5003",void 0,{message:"testFn: spec.input must be a Standard Schema with a callable validate."});let s=o.validate,n=s(e);if(n instanceof Promise&&(n=await n),n.issues!==void 0&&n.issues!==null)throw rcError("RC5002",void 0,{message:`testFn: input validation failed: ${formatSchemaIssues(n.issues)}`});let i={logger:r.logger??logger.child({test:"fn"}),abortSignal:r.signal??new AbortController().signal},a="value"in n?n.value:e;return await t.handler(a,i)}function H(t){return JSON.parse(readFileSync(t,"utf-8"))}function se(t,e,r){let o=H(t);if(!Array.isArray(o))throw new Error(`fixture.each: expected JSON array at "${t}", got ${typeof o}`);for(let s of o){if(typeof s?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(s)}`);e(s.name,()=>r(s));}}export{c as TestContext,l as TestContextBuilder,h as createNoopSpyLogger,u as createSpyFn,f as createSpyLogger,H as fixture,se as fixtureEach,O as mockAdapter,M as pseudo,K as sourceMessage,I as spy,b as testContext,N as testFn};//# sourceMappingURL=index.js.map
1
+ import {readFileSync}from'fs';import {ContextBuilder,logger,RC_ADAPTER_OVERRIDES,HeadersKeys,SOURCE_FIXTURE,rcError,formatSchemaIssues,isRoutecraftError}from'@routecraft/routecraft';function u(){let t,e=[],r=((...o)=>(e.push(o),t?.(...o)));return r.mock={calls:e},r.mockImplementation=o=>(t=o,r),r.mockClear=()=>{e.length=0;},r}function f(t=u){let e={info:t(),debug:t(),warn:t(),error:t(),trace:t(),fatal:t(),child:t()};return e.child.mockImplementation(()=>e),e}function g(t=u){let e=t(),r=t(),o={info:e,debug:e,warn:e,error:e,trace:e,fatal:e,child:r};return r.mockImplementation(()=>o),o}var m=Symbol.for("routecraft.testing.adapter-mock");function h(t){return typeof t=="object"&&t!==null&&t[m]===true}function P(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 A=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,o){this.ctx=e,this.client=r,this.logger=o?.spyLogger??g(),o?.restoreLoggerChild&&(this.restoreLoggerChild=o.restoreLoggerChild),this.routesReadyTimeoutMs=o?.routesReadyTimeoutMs??A;let s=n=>{this.errors.push(isRoutecraftError(n)?n:rcError("RC9901",n));};e.on("context:error",n=>{s(n.details.error);});}awaitRoutesReady(){let e=this.ctx,r=e.getRoutes().length;return r===0?Promise.resolve():new Promise((o,s)=>{let n=0,i=false,a=setTimeout(()=>{i||(y(),s(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),d=e.on("route:started",(()=>{i||(n++,n>=r&&(y(),o()));})),k=e.on("context:error",O=>{i||(y(),s(O.details.error));});function y(){i||(i=true,d(),k(),a!==void 0&&(clearTimeout(a),a=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,o=this.awaitRoutesReady(),s=r.start();s.catch(()=>{});try{await o;let n=e?.delayBeforeDrainMs??0;n>0&&await new Promise(i=>setTimeout(i,n)),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);}},l=class{builder=new ContextBuilder;routesReadyTimeoutMs;adapterOverrides=[];spyFactory;constructor(e){this.spyFactory=e?.fn??u,this.routesReadyTimeoutMs=e?.routesReadyTimeoutMs;}routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}override(e){let r=h(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=f(this.spyFactory),r=logger.child.bind(logger),o=this.spyFactory();o.mockImplementation(()=>e),logger.child=o;let s;try{s=await this.builder.build();}catch(d){throw logger.child=r,d}let{context:n,client:i}=s;if(this.adapterOverrides.length>0){let d=n.getStore(RC_ADAPTER_OVERRIDES)??[];n.setStore(RC_ADAPTER_OVERRIDES,[...d,...this.adapterOverrides]);}let a={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{logger.child=r;}};return new p(n,i,a)}};function M(t){return new l(t)}function S(){return t=>(t.ready(),Promise.resolve())}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=i=>i,n=S();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 w(){return {received:[],calls:{send:0,process:0,enrich:0}}}function K(){let t=w();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 N(t,e){return e===void 0?{[SOURCE_FIXTURE]:true,body:t}:{[SOURCE_FIXTURE]:true,body:t,headers:e}}async function H(t,e,r={}){let o=t.input["~standard"];if(typeof o?.validate!="function")throw rcError("RC5003",void 0,{message:"testFn: spec.input must be a Standard Schema with a callable validate."});let s=o.validate,n=s(e);if(n instanceof Promise&&(n=await n),n.issues!==void 0&&n.issues!==null)throw rcError("RC5002",void 0,{message:`testFn: input validation failed: ${formatSchemaIssues(n.issues)}`});let i={logger:r.logger??logger.child({test:"fn"}),abortSignal:r.signal??new AbortController().signal},a="value"in n?n.value:e;return await t.handler(a,i)}function _(t){let e=t.abortController??new AbortController;return {context:t.context,signal:e.signal,meta:t.meta??{routeId:"test"},ready:t.onReady??(()=>{}),complete:r=>e.abort(r),emit:async r=>await t.handler(r.message,r.headers,r.parse,r.parseFailureMode)}}function U(t){return JSON.parse(readFileSync(t,"utf-8"))}function ce(t,e,r){let o=U(t);if(!Array.isArray(o))throw new Error(`fixture.each: expected JSON array at "${t}", got ${typeof o}`);for(let s of o){if(typeof s?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(s)}`);e(s.name,()=>r(s));}}export{p as TestContext,l as TestContextBuilder,g as createNoopSpyLogger,u as createSpyFn,f as createSpyLogger,U as fixture,ce as fixtureEach,P as mockAdapter,L as pseudo,N as sourceMessage,K as spy,M as testContext,H as testFn,_ as testSubscription};//# 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/mock-adapter.ts","../src/test-context.ts","../src/adapters/pseudo/index.ts","../src/adapters/spy/shared.ts","../src/adapters/spy/index.ts","../src/source-message.ts","../src/test-fn.ts","../src/index.ts"],"names":["createSpyFn","impl","calls","fn","args","next","createSpyLogger","spy","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","childSpy","built","error","existing","RC_ADAPTER_OVERRIDES","testContext","createAdapter","runtime","fail","noopSend","noopProcess","exchange","noopSubscribe","_context","_handler","_abortController","onReady","pseudo","opts","createSpyState","state","HeadersKeys","sourceMessage","body","headers","SOURCE_FIXTURE","testFn","spec","input","standard","validate","result","formatSchemaIssues","defaultLogger","validated","fixture","path","readFileSync","fixtureEach","test","run","entries"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Minimal runner-agnostic spy function. Records calls in the jest-compatible\n * `mock.calls` shape so assertions like\n * `expect(t.logger.warn.mock.calls.some(...))` work under bun:test, Vitest,\n * and node:test without this package importing any runner.\n *\n * Runner mocks (`vi.fn` from Vitest, `mock` from bun:test) are structurally\n * assignable to this interface, so they can be injected via {@link SpyFactory}\n * when full matcher support (`expect(fn).toHaveBeenCalledWith(...)`) is\n * wanted.\n */\nexport interface SpyFn {\n (...args: unknown[]): unknown;\n mock: { calls: unknown[][] };\n mockImplementation(impl: (...args: unknown[]) => unknown): SpyFn;\n mockClear(): void;\n}\n\n/**\n * Factory producing spy functions. Defaults to the built-in {@link createSpyFn};\n * pass your runner's mock factory (`vi.fn`, or `mock` from bun:test) to get\n * native mocks that work with the runner's `expect` matchers.\n */\nexport type SpyFactory = () => SpyFn;\n\n/**\n * Create a built-in spy function. Dependency-free; records calls in\n * `fn.mock.calls` and supports `mockImplementation` / `mockClear`.\n */\nexport function createSpyFn(): SpyFn {\n let impl: ((...args: unknown[]) => unknown) | undefined;\n const calls: unknown[][] = [];\n const fn = ((...args: unknown[]): unknown => {\n calls.push(args);\n return impl?.(...args);\n }) as SpyFn;\n fn.mock = { calls };\n fn.mockImplementation = (next) => {\n impl = next;\n return fn;\n };\n fn.mockClear = () => {\n calls.length = 0;\n };\n return fn;\n}\n\n/**\n * Spy logger with spy methods for assertions (e.g.\n * `t.logger.info.mock.calls` under any runner, or\n * `expect(t.logger.info).toHaveBeenCalledWith(...)` when built from an\n * injected runner mock factory).\n */\nexport type SpyLogger = {\n info: SpyFn;\n debug: SpyFn;\n warn: SpyFn;\n error: SpyFn;\n trace: SpyFn;\n fatal: SpyFn;\n child: SpyFn;\n};\n\nexport function createSpyLogger(fn: SpyFactory = createSpyFn): SpyLogger {\n const spy: SpyLogger = {\n info: fn(),\n debug: fn(),\n warn: fn(),\n error: fn(),\n trace: fn(),\n fatal: fn(),\n child: fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\nexport function createNoopSpyLogger(fn: SpyFactory = createSpyFn): SpyLogger {\n const noop = fn();\n const childFn = 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 */\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 */\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 * @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 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 { SpyFactory, SpyLogger } from \"./spy-logger\";\nimport {\n createSpyFn,\n createSpyLogger,\n createNoopSpyLogger,\n} 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 * Mock factory used to build the spy logger. Defaults to the built-in\n * runner-agnostic spy. Pass your runner's factory (`vi.fn` from Vitest, or\n * `mock` from bun:test) to get native mocks that work with the runner's\n * `expect` matchers, e.g. `testContext({ fn: vi.fn })`.\n */\n fn?: SpyFactory;\n}\n\n/**\n * Options for TestContext.test().\n */\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 for asserting on log calls.\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. t.logger.info.mock.calls, or expect(t.logger.info).toHaveBeenCalledWith(...) with an injected runner mock factory */\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 */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n private adapterOverrides: AdapterOverride[] = [];\n private readonly spyFactory: SpyFactory;\n\n constructor(options?: TestContextOptions) {\n this.spyFactory = options?.fn ?? createSpyFn;\n this.routesReadyTimeoutMs = options?.routesReadyTimeoutMs;\n }\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 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(this.spyFactory);\n const originalChild = logger.child.bind(logger);\n const childSpy = this.spyFactory();\n childSpy.mockImplementation(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n );\n logger.child = childSpy as unknown as typeof logger.child;\n // Restore the patched child on build failure: the restore hook is only\n // handed to TestContext after a successful build, so without this a\n // rejected build (e.g. an invalid route) would leak the stub into every\n // later test in the process.\n let built: Awaited<ReturnType<ContextBuilder[\"build\"]>>;\n try {\n built = await this.builder.build();\n } catch (error) {\n logger.child = originalChild;\n throw error;\n }\n const { context: ctx, client } = built;\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 * Runner-agnostic by default: the spy logger uses a built-in spy that records\n * calls in the jest-compatible `mock.calls` shape. Pass `{ fn }` with your\n * runner's mock factory (`vi.fn` from Vitest, `mock` from bun:test) when you\n * want `expect(t.logger.info).toHaveBeenCalledWith(...)` matcher support.\n *\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n *\n * @example\n * // Vitest, with native matcher support:\n * const t = await testContext({ fn: vi.fn }).routes(myRoutes).build();\n */\nexport function testContext(options?: TestContextOptions): TestContextBuilder {\n return new TestContextBuilder(options);\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// 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 * @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 {\n SOURCE_FIXTURE,\n type ExchangeHeaders,\n type SourceFixture,\n} from \"@routecraft/routecraft\";\n\n/**\n * Wrap a message body with the headers a real source would have attached, for\n * use as a `mockAdapter(..., { source: [...] })` fixture.\n *\n * Envelope-carrying sources (mail, http) split each incoming message into a\n * payload on `exchange.body` and metadata on `routecraft.<adapter>.*` headers.\n * A bare fixture array only sets the body, so a route that reads those headers\n * cannot be exercised through the mock. `sourceMessage(body, headers)` lets the\n * mock reproduce that split.\n *\n * @example\n * ```typescript\n * const mailMock = mockAdapter(mail, {\n * source: [\n * sourceMessage(\n * { text: \"Tracking: ABC123\" },\n * {\n * \"routecraft.mail.from\": \"noreply@acme.test\",\n * \"routecraft.mail.subject\": \"Your order has shipped\",\n * },\n * ),\n * ],\n * });\n * ```\n *\n * @param body - The body the source would deliver on the exchange\n * @param headers - Headers the source would attach (omit for a body-only fixture)\n * @returns A branded fixture recognised by the source-mock dispatcher\n */\nexport function sourceMessage<M>(\n body: M,\n headers?: ExchangeHeaders,\n): SourceFixture<M> {\n return headers === undefined\n ? { [SOURCE_FIXTURE]: true, body }\n : { [SOURCE_FIXTURE]: true, body, headers };\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 */\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 */\nexport interface TestFnHandlerContext {\n logger: ReturnType<typeof defaultLogger.child>;\n abortSignal: AbortSignal;\n}\n\n/**\n * Options for {@link testFn}.\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 * @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\";\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 createSpyFn,\n createSpyLogger,\n createNoopSpyLogger,\n type SpyFn,\n type SpyFactory,\n type SpyLogger,\n} from \"./spy-logger\";\n\n// Re-export pseudo adapter\nexport {\n pseudo,\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// Source fixture helper: attach headers to a source-role mock fixture so\n// routes that read `routecraft.<adapter>.*` headers can be exercised.\nexport { sourceMessage } from \"./source-message\";\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 * @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/**\n * Fixture entry must have a `name` field used as the test name.\n */\nexport interface FixtureWithName {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * A test runner's `test` function, as accepted by {@link fixtureEach}.\n * Both `test` from bun:test and `test` from Vitest satisfy this shape.\n */\nexport type FixtureTestFn = (\n name: string,\n fn: () => void | Promise<void>,\n) => unknown;\n\n/**\n * Load a JSON array fixture and run one test per entry. Each entry must have\n * a `name` field (used as the test name). Runner-agnostic: pass your runner's\n * `test` function (bun:test, Vitest, node:test).\n *\n * @param path Path to a JSON file that parses to an array\n * @param test The runner's `test` function used to register each entry\n * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.\n *\n * @example\n * import { test } from \"bun:test\";\n * fixtureEach(\"./cases.json\", test, (entry) => {\n * expect(run(entry.input)).toEqual(entry.expected);\n * });\n */\nexport function fixtureEach<T extends FixtureWithName>(\n path: string,\n test: FixtureTestFn,\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/shared.ts","../src/adapters/pseudo/index.ts","../src/adapters/spy/shared.ts","../src/adapters/spy/index.ts","../src/source-message.ts","../src/test-fn.ts","../src/test-subscription.ts","../src/index.ts"],"names":["createSpyFn","impl","calls","fn","args","next","createSpyLogger","spy","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","childSpy","built","error","existing","RC_ADAPTER_OVERRIDES","testContext","createNoopSubscribe","sub","createAdapter","runtime","fail","noopSend","noopProcess","exchange","noopSubscribe","pseudo","opts","createSpyState","state","HeadersKeys","sourceMessage","body","headers","SOURCE_FIXTURE","testFn","spec","input","standard","validate","result","formatSchemaIssues","defaultLogger","validated","testSubscription","controller","reason","msg","fixture","path","readFileSync","fixtureEach","test","run","entries"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Minimal runner-agnostic spy function. Records calls in the jest-compatible\n * `mock.calls` shape so assertions like\n * `expect(t.logger.warn.mock.calls.some(...))` work under bun:test, Vitest,\n * and node:test without this package importing any runner.\n *\n * Runner mocks (`vi.fn` from Vitest, `mock` from bun:test) are structurally\n * assignable to this interface, so they can be injected via {@link SpyFactory}\n * when full matcher support (`expect(fn).toHaveBeenCalledWith(...)`) is\n * wanted.\n */\nexport interface SpyFn {\n (...args: unknown[]): unknown;\n mock: { calls: unknown[][] };\n mockImplementation(impl: (...args: unknown[]) => unknown): SpyFn;\n mockClear(): void;\n}\n\n/**\n * Factory producing spy functions. Defaults to the built-in {@link createSpyFn};\n * pass your runner's mock factory (`vi.fn`, or `mock` from bun:test) to get\n * native mocks that work with the runner's `expect` matchers.\n */\nexport type SpyFactory = () => SpyFn;\n\n/**\n * Create a built-in spy function. Dependency-free; records calls in\n * `fn.mock.calls` and supports `mockImplementation` / `mockClear`.\n */\nexport function createSpyFn(): SpyFn {\n let impl: ((...args: unknown[]) => unknown) | undefined;\n const calls: unknown[][] = [];\n const fn = ((...args: unknown[]): unknown => {\n calls.push(args);\n return impl?.(...args);\n }) as SpyFn;\n fn.mock = { calls };\n fn.mockImplementation = (next) => {\n impl = next;\n return fn;\n };\n fn.mockClear = () => {\n calls.length = 0;\n };\n return fn;\n}\n\n/**\n * Spy logger with spy methods for assertions (e.g.\n * `t.logger.info.mock.calls` under any runner, or\n * `expect(t.logger.info).toHaveBeenCalledWith(...)` when built from an\n * injected runner mock factory).\n */\nexport type SpyLogger = {\n info: SpyFn;\n debug: SpyFn;\n warn: SpyFn;\n error: SpyFn;\n trace: SpyFn;\n fatal: SpyFn;\n child: SpyFn;\n};\n\nexport function createSpyLogger(fn: SpyFactory = createSpyFn): SpyLogger {\n const spy: SpyLogger = {\n info: fn(),\n debug: fn(),\n warn: fn(),\n error: fn(),\n trace: fn(),\n fatal: fn(),\n child: fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\nexport function createNoopSpyLogger(fn: SpyFactory = createSpyFn): SpyLogger {\n const noop = fn();\n const childFn = 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 */\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 */\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 * @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 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 { SpyFactory, SpyLogger } from \"./spy-logger\";\nimport {\n createSpyFn,\n createSpyLogger,\n createNoopSpyLogger,\n} 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 * Mock factory used to build the spy logger. Defaults to the built-in\n * runner-agnostic spy. Pass your runner's factory (`vi.fn` from Vitest, or\n * `mock` from bun:test) to get native mocks that work with the runner's\n * `expect` matchers, e.g. `testContext({ fn: vi.fn })`.\n */\n fn?: SpyFactory;\n}\n\n/**\n * Options for TestContext.test().\n */\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 for asserting on log calls.\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. t.logger.info.mock.calls, or expect(t.logger.info).toHaveBeenCalledWith(...) with an injected runner mock factory */\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(\"route:started\", (() => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n }) as EventHandler<EventName>);\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 */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n private adapterOverrides: AdapterOverride[] = [];\n private readonly spyFactory: SpyFactory;\n\n constructor(options?: TestContextOptions) {\n this.spyFactory = options?.fn ?? createSpyFn;\n this.routesReadyTimeoutMs = options?.routesReadyTimeoutMs;\n }\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 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(this.spyFactory);\n const originalChild = logger.child.bind(logger);\n const childSpy = this.spyFactory();\n childSpy.mockImplementation(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n );\n logger.child = childSpy as unknown as typeof logger.child;\n // Restore the patched child on build failure: the restore hook is only\n // handed to TestContext after a successful build, so without this a\n // rejected build (e.g. an invalid route) would leak the stub into every\n // later test in the process.\n let built: Awaited<ReturnType<ContextBuilder[\"build\"]>>;\n try {\n built = await this.builder.build();\n } catch (error) {\n logger.child = originalChild;\n throw error;\n }\n const { context: ctx, client } = built;\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 * Runner-agnostic by default: the spy logger uses a built-in spy that records\n * calls in the jest-compatible `mock.calls` shape. Pass `{ fn }` with your\n * runner's mock factory (`vi.fn` from Vitest, `mock` from bun:test) when you\n * want `expect(t.logger.info).toHaveBeenCalledWith(...)` matcher support.\n *\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n *\n * @example\n * // Vitest, with native matcher support:\n * const t = await testContext({ fn: vi.fn }).routes(myRoutes).build();\n */\nexport function testContext(options?: TestContextOptions): TestContextBuilder {\n return new TestContextBuilder(options);\n}\n","import type { Exchange } from \"@routecraft/routecraft\";\n\nexport interface PseudoOptions {\n runtime?: \"throw\" | \"noop\";\n}\n\nexport interface PseudoKeyedOptions extends PseudoOptions {\n args: \"keyed\";\n}\n\n/**\n * Creates a noop send function that resolves to undefined.\n */\nexport function createNoopSend<R>(): () => Promise<R> {\n return (): Promise<R> => Promise.resolve(undefined as unknown as R);\n}\n\n/**\n * Creates a noop process function that returns the exchange as-is.\n */\nexport function createNoopProcess<R>(): <T>(\n exchange: Exchange<T>,\n) => Exchange<R> {\n return <T>(exchange: Exchange<T>): Exchange<R> =>\n exchange as unknown as Exchange<R>;\n}\n\n/**\n * Creates a noop subscribe function that signals readiness immediately.\n */\nexport function createNoopSubscribe(): (\n sub: import(\"@routecraft/routecraft\").Subscription,\n) => Promise<void> {\n return (sub): Promise<void> => {\n sub.ready();\n return Promise.resolve();\n };\n}\n\n/**\n * Creates a function that throws an error indicating the adapter is not implemented.\n */\nexport function createFailFunction(name: string): () => never {\n return (): never => {\n throw new Error(\n `Pseudo adapter \"${name}\" is not implemented. Replace with a real adapter.`,\n );\n };\n}\n","import type { Source, Destination, Processor } from \"@routecraft/routecraft\";\nimport {\n createNoopSubscribe,\n type PseudoOptions,\n type PseudoKeyedOptions,\n} 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 = createNoopSubscribe();\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// 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 * @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 {\n SOURCE_FIXTURE,\n type ExchangeHeaders,\n type SourceFixture,\n} from \"@routecraft/routecraft\";\n\n/**\n * Wrap a message body with the headers a real source would have attached, for\n * use as a `mockAdapter(..., { source: [...] })` fixture.\n *\n * Envelope-carrying sources (mail, http) split each incoming message into a\n * payload on `exchange.body` and metadata on `routecraft.<adapter>.*` headers.\n * A bare fixture array only sets the body, so a route that reads those headers\n * cannot be exercised through the mock. `sourceMessage(body, headers)` lets the\n * mock reproduce that split.\n *\n * @example\n * ```typescript\n * const mailMock = mockAdapter(mail, {\n * source: [\n * sourceMessage(\n * { text: \"Tracking: ABC123\" },\n * {\n * \"routecraft.mail.from\": \"noreply@acme.test\",\n * \"routecraft.mail.subject\": \"Your order has shipped\",\n * },\n * ),\n * ],\n * });\n * ```\n *\n * @param body - The body the source would deliver on the exchange\n * @param headers - Headers the source would attach (omit for a body-only fixture)\n * @returns A branded fixture recognised by the source-mock dispatcher\n */\nexport function sourceMessage<M>(\n body: M,\n headers?: ExchangeHeaders,\n): SourceFixture<M> {\n return headers === undefined\n ? { [SOURCE_FIXTURE]: true, body }\n : { [SOURCE_FIXTURE]: true, body, headers };\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 */\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 */\nexport interface TestFnHandlerContext {\n logger: ReturnType<typeof defaultLogger.child>;\n abortSignal: AbortSignal;\n}\n\n/**\n * Options for {@link testFn}.\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 * @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 type {\n CraftContext,\n Exchange,\n ExchangeHeaders,\n OnParseError,\n SourceMeta,\n Subscription,\n} from \"@routecraft/routecraft\";\n\n/**\n * Options for {@link testSubscription}. The `handler` mirrors the message\n * fields the engine receives, flattened into positional arguments so test\n * assertions read naturally.\n */\nexport interface TestSubscriptionOptions<T = unknown> {\n /** Context handed to the source (store access, logger). */\n context: CraftContext;\n /** Receives each emitted message; its return value resolves `emit`. */\n handler: (\n message: T,\n headers?: ExchangeHeaders,\n parse?: (raw: unknown) => unknown | Promise<unknown>,\n parseFailureMode?: OnParseError,\n ) => Promise<unknown> | unknown;\n /** Abort to stop the source; `complete()` calls its `abort`. */\n abortController?: AbortController;\n /** Called when the source signals readiness. */\n onReady?: () => void;\n /** Source meta; defaults to `{ routeId: \"test\" }`. */\n meta?: SourceMeta;\n}\n\n/**\n * Build a {@link Subscription} for driving a source adapter directly in a\n * unit test, without a running route. Wire-up mirrors the engine: `emit`\n * forwards to `handler`, `ready` to `onReady`, `complete` aborts the\n * controller, and `signal` observes it.\n *\n * @example\n * ```typescript\n * const received: unknown[] = [];\n * await adapter.subscribe(\n * testSubscription({\n * context: t.ctx,\n * handler: (message) => void received.push(message),\n * abortController,\n * }),\n * );\n * ```\n */\nexport function testSubscription<T = unknown>(\n options: TestSubscriptionOptions<T>,\n): Subscription<T> {\n const controller = options.abortController ?? new AbortController();\n return {\n context: options.context,\n signal: controller.signal,\n meta: options.meta ?? { routeId: \"test\" },\n ready: options.onReady ?? (() => {}),\n complete: (reason?: unknown) => controller.abort(reason),\n emit: async (msg) =>\n (await options.handler(\n msg.message,\n msg.headers,\n msg.parse,\n msg.parseFailureMode,\n )) as Exchange,\n };\n}\n","import { readFileSync } from \"node:fs\";\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 createSpyFn,\n createSpyLogger,\n createNoopSpyLogger,\n type SpyFn,\n type SpyFactory,\n type SpyLogger,\n} from \"./spy-logger\";\n\n// Re-export pseudo adapter\nexport {\n pseudo,\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// Source fixture helper: attach headers to a source-role mock fixture so\n// routes that read `routecraft.<adapter>.*` headers can be exercised.\nexport { sourceMessage } from \"./source-message\";\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 * @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/**\n * Fixture entry must have a `name` field used as the test name.\n */\nexport interface FixtureWithName {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * A test runner's `test` function, as accepted by {@link fixtureEach}.\n * Both `test` from bun:test and `test` from Vitest satisfy this shape.\n */\nexport type FixtureTestFn = (\n name: string,\n fn: () => void | Promise<void>,\n) => unknown;\n\n/**\n * Load a JSON array fixture and run one test per entry. Each entry must have\n * a `name` field (used as the test name). Runner-agnostic: pass your runner's\n * `test` function (bun:test, Vitest, node:test).\n *\n * @param path Path to a JSON file that parses to an array\n * @param test The runner's `test` function used to register each entry\n * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.\n *\n * @example\n * import { test } from \"bun:test\";\n * fixtureEach(\"./cases.json\", test, (entry) => {\n * expect(run(entry.input)).toEqual(entry.expected);\n * });\n */\nexport function fixtureEach<T extends FixtureWithName>(\n path: string,\n test: FixtureTestFn,\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\n// Subscription builder for driving source adapters directly in unit tests\nexport {\n testSubscription,\n type TestSubscriptionOptions,\n} from \"./test-subscription\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routecraft/testing",
3
- "version": "0.6.0-canary.18",
3
+ "version": "0.6.0-canary.19",
4
4
  "description": "Test utilities for Routecraft capabilities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,7 +22,7 @@
22
22
  "prepublishOnly": "bun run build"
23
23
  },
24
24
  "dependencies": {
25
- "@routecraft/routecraft": "^0.6.0-canary.18",
25
+ "@routecraft/routecraft": "^0.6.0-canary.19",
26
26
  "@standard-schema/spec": "^1.1.0"
27
27
  },
28
28
  "peerDependencies": {