# ADR-0030: CLI Snapshot Golden Testing for the Help Surface

## Status

✅ Accepted (2026-05-12)

## Context

omni-dev exposes a hierarchical command-line interface built with Clap
derive macros ([ADR-0016](adr-0016.md)). The CLI surface — every
subcommand name, every flag, every short alias, every help-text line —
is part of the project's public contract: scripts pin against flag
names, documentation cites them, and users build muscle memory around
them. Silent regressions on that surface are easy to ship:

- A `#[arg(hide = true)]` flipped during a refactor demotes a flag out
  of the help output without any code change a reviewer would notice.
- A subcommand renamed in one place but not in its parent's `Subcommand`
  enum produces a help tree where the flag exists but is no longer
  reachable through documented invocations.
- A flag's help-string lifted into a `const` for reuse drifts when the
  flag's semantics change but the string does not.
- A new subcommand added under the wrong parent compiles cleanly,
  passes unit tests, and only fails when a user types `--help`.

Conventional unit tests do not catch any of these. Hand-asserted
expected-output strings catch some but require the test author to know
in advance which lines to assert, which is exactly the failure mode at
the heart of the problem.

Two complementary commitments shape the response:

- **Coverage proportional to the surface area.** With ~200 visible
  flags and a multi-level subcommand tree, manual assertions decay into
  stale partial coverage faster than the surface evolves.
- **Forced review of intentional changes.** Most CLI surface changes
  are intentional. The mechanism should not block them — but it must
  not let them merge without an explicit, reviewer-visible diff of what
  the surface now looks like.

The project's `help-all` subcommand
([docs/plan/help-all-command.md](../../docs/plan/help-all-command.md))
renders the entire Clap command tree in one pass, including hidden
subcommands and every flag's full description. This already exists, is
already kept up to date, and produces deterministic output suitable for
a string-equality assertion.

## Decision

### A single whole-tree golden snapshot

[tests/integration_test.rs](../../tests/integration_test.rs) contains
the test `help_all_golden`:

```rust
#[test]
fn help_all_golden() -> Result<()> {
    use omni_dev::cli::help::HelpGenerator;
    let generator = HelpGenerator::new();
    let help_output = generator.generate_all_help()?;
    insta::assert_snapshot!("help_all_output", help_output);
    Ok(())
}
```

The asserted snapshot is
[tests/snapshots/integration_test__help_all_output.snap](../../tests/snapshots/integration_test__help_all_output.snap).
It is the canonical regression surface for the CLI shape and is
authoritative: any change to clap derives, flag attributes, subcommand
docstrings, or help-string constants must be reflected here before the
change can merge.

The assertion uses the [`insta`](https://insta.rs) crate's
`assert_snapshot!` macro. Choice rationale: `insta` ships a reviewer
workflow (`cargo insta accept`, `--review`), separates expected output
from test source for clean diffs, and is already a project dependency.

### What is asserted vs not asserted

**Asserted by `help_all_output.snap`:**

- Every subcommand name and its position in the tree.
- Every flag's long name, short alias, value-name, and full help text.
- Top-level program description and usage line.
- The tree structure that Clap renders — including hidden / advanced
  subcommands surfaced by `help-all` specifically (these are *not*
  visible from a bare `--help`).

**Deliberately not asserted:**

- Runtime behaviour of any command — the snapshot is a static
  description of the surface, not a behavioural test.
- Output formatting of business-logic commands (e.g. YAML structure for
  `git view-commits`) — those have their own per-command tests.
- Version strings (the binary version changes per release; asserting it
  would make every release fail this test).
- Environment-specific paths (the `help-all` generator is invoked in
  the test process itself, not via `Command::new`, precisely so that
  the captured output cannot contain shell or path differences).

### Intentional-change workflow

When a CLI change intentionally alters the help surface — adding a
flag, renaming a subcommand, rewording help text — the contributor
follows the workflow encoded in the
[`update-snapshots`](../../.claude/skills/update-snapshots/SKILL.md)
skill:

1. Make the source change.
2. Run `cargo test` (or `cargo insta test --test integration_test
   --review`). The test fails with a diff between the existing
   `.snap` and the actual output. `insta` writes the new output to a
   `.snap.new` sibling file.
3. Inspect the diff: `diff -u
   tests/snapshots/integration_test__help_all_output.snap{,.new}`.
4. *Only if* the diff matches the intended change, accept it: `cargo
   insta accept --test integration_test`. Stage the updated `.snap`
   alongside the source change in the same commit.
5. If the diff contains anything the contributor did not intend — an
   orphaned subcommand, an unexpected flag default, drift from a
   shared help-string constant — investigate the source change rather
   than accepting the snapshot.

CLAUDE.md (project AI assistant guide) reinforces step 5 by instructing
AI contributors not to blindly `cargo insta accept` after a CLI change.

### Why a snapshot rather than hand-asserted strings

Hand-asserted strings require the test author to enumerate exactly the
lines that matter. On a surface this size, that enumeration is wrong
within a release cycle. A whole-tree golden makes additions and
removals equally visible: a new flag adds a line, a removed flag
deletes one, and reviewers see both classes of change in the same
form. The `cargo insta accept` step is a forced review gate — there is
no path to a green build that does not include either the original
snapshot or an explicit acceptance of the new one.

### Out of scope

- Snapshotting behavioural output of business commands. That is the
  job of per-command tests; this snapshot is exclusively about the
  help surface.
- Snapshotting the surface from `--help` rather than `help-all`. The
  `help-all` output is a strict superset; asserting on `--help` would
  miss hidden / advanced subcommands.
- Snapshotting any output that depends on the host environment
  (cwd-derived paths, version strings, locale-dependent formatting).

## Consequences

**Positive:**

- Silent CLI regressions are no longer possible: every change to the
  surface forces a snapshot diff that must be reviewed in the PR.
- The snapshot doubles as up-to-date CLI documentation. A new
  contributor can read
  `tests/snapshots/integration_test__help_all_output.snap` and see the
  entire user-visible surface in one place.
- The reviewer signal is in the diff, not in test descriptions. A PR
  that touches `src/cli/**` and does *not* touch the `.snap` is a red
  flag the reviewer can see at a glance.

**Negative:**

- Every intentional CLI change carries a snapshot churn cost — the
  `.snap` is updated in lockstep, and PRs touching widely-shared help
  strings (e.g., a constant referenced by ten flags) produce diffs
  proportionate to the reach of the change.
- Reviewers must read the `.snap` diff, not just the source diff, to
  verify the user-visible effect of a change. This is a feature, not
  a bug, but it adds review overhead.
- A noisy whole-tree snapshot can mask subtle changes when the diff is
  large (e.g., a global help-string rename). Reviewers must read the
  full diff rather than glancing at it.

**Neutral:**

- The snapshot is a checked-in file, so its evolution is visible in
  `git log` against the snapshot path. This is the standard `insta`
  workflow.
- The `update-snapshots` skill automates the mechanical part of the
  workflow; the human review step is non-negotiable.
- This ADR governs only the `help-all` snapshot at
  `tests/snapshots/integration_test__help_all_output.snap`. Other
  `insta` snapshots that future work may add are unaffected by this
  decision — they each have their own scope and review surface.
