Skip to content

test(pacquet): port four peer-resolution cases from pnpm#11908

Closed
zkochan wants to merge 1 commit into
mainfrom
test/peer-resolution-coverage
Closed

test(pacquet): port four peer-resolution cases from pnpm#11908
zkochan wants to merge 1 commit into
mainfrom
test/peer-resolution-coverage

Conversation

@zkochan

@zkochan zkochan commented May 24, 2026

Copy link
Copy Markdown
Member

Refs #11907.

Why

Pacquet's mod peers test block in resolving-deps-resolver/src/tests.rs had five tests, all single-occurrence happy paths (pure leaf, sibling-resolved peer, missing peer, bad peer version, peer-edge post-pass). Upstream's installing/deps-resolver/test/resolvePeers.ts covers a much wider correctness surface: cycles, shared subtrees with divergent peer scope, npm-alias peers, peer issues that bubble up from subdependencies, dedupePeers variations.

That gap leaves the peer resolver under-tested even for the current algorithm, and would make it dangerous to land the peersCache + purePkgs ports 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 into node_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_peers pair via the existing StubResolver rather than hand-building a tree, matching the existing mod peers style:

pacquet test upstream line
cyclic_peer_dependencies_resolve_cleanly 'resolve peer dependencies of cyclic dependencies' resolvePeers.ts:14
revisit_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' :128
two_peer_chains_resolve_against_their_own_sibling 'resolve peer dependencies with npm aliases' (substituted) :573
bad_peer_inside_subtree_records_resolved_from_parent 'unmet peer dependency issue resolved from subdependency' describe block :502

Two of these are substitutions, with TODOs in the test bodies for what's missing:

  • npm-alias plumbing isn't wired through the test StubResolver yet (parse_bare_specifier doesn't route npm:foo@2 specifiers through stubbed entries). The ported test uses two parallel chains with distinct pkg names instead — same parentPkgs lookup branch, no alias-specific surface.
  • resolvedFrom field on PeerDependencyIssue isn'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_other fails when the isNew gate from #11906 is applied locally: the foo@1.0.0(qar@1.0.0) and zoo@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's node_dep_paths cache (keyed by NodeId only) 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 pass
  • cargo clippy --locked -p pacquet-resolving-deps-resolver --all-targets -- --deny warnings — clean
  • cargo fmt --check — clean

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

Summary by CodeRabbit

  • Tests
    • Extended peer dependency resolution test coverage with new scenarios for cyclic dependencies, cross-subtree references, aliased dependencies, and mixed resolution outcomes.

Review Change Stack

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-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds four new async Tokio tests to the resolving-deps-resolver test suite covering peer dependency resolution behavior in cyclic graphs, multi-occurrence scenarios, cross-subtree resolution chains, and peer version-mismatch reporting. No production logic is modified; only test cases are added.

Changes

Peer dependency resolution test coverage

Layer / File(s) Summary
Cyclic peer dependency handling
pacquet/crates/resolving-deps-resolver/src/tests.rs
cyclic_peer_dependencies_resolve_cleanly test constructs a multi-node peer cycle and asserts all nodes appear in the dependency graph with peer-suffixed depPaths using expected name@version prefixes, confirming cycle handling completes without panic.
Multiple-occurrence and cross-subtree peer resolution
pacquet/crates/resolving-deps-resolver/src/tests.rs
revisit_resolves_peer_in_one_occurrence_misses_in_other verifies a package referenced from two paths succeeds peer resolution in one and fails in the other, preserving both pure and peer-suffixed depPath variants. two_peer_chains_resolve_against_their_own_sibling ensures two parallel peer chains with distinct names resolve peers only within their own chain without cross-matching.
Peer version mismatch and resolved-from parent reporting
pacquet/crates/resolving-deps-resolver/src/tests.rs
bad_peer_inside_subtree_records_resolved_from_parent test creates a scenario where the importer lacks a required peer but a sibling subtree provides it at a wrong version; asserts a BAD peer issue is recorded with the expected found version and wanted range.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • pnpm/pnpm#11826: Also targets cyclic-peer resolution behavior—changes resolveDependencies to keep cyclic peer suffix assignment deterministic while this PR adds tests asserting the expected peer-suffixed outcomes.
  • pnpm/pnpm#11784: Introduces auto-install-peers behavior (resolve_importer + hoist_peers) that these new test cases directly exercise.
  • pnpm/pnpm#11847: Modifies core NodeId, tree construction, and peer-walk logic that directly impacts the dependency/peer graph behavior validated by these new tests.

Poem

🐰 Through cyclic paths and subtree halls,
Where peers align and version calls,
Our tests now hop from chain to chain,
No cross-pollination, clear and plain—
A fuzzy safety net of trust! 🧪

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds test cases for peer-resolution scenarios and does not address optional dependencies functionality from issue #39. Either link to a relevant peer-resolution issue if available, or clarify how these test changes relate to optional dependencies handling.
Out of Scope Changes check ⚠️ Warning The PR introduces test cases for peer-resolution scenarios which are not mentioned in the linked issue #39 about optional dependencies. Verify that peer-resolution testing is in scope or link to the appropriate issue tracking peer-resolution improvements.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: porting four peer-resolution test cases from pnpm into Pacquet's test suite.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 test/peer-resolution-coverage

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.

@github-actions

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.03      5.1±0.20ms   851.3 KB/sec    1.00      5.0±0.05ms   875.5 KB/sec

@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.

🧹 Nitpick comments (2)
pacquet/crates/resolving-deps-resolver/src/tests.rs (2)

789-789: 💤 Low value

Minor: 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 value

Consider 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_cleanly could 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

📥 Commits

Reviewing files that changed from the base of the PR and between e549cd1 and 6efb1b1.

📒 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 fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.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 plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as 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 manual impl only when conversion needs custom logic.
String-literal unions should become enums, 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 (&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, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use 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

@zkochan

zkochan commented May 24, 2026

Copy link
Copy Markdown
Member Author

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 perf/11900-isnew.

@zkochan zkochan closed this May 24, 2026
@zkochan zkochan deleted the test/peer-resolution-coverage branch May 24, 2026 18:09
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.86%. Comparing base (e549cd1) to head (6efb1b1).

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.
📢 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.

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.

2 participants