Skip to content

test: make snapshot env blocks robust to the contributor's environment - #3037

Merged
max-sixty merged 2 commits into
mainfrom
test/env-robust-snapshots
Jun 12, 2026
Merged

max-sixty merged 2 commits into
mainfrom
test/env-robust-snapshots

Conversation

@worktrunk-bot

Copy link
Copy Markdown
Collaborator

Opened at @max-sixty's request in #3021 (comment) — make running/regenerating tests robust to the contributor's local environment so nobody has to match CI's GIT_* setup.

The problem

insta_cmd records every env var a test command sets or removes — Rust's Command::env_remove registers in get_envs(), and insta-cmd serializes a removal as KEY: "". isolate_subprocess_env scrubs whichever GIT_* / WORKTRUNK_* keys exist in the parent environment, so the recorded env: block depends on the host:

  • CI has GIT_EDITOR set → GIT_EDITOR: ""
  • A contributor's box might have GIT_PAGER → GIT_PAGER: "", or neither, or both

This is what bit #3021: help_list_long.snap / help_list_narrow_80.snap were regenerated on a box with GIT_PAGER and picked up GIT_PAGER: "" instead of CI's GIT_EDITOR: "".

Note this is cosmetic, not a CI failure: insta's matches_fully compares only the snapshot body (self.contents == other.contents), never the info:/env: metadata. But the spurious machine-to-machine diff is exactly what makes contributors think they have to match CI's environment.

The fix

A dynamic .env redaction in add_standard_env_redactions (strip_host_scrubbed_env_markers) drops every empty-valued GIT_* / WORKTRUNK_* entry from the recorded block. It runs as its own pass after the per-key .env.* redactions, so affirmatively-set vars (config paths → [TEST_CONFIG], coverage → [LLVM_PROFILE_FILE]) already hold non-empty placeholders and are kept. The explicitly-named unconditional scrubs (NO_COLOR, SHELL, PSModulePath) are host-independent and left intact.

Result: regenerating on any machine produces the same env: block, regardless of the contributor's GIT_* environment.

Why no snapshots are regenerated

The info: block is never compared, so a metadata-only difference never triggers a rewrite — even INSTA_UPDATE=always leaves an unchanged-body snapshot alone. Existing committed snapshots keep their (now-redundant) markers until their body next changes, at which point the rewrite drops them cleanly. This avoids a disruptive ~1000-file cosmetic diff that would conflict with every in-flight snapshot PR; the robustness applies to all future regenerations from this point on.

If you'd prefer the tree uniform now, a follow-up cargo insta test --accept bulk regen is a clean separate commit — happy to push it.

Verification

  • New unit test strip_host_scrubbed_env_markers_normalizes_env_block covers drop/keep/passthrough.
  • Confirmed end-to-end: deleting help_list_long.snap and forcing a fresh write drops GIT_EDITOR: "" while keeping NO_COLOR/SHELL/PSModulePath and the "0"-valued WORKTRUNK_TEST_* vars.
  • help (57), snapshot_formatting_guard (6), and ci_status (68) snapshot suites pass; cargo fmt --check and cargo clippy --tests --all-features clean.

🤖 Generated with Claude Code

`insta_cmd` records every key a test command sets *or removes* (Rust's
`Command::env_remove` registers in `get_envs()`), mapping a removal to
`KEY: ""`. `isolate_subprocess_env` scrubs whichever `GIT_*`/`WORKTRUNK_*`
keys exist in the *parent* environment, so the recorded `env:` block
otherwise depends on the host: CI has `GIT_EDITOR`, a contributor's box
might have `GIT_PAGER`, neither, or both.

insta never compares the `info:`/`env:` metadata (only stdout/stderr/exit),
so this is cosmetic — but it makes regenerated snapshots diff spuriously
from machine to machine, which is exactly what makes contributors think
they have to match CI's environment.

Add a dynamic `.env` redaction in `add_standard_env_redactions` that drops
every empty-valued `GIT_*`/`WORKTRUNK_*` entry, so regeneration produces an
identical block on any machine. The explicitly-named unconditional scrubs
(`NO_COLOR`, `SHELL`, `PSModulePath`) are host-independent and left intact.

