refactor(config): require an explicit path for config mutations#2862
Conversation
The `Approvals` and `UserConfig` mutation methods took `Option<&Path>`, where `None` resolved the global config path. A test that passed `None` silently wrote to the real `~/.config/worktrunk/`: it passed locally and failed only in a sandbox with no writable config dir. Drop the `Option`: all ten mutation methods now take `&Path`. Production callers resolve explicitly through two new helpers, `require_approvals_path()` and `require_config_path()` (the `Result`-returning counterparts of `approvals_path()` / `config_path()`). A test can no longer reach the real config dir without typing the path. Also: `HookPlan::approve` skips `Approvals::load()` on the `--yes` path. That path approves unconditionally and persists nothing, so the load was dead work; skipping it also keeps `wt merge --yes` working when `approvals.toml` is malformed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
CI is red on lint, test (linux), and code-coverage — all three trace to one missed call site:
error[E0308]: mismatched types
--> src/config/approvals.rs:699:17
|
699 | Some(&path),
| ^^^^^^^^^^^ expected `&Path`, found `Option<&PathBuf>`
That call is inside test_literal_command_text_is_not_normalized_for_approval_matching, which was added in #2852 and merged into main between the time this branch was written and when CI ran. The diff hunks here update the surrounding tests at line 681 and line 711, but the new test fell in the gap and kept its Some(&path). Local pre-merge was green before the rebase pulled in the new call site, which matches the PR description.
The fix is the one-line change you'd expect (Some(&path) → &path). Happy to push it if you'd like, otherwise leaving it to you.
#2851 landed `test_literal_command_text_is_not_normalized_for_approval_matching` on main using `approve_command`'s old `Option<&Path>` argument. This branch changed that parameter to `&Path`, so the merge needs the new call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
codecov/patch is failing — 2 missed lines, both inside .ok_or_else(...) closures for the new require_*_path helpers:
src/config/user/path.rs:58—ConfigError("Cannot determine config directory. Set $HOME or $XDG_CONFIG_HOME".to_string())src/config/approvals.rs:98—ConfigError("Cannot determine approvals path. Set $HOME or $XDG_CONFIG_HOME".to_string())
The function bodies (signature, ok_or_else() call, closing brace) are covered via the happy path; only the closure bodies are uncovered. They fire only when config_path() / approvals_path() returns None, which on any supported platform requires both $HOME and $XDG_CONFIG_HOME to be unset and etcetera::choose_base_strategy() to fail — effectively unreachable from test code without process-wide env mutation.
CLAUDE.md says to investigate (write tests, remove unused code, or ask if it's a false positive). I think this is the "false positive" shape: the strings are user-facing error messages the existing pattern (src/shell/paths.rs:16, src/commands/config/state.rs:106) keeps verbatim, and removing them would lose a helpful diagnostic for a real-but-rare failure mode. No general handler to fall through to.
Dismissing my approval per CLAUDE.md. Two reasonable paths from here:
- Accept the gap — confirm this is a false positive, merge with codecov/patch failing.
- Cover via env-mutation tests — set
HOME=""+XDG_CONFIG_HOME=""+ ensureWORKTRUNK_CONFIG_PATH/WORKTRUNK_APPROVALS_PATHare unset in a serial test. Possible but fragile; the rest of the suite already avoids this pattern.
Happy to push a (1) re-approval or (2) a coverage commit — whichever you prefer.
codecov/patch failed — see follow-up review for analysis. Re-approve once the coverage gap is resolved or confirmed as a false positive.
…ath (#2872) `require_config_path()` (added in #2862) and the older `require_user_config_path()` in `src/commands/config/state.rs` both just wrapped `config_path()` with a `Result`. Two functions, two slightly different error messages, one purpose. This deletes `require_user_config_path()` and points its four callers — `config create` (×2) and `config show` (×2) — at the lib-crate `require_config_path()`. The two wrapper tests in `config/mod.rs` are removed with it. `test_require_user_config_path_returns_ok` tested a trivial wrapper. `test_require_user_config_path_matches_config_path` guarded issue #1134 (config-create resolving a *different* path than config-loading) — that regression is now structurally impossible: `config create` / `show` resolve through `require_config_path()`, which is `config_path()` itself, so there is no separate resolver left to diverge. One behavior delta: when no config directory can be resolved, `config create` / `config show` now error `Cannot determine config directory. Set $HOME or $XDG_CONFIG_HOME` instead of `Cannot determine config directory`. No snapshot test references either string. Follow-up to #2862. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The
ApprovalsandUserConfigmutation methods took anOption<&Path>:Nonemeant "resolve the global config path",Some(p)meant "usep".Nonewas a footgun. A unit test that passedNonesilently wrote to the developer's real~/.config/worktrunk/: it passed wherever$HOMEwas writable and failed only in a sandbox with no writable config dir. That is the class of bug behind the recentnix-flakefailure (#2853).This drops the
Option. All ten mutation methods (Approvals::{approve_command, approve_commands, revoke_project, clear_all, with_locked_mutation}andUserConfig::{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()andrequire_config_path(), theResult-returning counterparts ofapprovals_path()/config_path(), matching therequire_*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::approvealso skipsApprovals::load()on the--yespath. That path approves every project command unconditionally and persists nothing, so the load was dead work. Skipping it removes a useless I/O and keepswt merge --yesworking whenapprovals.tomlis malformed.tests/CLAUDE.mdgains a "Config Isolation for In-Process Unit Tests" section; theapprovals_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_plantests pass under a non-writable-HOMEsandbox 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, andcodecov/patchmay flag those two lines. The same closure existed before this change, insidewith_locked_mutation; the refactor moves it into the new helpers, where it counts as patched lines.