Skip to content

refactor(pacquet/config): replace env mutations with DI pattern#11718

Merged
zkochan merged 8 commits into
mainfrom
claude/fix-env-variable-mutation-jltSU
May 18, 2026
Merged

refactor(pacquet/config): replace env mutations with DI pattern#11718
zkochan merged 8 commits into
mainfrom
claude/fix-env-variable-mutation-jltSU

Conversation

@KSXGitHub

@KSXGitHub KSXGitHub commented May 18, 2026

Copy link
Copy Markdown
Contributor

Tracks pnpm/pacquet#343. The env-mutating default_store_dir tests came with the pacquet subtree merge, so the original fix still applies here — this PR threads them through the DI seam consolidated in #11708.

Summary

  • default_store_dir is now generic over Sys: EnvVar with home_dir / current_dir as FnOnce closures (same shape as Config::current). A thin args-less default_store_dir_host wrapper keeps the SmartDefault expression on Config::store_dir short.
  • The four affected tests (test_default_store_dir_with_pnpm_home_env, test_default_store_dir_with_xdg_env in defaults::tests; should_use_pnpm_home_env_var, should_use_xdg_data_home_env_var in lib::tests) now drive each branch with per-test unit-struct EnvVar fakes — no EnvGuard, no unsafe, no std::env::set_var. Closures the early returns don't consume call unreachable! to document the precondition.
  • Dropped the EnvGuard from npmrc_auth::tests::ignores_non_auth_keys: nothing in the crate mutates PNPM_HOME / XDG_DATA_HOME anymore.
  • EnvGuard itself stays — proxy-cascade and NPM_CONFIG_WORKSPACE_DIR tests, plus git-fetcher / executor, still rely on it. Retiring it entirely is out of scope.

Test plan

  • cargo check -p pacquet-config --tests
  • cargo fmt --check
  • cargo test -p pacquet-config (155 passed)
  • Full just ready (CI)

Written by an agent (Claude Code, claude-opus-4-7).

Summary by CodeRabbit

  • Documentation

    • Expanded dependency-injection guidance and added a "reading process state" section, explicitly allowing DI seams for shared process-global state (env, cwd, umask, etc.).
  • Refactor

    • Configuration defaults now obtain environment and directory values via dependency-injection seams instead of direct global reads.
  • Tests

    • Tests updated to use injected environment abstractions and no longer mutate process-global state or rely on global env guards.

Review Change Stack

… DI seam

Replaces process-environment mutation in the `default_store_dir` tests with
the dependency-injection pattern established in pnpm/pacquet#339 and
consolidated for the in-tree pacquet subtree in #11708. Tracks
pnpm/pacquet#343 — the original issue still applied after pacquet was
merged into this repo because the env-mutating tests came with it.

The four affected unit tests (`test_default_store_dir_with_pnpm_home_env`
and `test_default_store_dir_with_xdg_env` in `defaults::tests`, plus
`should_use_pnpm_home_env_var` and `should_use_xdg_data_home_env_var` in
`lib::tests`) now drive each branch with per-test unit structs that
satisfy `EnvVar`. No `EnvGuard` snapshot, no `unsafe` block, no
process-environment write. The `home_dir` and `current_dir` closures call
`unreachable!` for branches the early `PNPM_HOME` / `XDG_DATA_HOME`
returns short-circuit before consulting them, documenting the
precondition the way the style guide's worked example does.

`default_store_dir` is now generic over `Sys: EnvVar` and takes the
`home_dir` and `current_dir` lookups as `FnOnce` closures, mirroring the
shape of `Config::current`. A thin args-less wrapper
`default_store_dir_host` wires the production `Host` provider together
with `home::home_dir` and `env::current_dir` so the SmartDefault
expression on `Config::store_dir` stays short. The `have_default_values`
wiring assertion now compares against
`default_store_dir::<Host>(home::home_dir, env::current_dir)` instead of
the old args-less helper.

