test(cli): help-text snapshots for every CLI command (#28267)
This commit is contained in:
139
packages/opencode/test/cli/help/help-snapshots.test.ts
Normal file
139
packages/opencode/test/cli/help/help-snapshots.test.ts
Normal 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,
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user