refactor(pacquet): replace callback-DI with capability traits#11730
Conversation
PR #11708 introduced the proper trait-based DI seam (`Sys` generic + `Host` provider), but a handful of functions still emulated the JS/TS pattern by taking accessor closures (`FnOnce`/`FnMut`) — "poor man DI" that the seam was meant to retire. Convert them to the trait shape. Changes: - `crates/config/src/api.rs`: add `EnvVarOs`, `GetHomeDir`, and `GetCurrentDir` capability traits alongside the existing `EnvVar`, all implemented for `Host`. - `default_store_dir`: replace `HomeDir`/`CurrentDir` `FnOnce` closures with `Sys: EnvVar + GetHomeDir + GetCurrentDir`. The SmartDefault expression collapses from `default_store_dir::<Host, _, _, _>( home::home_dir, env::current_dir)` to `default_store_dir::<Host>()`. - `Config::current`: drop the `CurrentDir` closure in favour of a direct `start_dir: &Path` parameter (production always passed the canonicalized `--dir`, never the host cwd, so it was a parameter dressed as a DI seam); thread the home-dir lookup through `Sys: GetHomeDir`; route the inline `std::env::var_os( "NPM_CONFIG_WORKSPACE_DIR")` lookups through `Sys: EnvVarOs`. The three tests that previously held `EnvGuard` and called `unsafe { env::set_var(...) }` to drive the env-var override now use per-test `EnvVarOs` fakes instead. - `crates/workspace/src/api.rs` (new): pacquet-workspace's own `EnvVarOs` trait and `Host` provider, following the per-crate provider convention. - `find_workspace_dir_from_env_with`: replace the `FnMut(&str) -> Option<OsString>` closure with `Sys: EnvVarOs`. Test updates use per-test unit-struct fakes (or shared `HostNoHome` / `HostUnreachableHome` helpers in `pacquet-config`'s test module) per the convention in CODE_STYLE_GUIDE.md's "Dependency injection for tests" section. CODE_STYLE_GUIDE.md's "Reading process state" section is updated to mark the closure shape as discouraged and cite the new `default_store_dir<Sys: EnvVar + GetHomeDir + GetCurrentDir>()` signature as the canonical example. The remaining `FnOnce`/`FnMut` closures in the codebase (`retry_on_fd_pressure`, `StoreCommand::run`'s `config` factory, `State::init`'s `LoadLockfile`, `Config::current`'s `default`) are legitimate lazy/conditional patterns rather than callback-DI and stay as-is. https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📜 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). (3)
🧰 Additional context used📓 Path-based instructions (1)pacquet/**/*.rs📄 CodeRabbit inference engine (pacquet/AGENTS.md)
Files:
🔇 Additional comments (6)
📝 WalkthroughWalkthroughRefactor environment-variable and directory-lookup seams to capability traits ( ChangesTrait-Based Dependency Injection for Environment and Path Lookups
🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
The "poor man DI" / "JS/TS pattern" framing carried context from the discussion that produced the refactor, not the style guide itself. Restate the convention in its own terms: a closure that only defers a side-effecting read is a capability dressed as a parameter, and the right shape for a capability is a trait on `Sys`. https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
The "use a direct value parameter, not a FnOnce closure" addition was defending against a specific anti-pattern from the refactor that produced this section. The positive guidance (trait-based DI on Sys for side effects) already covers what readers need; the negative phrasing is too absolute, and the lazy-evaluation carve-out only exists to soften it. https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #11730 +/- ##
==========================================
- Coverage 89.88% 89.85% -0.03%
==========================================
Files 144 145 +1
Lines 16416 16424 +8
==========================================
+ Hits 14755 14758 +3
- Misses 1661 1666 +5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Micro-Benchmark ResultsLinux |
The "begrudging use behind pnpm/pacquet#343" phrasing reads as though #343 introduced the direct env reads in default_store_dir / default_modules_dir / default_virtual_store_dir. It did not — those helpers have read process state since they existed; #343 tracks retiring the env-mutating *tests* for them. The historical reference doesn't carry any rule the reader needs, so drop it and let the positive guidance stand on its own. https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
The `GetCurrentDir` doc comment linked to `crate::defaults::default_store_dir`, but `defaults` is a private module and the helper isn't re-exported, so rustdoc denies the intra-doc link under `-D rustdoc::private-intra-doc-links` (the Doc CI job runs with `-D warnings`). Point the example at `Config::store_dir` instead — that's the public SmartDefault site where the helper plugs in, which is the contract the trait's production consumer cares about. https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
Pulls in dfd8fbf "chore: update pnpm-lock.yaml" (#11559), which bumps `brace-expansion` from 5.0.5 to 5.0.6 — the upgrade the Audit dependencies CI job is gating on. Also pulls in e01d2bc "fix(pacquet/fs): fall back to absolute target across Windows drives" (#11721), which doesn't touch any file this branch edits. https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
Integrated-Benchmark Report (Linux)Scenario: Frozen Lockfile
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.41430144168,
"stddev": 0.14288989810496697,
"median": 2.38894992398,
"user": 2.7615984399999998,
"system": 3.6343512999999996,
"min": 2.29962797948,
"max": 2.79062900148,
"times": [
2.39067405048,
2.3872257974799997,
2.79062900148,
2.47478823148,
2.40911149848,
2.30185134248,
2.36640876148,
2.40295211248,
2.31974564148,
2.29962797948
]
},
{
"command": "pacquet@main",
"mean": 2.3915785409800003,
"stddev": 0.0768956008413182,
"median": 2.37774740348,
"user": 2.73857914,
"system": 3.6439927999999995,
"min": 2.29899098748,
"max": 2.50605577448,
"times": [
2.43875745248,
2.29899098748,
2.50605577448,
2.44397716548,
2.29995147848,
2.34746241948,
2.37974556548,
2.37574924148,
2.32607310448,
2.49902222048
]
},
{
"command": "pnpm",
"mean": 4.80582281618,
"stddev": 0.12271078781517894,
"median": 4.77339460748,
"user": 8.29218234,
"system": 4.1286124,
"min": 4.671062178480001,
"max": 5.02368115548,
"times": [
4.90037850948,
4.73775214548,
4.685754349480001,
4.671062178480001,
4.676704095480001,
4.800104512480001,
4.88800164648,
4.7466847024800005,
4.92810486648,
5.02368115548
]
}
]
}Scenario: Frozen Lockfile (Hot Cache)
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.7221656387200002,
"stddev": 0.01021492335387014,
"median": 0.7203392209200001,
"user": 0.4179009799999999,
"system": 1.61878332,
"min": 0.7126087474200001,
"max": 0.7483014054200001,
"times": [
0.7483014054200001,
0.7178336404200001,
0.7159410674200001,
0.7126087474200001,
0.72373485542,
0.7265018254200001,
0.7132864874200001,
0.7202033474200001,
0.7227699164200001,
0.7204750944200001
]
},
{
"command": "pacquet@main",
"mean": 0.7344691443200001,
"stddev": 0.03506663505004002,
"median": 0.7376287889200002,
"user": 0.40161868000000006,
"system": 1.61115362,
"min": 0.6880551984200001,
"max": 0.7986971064200001,
"times": [
0.7986971064200001,
0.7285552384200001,
0.7507327394200001,
0.7630821164200001,
0.7467023394200001,
0.6962455304200001,
0.7150897104200001,
0.7572089874200001,
0.7003224764200001,
0.6880551984200001
]
},
{
"command": "pnpm",
"mean": 2.4931143183200004,
"stddev": 0.12228393235799982,
"median": 2.45142915942,
"user": 3.0772481799999993,
"system": 2.22363662,
"min": 2.33931451842,
"max": 2.7296457744200002,
"times": [
2.5543186064200003,
2.44677034142,
2.5596382444200003,
2.7296457744200002,
2.4208150184200004,
2.45608797742,
2.3640183184200003,
2.4337400374200002,
2.62679434642,
2.33931451842
]
}
]
} |
There was a problem hiding this comment.
Pull request overview
This PR standardizes pacquet’s Rust DI seam by replacing callback-shaped “DI” (passing FnOnce/FnMut accessors) with capability traits on the Sys generic + a production Host provider. It also removes process-env mutation from several tests by routing env lookups through trait fakes.
Changes:
- Introduces capability traits (
EnvVarOs,GetHomeDir,GetCurrentDir) and implements them forHost, then refactorsdefault_store_dirandConfig::currentto use these traits (and a directstart_dir: &Pathparameter). - Adds
pacquet-workspace’s crate-localEnvVarOs+Hostprovider and refactors workspace root-finder env lookup to use the trait seam. - Updates docs to discourage callback-shaped DI and reflect the canonical “capability traits +
Hostturbofish” pattern.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pacquet/crates/workspace/src/root_finder/tests.rs | Updates tests to use per-test EnvVarOs fakes instead of closures. |
| pacquet/crates/workspace/src/root_finder.rs | Replaces env accessor closure with Sys: EnvVarOs capability trait. |
| pacquet/crates/workspace/src/lib.rs | Adds api module and re-exports EnvVarOs/Host. |
| pacquet/crates/workspace/src/api.rs | New workspace-local capability trait + Host provider for env var reads. |
| pacquet/crates/config/src/lib.rs | Refactors Config::current signature and store-dir default to capability traits; updates tests accordingly. |
| pacquet/crates/config/src/defaults/tests.rs | Updates default-store-dir tests to implement GetHomeDir/GetCurrentDir on fakes. |
| pacquet/crates/config/src/defaults.rs | Refactors default_store_dir to Sys: EnvVar + GetHomeDir + GetCurrentDir (no accessor closures). |
| pacquet/crates/config/src/api.rs | Adds capability traits (EnvVarOs, GetHomeDir, GetCurrentDir) and implements them for Host. |
| pacquet/crates/cli/src/cli_args.rs | Updates production call site to Config::current::<Host, _>(&dir, ...). |
| pacquet/CODE_STYLE_GUIDE.md | Updates guidance to discourage callback-DI and document the trait-based seam. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -9,16 +9,15 @@ | |||
| //! declares, with any per-test scenario data stored in a `static` | |||
| //! inside the test fn. | |||
| //! | |||
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
pacquet/crates/workspace/src/root_finder/tests.rs (1)
121-121: ⚡ Quick winUse an allocation-free lowercase key comparison in the fake env provider.
This currently allocates a
Stringon each call. Compare against a borrowed lowercase key constant/literal instead.♻️ Proposed fix
- (name == WORKSPACE_DIR_ENV_VAR.to_lowercase()) + (name == "npm_config_workspace_dir") .then(|| OsString::from("/lowercase/root"))As per coding guidelines, "Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies".
🤖 Prompt for 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. In `@pacquet/crates/workspace/src/root_finder/tests.rs` at line 121, The fake env provider currently does an allocation by calling WORKSPACE_DIR_ENV_VAR.to_lowercase() for each comparison (the expression name == WORKSPACE_DIR_ENV_VAR.to_lowercase()); change this to an allocation-free comparison by either comparing against a pre-lowercased borrowed constant (e.g. a &'static str like WORKSPACE_DIR_ENV_VAR_LOWER) or use name.eq_ignore_ascii_case(WORKSPACE_DIR_ENV_VAR) / name.eq_ignore_ascii_case(...) so no temporary String is created; update the comparison in the fake env provider where WORKSPACE_DIR_ENV_VAR is referenced to use one of these allocation-free approaches.pacquet/crates/workspace/src/root_finder.rs (1)
124-124: ⚡ Quick winAvoid per-call lowercase allocation for env-var fallback.
WORKSPACE_DIR_ENV_VAR.to_lowercase()allocates on every call. Prefer a lowercase&'static strconstant and pass it directly.♻️ Proposed fix
pub(crate) const WORKSPACE_DIR_ENV_VAR: &str = "NPM_CONFIG_WORKSPACE_DIR"; +pub(crate) const WORKSPACE_DIR_ENV_VAR_LOWER: &str = "npm_config_workspace_dir"; @@ Sys::var_os(WORKSPACE_DIR_ENV_VAR) - .or_else(|| Sys::var_os(&WORKSPACE_DIR_ENV_VAR.to_lowercase())) + .or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER)) .filter(|value| !value.is_empty()) .map(PathBuf::from)As per coding guidelines, "Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies".
🤖 Prompt for 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. In `@pacquet/crates/workspace/src/root_finder.rs` at line 124, The call uses WORKSPACE_DIR_ENV_VAR.to_lowercase() which allocates on every invocation; introduce a static &'static str constant (e.g. WORKSPACE_DIR_ENV_VAR_LOWERCASE) holding the precomputed lowercase name and replace the to_lowercase() call in the Sys::var_os fallback with that constant so no per-call allocation occurs; update any references in root_finder.rs that currently call to_lowercase() on WORKSPACE_DIR_ENV_VAR to use the new constant.
🤖 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 972-975: The test fakes currently delegate environment access to
the real host via Host::var_os (e.g., in the EnvVarOs impl for HostNoHome),
which makes tests flaky when ambient vars like NPM_CONFIG_WORKSPACE_DIR are set;
update these fake Sys/EnvVarOs implementations (including HostNoHome and the
other similar impls referenced around the other ranges) to return None
unconditionally (unless a specific test overrides behavior), so they do not read
real process env; locate the impl blocks named EnvVarOs for HostNoHome (and the
analogous fake types at the other cited locations) and replace the Host::var_os
call with a direct None return.
---
Nitpick comments:
In `@pacquet/crates/workspace/src/root_finder.rs`:
- Line 124: The call uses WORKSPACE_DIR_ENV_VAR.to_lowercase() which allocates
on every invocation; introduce a static &'static str constant (e.g.
WORKSPACE_DIR_ENV_VAR_LOWERCASE) holding the precomputed lowercase name and
replace the to_lowercase() call in the Sys::var_os fallback with that constant
so no per-call allocation occurs; update any references in root_finder.rs that
currently call to_lowercase() on WORKSPACE_DIR_ENV_VAR to use the new constant.
In `@pacquet/crates/workspace/src/root_finder/tests.rs`:
- Line 121: The fake env provider currently does an allocation by calling
WORKSPACE_DIR_ENV_VAR.to_lowercase() for each comparison (the expression name ==
WORKSPACE_DIR_ENV_VAR.to_lowercase()); change this to an allocation-free
comparison by either comparing against a pre-lowercased borrowed constant (e.g.
a &'static str like WORKSPACE_DIR_ENV_VAR_LOWER) or use
name.eq_ignore_ascii_case(WORKSPACE_DIR_ENV_VAR) /
name.eq_ignore_ascii_case(...) so no temporary String is created; update the
comparison in the fake env provider where WORKSPACE_DIR_ENV_VAR is referenced to
use one of these allocation-free approaches.
🪄 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: a9d37ea4-81c3-48b4-9925-3c810a8374f4
📒 Files selected for processing (10)
pacquet/CODE_STYLE_GUIDE.mdpacquet/crates/cli/src/cli_args.rspacquet/crates/config/src/api.rspacquet/crates/config/src/defaults.rspacquet/crates/config/src/defaults/tests.rspacquet/crates/config/src/lib.rspacquet/crates/workspace/src/api.rspacquet/crates/workspace/src/lib.rspacquet/crates/workspace/src/root_finder.rspacquet/crates/workspace/src/root_finder/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). (1)
- GitHub Check: Agent
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions must mirror pnpm'spnpm:<channel>events throughglobalLogger/logger.debug(...)/streamParser.write(...)calls with matching payload and ordering for@pnpm/cli.default-reportercompatibility
Prefer real fixtures (tempfile::TempDir, mocked registry, integration tests) over dependency-injection seams for testing, reserving DI only for branches real fixtures cannot cover (filesystem error kinds, deterministic time, process-global state, external-service paths)
Declare a newtype wrapper (struct) for branded string types instead of collapsing the brand into plain String or &str
If upstream pnpm always validates before construction of a branded string type, validate too by constructing only via TryFrom and/or FromStr
If upstream pnpm never validates a branded string type, brand only for type-safety by exposing an infallible From and From<&str> constructor
If upstream pnpm occasionally constructs a branded string type without validation, expose from_str_unchecked constructor alongside the validating constructor
Match upstream pnpm serde behavior for branded string types by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical branded string type conversions instead of handwriting impl blocks
Model upstream string literal unions or string literal types as Rust enums instead of newtype wrappers
Treat string template literal types (e.g.,${string}@${string}) as branded string types with the same validation discipline as other branded strings
Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site
Follow test-logging guidance in the style guide: log before...
Files:
pacquet/crates/workspace/src/lib.rspacquet/crates/workspace/src/root_finder/tests.rspacquet/crates/config/src/defaults.rspacquet/crates/workspace/src/api.rspacquet/crates/cli/src/cli_args.rspacquet/crates/workspace/src/root_finder.rspacquet/crates/config/src/api.rspacquet/crates/config/src/defaults/tests.rspacquet/crates/config/src/lib.rs
🔇 Additional comments (10)
pacquet/crates/config/src/api.rs (1)
12-18: LGTM!Also applies to: 36-76, 96-112
pacquet/crates/workspace/src/api.rs (1)
1-36: LGTM!pacquet/crates/workspace/src/lib.rs (1)
20-27: LGTM!pacquet/crates/config/src/defaults.rs (1)
1-1: LGTM!Also applies to: 62-75, 87-92
pacquet/crates/config/src/defaults/tests.rs (1)
5-8: LGTM!Also applies to: 22-28, 39-50, 68-79, 83-87, 94-110
pacquet/crates/workspace/src/root_finder.rs (1)
24-27: LGTM!Also applies to: 105-123, 125-127
pacquet/crates/workspace/src/root_finder/tests.rs (1)
5-120: LGTM!Also applies to: 122-129
pacquet/crates/config/src/lib.rs (1)
774-927: LGTM!pacquet/crates/cli/src/cli_args.rs (1)
102-106: LGTM!pacquet/CODE_STYLE_GUIDE.md (1)
511-514: LGTM!
The module-level doc on the Host provider showed `Config::current::<Host>(...)` (one generic), but the function now takes two generic parameters (`Sys, Default`). The other example lower in the same file already uses the two-param form (`Config::current::<Host, _>(&dir, Default::default)`); align the module-level mention with it. Caught by Copilot review on #11730. https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
The `HostNoHome` / `HostUnreachableHome` test fakes (and the two per-test `HostWithHome` fakes) delegated `EnvVarOs::var_os` to `Host::var_os`, which means a developer with an ambient `NPM_CONFIG_WORKSPACE_DIR` set in their shell could see those tests get steered down `Config::current`'s env-var workspace-dir branch unexpectedly. Tests that actually exercise that branch declare their own `EnvVarOs` fakes (`HostWithEnvWorkspaceDir`, `HostWithEmptyEnvWorkspaceDir`); the rest should return `None` unconditionally so the env-var branch is dead by design. `EnvVar::var` deliberately keeps the `Host` delegation because `proxy_env_fallback_applies_through_current` mutates `HTTPS_PROXY` and relies on the cascade reading it back. Also pre-allocates the lowercase spelling of `NPM_CONFIG_WORKSPACE_DIR` as `WORKSPACE_DIR_ENV_VAR_LOWER` and uses it directly in both the production fallback and the `lowercase_env_var_is_honored_as_fallback` test, dropping the per-call `.to_lowercase()` allocation. Negligible perf-wise — `find_workspace_dir_from_env` runs once per pacquet invocation — but the constant is clearer at the call site. Caught by CodeRabbit review on #11730. https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
pacquet/crates/config/src/lib.rs (1)
1476-1494: 💤 Low value
HostNoEnvNoHomeduplicatesHostNoHome— prefer the module-level fake.The
HostNoEnvNoHomestruct is functionally identical toHostNoHome(lines 966–986): both delegateEnvVartoHost, returnNoneforEnvVarOs, and returnNoneforGetHomeDir. The test can useHostNoHomedirectly.Suggested simplification
#[test] pub fn single_project_anchors_modules_at_cwd() { - struct HostNoEnvNoHome; - impl EnvVar for HostNoEnvNoHome { - fn var(name: &str) -> Option<String> { - Host::var(name) - } - } - impl EnvVarOs for HostNoEnvNoHome { - fn var_os(_: &str) -> Option<OsString> { - None - } - } - impl GetHomeDir for HostNoEnvNoHome { - fn home_dir() -> Option<PathBuf> { - None - } - } let tmp = tempdir().unwrap(); let config = - Config::current::<HostNoEnvNoHome, _>(tmp.path(), Config::new).expect("config loads"); + Config::current::<HostNoHome, _>(tmp.path(), Config::new).expect("config loads"); assert_eq!(config.modules_dir, tmp.path().join("node_modules")); assert_eq!(config.virtual_store_dir, tmp.path().join("node_modules/.pnpm")); }🤖 Prompt for 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. In `@pacquet/crates/config/src/lib.rs` around lines 1476 - 1494, The new test introduces a redundant fake HostNoEnvNoHome that duplicates the existing module-level HostNoHome; replace HostNoEnvNoHome with the existing HostNoHome in the call to Config::current (and remove the added struct impls), i.e. use Config::current::<HostNoHome, _>(tmp.path(), Config::new) so the test reuses the shared fake implementations (EnvVar, EnvVarOs, GetHomeDir) instead of duplicating HostNoEnvNoHome.
🤖 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/workspace/src/root_finder.rs`:
- Around line 127-129: The current chain calls
Sys::var_os(WORKSPACE_DIR_ENV_VAR).or_else(...).filter(...), so an empty
uppercase value (Some("")) prevents evaluating the lowercase fallback; change
the logic to treat empty as unset by applying the emptiness filter before the
fallback: call Sys::var_os(WORKSPACE_DIR_ENV_VAR).filter(|v|
!v.is_empty()).or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER).filter(|v|
!v.is_empty())). Update the code that uses Sys::var_os, WORKSPACE_DIR_ENV_VAR
and WORKSPACE_DIR_ENV_VAR_LOWER accordingly and add a test covering the case
where the uppercase env var is set to an empty string and the lowercase var is
valid.
---
Nitpick comments:
In `@pacquet/crates/config/src/lib.rs`:
- Around line 1476-1494: The new test introduces a redundant fake
HostNoEnvNoHome that duplicates the existing module-level HostNoHome; replace
HostNoEnvNoHome with the existing HostNoHome in the call to Config::current (and
remove the added struct impls), i.e. use Config::current::<HostNoHome,
_>(tmp.path(), Config::new) so the test reuses the shared fake implementations
(EnvVar, EnvVarOs, GetHomeDir) instead of duplicating HostNoEnvNoHome.
🪄 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: 0bcb36d2-a62b-4a6e-959d-fe678a17b68e
📒 Files selected for processing (3)
pacquet/crates/config/src/lib.rspacquet/crates/workspace/src/root_finder.rspacquet/crates/workspace/src/root_finder/tests.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- pacquet/crates/workspace/src/root_finder/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). (8)
- GitHub Check: Agent
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Code Coverage
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions must mirror pnpm'spnpm:<channel>events throughglobalLogger/logger.debug(...)/streamParser.write(...)calls with matching payload and ordering for@pnpm/cli.default-reportercompatibility
Prefer real fixtures (tempfile::TempDir, mocked registry, integration tests) over dependency-injection seams for testing, reserving DI only for branches real fixtures cannot cover (filesystem error kinds, deterministic time, process-global state, external-service paths)
Declare a newtype wrapper (struct) for branded string types instead of collapsing the brand into plain String or &str
If upstream pnpm always validates before construction of a branded string type, validate too by constructing only via TryFrom and/or FromStr
If upstream pnpm never validates a branded string type, brand only for type-safety by exposing an infallible From and From<&str> constructor
If upstream pnpm occasionally constructs a branded string type without validation, expose from_str_unchecked constructor alongside the validating constructor
Match upstream pnpm serde behavior for branded string types by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical branded string type conversions instead of handwriting impl blocks
Model upstream string literal unions or string literal types as Rust enums instead of newtype wrappers
Treat string template literal types (e.g.,${string}@${string}) as branded string types with the same validation discipline as other branded strings
Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site
Follow test-logging guidance in the style guide: log before...
Files:
pacquet/crates/workspace/src/root_finder.rspacquet/crates/config/src/lib.rs
🔇 Additional comments (13)
pacquet/crates/config/src/lib.rs (12)
8-8: LGTM!
187-188: LGTM!
774-780: LGTM!
784-796: LGTM!Also applies to: 808-809, 853-856, 876-880
962-1008: LGTM!
1010-1029: LGTM!
1057-1105: LGTM!
1107-1162: LGTM!
1164-1195: LGTM!Also applies to: 1225-1256
1271-1363: LGTM!Also applies to: 1419-1431
1499-1548: LGTM!
1550-1584: LGTM!pacquet/crates/workspace/src/root_finder.rs (1)
24-50: LGTM!Also applies to: 109-126
| Sys::var_os(WORKSPACE_DIR_ENV_VAR) | ||
| .or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER)) | ||
| .filter(|value| !value.is_empty()) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and examine the file
fd root_finder.rs --type fRepository: pnpm/pnpm
Length of output: 97
🏁 Script executed:
# Read the specific file and lines around 127-129
cat -n pacquet/crates/workspace/src/root_finder.rs | head -140 | tail -30Repository: pnpm/pnpm
Length of output: 1449
🏁 Script executed:
# Search for WORKSPACE_DIR_ENV_VAR definitions to understand the constants
rg "WORKSPACE_DIR_ENV_VAR" pacquet/crates/workspace/src/root_finder.rs -B 2 -A 2Repository: pnpm/pnpm
Length of output: 708
🏁 Script executed:
# Check for any documentation or tests related to this behavior
rg "workspace.*dir\|WORKSPACE_DIR" pacquet/crates/workspace/src/ --type rs -i -lRepository: pnpm/pnpm
Length of output: 80
🏁 Script executed:
# Search for tests or documentation related to workspace dir env logic
rg "workspace.*dir\|WORKSPACE_DIR" pacquet/crates/workspace/src/ -i --type-listRepository: pnpm/pnpm
Length of output: 443
🏁 Script executed:
# Search for tests related to this function
rg "find_workspace_dir_from_env" pacquet/ -i -A 5 -B 2Repository: pnpm/pnpm
Length of output: 8895
🏁 Script executed:
# Check for any test files for root_finder
fd root_finder pacquet/ --type fRepository: pnpm/pnpm
Length of output: 97
🏁 Script executed:
# Look at the doc comment from the function to understand expected behavior
cat -n pacquet/crates/workspace/src/root_finder.rs | head -125 | tail -25Repository: pnpm/pnpm
Length of output: 1348
🏁 Script executed:
# Read the test file to see what tests exist
cat -n pacquet/crates/workspace/src/root_finder/tests.rsRepository: pnpm/pnpm
Length of output: 6090
🏁 Script executed:
# Search for the specific test case: uppercase empty, lowercase set
rg "empty.*lowercase\|lowercase.*empty" pacquet/crates/workspace/src/root_finder/tests.rs -A 5 -B 2Repository: pnpm/pnpm
Length of output: 35
Empty uppercase env var blocks lowercase fallback.
If NPM_CONFIG_WORKSPACE_DIR is set to an empty string, .or_else() won't evaluate the lowercase fallback because the left side is Some("") (not None). The .filter() then drops the empty value, but the lowercase npm_config_workspace_dir is never tried. This contradicts the documented "empty means unset" behavior. A missing test case confirms this: there's no test covering uppercase=empty + lowercase=valid.
Proposed fix
- Sys::var_os(WORKSPACE_DIR_ENV_VAR)
- .or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER))
- .filter(|value| !value.is_empty())
+ Sys::var_os(WORKSPACE_DIR_ENV_VAR)
+ .filter(|value| !value.is_empty())
+ .or_else(|| {
+ Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER)
+ .filter(|value| !value.is_empty())
+ })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Sys::var_os(WORKSPACE_DIR_ENV_VAR) | |
| .or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER)) | |
| .filter(|value| !value.is_empty()) | |
| Sys::var_os(WORKSPACE_DIR_ENV_VAR) | |
| .filter(|value| !value.is_empty()) | |
| .or_else(|| { | |
| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER) | |
| .filter(|value| !value.is_empty()) | |
| }) |
🤖 Prompt for 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.
In `@pacquet/crates/workspace/src/root_finder.rs` around lines 127 - 129, The
current chain calls Sys::var_os(WORKSPACE_DIR_ENV_VAR).or_else(...).filter(...),
so an empty uppercase value (Some("")) prevents evaluating the lowercase
fallback; change the logic to treat empty as unset by applying the emptiness
filter before the fallback: call Sys::var_os(WORKSPACE_DIR_ENV_VAR).filter(|v|
!v.is_empty()).or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER).filter(|v|
!v.is_empty())). Update the code that uses Sys::var_os, WORKSPACE_DIR_ENV_VAR
and WORKSPACE_DIR_ENV_VAR_LOWER accordingly and add a test covering the case
where the uppercase env var is set to an empty string and the lowercase var is
valid.
There was a problem hiding this comment.
Skipping this — the proposed change would diverge from upstream pnpm.
Upstream's lookup at workspace/root-finder/src/index.ts is:
process.env['NPM_CONFIG_WORKSPACE_DIR'] ?? process.env['npm_config_workspace_dir']JavaScript's ?? only falls through on null / undefined, not on "". So when NPM_CONFIG_WORKSPACE_DIR="" and npm_config_workspace_dir="/foo", upstream resolves to "" (the lowercase is never consulted), and the subsequent if (workspaceDir) truthy check then filters it out — net result: no env override, lowercase ignored.
The current Rust shape mirrors that exactly: or_else only fires on None, and the trailing .filter(|v| !v.is_empty()) is the equivalent of if (workspaceDir). Adding .filter() before .or_else() would make the lowercase value win in the uppercase=empty case, which is not what upstream does. Per pacquet's cardinal rule (any change must match how upstream pnpm implements the same feature), this stays as-is.
If a future upstream change starts honouring lowercase when uppercase is empty, pacquet should follow then.
Written by an agent (Claude Code, claude-opus-4-7).
Generated by Claude Code
There was a problem hiding this comment.
@KSXGitHub, thanks for the detailed explanation — you're right, and I apologize for the noise. The current chain (or_else + trailing .filter) faithfully mirrors the upstream JS ?? + if (workspaceDir) semantics, and my suggested reordering would have broken that parity by letting the lowercase value win when the uppercase one is explicitly set to "". I'll retract this concern.
🐇 (ear droop)
✏️ Learnings added
Learnt from: KSXGitHub
Repo: pnpm/pnpm PR: 11730
File: pacquet/crates/workspace/src/root_finder.rs:127-129
Timestamp: 2026-05-19T00:19:48.809Z
Learning: In `pacquet/crates/workspace/src/root_finder.rs`, the function `find_workspace_dir_from_env_with` uses `.or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER)).filter(|v| !v.is_empty())` to mirror upstream pnpm's JS behavior: `process.env['NPM_CONFIG_WORKSPACE_DIR'] ?? process.env['npm_config_workspace_dir']` followed by `if (workspaceDir)`. JS `??` falls through only on null/undefined (not ""), so when the uppercase var is set to "" the lowercase is NOT consulted — the trailing `.filter` then discards the empty string. Applying `.filter` before `.or_else` would be wrong because it would let the lowercase var win when the uppercase is explicitly set to "" — contradicting upstream behavior.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| // module-level constant — declare a `LazyLock` in the test | ||
| // body to give the static-initialised home_dir something to | ||
| // resolve to before the trait impl runs. |
…ock comment `HostNoEnvNoHome` (declared inside `single_project_anchors_modules_at_cwd`) became functionally identical to the module-level `HostNoHome` once `HostNoHome::var_os` was pinned to `None` in the previous commit: both delegate `EnvVar` to `Host`, return `None` for `EnvVarOs`, and return `None` for `GetHomeDir`. Drop the per-test duplicate and reuse `HostNoHome`. Also retargets `npmrc_in_home_folder_applies_registry`'s per-test comment that called the home-dir holder a `LazyLock`; the code uses `OnceLock` (set once at the top of the test body, read by the associated-function `GetHomeDir::home_dir` impl). Caught by CodeRabbit and Copilot review on #11730. https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
| } | ||
|
|
||
| /// Variant of [`HostNoHome`] that panics if [`GetHomeDir::home_dir`] | ||
| /// is consulted — documents the precondition that the test under |
|
PR has been converted into draft again because Claude Code is applying some bad advices from the AI reviewers that caused compiler errors. |
…" doc The phrase "the test under test" was confusing — `HostUnreachableHome` documents a precondition of `Config::current` (the code under test), not of the test itself. Reword accordingly. Caught by Copilot review on #11730. Also drops the prior clippy-fix commit (090dd1e) per the maintainer's request — that commit's pre-existing-lint fixes are out of scope for this PR, and a separate merge-with-main brings other lint surface (a new `default_cache_dir` callback-DI helper) that needs to be addressed together rather than piecemeal. https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
090dd1e to
dde5162
Compare
Pulls in: - cddf522 feat(pacquet): port lockfile verification (minimumReleaseAge + trustPolicy) (#11729) — adds `default_cache_dir`, which was written against the pre-DI-refactor shape and uses the closure-based `HomeDir: FnOnce(...)` parameter plus a module-level `NoEnv` struct this branch's refactor retired. Followup commit ports it to the trait-based DI seam the PR introduces. - c8d8fde feat(config-deps): support optionalDependencies with platform filtering (#11725) — does not touch files this branch edits. https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
…I seam `default_cache_dir` was added on main (cddf522, #11729) in parallel with this PR's DI refactor, using the pre-refactor closure-based shape (`HomeDir: FnOnce() -> Option<PathBuf>` plus a module-level `NoEnv` test fake). This branch's earlier commit `5d14def` retired that module-level `NoEnv` in favour of per-test unit-struct fakes; after merging main the resulting code referenced `NoEnv` at `defaults/tests.rs:155` with no definition in scope (E0425). Port `default_cache_dir` to match the same trait-based shape now used by `default_store_dir`: - Drop the `HomeDir: FnOnce(...)` parameter; bound `Sys: EnvVar + GetHomeDir` and call `Sys::home_dir()` instead. - Collapse the `SmartDefault` expression on `Config::cache_dir` from `default_cache_dir::<Host, _>(home::home_dir)` to `default_cache_dir::<Host>()`. - Rewrite the two `default_cache_dir` tests to implement [`EnvVar`] and [`GetHomeDir`] on per-test unit struct fakes (no module-level `NoEnv` reference). https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
|
@coderabbitai review. |
|
(´・ω・`) Sorry to hear the earlier suggestions caused compiler errors — I'll take care to focus on correctness and avoid misleading advice this time. ✅ Actions performedReview triggered.
|
Summary
PR #11708 introduced the proper trait-based DI seam (
Sysgeneric +Hostprovider), but a few functions still emulated the JS/TS dependency-injection pattern by taking accessor closures (FnOnce/FnMut). Convert them to the trait shape so the seam is uniform across pacquet.Changes
crates/config/src/api.rs— addEnvVarOs,GetHomeDir, andGetCurrentDircapability traits alongside the existingEnvVar; all implemented forHost.default_store_dir— drop theHomeDir/CurrentDirFnOnceclosures. Signature collapses todefault_store_dir<Sys: EnvVar + GetHomeDir + GetCurrentDir>(); theSmartDefaultexpression collapses todefault_store_dir::<Host>().Config::current— three changes:CurrentDirclosure → directstart_dir: &Pathparameter. Production was always passing the canonicalized--dir, never the host cwd, so it was a parameter dressed as a DI seam.HomeDirclosure →Sys: GetHomeDir.std::env::var_os("NPM_CONFIG_WORKSPACE_DIR")→Sys::var_os(...), gated onSys: EnvVarOs. Three tests that heldEnvGuardand ranunsafe { env::set_var(...) }to drive the env-var override now use per-testEnvVarOsfakes instead — no more process-state mutation, nounsafe.crates/workspace/src/api.rs(new) — pacquet-workspace's ownEnvVarOstrait andHostprovider, per the "each crate declares its own provider" convention.find_workspace_dir_from_env_with—FnMut(&str) -> Option<OsString>closure →Sys: EnvVarOs.CODE_STYLE_GUIDE.md— "Reading process state" updated to discourage callback-shape DI and cite the newdefault_store_dir<Sys: EnvVar + GetHomeDir + GetCurrentDir>()signature as the canonical shape.The remaining
FnOnce/FnMutclosures in the codebase (retry_on_fd_pressure,StoreCommand::run'sconfigfactory,State::init'sLoadLockfile,Config::current'sdefault) are legitimate lazy/conditional patterns rather than callback-DI and stay as-is.Test plan
cargo test -p pacquet-config(155 tests pass)cargo test -p pacquet-workspace(26 tests pass)cargo clippy -p pacquet-config -p pacquet-workspace -p pacquet-cli --tests --locked -- --deny warningscargo fmt --checkWritten by an agent (Claude Code, claude-opus-4-7).
https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
Summary by CodeRabbit
Documentation
Refactor
Tests