test(cli): help-text snapshots for every CLI command (#28267)

This commit is contained in:
Kit Langton
2026-05-19 10:58:12 -04:00
committed by GitHub
parent 2932a7a35d
commit 18b9cec50d
3 changed files with 803 additions and 42 deletions

View File

@@ -0,0 +1,623 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode acp --help 1`] = `
"opencode acp
start ACP (Agent Client Protocol) server
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--port port to listen on [number] [default: 0]
--hostname hostname to listen on [string] [default: "127.0.0.1"]
--mdns enable mDNS service discovery (defaults hostname to 0.0.0.0)
[boolean] [default: false]
--mdns-domain custom domain name for mDNS service (default: opencode.local)
[string] [default: "opencode.local"]
--cors additional domains to allow for CORS [array] [default: []]
--cwd working directory [string] [default: "<HOME>"]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp --help 1`] = `
"opencode mcp
manage MCP (Model Context Protocol) servers
Commands:
opencode mcp add add an MCP server
opencode mcp list list MCP servers and their status [aliases: ls]
opencode mcp auth [name] authenticate with an OAuth-enabled MCP server
opencode mcp logout [name] remove OAuth credentials for an MCP server
opencode mcp debug <name> debug OAuth connection for an MCP server
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode attach --help 1`] = `
"opencode attach <url>
attach to a running opencode server
Positionals:
url http://localhost:4096 [string] [required]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--dir directory to run in [string]
-c, --continue continue the last session [boolean]
-s, --session session id to continue [string]
--fork fork the session when continuing (use with --continue or --session) [boolean]
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD) [string]
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')[string]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode run --help 1`] = `
"opencode run [message..]
run opencode with a message
Positionals:
message message to send [array] [default: []]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--command the command to run, use message for args [string]
-c, --continue continue the last session [boolean]
-s, --session session id to continue [string]
--fork fork the session before continuing (requires --continue or
--session) [boolean]
--share share the session [boolean]
-m, --model model to use in the format of provider/model [string]
--agent agent to use [string]
--format format: default (formatted) or json (raw JSON events)
[string] [choices: "default", "json"] [default: "default"]
-f, --file file(s) to attach to message [array]
--title title for the session (uses truncated prompt if no value
provided) [string]
--attach attach to a running opencode server (e.g.,
http://localhost:4096) [string]
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD)
[string]
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or
'opencode') [string]
--dir directory to run in, path on remote server if attaching
[string]
--port port for the local server (defaults to random port if no value
provided) [number]
--variant model variant (provider-specific reasoning effort, e.g., high,
max, minimal) [string]
--thinking show thinking blocks [boolean]
--replay replay visible session history on interactive resume
[boolean] [default: false]
--replay-limit cap visible interactive replay to the newest N messages
[number]
-i, --interactive run in direct interactive split-footer mode
[boolean] [default: false]
--dangerously-skip-permissions auto-approve permissions that are not explicitly denied
(dangerous!) [boolean] [default: false]
--demo enable direct interactive demo slash commands; pass one as the
message to run it immediately [boolean] [default: false]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode debug --help 1`] = `
"opencode debug
debugging and troubleshooting tools
Commands:
opencode debug config show resolved configuration
opencode debug lsp LSP debugging utilities
opencode debug rg ripgrep debugging utilities
opencode debug file file system debugging utilities
opencode debug scrap list all known projects
opencode debug skill list all available skills
opencode debug snapshot snapshot debugging utilities
opencode debug startup print startup timing
opencode debug agent <name> show agent configuration details
opencode debug v2 debug v2 catalog and built-in plugins
opencode debug info show debug information
opencode debug paths show global paths (data, config, cache, state)
opencode debug wait wait indefinitely (for debugging)
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers --help 1`] = `
"opencode providers
manage AI providers and credentials
Commands:
opencode providers list list providers and credentials [aliases: ls]
opencode providers login [url] log in to a provider
opencode providers logout log out from a configured provider
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode agent --help 1`] = `
"opencode agent
manage agents
Commands:
opencode agent create create a new agent
opencode agent list list all available agents
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode upgrade --help 1`] = `
"opencode upgrade [target]
upgrade opencode to the latest or a specific version
Positionals:
target version to upgrade to, for ex '0.1.48' or 'v0.1.48' [string]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
-m, --method installation method to use
[string] [choices: "curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode uninstall --help 1`] = `
"opencode uninstall
uninstall opencode and remove all related files
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
-c, --keep-config keep configuration files [boolean] [default: false]
-d, --keep-data keep session data and snapshots [boolean] [default: false]
--dry-run show what would be removed without removing [boolean] [default: false]
-f, --force skip confirmation prompts [boolean] [default: false]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode serve --help 1`] = `
"opencode serve
starts a headless opencode server
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--port port to listen on [number] [default: 0]
--hostname hostname to listen on [string] [default: "127.0.0.1"]
--mdns enable mDNS service discovery (defaults hostname to 0.0.0.0)
[boolean] [default: false]
--mdns-domain custom domain name for mDNS service (default: opencode.local)
[string] [default: "opencode.local"]
--cors additional domains to allow for CORS [array] [default: []]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode web --help 1`] = `
"opencode web
start opencode server and open web interface
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--port port to listen on [number] [default: 0]
--hostname hostname to listen on [string] [default: "127.0.0.1"]
--mdns enable mDNS service discovery (defaults hostname to 0.0.0.0)
[boolean] [default: false]
--mdns-domain custom domain name for mDNS service (default: opencode.local)
[string] [default: "opencode.local"]
--cors additional domains to allow for CORS [array] [default: []]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode models --help 1`] = `
"opencode models [provider]
list all available models
Positionals:
provider provider ID to filter models by [string]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--verbose use more verbose model output (includes metadata like costs) [boolean]
--refresh refresh the models cache from models.dev [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode stats --help 1`] = `
"opencode stats
show token usage and cost statistics
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--days show stats for the last N days (default: all time) [number]
--tools number of tools to show (default: all) [number]
--models show model statistics (default: hidden). Pass a number to show top N, otherwise
shows all
--project filter by project (default: all projects, empty string: current project)[string]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode export --help 1`] = `
"opencode export [sessionID]
export session data as JSON
Positionals:
sessionID session id to export [string]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--sanitize redact sensitive transcript and file data [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode import --help 1`] = `
"opencode import <file>
import session data from JSON file or URL
Positionals:
file path to JSON file or share URL [string] [required]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github --help 1`] = `
"opencode github
manage GitHub agent
Commands:
opencode github install install the GitHub agent
opencode github run run the GitHub agent
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode pr --help 1`] = `
"opencode pr <number>
fetch and checkout a GitHub PR branch, then run opencode
Positionals:
number PR number to checkout [number] [required]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session --help 1`] = `
"opencode session
manage sessions
Commands:
opencode session list list sessions
opencode session delete <sessionID> delete a session
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode plugin --help 1`] = `
"opencode plugin <module>
install plugin and update config
Positionals:
module npm module name [string] [required]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
-g, --global install in global config [boolean] [default: false]
-f, --force replace existing plugin version [boolean] [default: false]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode db --help 1`] = `
"opencode db
database tools
Commands:
opencode db [query] open an interactive sqlite3 shell or run a query [default]
opencode db path print the database path
opencode db migrate migrate JSON data to SQLite (merges with existing data)
Positionals:
query SQL query to execute [string]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--format Output format [string] [choices: "json", "tsv"] [default: "tsv"]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp list --help 1`] = `
"opencode mcp list
list MCP servers and their status
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp add --help 1`] = `
"opencode mcp add
add an MCP server
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp auth --help 1`] = `
"opencode mcp auth [name]
authenticate with an OAuth-enabled MCP server
Commands:
opencode mcp auth list list OAuth-capable MCP servers and their auth status [aliases: ls]
Positionals:
name name of the MCP server [string]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp logout --help 1`] = `
"opencode mcp logout [name]
remove OAuth credentials for an MCP server
Positionals:
name name of the MCP server [string]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers list --help 1`] = `
"opencode providers list
list providers and credentials
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers login --help 1`] = `
"opencode providers login [url]
log in to a provider
Positionals:
url opencode auth provider [string]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
-p, --provider provider id or name to log in to (skips provider selection) [string]
-m, --method login method label (skips method selection) [string]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers logout --help 1`] = `
"opencode providers logout
log out from a configured provider
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode agent create --help 1`] = `
"opencode agent create
create a new agent
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--path directory path to generate the agent file [string]
--description what the agent should do [string]
--mode agent mode [string] [choices: "all", "primary", "subagent"]
--permissions, --tools comma-separated list of permissions to allow (default: all).
Available: "bash, read, edit, glob, grep, webfetch, task, todowrite,
websearch, lsp, skill" [string]
-m, --model model to use in the format of provider/model [string]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode agent list --help 1`] = `
"opencode agent list
list all available agents
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session list --help 1`] = `
"opencode session list
list sessions
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
-n, --max-count limit to N most recent sessions [number]
--format output format [string] [choices: "table", "json"] [default: "table"]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session delete --help 1`] = `
"opencode session delete <sessionID>
delete a session
Positionals:
sessionID session ID to delete [string] [required]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github install --help 1`] = `
"opencode github install
install the GitHub agent
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github run --help 1`] = `
"opencode github run
run the GitHub agent
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--event GitHub mock event to run the agent for [string]
--token GitHub personal access token (github_pat_********) [string]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode db path --help 1`] = `
"opencode db path
print the database path
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]"
`;

View File

@@ -0,0 +1,139 @@
// Help-text snapshots for every CLI command + key subcommand. Catches
// accidental flag removals, renames, and reordering in a single sweep —
// any change to the user-visible CLI surface shows up here as a diff.
//
// This is the broad coverage layer that makes the future Effect CLI
// migration (yargs → effect-smol/cli) safe to attempt: if a refactor
// preserves the surface, the snapshots stay green; if it doesn't, the
// diff tells you exactly which command(s) changed.
//
// Snapshots are taken at COLUMNS=120 so wrapping is stable across
// terminal sizes. The default opencode tui command is excluded —
// `opencode --help` includes an ASCII banner that pulls in the install
// version (changes per release), so we'd snapshot a moving target.
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import fs from "node:fs"
import os from "node:os"
import { cliIt } from "../../lib/cli-process"
// Strips dynamic content that varies per run so snapshots are stable.
// Currently only the tmpdir prefix bleeds in (via `--cwd` defaults that
// resolve to `process.cwd()`). Add new patterns here as they surface.
//
// On macOS `os.tmpdir()` returns `/var/folders/...` but `process.cwd()`
// inside the child returns the realpath `/private/var/folders/...` — so
// we strip both forms.
const TMP = os.tmpdir()
const REAL_TMP = fs.realpathSync(TMP)
function normalize(text: string): string {
return (
text
// Windows emits CRLF on stderr; collapse first so the rest of the
// pipeline doesn't need separate Windows-vs-POSIX branches.
.replaceAll("\r\n", "\n")
.replaceAll(REAL_TMP, "<TMPDIR>")
.replaceAll(TMP, "<TMPDIR>")
// The harness writes the random home dir at `<TMPDIR>/oc-cli-XXX` on
// POSIX, `<TMPDIR>\oc-cli-XXX` on Windows. Strip either form.
.replace(/<TMPDIR>[/\\]oc-cli-[a-z0-9]+/g, "<HOME>")
// yargs wraps the `[string] [default: "..."]` clause based on the
// pre-normalized default's character length, so different random home
// path widths produce different leading-whitespace counts (or even
// line-wraps onto a fresh line on Windows). `\s+` matches both forms.
.replace(/\s+\[string\] \[default: "<HOME>"\]/g, ' [string] [default: "<HOME>"]')
)
}
// Top-level commands. Order matches what `opencode --help` prints today;
// keep it in that order so the snapshot file reads as a table of contents.
// `completion` is intentionally excluded — it's a yargs built-in that emits
// top-level help on `--help` and exits 1; not a real opencode command.
const TOP_LEVEL = [
"acp",
"mcp",
"attach",
"run",
"debug",
"providers", // aliased to `auth`
"agent",
"upgrade",
"uninstall",
"serve",
"web",
"models",
"stats",
"export",
"import",
"github",
"pr",
"session",
"plugin",
"db",
] as const
// Subcommands worth pinning. Not exhaustive — the goal is one snapshot per
// distinct argv shape, not every leaf. Add new entries when a subcommand
// gains user-visible flags that we want to lock in.
const SUBCOMMANDS = [
["mcp", "list"],
["mcp", "add"],
["mcp", "auth"],
["mcp", "logout"],
["providers", "list"],
["providers", "login"],
["providers", "logout"],
["agent", "create"],
["agent", "list"],
["session", "list"],
["session", "delete"],
["github", "install"],
["github", "run"],
["db", "path"],
] as const
// Fixed wrap width so a developer's terminal doesn't affect snapshots.
// yargs honors COLUMNS; CI runners typically default to 80 which produces
// different wraps from a 200-col local terminal.
const SNAPSHOT_ENV = { COLUMNS: "120" }
describe("opencode CLI help-text snapshots", () => {
// Single test, parallel spawns. Each command's help fires under
// `concurrency: 8` — wall-clock stays under ~10s even for ~35 commands,
// versus ~1 minute if we serialized.
cliIt.live(
"every documented command emits stable help text",
({ opencode }) =>
Effect.gen(function* () {
const argvs: Array<readonly string[]> = [...TOP_LEVEL.map((c) => [c] as const), ...SUBCOMMANDS]
// Spawn in parallel, then assert in argv order so snapshot output is
// deterministic and per-command failures don't abort the rest of
// the sweep. `Effect.partition` is the canonical "run all, separate
// failures from successes" primitive — no mutable accumulator needed.
const [failures, results] = yield* Effect.partition(
argvs,
(argv) =>
Effect.gen(function* () {
const result = yield* opencode.spawn([...argv, "--help"], { env: SNAPSHOT_ENV })
if (result.exitCode !== 0) {
return yield* Effect.fail(`opencode ${argv.join(" ")}: exit ${result.exitCode}`)
}
return { argv, result }
}),
{ concurrency: 8 },
)
for (const { argv, result } of results) {
// yargs writes --help to stderr, not stdout. Snapshotting stderr
// means our test catches the help body; stdout for these commands
// is expected to be empty.
expect(normalize(result.stderr)).toMatchSnapshot(`opencode ${argv.join(" ")} --help`)
}
if (failures.length > 0) {
throw new Error(`Help text failed for:\n ${failures.join("\n ")}`)
}
}),
180_000,
)
})

View File

@@ -33,6 +33,30 @@ const cliEntry = path.join(opencodeRoot, "src/index.ts")
export const testModelID = "test/test-model"
// Wrap a Bun subprocess pipe (or any ReadableStream<Uint8Array>) as a Stream.
// Centralizes the `evaluate` + `onError` boilerplate and tags errors with the
// stream name so a stderr/stdout failure is greppable in logs.
function fromBunStream(name: string, get: () => ReadableStream<Uint8Array>) {
return Stream.fromReadableStream({
evaluate: get,
onError: (cause) => new Error(`${name} stream error: ${String(cause)}`),
})
}
// Long-lived processes (serve, acp) all want the same stderr drain: read every
// chunk, push to a tail buffer, swallow stream errors (the child closing the
// pipe is normal). `log: true` surfaces a real protocol error to logs so a
// regression doesn't silently disappear.
function forkStderrDrain(stream: ReadableStream<Uint8Array>, into: string[]) {
return Effect.forkScoped(
fromBunStream("stderr", () => stream).pipe(
Stream.decodeText(),
Stream.runForEach((chunk) => Effect.sync(() => into.push(chunk))),
Effect.ignore({ log: true }),
),
)
}
function isolatedEnv(home: string, configJson: string): Record<string, string> {
return {
OPENCODE_TEST_HOME: home,
@@ -225,20 +249,10 @@ export function withCliFixture<A, E>(
}).pipe(Effect.ignore),
)
// Drain stderr in a scope-bound fork. Without this the OS pipe buffer
// eventually fills and the child blocks on its next log call. Kept as a
// tail buffer so timeout failures can include context.
// Tail buffer so timeout failures can include stderr context. The fork
// also keeps the OS pipe buffer from filling and wedging the child.
const stderrChunks: string[] = []
yield* Effect.forkScoped(
Stream.fromReadableStream({
evaluate: () => proc.stderr,
onError: () => new Error("stderr stream error"),
}).pipe(
Stream.decodeText(),
Stream.runForEach((chunk) => Effect.sync(() => stderrChunks.push(chunk))),
Effect.ignore,
),
)
yield* forkStderrDrain(proc.stderr, stderrChunks)
// Watch stdout line-by-line for the listening sentinel. Format
// (see src/cli/cmd/serve.ts):
@@ -246,17 +260,14 @@ export function withCliFixture<A, E>(
const readyRe = /listening on (http:\/\/([^\s:]+):(\d+))/
const readyDeferred = yield* Deferred.make<{ url: string; hostname: string; port: number }>()
yield* Effect.forkScoped(
Stream.fromReadableStream({
evaluate: () => proc.stdout,
onError: () => new Error("stdout stream error"),
}).pipe(
fromBunStream("stdout", () => proc.stdout).pipe(
Stream.decodeText(),
Stream.splitLines,
Stream.runForEach((line) => {
const m = line.match(readyRe)
return m ? Deferred.succeed(readyDeferred, { url: m[1], hostname: m[2], port: Number(m[3]) }) : Effect.void
}),
Effect.ignore,
Effect.ignore({ log: true }),
),
)
@@ -323,26 +334,14 @@ export function withCliFixture<A, E>(
)
const stderrChunks: string[] = []
yield* Effect.forkScoped(
Stream.fromReadableStream({
evaluate: () => proc.stderr,
onError: () => new Error("stderr stream error"),
}).pipe(
Stream.decodeText(),
Stream.runForEach((chunk) => Effect.sync(() => stderrChunks.push(chunk))),
Effect.ignore,
),
)
yield* forkStderrDrain(proc.stderr, stderrChunks)
// Each ndjson line becomes one queue entry. JSON.parse failures are
// surfaced as the raw string so a malformed protocol message doesn't
// silently wedge the test in `receive`.
const responses = yield* Queue.unbounded<unknown>()
yield* Effect.forkScoped(
Stream.fromReadableStream({
evaluate: () => proc.stdout,
onError: () => new Error("stdout stream error"),
}).pipe(
fromBunStream("stdout", () => proc.stdout).pipe(
Stream.decodeText(),
Stream.splitLines,
Stream.runForEach((line) => {
@@ -355,23 +354,23 @@ export function withCliFixture<A, E>(
}
return Queue.offer(responses, parsed)
}),
Effect.ignore,
Effect.ignore({ log: true }),
),
)
return {
// `proc.stdin.write` returns `number | Promise<number>`. The promise
// form is the backpressure signal — if we don't await it, rapid
// successive sends can interleave under pipe-buffer-full conditions
// and corrupt the ndjson framing.
send: (msg: object) =>
Effect.sync(() => {
proc.stdin.write(JSON.stringify(msg) + "\n")
Effect.promise(async () => {
const ret = proc.stdin.write(JSON.stringify(msg) + "\n")
if (typeof ret !== "number") await ret
}),
receive: Queue.take(responses),
close: () => {
try {
proc.stdin.end()
} catch {
// already closed
}
},
// proc.stdin.end() is idempotent in Bun; no try/catch needed.
close: () => proc.stdin.end(),
exited: proc.exited as Promise<number>,
} satisfies AcpHandle
})