rhythmjs

Search documentation

Search guides, the tutorial and every package.

On this page

Create a runner#

createCliRunner(appOrCli) from @rhythmjs/testing/cli wraps a Rhythm CLI app (or a bare RhythmCli, which it mounts for you) behind a single run(). The app's real middleware chain executes through callback(); nothing touches the process's actual stdio, argv, or exit code.

TypeScript
import { createCliRunner } from "@rhythmjs/testing/cli";

const runner = createCliRunner(cli);
const result = await runner.run("remote add origin https://x.git");
expect(result.stdout).toEqual(['added remote "origin" -> https://x.git']);
expect(result.exitCode).toBe(0);

Run a command#

run(argv, options?) accepts a command line as a string (split on whitespace, empty segments dropped) or as an argv array when arguments contain spaces. Flags are parsed with the real parseArgv, so --env=prod and boolean flags behave exactly as in production. The result is { ctx, stdout, stderr, exitCode }: stdout and stderr are copies of the response's line arrays (nothing was printed), and ctx is the full final context for asserting on anything else the pipeline derived.

Piped stdin#

Pass { stdin: "line1\nline2" } and the context's ctx.stdin becomes a real web ReadableStream over those bytes, exactly the shape piped input has under toCliHandler on Bun. Without the option, ctx.stdin is null, the TTY shape.

TypeScript
const result = await runner.run("import --format=json", {
  stdin: JSON.stringify({ users: ["ada"] }),
});
expect(result.exitCode).toBe(0);

Teardown#

runner.teardown() disposes the app's providers in reverse order, mirroring a real process exit. Prompts are a provider concern: a CLI that prompts should receive a fake RhythmPromptIO through its own providers, keeping the runner byte-deterministic.