BlaiseBlaise
Documentation
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;
}

On this page