`npmrc_auth::tests::ignores_non_auth_keys` no longer needs to hold the
EnvGuard global lock against the two `Config::new()` snapshots it
compares — nothing in the crate mutates `PNPM_HOME` or `XDG_DATA_HOME`
anymore, so the env-derived `store_dir` is observed identically by both
calls even under nextest's in-process parallelism.

`EnvGuard` itself stays in `pacquet-testing-utils` because the proxy
cascade and `NPM_CONFIG_WORKSPACE_DIR` tests in `lib::tests`, as well as
out-of-crate users in `git-fetcher` and `executor`, still rely on it.
Retiring it entirely is out of scope for this issue.

---
Written by an agent (Claude Code, claude-opus-4-7).
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8682b5e0-2c4a-4720-a42d-b6f9a36d57b8

📥 Commits

Reviewing files that changed from the base of the PR and between 49c690c and ef28426.

📒 Files selected for processing (3)
  • pacquet/CODE_STYLE_GUIDE.md
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/npmrc_auth/tests.rs
✅ Files skipped from review due to trivial changes (1)
  • pacquet/CODE_STYLE_GUIDE.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • pacquet/crates/config/src/npmrc_auth/tests.rs
  • pacquet/crates/config/src/lib.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Code Coverage

📝 Walkthrough

Walkthrough

This PR replaces direct process-global reads in config defaults with a DI seam: default_store_dir now accepts home_dir and current_dir closures plus a Sys: EnvVar capability; docs, Config wiring, and tests are updated to use the seam instead of mutating global state.

Changes

Process-global state dependency injection for config defaults

Layer / File(s) Summary
Design guidance and rationale for DI seams
pacquet/AGENTS.md, pacquet/CODE_STYLE_GUIDE.md
Documentation expanded to justify DI seams for testing process-global state (environment variables, cwd, umask) and shows preferred alternatives to direct std::env reads.
Core API refactoring: default_store_dir with injected providers
pacquet/crates/config/src/defaults.rs
default_store_dir signature changed to accept home_dir and current_dir closures plus a generic Sys: EnvVar, and implementation now reads PNPM_HOME/XDG_DATA_HOME via Sys::var instead of std::env.
Test helpers and DI-based test coverage
pacquet/crates/config/src/defaults/tests.rs
Adds NoEnv and per-test EnvVar impls; rewrites PNPM_HOME, XDG_DATA_HOME, and fallback tests to call default_store_dir via the injection seam instead of mutating process env.
Config struct wiring and integration tests
pacquet/crates/config/src/lib.rs
Config::store_dir SmartDefault updated to default_store_dir::<Host, _, _, _>(home::home_dir, env::current_dir); Config tests adjusted to assert the new wiring and use DI-based env fakes.
Downstream test simplification
pacquet/crates/config/src/npmrc_auth/tests.rs
Removes prior EnvGuard snapshot/lock; test relies on DI-driven store_dir initialization and no longer snapshots environment variables.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Suggested reviewers

  • zkochan

Poem

🐰 I hopped through configs, left no env to mar,
I stitched DI seams so tests can run in par.
PNPM_HOME and XDG flow tidy and clean,
No global muck — just closures in between.
Hooray for fakes and parallel tests—hip, hop, serene!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main refactoring: replacing environment variable mutations with a dependency-injection pattern in the pacquet/config crate.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-env-variable-mutation-jltSU

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter

codecov-commenter commented May 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.74359% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.73%. Comparing base (e2a3c68) to head (ef28426).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/config/src/lib.rs 75.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11718      +/-   ##
==========================================
- Coverage   89.74%   89.73%   -0.02%     
==========================================
  Files         129      129              
  Lines       14949    14970      +21     
==========================================
+ Hits        13416    13433      +17     
- Misses       1533     1537       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.02      7.5±0.21ms   578.0 KB/sec    1.00      7.4±0.05ms   588.9 KB/sec

claude added 5 commits May 18, 2026 13:44
…ameter

