Skip to content

refactor(mac): rework banner + chezmoi-fix UX, add bats harness#27

Merged
edjchapman merged 4 commits into
mainfrom
mac-ux-overhaul
May 19, 2026
Merged

refactor(mac): rework banner + chezmoi-fix UX, add bats harness#27
edjchapman merged 4 commits into
mainfrom
mac-ux-overhaul

Conversation

@edjchapman
Copy link
Copy Markdown
Owner

Summary

Reworks the mac (= chezmoi-fix) remediation surface and the drift banner in dot_zshrc after a real-world session exposed both a correctness bug and a handful of UX rough edges. Also adds a bats-core test harness so the same class of bug can't recur silently.

Plan that drove this: mac / drift-banner review — focused scope was the mac entry point only (not the wider zsh environment), output as findings + prioritised fix list, plus thin tests wired into CI.

What changed

fix(drift-check) — drop · run 'mac' to resolve from the summary line. The banner already adds its own CTA, and the suffix read awkwardly when echoed back from inside mac.

refactor(mac) — banner + chezmoi-fix UX:

  • Pluralise drift counts everywhere (1 thing / 2 things, 1 security baseline failure / 12 ... failures, etc.). New plural helper in chezmoi-fix; inline word-flips in the zsh banner block.
  • Compute menu description-column width dynamically so the arrow column self-aligns regardless of which options appear or which counts each label embeds. OPTS entries switched to id|description|command.
  • Reconcile cached vs. fresh totals: chezmoi-fix now captures the cached drift total before re-running the drift check, then prints (refreshed: banner showed N, now M) when they differ. Fixes the "banner said 2, menu shows 1" mystery surfaced today.
  • Hide "audit a clean baseline" entries when other categories are drifting — the fix surface no longer dilutes itself with audit-navigation in the common case.
  • Banner suffix (as of Nh ago) only when the cache is materially stale (≥ 1h).
  • TTL-gate the shell-open background refresh so a burst of new tmux panes doesn't spawn N redundant chezmoi-drift-check processes.
  • New CHEZMOI_FIX_TEST_MODE env var lets bats exercise menu logic without chezmoi/brew/TTY.

test(mac) — bats harness:

  • tests/mac.bats covers clean/singular/plural/error/inbox/alignment/audit-hygiene scenarios.
  • Makefile: new test-bats target, added to the ci aggregate.
  • .github/workflows/ci.yml: new bats job (apt-get installs bats on ubuntu-latest).
  • Brewfile.tmpl: declare bats-core so make ci works locally.
  • .chezmoiignore: keep tests/ in the repo, not deployed to \$HOME.

Before / after sample

Before (real session today):
```
chezmoi: 2 thing(s) need attention — run 'mac'

drift: security: 1 · run 'mac' to resolve

  1. Audit macOS defaults (no known drift) → chezmoi-defaults-audit
  2. Inspect 1 security baseline failure(s) → chezmoi-security-audit --drift
  3. Run chezmoi doctor (health diagnostic) → chezmoi doctor
  4. Silence drift banner for this shell session → export CHEZMOI_DRIFT_QUIET=1
    ```

After (synthetic home=1, brew_extra=1, security=1, inbox=1):
```
drift: home: 1, brew-extra: 1, security: 1
brew inbox: 1 event pending

  1. Merge 1 brew event into Brewfile.tmpl → chezmoi-brew-sync
  2. Preview 1 home-file change → chezmoi diff
  3. Apply pending changes → chezmoi diff, then chezmoi apply
  4. Investigate 1 brew-extra package → brew bundle cleanup --dry-run
  5. Inspect 1 security baseline failure → chezmoi-security-audit --drift
  6. Run chezmoi doctor (health diagnostic) → chezmoi doctor
  7. Silence drift banner for this shell session → export CHEZMOI_DRIFT_QUIET=1
    ```

Test plan

  • `make lint` passes
  • `make fmt-check` passes
  • `make verify-templates` passes (4-cell matrix)
  • `bash -n` clean on both modified scripts; `zsh -n` clean on `dot_zshrc`
  • Smoke-test `chezmoi-fix` in `CHEZMOI_FIX_TEST_MODE=1` with synthetic state files for clean / single-category / multi-category / inbox / plural scenarios — output verified by hand
  • CI `bats` job runs `tests/mac.bats` against the new test mode
  • After merge + `chezmoi apply`: open a new shell, confirm banner pluralises correctly; on a host with real drift, run `mac` and confirm header reconciliation note appears if the banner and menu disagree

The summary line was hard-coded with `· run 'mac' to resolve`, so it
read awkwardly when echoed back from inside `mac` itself. The banner
already adds its own call-to-action; let the data layer just describe
state.
Today's session surfaced a real-world drift report where the banner
said "2 thing(s) need attention" but the chezmoi-fix menu showed only
one actionable item, alongside several smaller rough edges. This
commit addresses them together since they all flow through the same
banner-state-menu pipeline.

- Pluralise drift counts everywhere: "1 thing" / "2 things",
  "1 security baseline failure" / "12 ... failures", etc. Adds a small
  `plural` helper in chezmoi-fix and inline word-flips in the banner
  block.
- Compute the menu's description column width dynamically, so the
  arrow column stays aligned regardless of which options appear or
  which counts they embed.
- Detect staleness: capture the cached drift total before chezmoi-fix
  re-runs the drift check, then print "(refreshed: banner showed N,
  now M)" when the in-`mac` numbers diverge from what the banner
  reported. The user is no longer left puzzling over the gap.
- Hide "audit a clean baseline" entries when other categories are
  drifting, so the fix surface doesn't dilute itself with audit
  navigation in the common case.
- Annotate the banner with "(as of Nh ago)" only when the cache is
  materially stale (>=1h), and TTL-gate the shell-open background
  refresh so a burst of new tmux panes doesn't spawn N redundant
  chezmoi-drift-check processes.
- Add CHEZMOI_FIX_TEST_MODE so the bats harness in tests/ can exercise
  the menu logic against synthetic state files without needing
  chezmoi/brew/TTY at all.
These drift helpers had only ShellCheck and shfmt coverage in CI, so
behavioural bugs (today's banner-vs-menu count mismatch, awkward
pluralisation, audit-clean entries leaking into the fix surface)
weren't catchable by the test suite.

Adds tests/mac.bats with scenarios for clean/single/multi/error
states, pluralisation, audit-entry hygiene, and arrow-column
alignment. The script gains a CHEZMOI_FIX_TEST_MODE env var so the
suite can run the menu logic against synthetic state files without a
real chezmoi/brew/TTY.

Wires it through:
- Makefile: new `test-bats` target, added to the `ci` aggregate
- .github/workflows/ci.yml: new `bats` job (apt-get install bats)
- Brewfile.tmpl: declare bats-core so `make ci` works locally
- .chezmoiignore: keep tests/ in the repo, not deployed to $HOME
Two tests were asserting the security menu entry appears with various
counts, but the menu code gates that entry on `command -v
chezmoi-security-audit` — which an empty test PATH defeated. Move the
no-op stubs into setup() so every test exercises the post-gate code
path; individual tests can override or unlink the stubs when needed.

Also renames the "audit-clean entries appear when nothing is drifting"
test to match what it actually proves (the hygiene rule still hides
those entries under HAD_ERROR), since the truly-clean case is
unreachable — chezmoi-fix exits at "Nothing to fix" before the menu.
@edjchapman edjchapman merged commit 5c143df into main May 19, 2026
12 checks passed
@edjchapman edjchapman deleted the mac-ux-overhaul branch May 19, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant