Runner and PlugsReference
Testing API types
The type declarations of @blaiseagent/runner/testing, with their TSDoc.
The harness, the in-memory host and state store, and the test fakes from
@blaiseagent/runner/testing. A guide is Test a plug.
createPlugHarness
/**
* Runs one plug in isolation, with the same rules as the real chain: `next()` exactly once,
* `ctx.agent` frozen after it, `onLoad` before the other handlers, persisted state validated and
* migrated first. A plug under test cannot tell the difference.
*/
export function createPlugHarness(plug: unknown, options?: HarnessOptions): PlugHarness;ExecCall
export interface ExecCall {
readonly argv: readonly string[];
readonly options: ExecOptions | undefined;
}fakeAgent
/**
* Behaviour is chosen by the prompt text:
* - `ask`: sends a `session/request_permission` request and answers when it is decided;
* - `malformed`: sends a `session/request_permission` request whose option has no `optionId`;
* - `read`: sends an `fs/read_text_file` request (the runner does not implement it);
* - `hang`: does not answer until a `session/cancel` arrives, then answers `cancelled`;
* - anything else: streams one `agent_message_chunk` echoing the prompt, then `end_turn`.
*/
export function fakeAgent(options?: FakeAgentOptions): FakeAgent;FakeAgent
/** A scripted ACP agent running in memory, for tests of the session engine and of plugs. */
export interface FakeAgent {
/** What the host's `spawn` returns. Pass it to `memoryHost({ spawn })`. */
spawn(spec: AgentSpawnSpec): Promise<HostProcess>;
/** Every message the agent received, in order. */
readonly received: readonly AnyMessage[];
/** The `params` of the requests and notifications received for a method. */
paramsOf(method: string): unknown[];
/** What the client answered to the agent's own requests (permission decisions, …), by method. */
readonly answers: ReadonlyMap<string, unknown>;
/** The JSON-RPC errors the client answered them with, by method. */
readonly failures: ReadonlyMap<
string,
{
code: number;
message: string;
}
>;
/** Ends the agent as if it crashed. */
crash(code?: number): void;
readonly specs: readonly AgentSpawnSpec[];
}FakeAgentOptions
export interface FakeAgentOptions {
/** Whether `initialize` advertises `loadSession`. Default true. */
readonly loadSession?: boolean;
/** Reject loading the old session with this message. */
readonly loadError?: string;
/** The protocol version `initialize` answers with. Default: the runner's own. */
readonly protocolVersion?: number;
/** The id `session/new` answers with. Default `acp-session-1`. */
readonly sessionId?: string;
/** Text chunks `session/load` replays as `session/update` before it answers. */
readonly history?: readonly string[];
/**
* Session config options `session/new` and `session/load` answer with (ACP `configOptions`).
* `session/set_config_option` changes their `currentValue`.
*/
readonly configOptions?: readonly Record<string, unknown>[];
/** Reject `session/set_config_option` with this message. */
readonly rejectConfig?: string;
}HarnessOptions
export interface HarnessOptions {
/** The plug's `with:`, already evaluated. */
readonly inputs?: unknown;
/** The plug's `id` in `setup:`. Required for plugs that persist state. */
readonly id?: string;
/** Position in `setup:`. Default 0. */
readonly index?: number;
readonly session?: Partial<SessionInfo>;
readonly run?: Partial<RunInfo>;
readonly agent?: Partial<AgentSpawnSpec>;
readonly env?: Readonly<Record<string, string | undefined>>;
/** Outputs of earlier plugs: `{ db: { url: "…" } }` is `setup.db.outputs.url`. */
readonly setup?: Readonly<Record<string, Readonly<Record<string, string>>>>;
/** The host the plug sees as `ctx.host`. Default: a fresh `memoryHost()`. */
readonly host?: Host;
readonly store?: PlugStateStore;
}LogEntry
export interface LogEntry {
readonly level: "debug" | "info" | "warn" | "error";
readonly message: string;
readonly fields: Record<string, unknown> | undefined;
}memoryHost
/** A host that keeps its file system in memory and records the commands it is asked to run. */
export function memoryHost(options?: MemoryHostOptions): MemoryHost;MemoryHost
export interface MemoryHost extends Host {
/** Every `exec`, in order. */
readonly calls: readonly ExecCall[];
/** Every `spawn`, in order. */
readonly spawned: readonly AgentSpawnSpec[];
}MemoryHostOptions
export interface MemoryHostOptions {
readonly info?: Partial<HostInfo>;
/** Files to create, by absolute path. Parent directories are created. */
readonly files?: Readonly<Record<string, string>>;
/** Answers `exec`. The default succeeds with no output. */
readonly exec?: (call: ExecCall) => Partial<ExecResult> | Promise<Partial<ExecResult>>;
/** Answers `spawn`. The default rejects: a memory host runs no processes. */
readonly spawn?: (spec: AgentSpawnSpec) => Promise<HostProcess>;
}memoryStateStore
/** Persisted plug state kept in memory, for tests. */
export function memoryStateStore(initial?: readonly StoredPlugState[]): MemoryStateStore;MemoryStateStore
export type MemoryStateStore = InMemoryStateStore;MessageOptions
export interface MessageOptions {
/** The result the rest of the chain produces. Default: `undefined`. */
readonly reply?: (message: Message) => MessageResult<Message>;
}MessageOutcome
export interface MessageOutcome {
readonly result: MessageResult<Message>;
/** The messages the plug passed to `next()`, in order. Empty if it answered itself. */
readonly forwarded: readonly Message[];
}PlugHarness
export interface PlugHarness {
readonly host: Host;
readonly store: PlugStateStore;
/** What the plug published with `ctx.setOutput`. */
readonly outputs: Readonly<Record<string, string>>;
readonly logs: readonly LogEntry[];
/** Validates inputs, loads persisted state (migrating it) and runs `onLoad`. */
load(): Promise<PlugContext<unknown, unknown, unknown>>;
/** Runs `onSession` with the contract of the real chain. */
session(options?: SessionOptions): Promise<SessionOutcome>;
/** Runs `onTeardown` with the contract of the real chain. */
teardown(options?: TeardownOptions): Promise<TeardownOutcome>;
/** Runs `onMessage` for one message. */
message(message: Message, options?: MessageOptions): Promise<MessageOutcome>;
}SessionOptions
export interface SessionOptions {
/** What `next()` resolves with. Default: an idle pause that may resume. */
readonly pause?: PauseInfo;
/** Stands in for the inner plugs and the agent. Receives the host they would run on. */
readonly inner?: (host: Host) => void | Promise<void>;
}SessionOutcome
export interface SessionOutcome {
readonly pause: PauseInfo;
/** The host passed to `next({ host })`, if the plug switched it. */
readonly switchedHost: Host | undefined;
/** The agent spec as the inner plugs would see it. Frozen. */
readonly agent: AgentSpawnSpec;
}SshTestServer
export interface SshTestServer {
readonly host: string;
readonly port: number;
/** The server's host key fingerprint (`SHA256:…`), for pinning. */
readonly hostKey: string;
/** A key the server accepts. */
readonly clientKey: {
readonly privateKey: string;
readonly publicKey: string;
};
/** Every command that was run through `exec`. */
readonly commands: readonly string[];
close(): Promise<void>;
}startSshServer
/**
* An SSH server on a loopback port, for tests of SSH hosts. It accepts one generated key, runs
* `exec` requests with `/bin/sh -c` on this machine, serves SFTP over this machine's file system,
* and supports local and remote port forwarding.
*/
export function startSshServer(): Promise<SshTestServer>;TeardownOptions
export interface TeardownOptions {
readonly info?: TeardownInfo;
readonly inner?: (host: Host) => void | Promise<void>;
}TeardownOutcome
export interface TeardownOutcome {
readonly info: TeardownInfo;
readonly switchedHost: Host | undefined;
}