test(pacquet): port four peer-resolution cases from pnpm#11908
Conversation
Pacquet's `mod peers` test block had five tests, all of which exercise single-occurrence happy paths. None covered the harder branches the upstream resolver is designed to handle — cycles, packages reached twice with divergent peer scope, parallel peer chains, transitive peer issues. That gap left the peer resolver under-tested even for the current algorithm, and would have made it dangerous to land the `peersCache` + `purePkgs` ports tracked in #11907 because the new cache lookup short-circuits exactly the branches no existing test exercises. Port four resolver-layer cases from [`installing/deps-resolver/test/resolvePeers.ts`](https://github.com/pnpm/pnpm/blob/c86c423bdc/installing/deps-resolver/test/resolvePeers.ts): - `cyclic_peer_dependencies_resolve_cleanly` — four-way cycle (foo ↔ bar ↔ qar ↔ zoo), every node lands in the graph without the walker panicking on cycle re-entry. Upstream `:14`. - `revisit_resolves_peer_in_one_occurrence_misses_in_other` — same package reached via two parent chains, one where the peer resolves and one where it's missing; both occurrences must surface with distinct depPaths. Upstream `:128`. - `two_peer_chains_resolve_against_their_own_sibling` — two parallel pkg-with-peer chains in the same importer; each picks its own sibling, no cross-pollination. Substitutes for upstream's `'resolve peer dependencies with npm aliases'` (`:573`) since npm-alias plumbing isn't yet wired through the test stub resolver — the TODO captures the gap so a follow-up can swap in the alias form once it lands. - `bad_peer_inside_subtree_records_resolved_from_parent` — a peer reachable through a subdependency but at the wrong version surfaces as a *bad* peer, not a missing one. Stands in for upstream's `'unmet peer dependency issue resolved from subdependency'` describe-block (`:502`); the `resolvedFrom` field upstream tracks isn't exposed on pacquet's `PeerDependencyIssue` yet, so the test asserts the bad/missing classification only. All four pass on `main`. Together they exercise the parent-context matching that future cache optimizations (#11907) need to get right — the second test in particular drives a shared-subtree shape where a naive NodeId-keyed cache returns stale depPaths. Refs #11907.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
📝 WalkthroughWalkthroughThis PR adds four new async Tokio tests to the ChangesPeer dependency resolution test coverage
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 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 |
Micro-Benchmark ResultsLinux |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
pacquet/crates/resolving-deps-resolver/src/tests.rs (2)
789-789: 💤 Low valueMinor: Simplify iteration to use names only.
The second tuple element is unused. Consider iterating over names directly for clarity:
♻️ Simplification
- for (name, _) in &[("foo", ""), ("bar", ""), ("qar", ""), ("zoo", "")] { + for name in &["foo", "bar", "qar", "zoo"] { let prefix = format!("{name}`@1.0.0`");🤖 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/resolving-deps-resolver/src/tests.rs` at line 789, The for-loop in tests.rs currently iterates over tuples with an unused second element using the pattern (name, _); replace that with a direct iteration over the names collection (e.g., iterate over a slice/array of string literals) and bind only a single variable (name) to improve clarity and remove the unused placeholder; update the loop header where it currently reads for (name, _) in &[("foo", ""), ("bar", ""), ("qar", ""), ("zoo", "")] to iterate over just the names.
691-696: 💤 Low valueConsider more concise test doc comments per coding guidelines.
The doc comments for these four tests extensively narrate the test structure, dependency shapes, and expected outcomes. According to the coding guidelines: "Tests are documentation. Do not duplicate them in prose" and "Doc comments document the contract... not a re-narration of the body."
The upstream test references (e.g.,
installing/deps-resolver/test/resolvePeers.ts:14) are valuable for traceability and should be retained. However, the detailed shape descriptions and expected-outcome narratives duplicate what the test code itself demonstrates.📝 Example of more concise documentation
For example,
cyclic_peer_dependencies_resolve_cleanlycould be trimmed to:/// Cyclic peer dependencies resolve without panicking and produce /// correctly prefixed peer-suffixed depPaths. Ports upstream's /// `'resolve peer dependencies of cyclic dependencies'` /// (installing/deps-resolver/test/resolvePeers.ts:14). #[tokio::test] async fn cyclic_peer_dependencies_resolve_cleanly() {The test code itself documents the specific dependency shape and assertions.
As per coding guidelines: "Write code that explains itself" and "Tests are documentation."
Also applies to: 798-819, 916-935, 1011-1022
🤖 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/resolving-deps-resolver/src/tests.rs` around lines 691 - 696, Trim the verbose doc comments for the four tests (including cyclic_peer_dependencies_resolve_cleanly and the three subsequent tests in the same test module) to a short 1–2 line contract-style summary that states the high-level behavior being verified and keeps the upstream reference (e.g., 'installing/deps-resolver/test/resolvePeers.ts:14'), removing the detailed dependency-shape narration and expected-outcome rehash that duplicates the test body; leave the assertions and test code intact so the specifics remain in code rather than prose.
🤖 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.
Nitpick comments:
In `@pacquet/crates/resolving-deps-resolver/src/tests.rs`:
- Line 789: The for-loop in tests.rs currently iterates over tuples with an
unused second element using the pattern (name, _); replace that with a direct
iteration over the names collection (e.g., iterate over a slice/array of string
literals) and bind only a single variable (name) to improve clarity and remove
the unused placeholder; update the loop header where it currently reads for
(name, _) in &[("foo", ""), ("bar", ""), ("qar", ""), ("zoo", "")] to iterate
over just the names.
- Around line 691-696: Trim the verbose doc comments for the four tests
(including cyclic_peer_dependencies_resolve_cleanly and the three subsequent
tests in the same test module) to a short 1–2 line contract-style summary that
states the high-level behavior being verified and keeps the upstream reference
(e.g., 'installing/deps-resolver/test/resolvePeers.ts:14'), removing the
detailed dependency-shape narration and expected-outcome rehash that duplicates
the test body; leave the assertions and test code intact so the specifics remain
in code rather than prose.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c0958476-e568-44c9-ad17-0a3a913ab49e
📒 Files selected for processing (1)
pacquet/crates/resolving-deps-resolver/src/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). (9)
- GitHub Check: Analyze (javascript)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Dylint
- 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: When porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(), orstreamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plainStringor&str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only viaTryFrom<String>and/orFromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallibleFrom<String>(andFrom<&str>when convenient).
If upstream occasionally constructs without validation, exposefrom_str_uncheckedas an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization.
Use#[derive(derive_more::From)]and#[derive(derive_more::Into)]for mechanical conversion impls. Fall back to manualimplonly when conversion needs custom logic.
String-literal unions should becomeenums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Pathover&PathBuf,&strover&String) when it doesn't force extra copies.
PreferArc::clone(&x)/Rc::clone(&x)overx.clone()for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;. Two forms stay allowed: external-crate preludes likeuse rayon::prelude::*;and root-of-module re-...
Files:
pacquet/crates/resolving-deps-resolver/src/tests.rs
🧠 Learnings (3)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/tests.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/tests.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/tests.rs
|
Folded into #11906 so CI gates the optimization on its own test coverage rather than landing the tests separately and risking the perf PR merging while still broken. Branch deleted; same four tests now sit alongside the isNew commit on |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #11908 +/- ##
=======================================
Coverage 87.86% 87.86%
=======================================
Files 206 206
Lines 24565 24565
=======================================
+ Hits 21584 21585 +1
+ Misses 2981 2980 -1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Refs #11907.
Why
Pacquet's
mod peerstest block inresolving-deps-resolver/src/tests.rshad five tests, all single-occurrence happy paths (pure leaf, sibling-resolved peer, missing peer, bad peer version, peer-edge post-pass). Upstream'sinstalling/deps-resolver/test/resolvePeers.tscovers a much wider correctness surface: cycles, shared subtrees with divergent peer scope, npm-alias peers, peer issues that bubble up from subdependencies,dedupePeersvariations.That gap leaves the peer resolver under-tested even for the current algorithm, and would make it dangerous to land the
peersCache+purePkgsports tracked in #11907: those caches short-circuit exactly the branches no existing test exercises, so a wrong implementation could silently produce incorrect depPaths (wrong slot in the virtual store → wrong package linked intonode_modules) and the existing 50-test suite would not catch it.What this PR adds
Four cases ported from upstream's resolver-layer test file. Pacquet drives the same scenarios through the actual
resolve_dependency_tree+resolve_peerspair via the existingStubResolverrather than hand-building a tree, matching the existingmod peersstyle:cyclic_peer_dependencies_resolve_cleanly'resolve peer dependencies of cyclic dependencies'resolvePeers.ts:14revisit_resolves_peer_in_one_occurrence_misses_in_other'when a package is referenced twice in the dependencies graph and one of the times it cannot resolve its peers, still try to resolve it in the other occurrence':128two_peer_chains_resolve_against_their_own_sibling'resolve peer dependencies with npm aliases'(substituted):573bad_peer_inside_subtree_records_resolved_from_parent'unmet peer dependency issue resolved from subdependency'describe block:502Two of these are substitutions, with TODOs in the test bodies for what's missing:
StubResolveryet (parse_bare_specifierdoesn't routenpm:foo@2specifiers through stubbed entries). The ported test uses two parallel chains with distinct pkg names instead — sameparentPkgslookup branch, no alias-specific surface.resolvedFromfield onPeerDependencyIssueisn't exposed by pacquet's diagnostics yet, so the bad-peer-in-subtree test asserts the bad-vs-missing classification only, not the parent chain.Real bug surfaced
revisit_resolves_peer_in_one_occurrence_misses_in_otherfails when the isNew gate from #11906 is applied locally: thefoo@1.0.0(qar@1.0.0)andzoo@1.0.0(qar@1.0.0)depPaths never make it into the graph. The shared-child-NodeIds that #11906 introduces cause the peer resolver'snode_dep_pathscache (keyed byNodeIdonly) to return the first-walk result when the same NodeId is walked again under a different parent peer context.That bug isn't in this PR's diff — main is fine and all 54 tests pass on this branch — but it confirms why the tests need to land before any peer-resolution cache work. I'm flagging it on #11906 so the gate doesn't ship as-is.
Test plan
cargo nextest run -p pacquet-resolving-deps-resolver— 54/54 passcargo clippy --locked -p pacquet-resolving-deps-resolver --all-targets -- --deny warnings— cleancargo fmt --check— cleanWritten by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit