Skip to content

test(hook-plan): isolate approval test from the real user config dir - #2853

Merged
max-sixty merged 1 commit into
mainfrom
nix-flake-test-isolation
May 21, 2026
Merged

test(hook-plan): isolate approval test from the real user config dir#2853
max-sixty merged 1 commit into
mainfrom
nix-flake-test-isolation

Conversation

@max-sixty

Copy link
Copy Markdown
Owner

The nix-flake CI job was red on main: the unit test commands::hook_plan::tests::approve_readonly_drops_unapproved_project_keeps_user panicked with Failed to create config directory: Permission denied.

The test called approve_command(.., None). The None for approvals_path makes Approvals resolve the path through approvals_path(), which — in the lib crate compiled in non-test mode — points at the real ~/.config/worktrunk/ directory. approve_command then create_dir_alls it. Outside a sandbox $HOME is writable, so the test silently passed while writing a stray [projects.proj] entry into the user's approvals.toml. The nix build sandbox has no writable config dir, so it panicked instead.

This was latent on main since #2806 (which added the test). It only started tripping the gate now because the nix-flake job runs when a PR touches Cargo.{toml,lock}, and #2849 was the first to do so — so nix-flake failed there for a reason unrelated to that PR's change.

The fix points approve_command at a tempfile::tempdir()-backed approvals path via the Some(&path) parameter it already accepts — no process-env mutation (tests/CLAUDE.md bans it), matching the in-file pattern in src/config/approvals.rs's own tests. Same class of fix as #2615 and #2624.

The two sibling tests in hook_plan.rs only read via Approvals::load(), which returns the default when no file exists — never creating a directory or writing — so they pass in the sandbox unchanged and were left as-is.

Verified by reproducing the sandbox condition locally (test binary run with HOME / XDG_CONFIG_HOME pointed at a non-writable dir): the target test fails before the fix and passes after, and all three hook_plan tests still pass under a plain cargo test.

`approve_readonly_drops_unapproved_project_keeps_user` called
`approve_command(.., None)`, which resolves the approvals path to the
real user config dir and writes there. Outside a sandbox `$HOME` is
writable so it silently passed (while polluting the user's
`approvals.toml`); the nix build sandbox makes that dir unwritable, so
the test panicked with `Permission denied`. This had been latent on
`main` since #2806 — it only started tripping the gate now that #2849
made a PR touch `Cargo.{toml,lock}`, the trigger for the `nix-flake`
job.

Point the call at a tempdir-backed approvals path via the `Some(&path)`
parameter `approve_command` already accepts, matching the in-file
pattern in `src/config/approvals.rs` tests. The two sibling tests only
read via `Approvals::load()` (returns default when no file exists), so
they pass in the sandbox unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@max-sixty max-sixty added the nightly Trigger the nightly workflow on this PR label May 21, 2026
@max-sixty
max-sixty merged commit 4b0ff70 into main May 21, 2026
45 checks passed
@max-sixty
max-sixty deleted the nix-flake-test-isolation branch May 21, 2026 05:53
max-sixty added a commit that referenced this pull request May 21, 2026
The `Approvals` and `UserConfig` mutation methods took an
`Option<&Path>`: `None` meant "resolve the global config path",
`Some(p)` meant "use `p`". `None` was a footgun. A unit test that passed
`None` silently wrote to the developer's real `~/.config/worktrunk/`: it
passed wherever `$HOME` was writable and failed only in a sandbox with
no writable config dir. That is the class of bug behind the recent
`nix-flake` failure (#2853).

This drops the `Option`. All ten mutation methods
(`Approvals::{approve_command, approve_commands, revoke_project,
clear_all, with_locked_mutation}` and
`UserConfig::{set_skip_shell_integration_prompt,
set_skip_commit_generation_prompt, set_project_worktree_path,
set_commit_generation_command, with_locked_mutation}`) now take `&Path`.
Production callers resolve the path explicitly through two new helpers:
`require_approvals_path()` and `require_config_path()`, the
`Result`-returning counterparts of `approvals_path()` / `config_path()`,
matching the `require_*` accessor convention. A test can no longer reach
the real config dir without typing the path; the resolver is no longer a
silent default.

`HookPlan::approve` also skips `Approvals::load()` on the `--yes` path.
That path approves every project command unconditionally and persists
nothing, so the load was dead work. Skipping it removes a useless I/O
and keeps `wt merge --yes` working when `approvals.toml` is malformed.

`tests/CLAUDE.md` gains a "Config Isolation for In-Process Unit Tests"
section; the `approvals_path()` doc is updated to describe the
lib-crate-only `#[cfg(test)]` guard.

## Testing

Full pre-merge gate green (3811 tests, fmt, clippy, doctests). The three
`hook_plan` tests pass under a non-writable-`HOME` sandbox repro of the
build-sandbox condition.

One coverage note: `require_approvals_path()` / `require_config_path()`
each have an error branch (`ok_or_else(|| ConfigError("Cannot determine
…"))`) that fires only when no config directory is resolvable. CI always
sets `$HOME` / `WORKTRUNK_*_PATH`, so that branch is unreachable in
tests, and `codecov/patch` may flag those two lines. The same closure
existed before this change, inside `with_locked_mutation`; the refactor
moves it into the new helpers, where it counts as patched lines.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
max-sixty added a commit that referenced this pull request May 23, 2026
…2853)

