/** * Test-oriented Engine subclass with virtual time control and activity mocking. * * Wraps Engine with a MemoryStorage, TimeControl, and ActivityMockRegistry * so tests can advance time deterministically and substitute activity * implementations without touching real infrastructure. * * @module testing/test-engine */ import { Engine } from '../core/engine.ts'; import type { Duration } from '../core/types.ts'; import { MemoryStorage } from '../storage/memory.ts'; import type { ChaosScenario, FailureCategory } from './chaos.ts'; import type { MockHandle } from './mocks.ts'; import { ActivityMockRegistry } from './mocks.ts'; /** * Options for {@link TestEngine.runN}. * * @example * ```ts * import { TestEngine, type RunNOptions } from '@lostgradient/weft/testing'; * * const options: RunNOptions = { * runs: 50, * chaos: { faultRate: 0.2, faults: ['transient'], seed: 7 }, * }; * const engine = new TestEngine(); * // const result = await engine.runN('my-workflow', {}, options); * ``` */ export interface RunNOptions { /** Number of independent runs to execute. */ runs: number; /** Optional chaos scenario to apply across all runs. */ chaos?: ChaosScenario; } /** * Aggregate reliability metrics returned by {@link TestEngine.runN}. * * @example * ```ts * import { workflow } from '@lostgradient/weft'; * import { TestEngine, type RunNResult } from '@lostgradient/weft/testing'; * * const ping = workflow({ name: 'ping' }).execute(async function* () { return 'pong'; }); * const engine = new TestEngine(); * engine.register(ping); * const result: RunNResult = await engine.runN('ping', null, { runs: 10 }); * console.log(result.passRate); // 1 (all passed) * console.log(result.consistency); // 1 (all identical) * ``` */ export interface RunNResult { /** Fraction of runs [0, 1] that completed successfully. */ passRate: number; /** * Fraction of successful runs [0, 1] that returned the same output as the * first successful run. `1.0` means all successes were identical. * `NaN` if there were no successful runs. */ consistency: number; /** Count of failures bucketed by failure category. */ categories: Record; } /** * Test-oriented {@link Engine} subclass with virtual time control and * activity mocking for deterministic, fast workflow tests. * * Wraps the engine with a {@link MemoryStorage}, a {@link TimeControl} * instance, and an {@link ActivityMockRegistry}. Use `engine.mock(activityFn, * impl)` to replace real activities with stubs, and * `await engine.advanceTime('5m')` to advance virtual time without waiting on * real timers. * * @example * ```ts * import { workflow, type WorkflowContext } from '@lostgradient/weft'; * import { TestEngine } from '@lostgradient/weft/testing'; * * const engine = new TestEngine(); * * async function fetchPrice(ticker: unknown): Promise { * return 0; // real implementation * } * * const mock = engine.mock(fetchPrice, async (_ticker: unknown) => 42); * const priceCheck = workflow({ name: 'price-check' }).execute( * async function* (_ctx: WorkflowContext, _input: unknown) { * return 42; // simplified test example * }, * ); * engine.register(priceCheck); * * const handle = await engine.start('price-check', 'ACME'); * console.log(await handle.result()); // 42 * console.log(mock.callCount); // 0 * ``` */ export declare class TestEngine extends Engine { #private; constructor(options?: { startTime?: number; }); /** * Advance virtual time by the given duration. Fires any scheduler * timers that fall within the advanced window. */ advanceTime(duration: Duration): Promise; /** Current virtual time in milliseconds since epoch. */ get now(): number; /** * Register a mock implementation for an activity function. * When the engine encounters this activity, it will call the mock instead. */ mock(activity: () => Promise | TResult, implementation: () => TResult | Promise): MockHandle; mock(activity: (input: TInput) => Promise | TResult, implementation: (input: TInput) => TResult | Promise): MockHandle; /** * Create a new TestEngine backed by the same storage, simulating * engine recovery (like a process restart). The new engine sees all * persisted state but has fresh in-memory structures. */ recover(): TestEngine; /** Direct access to the underlying MemoryStorage for assertions. */ get storage(): MemoryStorage; /** Direct access to mock registry. */ get mocks(): ActivityMockRegistry; /** * Run the named workflow N times and return aggregate reliability metrics. * * Runs are serial and independent. If a `chaos` scenario is provided, each * registered activity mock is temporarily wrapped with `withChaos` for the * duration of that run, then restored. This ensures that the workflow * functions — which close over `this` engine's mock registry — observe the * chaos-injected implementations during each run. * * Workflow state isolation is achieved by using unique per-run workflow IDs, * so previous results do not influence subsequent runs. * * @param type The registered workflow type name. * @param input Input passed to each run. * @param options `{ runs, chaos? }` — number of runs and optional scenario. * @returns `{ passRate, consistency, categories }` aggregate metrics. */ runN(type: string, input: unknown, options: RunNOptions): Promise; }