The Windows branch of `default_store_dir` calls
`current_dir().expect("current directory is unavailable")`, which
requires `Error: Debug` on the `Result<PathBuf, Error>` returned by the
`CurrentDir` closure. The original `where` clause didn't declare that
bound, so the function compiled on Linux/macOS (where the Windows
branch is `#[cfg(windows)]`-gated out) but failed on
`Lint and Test (windows-latest)` once the cfg fired.

All current callers pass `env::current_dir` (Error = `io::Error`) or
pin `Error = std::io::Error` via turbofish on test fakes — both
already satisfy `Debug` — so this is a pure type-bound fix with no
behaviour or call-site change.

---
Written by an agent (Claude Code, claude-opus-4-7).
The "Dependency injection for tests" gating list in
`pacquet/CODE_STYLE_GUIDE.md` enumerated four reasons to reach past
real fixtures for the DI seam: filesystem error kinds, deterministic
time, external-service happy paths, and unreachable-by-design
preconditions. It did not cover the case this PR retired from
`default_store_dir`: tests that mutate a single per-process slot
(env vars, cwd, umask, signal handlers, …) and so race with every
other test in the same process.

The pre-existing `EnvGuard` workaround restored correctness only by
holding a binary-wide mutex around `unsafe { env::set_var(...) }`,
which forecloses parallelism inside the affected tests and leaves
`unsafe` in the test source. A capability-trait fake keeps the read
deterministic and the mutation contained to the test that needs it.

Adds that bullet to the style guide between "Deterministic time" and
"External-service happy paths," and updates rule 7 in
`pacquet/AGENTS.md` (and its `CLAUDE.md` / `GEMINI.md` symlink
targets) so the summary mirrors the longer list.

---
Written by an agent (Claude Code, claude-opus-4-7).
The previous commit cited `set_current_dir` and "the umask" as
illustrative examples in both the style guide and the AGENTS rule 7
summary. Neither is actually used anywhere in the pacquet codebase
today — only `env::set_var` is. Speculative examples in a style
guide invite contributors to chase patterns that don't exist, so
narrow the wording to the one slot we have a concrete example for
while keeping the principle generalisable (the bullet still says
"any future analogue").

---
Written by an agent (Claude Code, claude-opus-4-7).
…ullet

Reviewer feedback: keep the broader `set_current_dir` / umask /
signal-handler / global-allocator examples in the DI gating bullet
and rule 7 summary. They're not used in the codebase today, but the
analogues are useful as forward-looking illustrations so the next
contributor recognises the same shape when it comes up.

Restores the wording introduced in 96040d7; reverses the narrowing in
d930bed.

---
Written by an agent (Claude Code, claude-opus-4-7).
Adds a "Reading process state" section to CODE_STYLE_GUIDE.md, placed
right before "Dependency injection for tests" so the reading order
captures the two questions in dependency order: first ask whether you
should be reaching for `env::var` / `env::current_dir` at all, then
ask how to inject it if the answer is yes.

The section pins:

1. Library code should rarely call `std::env::var`, `std::env::var_os`,
   or `std::env::current_dir` directly. The default fix is a parameter
   (`&Path`, `&str`, an `Option<String>`, or a `FnOnce` closure) so the
   caller decides which value to read; `Config::current` and
   `workspace::find_workspace_dir_from_env_with` already follow this.
2. When the read genuinely needs to happen, route through the DI seam
   (`Sys: EnvVar`, `Sys::var(name)`).
3. The narrow legitimate direct-call case is computing a `Config`
   default that has no caller — `default_store_dir`,
   `default_modules_dir`, `default_virtual_store_dir`. This is the
   "begrudging" use that pnpm/pacquet#343 + #11718 finally
   threaded through the DI seam.
4. Other accepted boundary reads are program-entry knobs
   (`RAYON_NUM_THREADS`, `TRACE`) and the lifecycle-script env snapshot
   in `crates/executor` / `crates/git-fetcher` that forwards the parent
   env to spawned children verbatim.

The section belongs in CODE_STYLE_GUIDE.md (code-level convention),
not CONTRIBUTING.md (PR workflow) or AGENTS.md (agent-specific
operating rules); rule 7 in AGENTS.md already cross-links the style
guide, so no change there.

