Test Utilities
Rune provides helpers for testing commands in-process without spawning a child process, exported from @rune-cli/rune/test. runCommand() is the base helper for running a single command, and createRunCommand() builds a runner that bakes in your project config.
import { runCommand } from "@rune-cli/rune/test";import { expect, test } from "vitest";
import greeting from "../src/commands/index.ts";
test("greets by name", async () => { const result = await runCommand(greeting, ["world"]);
expect(result.exitCode).toBe(0); expect(result.stdout).toBe("Hello, world!\n");});runCommand()
Section titled “runCommand()”Exercises a command through Rune’s parse-and-execute pipeline. Input is passed as a string[] of CLI tokens, so argv parsing, type coercion, schema validation, and default handling all run exactly as they do at real invocation.
function runCommand( command: DefinedCommand, argv?: string[], context?: RunCommandContext,): Promise<CommandExecutionResult<TCommandDocument, TCommandRecord>>The output shape is inferred from the passed command. Text commands return output.kind === "text", json: true commands expose the run() return type through output.document, and jsonl: true commands expose yielded records through output.records.
Parameters
Section titled “Parameters”command
Section titled “command”- Type:
DefinedCommand - Required
A command created by defineCommand().
- Type:
string[] - Default:
[]
CLI tokens forwarded to the command.
context
Section titled “context”- Type:
RunCommandContext - Default:
{}
Optional execution context.
RunCommandContext
Section titled “RunCommandContext”- Type:
string - Optional
Working directory value injected into ctx.cwd. Does not change process.cwd().
- Type:
Record<string, string | undefined> - Optional
Environment variables used for option env fallbacks. This replaces process.env for the command under test; it is not merged automatically. When omitted, runCommand() uses an empty env map so tests stay isolated from the host environment.
const command = defineCommand({ options: [{ name: "port", type: "number", env: "PORT", default: 3000 }], run({ options, output }) { output.log(String(options.port)); },});
test("uses PORT from env", async () => { const result = await runCommand(command, [], { env: { PORT: "4000" } });
expect(result.stdout).toBe("4000\n");});If you intentionally want to inherit the current process environment, merge it explicitly:
const result = await runCommand(command, [], { env: { ...process.env, PORT: "4000" },});- Type:
string | Buffer | Uint8Array - Optional
Stdin injected into ctx.stdin. When provided, ctx.stdin.isPiped is true
and ctx.stdin.isTTY is false. When omitted, runCommand() uses an isolated
empty stdin with isPiped: false and isTTY: true; it does not inherit
process.stdin.
const command = defineCommand({ async run({ stdin, output }) { const input = stdin.isPiped ? await stdin.text() : ""; output.log(input.trim()); },});
test("reads stdin", async () => { const result = await runCommand(command, [], { stdin: "hello\n" });
expect(result.stdout).toBe("hello\n");});globalOptions
Section titled “globalOptions”- Type:
CommandOptionField[] - Optional
Low-level injection point for global options. Prefer createRunCommand(config) for normal tests.
globalHooks
Section titled “globalHooks”- Type:
RuneHooks - Optional
Low-level injection point for global hooks. Prefer createRunCommand(config) for normal tests.
createLocals
Section titled “createLocals”- Type:
(ctx: LocalsFactoryContext) => unknown - Optional
Low-level injection point for project locals. Prefer createRunCommand(config) for normal tests.
locals
Section titled “locals”- Type:
RuneConfigLocals - Optional
Shorthand for injecting a fixed ctx.locals value in a command test.
const result = await runCommand(command, [], { locals: { workspace: fakeWorkspace, api: fakeApi },});Pass either createLocals or locals, not both.
commandMetadata
Section titled “commandMetadata”- Type:
RunHookCommandMetadata - Optional
Command route metadata exposed to hooks. runCommand() does not perform manifest routing, so omitted metadata defaults to empty values. Pass this when testing hooks that branch on ctx.command.cliName or ctx.command.path.
createRunCommand()
Section titled “createRunCommand()”Creates a runCommand() helper that bakes in your project config. Use this when your project defines defineConfig({ options }), defineConfig({ hooks }), or defineConfig({ locals }).
import { createRunCommand } from "@rune-cli/rune/test";import config from "../rune.config";
const runCommand = createRunCommand(config);The returned function has the same call shape as runCommand(command, argv, context) and injects config.options, config.hooks, and config.locals into each command execution. Pass context.globalOptions, context.globalHooks, context.createLocals, or context.locals to override the config-provided values for a specific test.
CommandExecutionResult
Section titled “CommandExecutionResult”exitCode
Section titled “exitCode”- Type:
number
Process exit code (0 for success).
stdout
Section titled “stdout”- Type:
string
Captured stdout output.
stderr
Section titled “stderr”- Type:
string
Captured stderr output.
- Type:
CommandFailure | undefined
Structured error information, if the command failed.
output
Section titled “output”- Type:
{ kind: "text" } | { kind: "json"; document: TCommandDocument | undefined } | { kind: "jsonl"; records: TCommandRecord[] }
Captured structured output for the command.
For text commands, output is { kind: "text" }.
For json: true commands, output.document is the return value from run(). It is populated regardless of whether --json is passed; the --json flag controls whether output.log() is suppressed, not whether the document is captured.
For jsonl: true commands, output.records is the list of yielded records. It is an empty array when parsing fails or the command fails before yielding any records.
Examples
Section titled “Examples”Testing validation errors
Section titled “Testing validation errors”test("requires an id argument", async () => { const result = await runCommand(command, []);
expect(result.exitCode).toBe(1); expect(result.stderr).not.toBe("");});Testing default values
Section titled “Testing default values”const command = defineCommand({ options: [{ name: "count", type: "number", default: 1 }], run({ options, output }) { output.log(`count=${options.count}`); },});
test("uses default count", async () => { const result = await runCommand(command, []);
expect(result.stdout).toBe("count=1\n");});Testing JSON mode
Section titled “Testing JSON mode”const command = defineCommand({ json: true, run() { return { items: [1, 2, 3] }; },});
test("returns structured document", async () => { const result = await runCommand(command, ["--json"]);
expect(result.output).toEqual({ kind: "json", document: { items: [1, 2, 3] }, }); expect(result.stdout).toBe("");});When a JSON-mode command fails, runCommand() captures the compact JSON error envelope in result.stderr and exposes the normalized failure through result.error.