No snapshots are regenerated here: the `info:` block isn't compared, so a
metadata-only change never triggers a rewrite. Existing snapshots keep their
(now-redundant) markers until their body next changes; future regenerations
are host-independent from this point on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One observation on the "affirmatively-set vars are kept" framing — it has a real counterexample in the existing suite. Not a correctness issue (insta never compares the info: block), but worth tightening the comment/doc since a future regen will silently drop a documented test input. Detail inline.

Comment thread tests/common/mod.rs Outdated
//
// This runs as its own pass after the per-key `.env.*` redactions above, so
// affirmatively-set vars (config paths → `[TEST_CONFIG]`, coverage →
// `[LLVM_PROFILE_FILE]`, etc.) already hold non-empty placeholders and are

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The predicate is "empty-valued GIT_* / WORKTRUNK_*", but that also matches affirmatively-set empty values — not just env_remove scrub markers. In the recorded YAML the two are indistinguishable (both serialize as KEY: ""), so the strip pass can't separate them.

Concrete case: test_list_config_env_override_validation_failure sets WORKTRUNK_WORKTREE_PATH="" — the empty value is the test's subject (it triggers worktree-path cannot be empty). It's recorded as WORKTRUNK_WORKTREE_PATH: "" in the committed list_config_env_override_validation_failure.snap env block, and it's host-independent (deterministic on every machine), so it's not the churn this PR targets. After this change, the next regen of that snapshot drops it — and since the body doesn't show which input produced the warning, the env block was the only place documenting it.

Harmless to the comparison, but the claim here (and the matching lines in tests/CLAUDE.md) reads as if affirmatively-set vars always survive. Worth a sentence noting that affirmatively-set empty GIT_* / WORKTRUNK_* vars are dropped too (indistinguishable from scrub markers), with WORKTRUNK_WORKTREE_PATH as the known case.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@worktrunk-bot please fix

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6ab6422. The strip pass keys on the empty value, so a scrub marker and an affirmatively-set empty GIT_* / WORKTRUNK_* var are indistinguishable (both serialize as KEY: "") and both get dropped.

Tightened the framing in three places: the add_standard_env_redactions comment now says affirmatively-set non-empty vars are kept and carries a caveat naming WORKTRUNK_WORKTREE_PATH="" (from test_list_config_env_override_validation_failure) as the known affirmatively-set-empty case; the strip_host_scrubbed_env_markers docstring notes the same; and the matching paragraph in tests/CLAUDE.md spells out that its committed WORKTRUNK_WORKTREE_PATH: "" line stays until the body next changes, then the regen drops it — harmless since the env: block is never compared.

