/** * The workflow test driver: run a committed workflow against a fake host and walk it through its * states — blocked, answered, re-blocked, retried, cancelled — without touching PI, the filesystem, or * the network. * * The engine is deterministic, so a workflow's behaviour is fully pinned by three inputs: the initial * input, the answers delivered to questionnaire/Q&A steps, and what agent steps say. The first two are * arguments here; the third is scripted per step by {@link createAgentDouble}. * * Each transition returns a NEW {@link TestRun} rather than mutating: `start()`, `answer()` and * `resume()` read as a chain of states, and an earlier state stays inspectable for comparison. */ import { type StepState } from "../engine/step-state.ts"; import type { RunEvent, RunResult } from "../engine/types.ts"; import type { Questionnaire } from "../flow/questionnaire.ts"; import type { WorkflowDefinition } from "../flow/types.ts"; import { type AgentRecord, type AgentScripts } from "./agent-double.ts"; import { type StepOverrides } from "./step-override.ts"; export interface TestRunOptions { /** The workflow's initial input (spec §3.6), validated against its declared input schema. */ input?: unknown; /** What each agent step says, keyed by step name (see `ask`/`reply`/`raw`/`throws`). */ agents?: AgentScripts; /** * Replace any step, by name, with a stub (spec §13.2/§13.3) — schema-checked against the REAL step's * declared output schema, and rejected at construction if the name does not match a step in the * workflow. A stub that throws drives that step's own retry/crash/resume policy, making an otherwise * unreachable failure path directly testable. See {@link StepOverrides}. */ steps?: StepOverrides; /** Fixed run id, so event-log assertions are stable. Default: `"test-run"`. */ runId?: string; /** Fixed clock. Default: a frozen epoch, so timestamps never vary between runs. */ now?: () => Date; /** * Delay used for retry backoff and time budgets. Default: instant + recorded, so retry tests are * fast and can assert the requested backoff via {@link TestRun.sleepCalls}. */ sleep?: (ms: number, signal?: AbortSignal) => Promise; /** * Cancel the run just as this step is about to run (spec §8.6): the abort fires when the step's * `step-started` is emitted, which the retry loop observes before the step body executes — so the * named step does NOT run. Fires once, so a following `resume()` proceeds normally. * * Only function and agent steps can be cancelled this way. A questionnaire step is never cancelled * by this or anything else: unanswered or invalid answers re-block it (spec §2.4). */ cancelAt?: string; } /** One step CURRENTLY blocked (spec §8.6): what `TestRun.pendingQuestions` exposes, in FIFO ask order. */ export interface PendingQuestion { /** The step's full node path (spec §8.5) — pass to a future path-addressed `answer()` to disambiguate. */ readonly path: string; readonly questionnaire: Questionnaire; /** Set only on a RE-block: why the previously-delivered answers were rejected (spec §2.4). */ readonly violation: string | undefined; } /** * One observed state of a run, plus the transitions out of it. Wraps the engine's `RunResult` and * holds the host/store, so a resume needs no event-log plumbing from the test. */ export interface TestRun { readonly runId: string; readonly status: RunResult["status"]; readonly output: unknown; readonly error: string | undefined; /** The pending questionnaire when `blocked`. */ readonly questionnaire: Questionnaire | undefined; /** The full node path (spec §8.5) of the step that blocked, or that a crash/cancel occurred at. */ readonly path: string | undefined; /** Why a questionnaire step RE-blocked: the schema violation in the delivered answers (spec §2.4). */ readonly violation: string | undefined; /** The full event log so far, across every transition of this run. */ readonly events: readonly RunEvent[]; /** Every `sleep(ms)` the engine requested (retry backoff, time budgets), in order. */ readonly sleepCalls: readonly number[]; /** * Every step currently `blocked` (spec §8.6/§13.4), FIFO by original ask order — what `answer()` * targets by default when more than one is pending. Empty outside concurrency, where at most one * step is ever blocked at a time and `questionnaire`/`path`/`violation` above already cover it. */ readonly pendingQuestions: readonly PendingQuestion[]; /** The keys of the pending questionnaire — the questions currently being asked. */ questionKeys(): string[]; /** Events of one type, narrowed. */ eventsOf(type: T): Extract[]; /** A completed step's (or node's) recorded output, addressed by bare name (top-level) or static node path. */ stepOutput(name: string): unknown; /** * A step's (or node's) current lifecycle state (spec §5.1/§13.4) — `todo` if never reached — addressed * the same way as {@link stepOutput}: a bare name (top-level) or an explicit static node path * (`until-valid/design`, spec §5.4/§8.5). */ stepState(path: string): StepState; /** What an agent step's double recorded: messages sent to it, models, sessions opened. */ agent(stepName: string): AgentRecord; /** * Deliver structured answers to the blocked step (spec §8.4). Complete + valid answers let the run * continue; incomplete or invalid ones re-block with `violation` set — a questionnaire step is never * cancelled by a bad answer, exactly as leaving a mandatory question blank leaves it pending. * * The answers go to the step this state is REPORTING ({@link TestRun.path}) — the one whose * `questionnaire` was just read — so a test answers the question it looked at even when several steps * are blocked at once (spec §8.6). Pass a `path` from {@link TestRun.pendingQuestions} to target a * different pending block instead. */ answer(answers: Record, path?: string): Promise; /** * Node-atomic resume of a `crashed`/`cancelled` run (spec §8.2/§8.3): completed nodes are skipped * and the first incomplete node re-runs wholesale. Not the answer path — see {@link answer}. */ resume(): Promise; } /** * Start `workflow` under a fresh fake host and resolve at its first terminal-or-blocked state. * * One call per run, deliberately: the agent queues, the event store, and the fixed run id are all * per-run state, so a reusable factory would let a second run silently inherit the first's consumed * replies and duplicate its run id. Continue a run through {@link TestRun.answer}/{@link TestRun.resume}. * * Agent scripts are validated against the workflow's steps up front, so a script naming an unknown * step — or asking from a step that cannot block — fails here with a clear message rather than * surfacing mid-run as an opaque schema violation. */ export declare function createTestRun(workflow: WorkflowDefinition, options?: TestRunOptions): Promise; //# sourceMappingURL=test-run.d.ts.map