The `nix-flake` CI job was red on `main`: the unit test
`commands::hook_plan::tests::approve_readonly_drops_unapproved_project_keeps_user`
panicked with `Failed to create config directory: Permission denied`.

The test called `approve_command(.., None)`. The `None` for
`approvals_path` makes `Approvals` resolve the path through
`approvals_path()`, which — in the lib crate compiled in non-test mode —
points at the real `~/.config/worktrunk/` directory. `approve_command`
then `create_dir_all`s it. Outside a sandbox `$HOME` is writable, so the
test silently passed while writing a stray `[projects.proj]` entry into
the user's `approvals.toml`. The nix build sandbox has no writable
config dir, so it panicked instead.

This was latent on `main` since #2806 (which added the test). It only
started tripping the gate now because the `nix-flake` job runs when a PR
touches `Cargo.{toml,lock}`, and #2849 was the first to do so — so
`nix-flake` failed there for a reason unrelated to that PR's change.

The fix points `approve_command` at a `tempfile::tempdir()`-backed
approvals path via the `Some(&path)` parameter it already accepts — no
process-env mutation (`tests/CLAUDE.md` bans it), matching the in-file
pattern in `src/config/approvals.rs`'s own tests. Same class of fix as
#2615 and #2624.

The two sibling tests in `hook_plan.rs` only *read* via
`Approvals::load()`, which returns the default when no file exists —
never creating a directory or writing — so they pass in the sandbox
unchanged and were left as-is.

Verified by reproducing the sandbox condition locally (test binary run
with `HOME` / `XDG_CONFIG_HOME` pointed at a non-writable dir): the
target test fails before the fix and passes after, and all three
`hook_plan` tests still pass under a plain `cargo test`.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
max-sixty added a commit that referenced this pull request May 23, 2026
The `Approvals` and `UserConfig` mutation methods took an
`Option<&Path>`: `None` meant "resolve the global config path",
`Some(p)` meant "use `p`". `None` was a footgun. A unit test that passed
`None` silently wrote to the developer's real `~/.config/worktrunk/`: it
passed wherever `$HOME` was writable and failed only in a sandbox with
no writable config dir. That is the class of bug behind the recent
`nix-flake` failure (#2853).

This drops the `Option`. All ten mutation methods
(`Approvals::{approve_command, approve_commands, revoke_project,
clear_all, with_locked_mutation}` and
`UserConfig::{set_skip_shell_integration_prompt,
set_skip_commit_generation_prompt, set_project_worktree_path,
set_commit_generation_command, with_locked_mutation}`) now take `&Path`.
Production callers resolve the path explicitly through two new helpers:
`require_approvals_path()` and `require_config_path()`, the
`Result`-returning counterparts of `approvals_path()` / `config_path()`,
matching the `require_*` accessor convention. A test can no longer reach
the real config dir without typing the path; the resolver is no longer a
silent default.

`HookPlan::approve` also skips `Approvals::load()` on the `--yes` path.
That path approves every project command unconditionally and persists
nothing, so the load was dead work. Skipping it removes a useless I/O
and keeps `wt merge --yes` working when `approvals.toml` is malformed.

`tests/CLAUDE.md` gains a "Config Isolation for In-Process Unit Tests"
section; the `approvals_path()` doc is updated to describe the
lib-crate-only `#[cfg(test)]` guard.

## Testing

Full pre-merge gate green (3811 tests, fmt, clippy, doctests). The three
`hook_plan` tests pass under a non-writable-`HOME` sandbox repro of the
build-sandbox condition.

One coverage note: `require_approvals_path()` / `require_config_path()`
each have an error branch (`ok_or_else(|| ConfigError("Cannot determine
…"))`) that fires only when no config directory is resolvable. CI always
sets `$HOME` / `WORKTRUNK_*_PATH`, so that branch is unreachable in
tests, and `codecov/patch` may flag those two lines. The same closure
existed before this change, inside `with_locked_mutation`; the refactor
moves it into the new helpers, where it counts as patched lines.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

nightly Trigger the nightly workflow on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants