Skip to content

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");
});

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.

  • Type: DefinedCommand
  • Required

A command created by defineCommand().

  • Type: string[]
  • Default: []

CLI tokens forwarded to the command.

  • Type: RunCommandContext
  • Default: {}

Optional execution context.

  • 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");
});
  • Type: CommandOptionField[]
  • Optional

Low-level injection point for global options. Prefer createRunCommand(config) for normal tests.

  • Type: RuneHooks
  • Optional

Low-level injection point for global hooks. Prefer createRunCommand(config) for normal tests.

  • Type: (ctx: LocalsFactoryContext) => unknown
  • Optional

Low-level injection point for project locals. Prefer createRunCommand(config) for normal tests.

  • 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.

  • 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.

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.

  • Type: number

Process exit code (0 for success).

  • Type: string

Captured stdout output.

  • Type: string

Captured stderr output.

  • Type: CommandFailure | undefined

Structured error information, if the command failed.

  • 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.

test("requires an id argument", async () => {
const result = await runCommand(command, []);
expect(result.exitCode).toBe(1);
expect(result.stderr).not.toBe("");
});
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");
});
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.