The strip pass keys on the empty *value*, so it can't distinguish a
scrub marker from an affirmatively-set empty `GIT_*` / `WORKTRUNK_*`
var — both serialize as `KEY: ""`. The comment and `tests/CLAUDE.md`
read as if affirmatively-set vars always survive; document the
`WORKTRUNK_WORKTREE_PATH=""` counterexample
(`test_list_config_env_override_validation_failure`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@max-sixty
max-sixty enabled auto-merge (squash) June 12, 2026 07:29
@max-sixty
max-sixty merged commit 4bf6ef7 into main Jun 12, 2026
36 checks passed
@max-sixty
max-sixty deleted the test/env-robust-snapshots branch June 12, 2026 07:34
max-sixty added a commit that referenced this pull request Jun 14, 2026
The two wide colored-table snapshots were captured before merging main and
missed two cosmetic changes the merge introduced: the per-row trailing
ANSI reset (#3056/#3061) and dropped empty-valued env-header entries
(#3037/#3063). Table content is unchanged; only the line-final `\x1b[0m`
and the snapshot env block differ, matching every other wide list snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
worktrunk 0.58.0

Created-by: HarmonybrewBot
Commit-by: HarmonybrewBot
Merged-by: HarmonybrewBot
Description: Created by `brew bump`

---

Created with `brew bump-formula-pr`.<details>
  <summary>release notes</summary>
  <pre>## Release Notes

### Improved

- **Table-form `pre-*` hooks run concurrently**: A multi-entry table hook (`[pre-merge]` with several keys) now runs its commands concurrently, completing the cut-over announced in v0.37.0: every hook type has one execution semantic, where table form is concurrent and pipeline form (`[[pre-merge]]` blocks) is serial. Configs that need ordering should use pipeline form; affected configs have warned on every invocation since v0.37.0, with `wt config update` offering a serial-preserving migration. Two adjacent inconsistencies are also gone: `wt hook <post-type>` foreground runs of multi-entry table hooks now run concurrently like their background counterpart, and single-entry table aliases write to stdout like the other alias spellings, so `wt <alias> | …` works for that spelling too. (Breaking: multi-entry table-form `pre-*` hooks no longer run serially) ([#3052](max-sixty/worktrunk#3052))

- **CI column shows the PR/MR number and review state**: The CI column in `wt list --full` and the statusline shows the branch's open PR/MR reference (`#3041` on GitHub/Gitea/Azure DevOps, `!3041` on GitLab) instead of a plain dot: colored by CI status, hyperlinked to the PR, dimmed when stale or draft. Review state folds into the color, with magenta for changes requested and cyan for awaiting review. `wt list --format=json` gains `ci.number` and `ci.review_state`. ([#3041](max-sixty/worktrunk#3041), [#3044](max-sixty/worktrunk#3044))

- **Hook and alias templates render when each step runs**: Templates are syntax-checked before the first step runs and rendered as each step executes, so `{{ vars.* }}` always reads fresh values and hooks and aliases share one expansion model. Two consequences: a pipeline step with an undefined variable fails when that step is reached (earlier steps run first), and a template error in a background hook surfaces in the hook log rather than failing the foreground command. The background runner also labels template errors the way the foreground does. ([#3042](max-sixty/worktrunk#3042), [#3047](max-sixty/worktrunk#3047))

- **`wt config state cache`**: The regenerable caches (CI status, branch summaries, git-command caches, hints, the `wt switch -` target) are consolidated under `wt config state cache`; `cache clear` drops them all without prompting, since everything regenerates. `wt config state clear`, which also wipes hand-authored markers and vars, now asks for confirmation; `--yes` skips, and non-interactive runs without `--yes` cancel. The `ci-status`, `hints`, and `previous-branch` subcommands are deprecated but still work. ([#3027](max-sixty/worktrunk#3027))

- **Statusline rate-limit segment**: Above 90% of the binding window the segment shows the used percentage (`95%(8:30am–1:30pm)`) instead of the pace ratio; near the cap, what's left matters more than the rate. The pace form drops the word "pace" (`2.9×(Tue–Tue 5pm)`), and `-vv` logs each window's inputs and selection to `.git/wt/logs/trace.log` so the binding-window choice can be reconstructed. ([#3057](max-sixty/worktrunk#3057), [#3053](max-sixty/worktrunk#3053), [#3029](max-sixty/worktrunk#3029))

- **`/wt-switch-create` branch name is optional**: The Claude Code plugin command picks a branch name from the task when none is given, and the skill's workflow is one route (create, then enter) with an error-driven fallback instead of three guarded paths. Worktrees created mid-session persist after the session ends. ([#3058](max-sixty/worktrunk#3058))

### Fixed

- **Plugin worktree hooks fail before side effects**: The Claude Code plugin's `WorktreeCreate`/`WorktreeRemove` hooks validate the payload before running `wt`; a malformed payload previously created a branch named `null` or could remove the wrong worktree. `wt remove`'s help now documents that the positional argument accepts a worktree path as well as a branch name. ([#3058](max-sixty/worktrunk#3058), [#3060](max-sixty/worktrunk#3060))

- **Waiting-for-input marker covers questions, permission prompts, and turn end**: The Claude Code plugin sets the 💬 marker on `AskUserQuestion`, permission requests, and turn end. Previously only the `Notification` event set it, which never fires for the built-in question picker (and on some platforms not for permission prompts), so the marker stayed 🤖 while Claude waited. Part of [#2916](max-sixty/worktrunk#2916). ([#3023](max-sixty/worktrunk#3023), thanks @Ismael for reporting)

- **`wt list --help` no longer panics when piped**: With no detectable terminal width (output piped, `COLUMNS` unset), `wt list --help` panicked with a capacity overflow, and the post-commit diffstat truncated filenames to ~10 characters. Both now handle unknown width. ([#3040](max-sixty/worktrunk#3040))

- **Deprecation warnings match `wt config update`**: A config deprecation warning now fires exactly when `wt config update` would change the file. The old detection and migration logic had drifted in several places: `ff` and `no-ff` together silently dropped `no-ff`, a scalar `forge` key was overwritten by the `[ci]` migration, an empty `[ci] platform` migrated without warning, and an empty `approved-commands = []` could be removed by an unrelated update. ([#3055](max-sixty/worktrunk#3055))

- **Migrated `[forge]` keeps `[ci]`'s spot**: The `[ci]` → `[forge]` migration rendered the new section at the end of the file and could drop comments above `[ci]`; it now takes over `[ci]`'s position, comments included, so `wt config show` and `wt config update` diffs stay minimal. ([#3051](max-sixty/worktrunk#3051))

- **`wt config state get` is read-only**: The aggregate dump resolved (and re-cached) the default branch, so running it right after `clear` silently repopulated the cache, and on a cold clone it could hit the network. It now reports the cached value or `(none)`. `wt config state default-branch get` still resolves and caches. ([#3024](max-sixty/worktrunk#3024))

### Documentation

- **Reading vs resolving cached state**: `wt config state default-branch get` resolves and caches; the aggregate `wt config state get` only reports the cache. The help text and module docs now state the split. ([#3028](max-sixty/worktrunk#3028))

### Internal

- `wt switch`/`wt remove` orchestration moved from `main.rs` to their command modules, and the hook execution call chain lost two delegation layers. ([#3049](max-sixty/worktrunk#3049), [#3036](max-sixty/worktrunk#3036))

- Config deprecations are driven by a single rule table; each rule is one function that migrates and reports what it changed. ([#3045](max-sixty/worktrunk#3045), [#3055](max-sixty/worktrunk#3055))

- `terminal_width()` returns `Option<usize>` instead of a `usize::MAX` sentinel, making the no-width case a compile-time concern. ([#3043](max-sixty/worktrunk#3043))

- Snapshot tests regenerate identically across machines: host-specific paths are guarded, host-dependent env-block markers are normalized, and help snapshots share one settings builder. ([#3009](max-sixty/worktrunk#3009), [#3026](max-sixty/worktrunk#3026), [#3037](max-sixty/worktrunk#3037), [#3061](max-sixty/worktrunk#3061))

- Nightly CI runs lib tests across the full feature powerset. ([#3059](max-sixty/worktrunk#3059))

- MSRV bumped from 1.94 to 1.95. ([#2948](max-sixty/worktrunk#2948))

## Install worktrunk 0.58.0

### Install prebuilt binaries via shell script

```sh
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/download/v0.58.0/worktrunk-installer.sh | sh && wt config shell install
```

### Install prebuilt binaries via powershell script

```sh
powershell -ExecutionPolicy Bypass -c "irm https://github.com/max-sixty/worktrunk/releases/download/v0.58.0/worktrunk-installer.ps1 | iex"; git-wt config shell install
```

### Install prebuilt binaries via Homebrew

```sh
brew install worktrunk && wt config shell install
```

## Download worktrunk 0.58.0

|  File  | Platform | Checksum |
|--------|----------|----------|
| [worktrunk-aarch64-apple-darwin.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.58.0/worktrunk-aarch64-apple-darwin.tar.xz) | Apple Silicon macOS | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.58.0/worktrunk-aarch64-apple-darwin.tar.xz.sha256) |
| [worktrunk-x86_64-apple-darwin.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.58.0/worktrunk-x86_64-apple-darwin.tar.xz) | Intel macOS | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.58.0/worktrunk-x86_64-apple-darwin.tar.xz.sha256) |
| [worktrunk-x86_64-pc-windows-msvc.zip](https://github.com/max-sixty/worktrunk/releases/download/v0.58.0/worktrunk-x86_64-pc-windows-msvc.zip) | x64 Windows | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.58.0/worktrunk-x86_64-pc-windows-msvc.zip.sha256) |
| [worktrunk-aarch64-unknown-linux-musl.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.58.0/worktrunk-aarch64-unknown-linux-musl.tar.xz) | ARM64 MUSL Linux | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.58.0/worktrunk-aarch64-unknown-linux-musl.tar.xz.sha256) |

See merge request: Harmonybrew/homebrew-core!11501
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.

2 participants