help-snapshots.test.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. // Help-text snapshots for every CLI command + key subcommand. Catches
  2. // accidental flag removals, renames, and reordering in a single sweep —
  3. // any change to the user-visible CLI surface shows up here as a diff.
  4. //
  5. // This is the broad coverage layer that makes the future Effect CLI
  6. // migration (yargs → effect-smol/cli) safe to attempt: if a refactor
  7. // preserves the surface, the snapshots stay green; if it doesn't, the
  8. // diff tells you exactly which command(s) changed.
  9. //
  10. // Snapshots are taken at COLUMNS=120 so wrapping is stable across
  11. // terminal sizes. The default kirincode tui command is excluded —
  12. // `opencode --help` includes an ASCII banner that pulls in the install
  13. // version (changes per release), so we'd snapshot a moving target.
  14. import { describe, expect } from "bun:test"
  15. import { Effect } from "effect"
  16. import { cliIt } from "../../lib/cli-process"
  17. import { normalizeForSnapshot, PATH_SEP } from "../../lib/snapshot"
  18. // Composes `normalizeForSnapshot` (CRLF + tmpdir) with two help-specific
  19. // rules:
  20. //
  21. // 1. The harness's `oc-cli-XXX` subdir under TMPDIR collapses to `<HOME>`.
  22. // `PATH_SEP` matches `/` and `\\` so the rule works on POSIX + Windows.
  23. //
  24. // 2. yargs wraps the `[string] [default: "..."]` clause based on the
  25. // pre-normalized default's character length, so different random home
  26. // path widths produce different leading-whitespace counts (or even
  27. // line-wraps onto a fresh line on Windows). `\s+` matches both forms.
  28. function normalize(text: string): string {
  29. return normalizeForSnapshot(text, {
  30. pathReplacements: [
  31. // Mixed-case [A-Za-z0-9] because node's mkdtemp suffix is mixed-case
  32. // (the harness now uses FileSystem.makeTempDirectoryScoped under the
  33. // hood). A `[a-z0-9]+` regex would leave uppercase chars trailing.
  34. [new RegExp(`<TMPDIR>${PATH_SEP}oc-cli-[A-Za-z0-9]+`, "g"), "<HOME>"],
  35. [/\s+\[string\] \[default: "<HOME>"\]/g, ' [string] [default: "<HOME>"]'],
  36. ],
  37. })
  38. }
  39. // Top-level commands. Order matches what `opencode --help` prints today;
  40. // keep it in that order so the snapshot file reads as a table of contents.
  41. // `completion` is intentionally excluded — it's a yargs built-in that emits
  42. // top-level help on `--help` and exits 1; not a real kirincode command.
  43. const TOP_LEVEL = [
  44. "acp",
  45. "mcp",
  46. "attach",
  47. "run",
  48. "debug",
  49. "providers", // aliased to `auth`
  50. "agent",
  51. "upgrade",
  52. "uninstall",
  53. "serve",
  54. "web",
  55. "models",
  56. "stats",
  57. "export",
  58. "import",
  59. "github",
  60. "pr",
  61. "session",
  62. "plugin",
  63. "db",
  64. ] as const
  65. // Subcommands worth pinning. Not exhaustive — the goal is one snapshot per
  66. // distinct argv shape, not every leaf. Add new entries when a subcommand
  67. // gains user-visible flags that we want to lock in.
  68. const SUBCOMMANDS = [
  69. ["mcp", "list"],
  70. ["mcp", "add"],
  71. ["mcp", "auth"],
  72. ["mcp", "logout"],
  73. ["providers", "list"],
  74. ["providers", "login"],
  75. ["providers", "logout"],
  76. ["agent", "create"],
  77. ["agent", "list"],
  78. ["session", "list"],
  79. ["session", "delete"],
  80. ["github", "install"],
  81. ["github", "run"],
  82. ["db", "path"],
  83. ] as const
  84. // Fixed wrap width so a developer's terminal doesn't affect snapshots.
  85. // yargs honors COLUMNS; CI runners typically default to 80 which produces
  86. // different wraps from a 200-col local terminal.
  87. const SNAPSHOT_ENV = { COLUMNS: "120" }
  88. describe("kirincode CLI help-text snapshots", () => {
  89. // Single test, parallel spawns. Each command's help fires under
  90. // `concurrency: 8` — wall-clock stays under ~10s even for ~35 commands,
  91. // versus ~1 minute if we serialized.
  92. cliIt.live(
  93. "every documented command emits stable help text",
  94. ({ kirincode }) =>
  95. Effect.gen(function* () {
  96. const topLevel = yield* kirincode.spawn(["--help"], { env: SNAPSHOT_ENV })
  97. expect(topLevel.exitCode).toBe(0)
  98. expect(topLevel.stderr.endsWith("\n")).toBe(true)
  99. expect(topLevel.stderr).toContain("--mini")
  100. expect(topLevel.stderr).not.toContain("--thinking")
  101. expect(topLevel.stderr).not.toContain("--variant")
  102. expect(topLevel.stderr).not.toContain("--demo")
  103. const argvs: Array<readonly string[]> = [...TOP_LEVEL.map((c) => [c] as const), ...SUBCOMMANDS]
  104. // Spawn in parallel, then assert in argv order so snapshot output is
  105. // deterministic and per-command failures don't abort the rest of
  106. // the sweep. `Effect.partition` is the canonical "run all, separate
  107. // failures from successes" primitive — no mutable accumulator needed.
  108. const [failures, results] = yield* Effect.partition(
  109. argvs,
  110. (argv) =>
  111. Effect.gen(function* () {
  112. const result = yield* kirincode.spawn([...argv, "--help"], { env: SNAPSHOT_ENV })
  113. if (result.exitCode !== 0) {
  114. return yield* Effect.fail(`opencode ${argv.join(" ")}: exit ${result.exitCode}`)
  115. }
  116. return { argv, result }
  117. }),
  118. { concurrency: 8 },
  119. )
  120. for (const { argv, result } of results) {
  121. // yargs writes --help to stderr, not stdout. Snapshotting stderr
  122. // means our test catches the help body; stdout for these commands
  123. // is expected to be empty.
  124. expect(normalize(result.stderr)).toMatchSnapshot(`opencode ${argv.join(" ")} --help`)
  125. }
  126. if (failures.length > 0) {
  127. throw new Error(`Help text failed for:\n ${failures.join("\n ")}`)
  128. }
  129. }),
  130. 180_000,
  131. )
  132. })