---
Written by an agent (Claude Code, claude-opus-4-7).
Comment thread pacquet/crates/config/src/defaults.rs
Comment thread pacquet/crates/config/src/lib.rs Outdated
Reviewer pointed out that the args-less `default_store_dir_host`
wrapper could be inlined into the `#[default(_code = ...)]`
expression on `Config::store_dir` — the SmartDefault macro accepts
the turbofish form `default_store_dir::<Host, _, _, _>(home::home_dir,
env::current_dir)` verbatim, and once the wrapper is gone there's
nothing for the indirection to earn.

Dropping the wrapper also removes a small motivation for the
`Host` import inside `defaults.rs` — adjust the doc comment on
`default_store_dir` to cite `crate::Host` by full path instead.

The `#[cfg(windows)] Error: std::fmt::Debug` bound that the same
reviewer asked about is not feasible: attributes on where-clause
predicates are still unstable (rust-lang/rust#115590), so the bound
stays unconditional with a comment explaining why.

---
Written by an agent (Claude Code, claude-opus-4-7).
@KSXGitHub KSXGitHub marked this pull request as ready for review May 18, 2026 14:21
Copilot AI review requested due to automatic review settings May 18, 2026 14:21
@KSXGitHub KSXGitHub requested a review from zkochan as a code owner May 18, 2026 14:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pacquet/crates/config/src/lib.rs`:
- Around line 977-986: Update the stale comment so it no longer references the
removed `default_store_dir_host` wrapper and instead documents the direct call
to `default_store_dir::<Host, _, _, _>(home::home_dir, env::current_dir)` for
`store_dir`; edit the paragraph that currently mentions `default_store_dir_host`
and replace that wording with the direct generic `default_store_dir` invocation
and a brief note that the SmartDefault expression resolves via the production
capability using `Host`, `home::home_dir`, and `env::current_dir`, leaving the
rest of the explanation about wiring and tests (`defaults::tests`) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c6bdd0be-23ad-4c42-b782-d3cf7f643e30

📥 Commits

Reviewing files that changed from the base of the PR and between 2a9bd89 and 49c690c.

📒 Files selected for processing (6)
  • pacquet/AGENTS.md
  • pacquet/CODE_STYLE_GUIDE.md
  • pacquet/crates/config/src/defaults.rs
  • pacquet/crates/config/src/defaults/tests.rs
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/npmrc_auth/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Agent
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
🧰 Additional context used
📓 Path-based instructions (3)
pacquet/**/AGENTS.md

📄 CodeRabbit inference engine (pacquet/CLAUDE.md)

Document agent functionality and interactions in AGENTS.md

pacquet/**/AGENTS.md: Document agent responsibilities, capabilities, and interactions in AGENTS.md file
Maintain clear and up-to-date documentation of all agent definitions and their purposes
Include agent capabilities, input/output specifications, and integration guidelines in agent documentation

Files:

  • pacquet/AGENTS.md
pacquet/**/*.{rs,md,txt}

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

When citing code anywhere (code comments, doc comments, Markdown docs, PR descriptions, or commit messages), link to a specific commit SHA (first 10 hex characters), not a branch name. Branch links are impermanent and may 404 if files change.

Files:

  • pacquet/AGENTS.md
  • pacquet/crates/config/src/defaults.rs
  • pacquet/crates/config/src/defaults/tests.rs
  • pacquet/crates/config/src/npmrc_auth/tests.rs
  • pacquet/CODE_STYLE_GUIDE.md
  • pacquet/crates/config/src/lib.rs
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

Any change in pacquet must match how the same feature is implemented in the TypeScript pnpm CLI. Find the equivalent code in pnpm workspaces, read the implementation including logic and edge cases, and port the behavior faithfully with structural similarity.

Do not invent behavior that pnpm does not have. Do not 'fix' pnpm quirks unless the same fix has landed in pnpm. If pnpm and pacquet disagree, pnpm is the source of truth.

Log emissions are part of 'match pnpm'. When porting a function that fires events through globalLogger/logger.debug/streamParser.write, mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way it parses pnpm's.

Prefer real fixtures (tempfile::TempDir, mocked registry, integration tests) over dependency-injection seams for most happy paths and error paths. Use the DI seam (capability trait on Host provider) only for branches real fixtures cannot reach: filesystem error kinds, deterministic time, process-global state mutations, or external-service happy paths.

Declare a newtype wrapper for branded string types. Do not collapse the brand into plain String or &str. Give the type its own struct so misuse is a type error.

If upstream always validates before construction of a branded string type, validate too in the Rust wrapper via TryFrom and/or FromStr. Do not provide an infallible public constructor.

If upstream never validates a branded string type, just brand for type-safety. Expose an infallible From and From<&str> where convenient. The type-safety win is the whole point.

If upstream occasionally constructs a branded string type without validation via bare 'as' assertion, expose from_str_unchecked or similarly named constructor on the Rust side. Keep the validating constructor as well.

Match upstream serde behavior for branded types crossing JSON/YAML/INI boundaries. Use #[serde(try_from = "String")] for deserialization (validates) and #[serde(into = "String")] for s...

Files:

  • pacquet/crates/config/src/defaults.rs
  • pacquet/crates/config/src/defaults/tests.rs
  • pacquet/crates/config/src/npmrc_auth/tests.rs
  • pacquet/crates/config/src/lib.rs
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: pnpm/pnpm

Timestamp: 2026-05-18T14:22:41.046Z
Learning: Follow the contributing guide in CONTRIBUTING.md, including commit message format, writing style, setup, and automated checks to run before committing.
Learnt from: CR
Repo: pnpm/pnpm

Timestamp: 2026-05-18T14:22:41.046Z
Learning: Follow the code style guide in CODE_STYLE_GUIDE.md for code-level conventions beyond what cargo fmt, taplo, and clippy enforce: imports, modules, naming, ownership and borrowing, trait bounds, pattern matching, pipe-trait, error handling, test layout, and Arc/Rc cloning.
Learnt from: CR
Repo: pnpm/pnpm

Timestamp: 2026-05-18T14:22:41.046Z
Learning: Keep dependencies at the right level. Add a new dependency to the specific crate that needs it, not to the workspace root or a shared crate unless multiple crates actually depend on it.
Learnt from: CR
Repo: pnpm/pnpm

Timestamp: 2026-05-18T14:22:41.046Z
Learning: User-facing errors go through miette via the pacquet-diagnostics crate. Match pnpm's error codes and messages where pnpm defines them — error codes are part of the public contract.
Learnt from: CR
Repo: pnpm/pnpm

Timestamp: 2026-05-18T14:22:41.046Z
Learning: Keep commits focused. A bug fix commit should not also refactor or reformat unrelated code. Reference the upstream pnpm commit/PR you ported from when applicable.
📚 Learning: 2026-05-14T17:38:34.573Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11640
File: pacquet/AGENTS.md:1-6
Timestamp: 2026-05-14T17:38:34.573Z
Learning: In this repository, files named `AGENTS.md` are agent-guide instructions for AI coding tools (e.g., Claude Code/Codex/Cursor) following the agentsmd.net convention, not documentation of software agents or an agentic framework. When reviewing `**/AGENTS.md`, do not apply review rules intended for agentic-framework documentation (e.g., documenting agent capabilities, inputs/outputs, or integration points); instead, treat these files as tooling/assistive guidance for contributors.

Applied to files:

  • pacquet/AGENTS.md
🔇 Additional comments (6)
pacquet/AGENTS.md (1)

52-54: LGTM!

pacquet/CODE_STYLE_GUIDE.md (1)

507-515: LGTM!

Also applies to: 524-524

pacquet/crates/config/src/defaults.rs (1)

1-1: LGTM!

Also applies to: 56-90, 93-93, 97-97, 103-103, 107-107, 110-112

pacquet/crates/config/src/defaults/tests.rs (1)

5-5: LGTM!

Also applies to: 18-26, 28-38, 41-50, 54-60, 63-73, 76-100

pacquet/crates/config/src/lib.rs (1)

18-18: LGTM!

Also applies to: 187-189, 959-963, 1007-1053

pacquet/crates/config/src/npmrc_auth/tests.rs (1)

59-69: LGTM!

Comment thread pacquet/crates/config/src/lib.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Refactors pacquet-config’s default_store_dir logic and tests to avoid mutating the process environment, by routing env reads through the existing DI seam (Sys: EnvVar) and injecting home_dir / current_dir as closures. This aligns the crate with the pacquet DI conventions introduced in the earlier seam consolidation work.

Changes:

  • Made default_store_dir generic over Sys: EnvVar and injected home_dir / current_dir closures to keep tests deterministic without std::env::set_var / EnvGuard.
  • Updated pacquet-config tests to use per-test EnvVar fakes to cover PNPM_HOME and XDG_DATA_HOME precedence branches.
  • Updated pacquet documentation to recommend avoiding direct process-state reads in library code and to prefer parameterization/DI.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pacquet/crates/config/src/npmrc_auth/tests.rs Removes EnvGuard usage and updates rationale comments around env-derived defaults.
pacquet/crates/config/src/lib.rs Updates Config::store_dir SmartDefault wiring and rewrites env-dependent tests to use DI fakes instead of env mutation.
pacquet/crates/config/src/defaults/tests.rs Replaces env-mutation tests with EnvVar fake providers and adds deterministic fall-through coverage.
pacquet/crates/config/src/defaults.rs Refactors default_store_dir to accept Sys: EnvVar plus injected home/cwd closures.
pacquet/CODE_STYLE_GUIDE.md Adds guidance on avoiding direct process-state reads and when to use DI.
pacquet/AGENTS.md Updates contributor checklist to include “shared process-global state that tests would otherwise mutate” as a DI-appropriate case.
Comments suppressed due to low confidence (1)

pacquet/CODE_STYLE_GUIDE.md:524

  • The DI guidance references pnpm/pnpm#11718 as the PR that retired EnvGuard from default_store_dir, but elsewhere in this document/PR the referenced change is pnpm/pnpm#11708 (and this PR). Please reconcile the PR number so the historical link is accurate.
- **Shared process-global state that tests would otherwise mutate.** When a branch depends on a single per-process slot — environment variables, the current working directory, the umask, signal handlers, the global allocator — the only way to exercise it without DI is to write to that slot, and the write is observed by every other test in the same process. A serialisation lock (the `EnvGuard` pattern that pnpm/pacquet#343 + pnpm/pnpm#11718 retired from `default_store_dir`) restores correctness only by forcing the affected tests to run single-threaded, and leaves an `unsafe { env::set_var(...) }` block at the call site. A capability-trait fake keeps the read deterministic and the mutation contained to the test that needs it, so nextest's in-process parallelism stays useful and `unsafe` stays out of test code. The reverse follows too: a real-fixture happy path should not be promoted to DI just because it crosses a process-global slot — only the *mutation* side of the slot is the problem; reading whatever the shell already has is fine.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pacquet/crates/config/src/lib.rs Outdated
Comment on lines +977 to +986
// The SmartDefault expression for `store_dir` resolves to
// `default_store_dir::<Host>(home::home_dir, env::current_dir)`
// via the thin `default_store_dir_host` wrapper, so calling
// the generic helper here with the same `Host` capability and
// the same OS closures must produce the same value — even on
// a developer machine with `PNPM_HOME` / `XDG_DATA_HOME` set.
// This is the wiring assertion that proves the SmartDefault
// field still goes through the production capability; the
// per-branch behaviour of `default_store_dir` is exercised
// with fake-`Sys` structs in `defaults::tests`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same stale comment as CodeRabbit's; updated to match the current direct call in ef28426.


Written by an agent (Claude Code, claude-opus-4-7).


Generated by Claude Code

Comment on lines +59 to +69
// `Config::new()` reads `PNPM_HOME` / `XDG_DATA_HOME` via
// `default_store_dir::<Host>` to compute `store_dir`. Both
// values come from the real process environment, but no other
// test in this crate mutates them anymore — the per-branch
// tests in `defaults::tests` and `lib::tests` drive
// `default_store_dir` through the dependency-injection seam
// (pnpm/pacquet#339, pnpm/pnpm#11708, pnpm/pacquet#343) with
// fake `Sys` providers, so the two `Config::new()` snapshots
// compared below observe the same env-derived `store_dir` even
// under nextest's in-process parallelism without an `EnvGuard`
// lock.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right — the comment cited the old single-type-param signature. Updated in ef28426 to spell out the current four-parameter turbofish: default_store_dir::<Host, _, _, _>(home::home_dir, env::current_dir).


Written by an agent (Claude Code, claude-opus-4-7).


Generated by Claude Code

Comment thread pacquet/CODE_STYLE_GUIDE.md Outdated
- **Take the value as a parameter.** A function that needs a path declares `path: &Path`; one that needs an env-var value takes it as a `&str` argument, an `Option<String>`, or a `FnOnce` closure that produces it. `Config::current` threads the cwd through a caller-supplied `FnOnce() -> Result<PathBuf, _>` closure rather than calling `env::current_dir` itself, and `crates/workspace`'s `find_workspace_dir_from_env_with` does the same for an env-var lookup — both let tests drive the read without touching the process environment.
- **Thread the read through the DI seam.** Declare `Sys: EnvVar` and call `Sys::var(name)`. See [Dependency injection for tests](#dependency-injection-for-tests) for the convention.

The narrow case where a direct call is acceptable is computing the default of a `Config` field that has no caller to take the value from — `default_store_dir`, `default_modules_dir`, and `default_virtual_store_dir` are the canonical examples; this is the "begrudging" use behind pnpm/pacquet#343. Even there, route the lookup through the DI seam: the generic `default_store_dir<Sys: EnvVar>` form keeps the reads testable, and the args-less wrapper that feeds `SmartDefault` (`default_store_dir_host`) is a single-line composition of the production capabilities (`Host`, `home::home_dir`, `env::current_dir`) that exists only because the `SmartDefault` macro needs an args-less expression. The other accepted boundary reads are program-entry knobs (`RAYON_NUM_THREADS` in `crates/cli`, `TRACE` in `crates/diagnostics`) and the lifecycle-script env snapshot in `crates/executor` and `crates/git-fetcher`, which has to forward the parent environment to spawned children verbatim. If new library code seems to need `env::var` / `env::current_dir` for any other reason, the answer is almost always a `&Path` (or equivalent) parameter, not a process-state read.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ef28426. The style guide now recommends inlining the production composition at the SmartDefault site directly (#[default(_code = "default_store_dir::<Host, _, _, _>(home::home_dir, env::current_dir)")]) rather than introducing an args-less wrapper.

On the suppressed low-confidence comment in the same review about the PR number (pnpm/pnpm#11718 vs #11708) — #11708 introduced the DI seam consolidation, and this PR (#11718) is the one that actually retires the EnvGuard pattern from default_store_dir. The citation is correct as-is.


Written by an agent (Claude Code, claude-opus-4-7).


Generated by Claude Code

Commit 49c690c inlined the SmartDefault expression for
`Config::store_dir` and dropped the `default_store_dir_host`
wrapper, but three comments still pointed at the wrapper or at the
old single-type-parameter signature of `default_store_dir`.
CodeRabbit and Copilot flagged all three on review:

- `crates/config/src/lib.rs`, `have_default_values`: the comment said
  the SmartDefault resolved "via the thin `default_store_dir_host`
  wrapper". Now reflects the direct
  `default_store_dir::<Host, _, _, _>(home::home_dir,
  env::current_dir)` call.
- `crates/config/src/npmrc_auth/tests.rs`, `ignores_non_auth_keys`:
  the comment cited `default_store_dir::<Host>` with no closure
  parameters. Now spells out the four-parameter turbofish that
  matches the current signature.
- `CODE_STYLE_GUIDE.md`, "Reading process state": the paragraph
  recommended an args-less `default_store_dir_host` wrapper as the
  pattern to follow. Now recommends inlining the production
  composition at the `SmartDefault` site directly.

Copilot also queried whether `#11718` was the right number
in the DI section's `EnvGuard` retirement citation — confirmed it
is. #11708 introduced the DI seam consolidation; this PR is the
one that retires the env-mutation pattern from `default_store_dir`.

---
Written by an agent (Claude Code, claude-opus-4-7).
@github-actions

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.447 ± 0.078 2.343 2.550 1.02 ± 0.04
pacquet@main 2.404 ± 0.051 2.356 2.513 1.00
pnpm 4.823 ± 0.046 4.737 4.898 2.01 ± 0.05
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.44746032906,
      "stddev": 0.07750584005097994,
      "median": 2.44802273846,
      "user": 2.65026798,
      "system": 3.7087297199999996,
      "min": 2.3426409989600003,
      "max": 2.5503736049600003,
      "times": [
        2.52185342996,
        2.36222052296,
        2.5358972899600003,
        2.5503736049600003,
        2.4486619589600003,
        2.44738351796,
        2.49716208296,
        2.3426409989600003,
        2.3578353889600003,
        2.41057449496
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.4035835999600006,
      "stddev": 0.05095639506254182,
      "median": 2.39778391346,
      "user": 2.6613895800000003,
      "system": 3.7110050200000004,
      "min": 2.35567497796,
      "max": 2.5127644419600004,
      "times": [
        2.39227323796,
        2.36901105096,
        2.35567497796,
        2.5127644419600004,
        2.40635619796,
        2.4100352169600003,
        2.40329458896,
        2.36261885096,
        2.35729530996,
        2.46651212596
      ]
    },
    {
      "command": "pnpm",
      "mean": 4.82267371796,
      "stddev": 0.04583083190450137,
      "median": 4.82486827796,
      "user": 8.28508578,
      "system": 4.12960382,
      "min": 4.73652744996,
      "max": 4.89758743196,
      "times": [
        4.82416828896,
        4.87628666996,
        4.84113149196,
        4.822546441959999,
        4.79256983296,
        4.82891030496,
        4.89758743196,
        4.8255682669599995,
        4.73652744996,
        4.78144099996
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 674.6 ± 22.1 656.8 735.8 1.00
pacquet@main 706.8 ± 52.7 658.9 827.8 1.05 ± 0.09
pnpm 2583.6 ± 141.6 2465.2 2897.0 3.83 ± 0.24
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.67458871466,
      "stddev": 0.022132698632454404,
      "median": 0.66994754156,
      "user": 0.37849946,
      "system": 1.5504529999999999,
      "min": 0.65679761906,
      "max": 0.73577442106,
      "times": [
        0.73577442106,
        0.67112866206,
        0.66248265306,
        0.67137322906,
        0.66876642106,
        0.66514512506,
        0.67515804106,
        0.66775263006,
        0.65679761906,
        0.67150834506
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.70678647046,
      "stddev": 0.0527348985626677,
      "median": 0.69511974006,
      "user": 0.36626756000000005,
      "system": 1.5739554999999998,
      "min": 0.65888172406,
      "max": 0.8278255450600001,
      "times": [
        0.76738284806,
        0.70668373306,
        0.66534019906,
        0.8278255450600001,
        0.68646658106,
        0.65888172406,
        0.70525097506,
        0.67451550306,
        0.70377289906,
        0.67174469706
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.5835932454600004,
      "stddev": 0.14161888940838308,
      "median": 2.52395747106,
      "user": 3.2075173600000007,
      "system": 2.2070575000000003,
      "min": 2.46516238406,
      "max": 2.89700385706,
      "times": [
        2.89700385706,
        2.46516238406,
        2.53022899706,
        2.69122918406,
        2.50368231306,
        2.51768594506,
        2.46832524806,
        2.5440317400600003,
        2.7252994250600002,
        2.49328336106
      ]
    }
  ]
}

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.

5 participants