Skip to content

fix(pnpr): repair registry-mock routing and migrate tests off real-npm writes#12769

Merged
zkochan merged 4 commits into
mainfrom
fix/registry-mock-upstream-mirror
Jul 2, 2026
Merged

fix(pnpr): repair registry-mock routing and migrate tests off real-npm writes#12769
zkochan merged 4 commits into
mainfrom
fix/registry-mock-upstream-mirror

Conversation

@zkochan

@zkochan zkochan commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

The registry-mounts merge (#12747) broke the TypeScript test suite, unnoticed because TS CI was path-filter-skipped on that pnpr-only merge (diagnosis on pnpm/pnpm#12698). Two contract breaks with the old verdaccio-style mock:

  1. Writes to upstream-routed names are rejected. Tests dist-tagged real npm packages (is-negative, is-positive, lodash, micromatch, es5-ext, es6-iterator, @rstacruz/tap-spec, @monorepolint/core) and published unscoped names; the old server materialized an overlay on first write, the mount model rejects the write by design.
  2. Fixture-scope routes claimed real npm scopes with no fall-through. @zkochan/* → local 404'd @zkochan/async-regex-replace; @pnpm/* → local 404'd real @pnpm/error/@pnpm/git-resolver pulled by a huge-package test.

The mount model and the mock stay exactly as they are — no overlay, no mirror feature, provenance stays declared. The fix is config + test migration:

pnpr config (first commit):

  • Fixture packages living in real npm scopes (@pnpm, @zkochan) are routed by exact name; the rest of those scopes proxies npm again.
  • Every unscoped name tests publish is enumerated exactly; @pnpmtest/* covers dynamically-suffixed publish names. Deliberately no unscoped prefix wildcards — a test-* route would swallow the real test-exclude from istanbul's dependency tree.
  • The '**' ACL entry the migration dropped is restored (the built-in default admits no one to unpublish, so unpublish tests got 403).

Test migration (second commit): every test that writes to a real npm package now uses a dedicated in-repo fixture — @pnpm.e2e/multi-version-{a,b,c} for the update/dist-tag flows, @pnpm.e2e/circular-{iterator,ext,symbol} for the concurrent-circular-deps test, @pnpm.e2e/function-with-clone where the test executes the installed code, @scoped/exports-function for the scoped devDeps-save test, @pnpm.e2e/has-build-metadata{,-dep} for the build-metadata lockfile test (hardcoded real-npm integrity → getIntegrity()), and dynamically-suffixed publish names move into @pnpmtest/. Read-only usages of real npm packages are untouched and keep proxying. getIntegrity() also learns the proxy cache's post-mounts layout (.pnpr-cache/~public/<digest>/), which the patch tests rely on.

Verified locally against the real mock: installing/commands 31/31 suites (the two original CI failures pass), installing/deps-installer 76/76 (787 tests, incl. patch, circular, catalogs), installing/deps-restorer, registry-access/commands 9/9, releasing/commands 23/23 (publish, recursive, batch), cache/commands 3/3. pnpr: 568/568, clippy/fmt/dylint clean.

Squash Commit Body

The registry mounts model (pnpm/pnpm#12747) routes every package to exactly
one declared origin with no cross-origin fall-through, and rejects writes to
upstream-routed names. The TypeScript test suite relied on the old
verdaccio-style overlay in ways TS CI never caught (it was
path-filter-skipped on the pnpr-only merge): tests dist-tagged and published
over real npm packages, and the fixture-scope routes (@zkochan/*, @pnpm/*)
claimed real npm scopes whose other packages tests still pull through the
proxy.

Keep the mount model and the mock as they are; fix the config and migrate
the tests:

- Route fixture packages living in real npm scopes by exact name, so the
  rest of those scopes proxies npm again. Enumerate the unscoped names
  tests publish (no unscoped prefix wildcards - test-* would swallow the
  real test-exclude package); dynamically-suffixed publish names move into
  the @pnpmtest scope. Restore the '**' ACL entry the migration dropped -
  the built-in default admits no one to unpublish.
- Migrate every test that writes to a real npm package onto a dedicated
  fixture: @pnpm.e2e/multi-version-{a,b,c} for update/dist-tag flows,
  @pnpm.e2e/circular-{iterator,ext,symbol} for the concurrent circular
  install, @pnpm.e2e/function-with-clone where the installed code is
  executed, @scoped/exports-function for the scoped devDeps save,
  @pnpm.e2e/has-build-metadata{,-dep} for the build-metadata range of
  pnpm/pnpm#2928 (hardcoded real-npm integrity becomes getIntegrity()).
  Read-only usages of real npm packages keep proxying, untouched.
- getIntegrity() in the registry-mock helper learns the proxy cache's
  post-mounts layout (.pnpr-cache/~public/<digest>/), which the patch
  tests depend on for proxied packages.

Checklist

  • The change is implemented in both the TypeScript CLI and the Rust
    pacquet/ port, or the description notes what still needs porting. (Test infrastructure only: pnpr config + TS tests/fixtures. pacquet's in-process registry is unchanged — its tests never write to these packages.)
  • Added or updated tests. (The change is test migration; the pnpr config test pins the new routing.)

Written by an agent (Claude Code, claude-fable-5).

Summary by CodeRabbit

  • New Features

    • Tightened package routing for fixture-scoped packages by using more precise allowlisting rules for when content is served locally versus from the upstream registry.
    • Updated default access control behavior so unmatched packages remain broadly readable while publishing/unpublishing requires authentication.
  • Bug Fixes

    • Updated install/update scenarios to reflect new multi-version fixture coverage, including overwrite behavior and dependency updates.
    • Refreshed search and lockfile expectations to keep build metadata from leaking into the lockfile.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix pnpr registry mock by mirroring upstream packages tests write to

🐞 Bug fix ✨ Enhancement ⚙️ Configuration changes 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Seed the registry mock with real npm packages that tests dist-tag/publish over.
• Route fixtures and published test names by exact package name to avoid scope collisions.
• Restore default unpublish ACL behavior to match prior registry-mock contract.
Diagram

graph TD
  A["Jest globalSetup.js"] --> B["pnpr-prepare --mirror"] --> F[("Registry storage")]
  B --> C["upstream_mirror.rs"] --> D{{"registry.npmjs.org"}}
  C --> E[("upstream-cache/")]
  I["upstream-mirror.yaml"] --> C
  H["pnpr config.yaml"] --> G["pnpr server"]
  F --> G

  subgraph Legend
    direction LR
    _p["Process/Code"] ~~~ _s[("Storage")] ~~~ _e{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reintroduce overlay/fall-through behavior for upstream-routed writes
  • ➕ Avoids downloading/caching upstream tarballs during test setup
  • ➕ Keeps pnpr-prepare simpler; fewer moving parts
  • ➖ Violates the declared-provenance/no-fallthrough mounts model this PR explicitly preserves
  • ➖ Harder to reason about origin correctness; can mask routing mistakes
  • ➖ Still needs careful handling for integrity-locked fixtures and real-code execution expectations
2. Rewrite tests to avoid dist-tag/publish-over real npm package names
  • ➕ Eliminates the need for mirroring upstream packages
  • ➕ Reduces coupling to npm registry content and availability
  • ➖ Large, invasive test refactor across many suites
  • ➖ Loses coverage of real-world behaviors that currently depend on known package names/graphs
  • ➖ Doesn’t address scope-route collisions unless routes are still tightened
3. Vendor minimal local fixture packages instead of downloading real npm bytes
  • ➕ Fully offline/deterministic test setup
  • ➕ No external HTTP dependency in pnpr-prepare
  • ➖ Incompatible with tests that assert real tarball integrity, execute real code, or rely on real dependency graphs (including circular ones)
  • ➖ Ongoing maintenance burden to keep fixtures equivalent to upstream behavior

Recommendation: Keep the PR’s approach: mirror only the specific upstream package names that tests must write to, and route them by exact name to the hosted mount. This preserves the mounts model (single declared origin, no fall-through), avoids wildcard route footguns, and satisfies tests that require real upstream bytes/integrities while keeping the blast radius limited to the with-registry harness.

Files changed (10) +453 / -26

Enhancement (3) +252 / -1
pnpr-prepare.rsAdd --mirror flag to pnpr-prepare +9/-1

Add --mirror flag to pnpr-prepare

• Extends the CLI with an optional --mirror manifest path. When provided, pnpr-prepare seeds upstream-mirrored packages into the generated storage alongside local fixtures.

pnpr/crates/pnpr-fixtures/src/bin/pnpr-prepare.rs

lib.rsExpose upstream mirror materialization and cache directory helper +12/-0

Expose upstream mirror materialization and cache directory helper

• Wires in the new upstream_mirror module, re-exporting the materializer and adding upstream_cache_dir() under target/pnpr-fixtures. Keeps cache lifecycle aligned with cargo target cleanup.

pnpr/crates/pnpr-fixtures/src/lib.rs

upstream_mirror.rsImplement upstream packument/tarball mirroring with caching and integrity checks +231/-0

Implement upstream packument/tarball mirroring with caching and integrity checks

• Adds a materializer that downloads upstream packuments and all version tarballs for mirrored package names, caches them under target/, and writes them into verdaccio-shaped hosted storage. Validates required versions exist and verifies sha512 dist.integrity when present; uses bounded concurrency and write-then-rename caching for safety.

pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs

Bug fix (2) +101 / -7
globalSetup.jsSeed pnpr registry storage with upstream mirror during Jest setup +9/-2

Seed pnpr registry storage with upstream mirror during Jest setup

• Extends the with-registry Jest global setup to pass a new --mirror manifest into pnpr-prepare when building the temp storage. Documents that mirroring is specific to this harness and not used by pacquet’s in-process registry.

pnpm11/utils/jest-config/with-registry/globalSetup.js

config.yamlRoute hosted names by exact match and restore default unpublish ACL +92/-5

Route hosted names by exact match and restore default unpublish ACL

• Replaces broad scope routing for @pnpm/* and @zkochan/* with explicit fixture package names, and enumerates upstream-mirrored and test-published package names to ensure they are hosted. Restores a '**' ACL rule to allow authenticated unpublish for non-tightened packages, matching historical registry-mock expectations.

pnpr/crates/pnpr/config.yaml

Tests (3) +25 / -10
lockfile-verified.jsonlAdd lockfile verification records for git-protocol fixture +3/-0

Add lockfile verification records for git-protocol fixture

• Introduces cached lockfile verification entries used by the pnpm11 fixture suite. Captures verification policy and timestamps for multiple lockfile states.

pnpm11/fixtures/with-git-protocol-dep/cache/lockfile-verified.jsonl

pnpm-lock.yamlUpdate git-protocol fixture lockfile for new dependency/integrity shape +11/-3

Update git-protocol fixture lockfile for new dependency/integrity shape

• Adds the is-number dependency and updates the git-hosted resolution metadata for is-negative to include integrity and gitHosted markers. Adjusts snapshots accordingly to match the new lockfile format.

pnpm11/fixtures/with-git-protocol-dep/pnpm-lock.yaml

tests.rsUpdate config test to assert hosted routing for mirrored/published names +11/-7

Update config test to assert hosted routing for mirrored/published names

• Expands the bundled-config parsing test to verify that fixture scopes, mirrored packages (e.g. lodash), and published test names resolve to the local hosted mount, while unrelated packages (e.g. react) still proxy to npmjs.

pnpr/crates/pnpr/src/config/tests.rs

Other (2) +75 / -8
upstream-mirror.yamlDefine upstream packages and versions required by tests +64/-0

Define upstream packages and versions required by tests

• Adds a manifest listing real npm package names and versions that tests dist-tag/publish over or assert by integrity. Documents mirroring semantics (all versions mirrored; listed versions are existence assertions) and cache behavior.

pnpr/.fixtures/upstream-mirror.yaml

Cargo.tomlAdd async HTTP + YAML deps for upstream mirroring +11/-8

Add async HTTP + YAML deps for upstream mirroring

• Introduces reqwest, tokio, and serde-saphyr dependencies needed to download and parse the upstream mirror manifest and fetch packuments/tarballs.

pnpr/crates/pnpr-fixtures/Cargo.toml

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR tightens pnpr routing and ACL rules, adds new fixture packages, updates pnpm tests to use those fixtures, and changes registry-mock package metadata lookup to scan namespaced public cache paths.

Changes

pnpr routing tightening and fixture-based test migration

Layer / File(s) Summary
Router allowlist and catch-all ACL tightening
pnpr/crates/pnpr/config.yaml, pnpr/crates/pnpr/src/config/tests.rs
Removes broad @pnpm/* and @zkochan/* routing patterns, adds explicit fixture patterns and a catch-all ACL rule, and expands routing assertions for local and npmjs packages.
multi-version fixtures and update/install tests
pnpr/.fixtures/packages/@pnpm.e2e/multi-version-{a,b,c}/*, pnpm11/installing/commands/test/update/interactive.ts, pnpm11/installing/deps-installer/test/hoistedNodeLinker/install.ts, pnpm11/installing/deps-installer/test/install/misc.ts, pnpm11/installing/deps-installer/test/install/updatingPkgJson.ts
Adds multi-version fixture manifests and updates interactive update, hoisted install, overwrite, and package.json-saving tests to use them.
Circular dependency fixtures and concurrent install test
pnpr/.fixtures/packages/@pnpm.e2e/circular-{ext,iterator,symbol}/*, pnpm11/installing/deps-installer/test/install/misc.ts
Adds circular dependency fixtures and rewrites the concurrent circular-deps install test to use them.
function-with-clone, build-metadata, and scoped fixtures with tests
pnpr/.fixtures/packages/@pnpm.e2e/function-with-clone/*, pnpr/.fixtures/packages/@pnpm.e2e/has-build-metadata*/*, pnpr/.fixtures/packages/@scoped/exports-function/*, pnpm11/installing/deps-installer/test/install/misc.ts, pnpm11/installing/deps-installer/test/lockfile.ts, pnpm11/installing/deps-installer/test/install/updatingPkgJson.ts
Adds clone, build-metadata, and scoped fixtures, and updates install, lockfile, and devDependencies tests to use them.
Search, publish, and workspace-protocol test updates
pnpm11/registry-access/commands/test/search.ts, pnpm11/releasing/commands/test/publish/*, pnpm11/installing/deps-installer/test/install/multipleImporters.ts
Switches search and publish tests to scoped or local fixture names and removes dist-tag setup from workspace-protocol tests.
Registry-mock proxy cache namespace lookup
pnpm11/testing/registry-mock/src/index.ts
Changes getIntegrity to probe namespace-based public proxy cache paths and adds namespace enumeration for candidate lookup.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#12747: The main PR refines the RFC #13 mount-model routing by tightening mounts.main package-pattern allowlists and adjusting per-package ACL catch-all behavior in pnpr/crates/pnpr/config.yaml and related routing code.
  • pnpm/pnpm#12205: Related proxy-cache metadata lookup changes touched the same registry-mock path resolution area.

Suggested labels: product: pnpm@11, product: pnpr

✨ 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 fix/registry-mock-upstream-mirror

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.

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

Updates the pnpr-based registry mock to restore TypeScript test stability after the registry-mounts routing model change, by explicitly seeding (“mirroring”) specific real npm packages into the hosted storage and tightening routing to exact names (avoiding wildcard scope/prefix routes that accidentally capture real packages).

Changes:

  • Add pnpr-prepare --mirror support that downloads real npm packuments/tarballs listed in pnpr/.fixtures/upstream-mirror.yaml into hosted storage.
  • Update the bundled pnpr config.yaml routing to exact-name patterns (fixture packages, mirrored packages, and test-published names) and restore the ** ACL rule to allow authenticated unpublish.
  • Wire the TS Jest “with-registry” harness to invoke pnpr-prepare with the mirror manifest; adjust a lockfile fixture accordingly.

Reviewed changes

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

Show a summary per file
File Description
pnpr/crates/pnpr/src/config/tests.rs Updates the bundled-config parsing test to assert the new “hosted vs upstream” routing expectations.
pnpr/crates/pnpr/config.yaml Switches fixture/mirror/test-publish routing to exact-name patterns; restores ** ACL for unpublish/publish defaults.
pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs New implementation for downloading/caching upstream packuments + tarballs and writing them into hosted storage.
pnpr/crates/pnpr-fixtures/src/lib.rs Exposes the mirror helper and adds an upstream-cache directory helper under target/.
pnpr/crates/pnpr-fixtures/src/bin/pnpr-prepare.rs Adds --mirror flag and invokes the upstream mirror after building fixture storage.
pnpr/crates/pnpr-fixtures/Cargo.toml Adds dependencies needed for mirroring (reqwest, tokio, serde-saphyr).
pnpr/.fixtures/upstream-mirror.yaml New manifest enumerating real npm packages + “existence assertion” versions required by tests.
pnpm11/utils/jest-config/with-registry/globalSetup.js Updates Jest global setup to run pnpr-prepare with --mirror using the new manifest.
pnpm11/fixtures/with-git-protocol-dep/pnpm-lock.yaml Updates the fixture lockfile (adds is-number and updates git-hosted resolution shape).
pnpm11/fixtures/with-git-protocol-dep/cache/lockfile-verified.jsonl Adds a lockfile verification cache file under the fixture (currently machine-specific / likely unintended).
Cargo.lock Locks new Rust deps pulled in by pnpr-fixtures mirroring.
Files not reviewed (1)
  • pnpm11/fixtures/with-git-protocol-dep/pnpm-lock.yaml: Generated file

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

Comment thread pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs Outdated
Comment thread pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs Outdated
Comment thread pnpm11/__fixtures__/with-git-protocol-dep/cache/lockfile-verified.jsonl Outdated
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (10) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used

Grey Divider


Action required

1. Proxy constructor scope misroute 🐞 Bug ≡ Correctness
Description
pnpr’s bundled config.yaml was updated to stop routing whole real npm scopes (@pnpm/*,
@zkochan/*) to the hosted store, but Config::proxy() still uses REGISTRY_MOCK_LOCAL_PATTERNS
with those broad scope wildcards. Any code path that uses Config::proxy() (instead of parsing the
bundled YAML) will still resolve packages like @pnpm/error to the hosted mount rather than the npm
upstream, diverging from the fixed behavior and risking the same 404 routing break in those
environments.
Code

pnpr/crates/pnpr/config.yaml[R100-125]

+          # Scopes seeded from the in-repo fixture sources
+          # (`pnpr/.fixtures/packages/`). Scopes shared with real, active npm
+          # scopes (`@pnpm`, `@zkochan`) are routed by exact name instead —
+          # see below — so proxied dependency trees can still pull the scope's
+          # other real packages from npm.
       - '@foo/*'
       - '@having/*'
       - '@jsr/*'
-          - '@pnpm/*'
       - '@pnpm.e2e/*'
       - '@private/*'
       - '@scoped/*'
-          - '@zkochan/*'
       - 'create-touch-file-one-bin'
+          # The fixture packages in the real `@pnpm` and `@zkochan` scopes,
+          # each routed by exact name.
+          - '@pnpm/plugin-pnpmfile'
+          - '@pnpm/postinstall-modifies-source'
+          - '@pnpm/x'
+          - '@pnpm/xyz'
+          - '@pnpm/xyz-parent'
+          - '@pnpm/xyz-parent-parent'
+          - '@pnpm/xyz-parent-parent-parent'
+          - '@pnpm/xyz-parent-parent-parent-parent'
+          - '@pnpm/xyz-parent-parent-with-xyz'
+          - '@pnpm/y'
+          - '@pnpm/z'
+          - '@zkochan/test-pnpm-issue219'
Evidence
The PR’s bundled YAML routes only exact @pnpm/* fixture names to local, but the Rust proxy
constructor still declares @pnpm/* and @zkochan/* as locally hosted patterns. Because pnpr’s
pattern parser treats @scope/* as a scope-wide matcher, Config::proxy() will still route any
@pnpm/ (including @pnpm/error) to local, contradicting the new bundled-config test that
expects @pnpm/error to proxy to npmjs.

pnpr/crates/pnpr/config.yaml[99-125]
pnpr/crates/pnpr/src/config.rs[1148-1162]
pnpr/crates/pnpr/src/mount.rs[48-96]
pnpr/crates/pnpr/src/config/tests.rs[433-456]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The bundled `pnpr` default YAML config now routes only specific fixture packages in real npm scopes (e.g. `@pnpm/y`) to the hosted mount and proxies the rest of the scope to npmjs. However, the Rust `Config::proxy()` constructor still hard-codes broad patterns (`@pnpm/*`, `@zkochan/*`) that route the entire scopes to the hosted mount, which reintroduces the original breakage (real packages under those scopes won’t proxy).
### Issue Context
- `Config::proxy()` is explicitly documented as mirroring the bundled `config.yaml` router; after this PR, it no longer does.
- Routing uses first-match semantics and `@scope/*` matches any well-formed `@scope/name`.
### Fix Focus Areas
- pnpr/crates/pnpr/src/config.rs[1148-1162]
- pnpr/crates/pnpr/src/config.rs[1190-1206]
- pnpr/crates/pnpr/src/config/tests.rs[397-417]
### Suggested fix
1. Update `REGISTRY_MOCK_LOCAL_PATTERNS` to match the new bundled `config.yaml` intent:
- Remove `@pnpm/*` and `@zkochan/*`.
- Add exact-name patterns for the fixture packages that live in those real scopes (the same set as in `config.yaml`).
- If `Config::proxy()` must also support test-published unscoped names, include those patterns too, or explicitly document that `Config::proxy()` is *not* identical to the bundled YAML routing.
2. Strengthen `proxy_constructor_serves_fixtures_locally_and_proxies_the_rest` to assert at least one real-scope package proxies (e.g. `@pnpm/error` -> upstream) and one fixture in the scope is hosted (e.g. `@pnpm/y` -> hosted), so this doesn’t regress again.
(Alternative) Consider making `Config::proxy()` derive its router patterns by parsing `DEFAULT_CONFIG_YAML` (and then overriding listen/storage), eliminating the dual-source-of-truth pattern list.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. getIntegrity sync dir-scan overhead 🐞 Bug ➹ Performance
Description
getIntegrity() now calls fs.readdirSync() to enumerate .pnpr-cache/~public namespaces on every
call and on every retry attempt, even when the hosted packument path exists and would succeed
immediately. This adds avoidable synchronous filesystem work on a heavily-used test helper (e.g.
assertStore() calls getIntegrity() for store index keys), increasing test runtime and variance.
Code

pnpm11/testing/registry-mock/src/index.ts[R105-112]

+    // Rebuilt on every attempt: the proxy-cache namespace directory itself is
+    // created lazily along with the first cached packument, so it may not
+    // exist yet on the first attempt.
+    const candidatePaths = [
+      path.join(storage, pkgName, 'package.json'),
+      ...listPublicProxyNamespaces(path.join(storage, PROXY_CACHE_DIR, '~public'))
+        .map((namespace) => path.join(namespace, pkgName, 'package.json')),
+    ]
Evidence
The new implementation unconditionally calls listPublicProxyNamespaces() (which uses
fs.readdirSync) to build candidatePaths inside the retry loop. assertStore() (used by many
tests) calls getIntegrity() per package/version when computing store index keys, multiplying the
added synchronous directory scan cost across the suite.

pnpm11/testing/registry-mock/src/index.ts[94-136]
pnpm11/utils/assert-store/src/index.ts[17-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getIntegrity()` rebuilds `candidatePaths` each retry by always enumerating public proxy namespaces via `fs.readdirSync()`. For the common case (hosted fixture packuments), this directory scan is unnecessary overhead and is paid per `getIntegrity()` call.
### Issue Context
`getIntegrity()` is used broadly in tests (directly and via helpers like `assertStore()`), so even small per-call synchronous filesystem overhead can noticeably slow CI.
### Fix Focus Areas
- pnpm11/testing/registry-mock/src/index.ts[94-136]
- pnpm11/__utils__/assert-store/src/index.ts[17-33]
### Suggested fix
Implement a cheap fast path and/or memoization:
1. Try reading the hosted packument first without scanning namespaces:
- Attempt `readPackument([path.join(storage, pkgName, 'package.json')])`.
- Only if that returns `undefined`, then enumerate namespaces and try the proxy candidates.
2. Optionally cache the computed namespace directories after the first successful `readdirSync()` (or cache only once non-empty) to avoid repeated scans.
3. If you keep enumeration, sort the `readdirSync()` results to keep deterministic search order.
This preserves the retry behavior for lazy-created namespaces while eliminating the unconditional per-call directory scan in the common case.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Proxy namespace ENOTDIR crash 🐞 Bug ☼ Reliability
Description
getIntegrity() enumerates every entry under .pnpr-cache/~public without filtering to
directories, but readPackument() only treats ENOENT as retryable. If ~public contains a stray
file or non-directory entry, reads can throw ENOTDIR/EISDIR and fail the registry-mock test
harness instead of retrying.
Code

pnpm11/testing/registry-mock/src/index.ts[R124-133]

+function listPublicProxyNamespaces (publicCacheDir: string): string[] {
+  let entries: string[]
+  try {
+    entries = fs.readdirSync(publicCacheDir)
+  } catch (err: unknown) {
+    if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []
+    throw err
+  }
+  return entries.map((entry) => path.join(publicCacheDir, entry))
+}
Evidence
The new helper returns unfiltered readdirSync() entries as namespaces, and the packument reader
only skips ENOENT, so a non-directory namespace entry can cause a hard failure. The pnpr Rust
tests for the same cache layout explicitly filter to directories and mention stray files, indicating
this is a real concern for this directory.

pnpm11/testing/registry-mock/src/index.ts[102-146]
pnpr/crates/pnpr/tests/server.rs[46-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`listPublicProxyNamespaces()` returns every `readdirSync()` entry as a candidate namespace. If `.pnpr-cache/~public` contains a non-directory entry, subsequent reads of `<entry>/<pkg>/package.json` can raise `ENOTDIR`/`EISDIR`, and `readPackument()` will rethrow (it only treats `ENOENT` as retryable).
### Issue Context
The pnpr Rust tests already treat “stray files” in this directory as plausible and explicitly filter to directories + sort.
### Fix Focus Areas
- pnpm11/testing/registry-mock/src/index.ts[102-146]
- pnpm11/testing/registry-mock/src/index.ts[121-133]
### Suggested fix
- Use `fs.readdirSync(publicCacheDir, { withFileTypes: true })` and keep only `dirent.isDirectory()` entries.
- Optionally sort entries for determinism.
- Optionally treat `ENOTDIR`/`EISDIR` like `ENOENT` in `readPackument()` (skip and continue) to be resilient to unexpected filesystem state.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Bundled config allows unpublish 🐞 Bug ⛨ Security
Description
The bundled pnpr default config now grants unpublish: $authenticated for **, so any
authenticated user can unpublish any hosted package. Because pnpr falls back to this bundled config
when no config file exists, this broad destructive permission becomes the out-of-box server
behavior, increasing the blast radius of a compromised token/user.
Code

pnpr/crates/pnpr/config.yaml[R195-198]

+  '**':
+    access: $all
+    publish: $authenticated
+    unpublish: $authenticated
Evidence
The diff adds a catch-all rule granting authenticated unpublish. The pnpr CLI docs and config code
show the bundled YAML is used as the default when no config file exists, so this is a
default-behavior permission expansion rather than only a test-local change.

pnpr/crates/pnpr/config.yaml[179-199]
pnpr/crates/pnpr/src/main.rs[9-13]
pnpr/crates/pnpr/src/config.rs[20-40]
pnpr/crates/pnpr/src/config.rs[1961-1965]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The bundled default `config.yaml` now includes a catch-all `packages: '**'` rule with `unpublish: $authenticated`, making destructive writes broadly available to any authenticated account when pnpr is run with default config resolution.
### Issue Context
pnpr uses `DEFAULT_CONFIG_YAML` as a fallback when no config file is found, so changes here affect out-of-box behavior, not just the test harness.
### Fix Focus Areas
- pnpr/crates/pnpr/config.yaml[179-199]
- pnpr/crates/pnpr/src/main.rs[9-13]
- pnpr/crates/pnpr/src/config.rs[20-40]
### Suggested fix options
1) Keep bundled defaults safer: remove `unpublish` from the `**` rule (or remove the `**` rule entirely) and have the *test harness* inject the permissive unpublish rule when generating its test config (similar to how it already rewrites `max_users`).
2) If tests require unpublish broadly, scope it to the explicit hosted test-published names and `@pnpmtest/*` (and any other known local patterns), leaving `**` without unpublish to avoid granting global destructive permission by default.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (5)
5. Namespace snapshot breaks retries 🐞 Bug ☼ Reliability
Description
getIntegrity() builds its candidate packument paths once, so if the ~public/ proxy-cache
namespace is created after the first read attempt, subsequent retries will never look in the new
namespace and the helper can fail even though the cache write completes. This can introduce CI-only
flakiness for tests that call getIntegrity() soon after the first upstream fetch populates the
cache.
Code

pnpm11/testing/registry-mock/src/index.ts[R102-108]

const candidatePaths = [
path.join(storage, pkgName, 'package.json'),
-    path.join(storage, PROXY_CACHE_DIR, pkgName, 'package.json'),
+    ...listPublicProxyNamespaces(path.join(storage, PROXY_CACHE_DIR, '~public'))
+      .map((namespace) => path.join(namespace, pkgName, 'package.json')),
]
const maxRetries = 4
let delay = 200 // milliseconds
Evidence
The helper constructs candidatePaths once from whatever ~public entries exist at call time, then
retries only against that fixed list; if ~public doesn’t exist yet it returns [], and the list
never updates across retries. pnpr explicitly places public uplink cache entries under ~public/,
which can be created on demand when the first upstream fetch is cached.

pnpm11/testing/registry-mock/src/index.ts[94-117]
pnpm11/testing/registry-mock/src/index.ts[121-133]
pnpr/crates/pnpr/src/server.rs[1165-1208]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getIntegrity()` snapshots the list of proxy-cache namespaces (`.pnpr-cache/~public/<digest>`) before it enters its retry loop. If the namespace directory appears after the first attempt (e.g., first upstream fetch creates it), later retries never include it, defeating the retry mechanism and causing intermittent failures.
## Issue Context
pnpr stores upstream cache entries under a per-uplink digest namespace (`~public/<digest>`). The test helper intentionally retries to tolerate “not ready yet” states, but it currently only retries reads against a fixed, potentially incomplete set of paths.
## Fix
Move the namespace enumeration into the retry loop (or refresh it when the read fails), so retries can observe newly-created digest namespaces. While touching this code, also consider treating `ENOTDIR` like `ENOENT` to avoid hard failures if `~public` contains unexpected non-directory entries.
## Fix Focus Areas
- pnpm11/testing/registry-mock/src/index.ts[94-117]
- pnpm11/testing/registry-mock/src/index.ts[121-133]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Tarball URL trust gap 🐞 Bug ⛨ Security
Description
pnpr-prepare --mirror downloads tarballs from the upstream packument’s dist.tarball URL
verbatim, so a poisoned cached packument (or unexpected upstream metadata) can redirect downloads to
an unintended origin and/or persist credential/query-bearing URLs into the local fixture
cache/storage. pnpr itself treats dist.tarball as untrusted metadata and sanitizes it before
emission/caching, but the mirror path currently does not apply equivalent constraints.
Code

pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[R135-213]

+        let dist = manifest
+            .get("dist")
+            .and_then(Value::as_object)
+            .unwrap_or_else(|| panic!("upstream manifest for {name}@{version} has no dist object"));
+        let url = dist.get("tarball").and_then(Value::as_str).unwrap_or_else(|| {
+            panic!("upstream manifest for {name}@{version} has no dist.tarball URL")
+        });
+        // The server's tarball lookup and URL rewrite both key on the
+        // packument's own tarball basename, so store the file under exactly
+        // that name.
+        let file_name = url.rsplit('/').next().expect("split always yields at least one part");
+        TarballJob {
+            name: name.to_string(),
+            version: version.to_string(),
+            url: url.to_string(),
+            integrity: dist.get("integrity").and_then(Value::as_str).map(str::to_string),
+            cache_path: cache_root.join(name).join(file_name),
+            storage_path: package_dir.join(file_name),
+        }
+    }
+
+    async fn run(self, client: reqwest::Client) {
+        let bytes = match fs::read(&self.cache_path) {
+            Ok(cached) if integrity_matches(&cached, self.integrity.as_deref()) => cached,
+            _ => {
+                let fetched = fetch(&client, &self.url).await;
+                write_cached(&self.cache_path, &fetched);
+                fetched
+            }
+        };
+        assert!(
+            integrity_matches(&bytes, self.integrity.as_deref()),
+            "tarball for {}@{} does not match its upstream dist.integrity",
+            self.name,
+            self.version,
+        );
+        fs::write(&self.storage_path, bytes).expect("write mirrored tarball to registry storage");
+    }
+}
+
+async fn download_tarballs(client: &reqwest::Client, jobs: Vec<TarballJob>) {
+    let semaphore = Arc::new(tokio::sync::Semaphore::new(CONCURRENT_DOWNLOADS));
+    let mut tasks = tokio::task::JoinSet::new();
+    for job in jobs {
+        let client = client.clone();
+        let semaphore = Arc::clone(&semaphore);
+        tasks.spawn(async move {
+            let _permit = semaphore.acquire().await.expect("semaphore is never closed");
+            job.run(client).await;
+        });
+    }
+    while let Some(result) = tasks.join_next().await {
+        result.expect("mirror a tarball");
+    }
+}
+
+/// Whether `bytes` match a `dist.integrity` value. Vacuously true when the
+/// integrity is absent or carries no sha512 entry.
+fn integrity_matches(bytes: &[u8], integrity: Option<&str>) -> bool {
+    let Some(sha512) = integrity
+        .into_iter()
+        .flat_map(str::split_whitespace)
+        .find_map(|entry| entry.strip_prefix("sha512-"))
+    else {
+        return true;
+    };
+    general_purpose::STANDARD.encode(Sha512::digest(bytes)) == sha512
+}
+
+async fn fetch(client: &reqwest::Client, url: &str) -> Vec<u8> {
+    let response = client
+        .get(url)
+        .send()
+        .await
+        .unwrap_or_else(|err| panic!("fetch {url}: {err}"))
+        .error_for_status()
+        .unwrap_or_else(|err| panic!("fetch {url}: {err}"));
+    response.bytes().await.unwrap_or_else(|err| panic!("read body of {url}: {err}")).to_vec()
+}
Evidence
The mirror path reads dist.tarball and fetches it without sanitization/constraints, while pnpr’s
routing code documents that dist.tarball is untrusted and must have userinfo/query stripped for
safety.

pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[135-153]
pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[204-213]
pnpr/crates/pnpr/src/resolver.rs[772-787]
pnpr/crates/pnpr/src/route.rs[641-693]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpr-prepare --mirror` currently trusts `dist.tarball` from the fetched/cached packument and downloads it verbatim. This creates a trust-boundary gap versus pnpr’s normal handling of `dist.tarball` as untrusted upstream metadata (userinfo/query stripping), and it makes the mirror path more vulnerable to cache poisoning or unexpected upstream metadata.
### Issue Context
- The mirror tool reads a cached packument from disk when it “covers” required versions, then uses its `versions[*].dist.tarball` URLs for downloading tarballs.
- pnpr’s main codebase explicitly treats `dist.tarball` as untrusted and sanitizes it before use/emission.
### Fix Focus Areas
- pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[135-153]
- pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[204-213]
### What to change
- Parse `dist.tarball` as a URL and enforce a strict allowlist policy appropriate for this tool (e.g., require `https`, and require host to match `registry.npmjs.org` or whatever set you consider valid for npm’s tarball CDN behavior).
- Strip inline userinfo and drop query/fragment before:
- deriving `file_name`
- issuing the request
- Consider rejecting (or at least not following) redirects that change scheme/host, to avoid silently hopping to an unintended origin during mirror seeding.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Partial mirror storage state 🐞 Bug ☼ Reliability
Description
materialize_upstream_mirror() writes /package.json before downloading/writing any tarballs, so
an interrupted run can leave a storage directory advertising versions whose .tgz files do not
exist. This breaks the fixture-storage atomicity pattern used elsewhere in pnpr-fixtures (tarballs
first, then packument / completion marker) and can yield confusing, sticky failures when reusing a
partially-written storage dir.
Code

pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[R59-76]

+        let package_dir = storage_root.join(name);
+        assert!(
+            !package_dir.join("package.json").exists(),
+            "upstream mirror entry {name} collides with a local package fixture",
+        );
+        fs::create_dir_all(&package_dir).expect("create mirrored package storage dir");
+        fs::write(package_dir.join("package.json"), &packument_bytes)
+            .expect("write mirrored packument to registry storage");
+        let versions = packument
+            .get("versions")
+            .and_then(Value::as_object)
+            .unwrap_or_else(|| panic!("upstream packument for {name} has no versions object"));
+        for (version, manifest) in versions {
+            tarballs.push(TarballJob::new(cache_root, &package_dir, name, version, manifest));
+        }
+    }
+    runtime.block_on(download_tarballs(&client, tarballs));
+}
Evidence
The new mirror path writes package.json before the tarball download phase, unlike the existing
fixture storage builder which writes tarballs first and has an atomic publication approach for
cached storage.

pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[59-76]
pnpr/crates/pnpr-fixtures/src/lib.rs[145-157]
pnpr/crates/pnpr-fixtures/src/lib.rs[78-100]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The mirror writes `package.json` into storage before tarballs are present. If the process is killed mid-download, the storage can look “published” but be incomplete (packument references tarballs that aren’t there).
### Issue Context
The existing fixture builder already follows a safer publication order (tarballs first, then packument) and, for the cached/ensure path, uses a completion marker + rename to avoid exposing partial trees.
### Fix Focus Areas
- pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[59-76]
- pnpr/crates/pnpr-fixtures/src/lib.rs[145-157]
- pnpr/crates/pnpr-fixtures/src/lib.rs[78-100]
### What to change
- Prefer writing tarballs first and `package.json` last for each mirrored package.
- Optionally: mirror into a temp directory and atomically rename into place (or add a `.complete` marker like `ensure_storage_for_fingerprint`) to prevent readers from observing partial state.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Unbounded mirror downloads 🐞 Bug ☼ Reliability
Description
fetch() has no request timeout and buffers entire responses into memory via response.bytes(), so
slow/large upstream responses can hang pnpr-prepare --mirror or spike memory usage (amplified by
concurrent downloads). This can make the registry-mock Jest globalSetup flaky or slow when it seeds
the hosted store.
Code

pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[R204-213]

+async fn fetch(client: &reqwest::Client, url: &str) -> Vec<u8> {
+    let response = client
+        .get(url)
+        .send()
+        .await
+        .unwrap_or_else(|err| panic!("fetch {url}: {err}"))
+        .error_for_status()
+        .unwrap_or_else(|err| panic!("fetch {url}: {err}"));
+    response.bytes().await.unwrap_or_else(|err| panic!("read body of {url}: {err}")).to_vec()
+}
Evidence
The HTTP client is built without any timeout configuration, and fetch() reads the entire response
body into memory; at the same time tarballs are fetched concurrently, so stalls and memory use scale
with the number of in-flight downloads. This path is exercised by the Jest registry harness, which
runs pnpr-prepare with --mirror.

pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[44-52]
pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[175-188]
pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[204-213]
pnpm11/utils/jest-config/with-registry/globalSetup.js[107-120]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpr-prepare --mirror` fetches packuments/tarballs without timeouts and reads each response fully into memory. This can hang CI (no timeout) and cause avoidable memory pressure (full buffering, multiplied by concurrency).
### Issue Context
This code runs from the pnpm test harness (Jest globalSetup) when building the registry mock storage with `--mirror`, so transient upstream slowness or unexpectedly large payloads can impact test reliability.
### Fix Focus Areas
- pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[44-51]
- pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[175-188]
- pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[204-213]
### Suggested fix
- Configure the `reqwest::Client` with sensible `timeout` and `connect_timeout` (and optionally a per-request timeout override).
- Avoid full-body buffering for tarballs: stream the response body to a temp file while hashing, and enforce a maximum size (e.g., via `Content-Length` precheck + a capped stream read).
- Keep packuments as buffered reads if desired (they’re typically small), but still apply timeouts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Blocking IO in async 🐞 Bug ➹ Performance
Description
download_tarballs() spawns async tasks on a single-thread Tokio runtime, but those tasks perform
blocking std::fs::read/std::fs::write, which can stall the executor and reduce effective
concurrency. This can significantly increase wall time when mirroring many versions and make
performance more variable on slower disks.
Code

pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[R44-47]

+    let runtime = tokio::runtime::Builder::new_current_thread()
+        .enable_all()
+        .build()
+        .expect("build tokio runtime for upstream mirror downloads");
Evidence
The runtime is explicitly built as new_current_thread(), while tarball jobs do synchronous
filesystem reads and writes inside an async run() method executed by spawned tasks, which will
block the only executor thread during disk I/O.

pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[44-47]
pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[156-172]
pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[175-188]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The mirror downloader uses a `current_thread` Tokio runtime and spawns many tasks, but uses blocking filesystem I/O (`std::fs`) inside async tasks. On a single-thread runtime this blocks the reactor, undermining concurrency and increasing total time.
### Issue Context
This is a test/fixture builder, but it runs in CI and can download many tarballs (all versions of each mirrored package), so executor stalls can materially affect CI duration and flake rates.
### Fix Focus Areas
- pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[44-47]
- pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[156-172]
- pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[215-225]
### Suggested fix
Pick one:
- Switch to `tokio::runtime::Builder::new_multi_thread()` with a small worker count, **and** wrap `std::fs` calls in `tokio::task::spawn_blocking`.
- Or migrate reads/writes to `tokio::fs` and stream writes with `tokio::io` so tasks stay non-blocking.
Either approach prevents blocking filesystem operations from stalling the async download executor.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

10. Per-tarball task explosion 🐞 Bug ➹ Performance
Description
download_tarballs() spawns one Tokio task per tarball job and holds them all in a JoinSet; for
packages with many versions this adds avoidable scheduling/memory overhead even though concurrency
is already limited by a semaphore. This can increase fixture build latency/variance in CI when
mirroring a large version set.
Code

pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[R175-189]

+async fn download_tarballs(client: &reqwest::Client, jobs: Vec<TarballJob>) {
+    let semaphore = Arc::new(tokio::sync::Semaphore::new(CONCURRENT_DOWNLOADS));
+    let mut tasks = tokio::task::JoinSet::new();
+    for job in jobs {
+        let client = client.clone();
+        let semaphore = Arc::clone(&semaphore);
+        tasks.spawn(async move {
+            let _permit = semaphore.acquire().await.expect("semaphore is never closed");
+            job.run(client).await;
+        });
+    }
+    while let Some(result) = tasks.join_next().await {
+        result.expect("mirror a tarball");
+    }
+}
Evidence
The code enqueues all jobs into a JoinSet via spawn, while separately using a semaphore for
concurrency, which means the per-job task overhead is not necessary to achieve the concurrency cap.

pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[175-189]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`download_tarballs()` spawns a task per job and relies on a semaphore to bound concurrency. For large `jobs` lists, task creation and JoinSet bookkeeping become extra overhead.
### Issue Context
Concurrency is already explicitly capped by `CONCURRENT_DOWNLOADS`, so you can process jobs with bounded concurrency without spawning N tasks.
### Fix Focus Areas
- pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs[175-189]
### What to change
- Replace “spawn everything + semaphore” with a bounded-concurrency loop/stream pattern (e.g., `for_each_concurrent(CONCURRENT_DOWNLOADS, ...)`) so only ~N in-flight futures exist at once.
- Keep the current failure semantics (a single tarball failure should fail the mirror run).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@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

🧹 Nitpick comments (1)
pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs (1)

118-122: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Legacy (shasum-only) packages bypass integrity verification entirely.

When dist.integrity has no sha512- entry, integrity_matches returns true unconditionally, so packages with only a legacy shasum are never actually verified against anything — a corrupted/tampered download for these packages would silently pass. Since npm still always populates shasum (SHA-1) even on legacy manifests, falling back to a sha1 comparison would close this gap cheaply.

Based on path instructions ("validate integrity/hashes ... your mirror flow verifies dist.integrity") — this flow should also verify the shasum fallback rather than skipping verification.

Also applies to: 193-202

🤖 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 `@pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs` around lines 118 - 122, The
integrity check in `integrity_matches` currently treats legacy `shasum`-only
packuments as automatically valid, so add a fallback verification path instead
of returning true when there is no `sha512-` entry. Use the existing
`integrity`/packument data in `upstream_mirror.rs` to compare the downloaded
bytes against the npm `shasum` (SHA-1) for legacy manifests, and keep the
current `sha512` behavior for modern ones so the mirror flow still validates
hashes consistently.

Source: Path instructions

🤖 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 `@pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs`:
- Around line 48-51: Add a timeout to the HTTP client built in
upstream_mirror::fetch by extending the reqwest::Client::builder() configuration
alongside the existing USER_AGENT setup. Use a reasonable bounded timeout for
registry requests so slow or hanging registry.npmjs.org responses cannot block
pnpr-prepare --mirror indefinitely on the current-thread runtime. Keep the
change localized to the client construction path and ensure any related request
code that relies on this client continues to use the same fetch flow and error
handling.

---

Nitpick comments:
In `@pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs`:
- Around line 118-122: The integrity check in `integrity_matches` currently
treats legacy `shasum`-only packuments as automatically valid, so add a fallback
verification path instead of returning true when there is no `sha512-` entry.
Use the existing `integrity`/packument data in `upstream_mirror.rs` to compare
the downloaded bytes against the npm `shasum` (SHA-1) for legacy manifests, and
keep the current `sha512` behavior for modern ones so the mirror flow still
validates hashes consistently.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e522e98c-2cc2-493e-b2f6-f0cf9f34a61b

📥 Commits

Reviewing files that changed from the base of the PR and between 9375797 and 42b6bc8.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
  • pnpm11/__fixtures__/with-git-protocol-dep/cache/lockfile-verified.jsonl is excluded by !**/__fixtures__/**
  • pnpm11/__fixtures__/with-git-protocol-dep/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/__fixtures__/**
📒 Files selected for processing (8)
  • pnpm11/__utils__/jest-config/with-registry/globalSetup.js
  • pnpr/.fixtures/upstream-mirror.yaml
  • pnpr/crates/pnpr-fixtures/Cargo.toml
  • pnpr/crates/pnpr-fixtures/src/bin/pnpr-prepare.rs
  • pnpr/crates/pnpr-fixtures/src/lib.rs
  • pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs
  • pnpr/crates/pnpr/config.yaml
  • pnpr/crates/pnpr/src/config/tests.rs

Comment thread pnpr/crates/pnpr-fixtures/src/upstream_mirror.rs Outdated
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      6.7±0.18ms   651.8 KB/sec    1.01      6.8±0.31ms   642.6 KB/sec

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 42b6bc8

@codecov-commenter

codecov-commenter commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.49%. Comparing base (9375797) to head (6e0186b).

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #12769   +/-   ##
=======================================
  Coverage   85.49%   85.49%           
=======================================
  Files         412      412           
  Lines       63758    63758           
=======================================
+ Hits        54512    54513    +1     
+ Misses       9246     9245    -1     

☔ View full report in Codecov by Harness.
📢 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.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: Rust CI / Success

Failed stage: Fail if any dependency failed or was cancelled [❌]

Failed test name: ""

Failure summary:

The action failed because a workflow step executed exit 1, which intentionally forces a non-zero
exit status.
The log does not show any underlying command output or error before exit 1, so the root
cause (what led to this explicit failure) cannot be determined from the provided snippet.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

17:  Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu24%2F20260628.225
18:  ##[endgroup]
19:  ##[group]GITHUB_TOKEN Permissions
20:  Contents: read
21:  Metadata: read
22:  PullRequests: read
23:  ##[endgroup]
24:  Secret source: Actions
25:  Prepare workflow directory
26:  Prepare all required actions
27:  Complete job name: Rust CI / Success
28:  ##[group]Run exit 1
29:  �[36;1mexit 1�[0m
30:  shell: /usr/bin/bash -e {0}
31:  ##[endgroup]
32:  ##[error]Process completed with exit code 1.
33:  Cleaning up orphan processes

zkochan added 2 commits July 2, 2026 20:52
…e ACL

The full-purity registry-mock config (#12747) broke the TypeScript
test suite in ways TS CI never caught (it was path-filter-skipped on that
pnpr-only merge):

- The @zkochan/* and @pnpm/* routes claimed those entire REAL npm scopes
  with no fall-through, 404ing real packages that proxied dependency trees
  need (@zkochan/async-regex-replace, @pnpm/error). The fixture packages in
  those scopes are now routed individually; the rest of each scope proxies
  npm again.
- Unscoped names tests publish to the mock (test-publish-*, batch-*,
  project-100, ...) routed to the npmjs upstream, where a write is
  rejected. They are enumerated exactly; @pnpmtest/* covers
  dynamically-suffixed publish tests. Deliberately no unscoped prefix
  wildcards: a test-* route would swallow real packages like test-exclude
  (istanbul's dependency tree).
- The migration dropped the '**' ACL entry, and the built-in default
  admits no one to unpublish, so unpublish tests got 403. Restored:
  $all access, $authenticated publish and unpublish.
Under the mounts model a write to an upstream-routed name is rejected, and
the old materialize-on-write overlay is gone on purpose — so tests may only
write to packages the mock hosts. Migrate every real-npm write target to a
dedicated fixture:

- @pnpm.e2e/multi-version-{a,b,c} replace is-negative/is-positive/micromatch
  in the update, overwrite, and interactive-update tests.
- @pnpm.e2e/circular-{iterator,ext,symbol} replace the
  es6-iterator/es5-ext/es6-symbol circular trio; circular-ext requires
  ^2.0.1 so both circular-iterator versions land in the tree, which is the
  point of the concurrency test.
- @pnpm.e2e/function-with-clone replaces lodash where the test executes the
  installed code (module and module.clone are functions).
- @scoped/exports-function replaces @rstacruz/tap-spec in the scoped
  devDependencies-save test.
- @pnpm.e2e/has-build-metadata{,-dep} replace @monorepolint/{core,cli}: the
  dependency range carries build metadata (^0.5.0-alpha.51+f10fea0), which
  is what #2928 is about; the hardcoded real-npm integrity becomes
  getIntegrity().
- The search tests query a hosted fixture (search scans hosted stores only).
- Dynamically-suffixed publish names move into the @pnpmtest scope, since
  exact routes cannot cover generated names.
- The vestigial addDistTag('foo') calls in the workspace-protocol tests are
  dropped; those resolve via workspace:, never the registry.

Read-only usages of real npm packages are untouched — they keep proxying.
getIntegrity() in the registry-mock helper also learns the proxy cache's
post-mounts layout (.pnpr-cache/~public/<digest>/), which the patch tests
depend on for proxied is-positive.
@zkochan zkochan force-pushed the fix/registry-mock-upstream-mirror branch from 42b6bc8 to c101fef Compare July 2, 2026 18:55
@zkochan zkochan changed the title fix(pnpr): mirror the real npm packages tests write to into the registry mock fix(pnpr): repair registry-mock routing and migrate tests off real-npm writes Jul 2, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c101fef

@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 `@pnpm11/testing/registry-mock/src/index.ts`:
- Around line 102-106: The retry logic in getIntegrity currently builds
candidatePaths once using listPublicProxyNamespaces, so later retries never see
namespaces that appear after the first read. Move the namespace enumeration
inside the retry loop in getIntegrity (and any related retry helpers used in the
same flow) so each attempt recomputes candidatePaths from the current state of
the ~public cache before checking package.json paths. This ensures newly created
namespace directories can be discovered during retries instead of being
permanently missed by a stale path list.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: eb18845a-74aa-4fb8-88aa-0f51c6dbe9a6

📥 Commits

Reviewing files that changed from the base of the PR and between 42b6bc8 and c101fef.

📒 Files selected for processing (37)
  • pnpm11/installing/commands/test/update/interactive.ts
  • pnpm11/installing/deps-installer/test/hoistedNodeLinker/install.ts
  • pnpm11/installing/deps-installer/test/install/misc.ts
  • pnpm11/installing/deps-installer/test/install/multipleImporters.ts
  • pnpm11/installing/deps-installer/test/install/updatingPkgJson.ts
  • pnpm11/installing/deps-installer/test/lockfile.ts
  • pnpm11/registry-access/commands/test/search.ts
  • pnpm11/releasing/commands/test/publish/batchPublish.test.ts
  • pnpm11/releasing/commands/test/publish/publish.ts
  • pnpm11/testing/registry-mock/src/index.ts
  • pnpr/.fixtures/packages/@pnpm.e2e/circular-ext/0.10.31/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/circular-iterator/2.0.0/index.js
  • pnpr/.fixtures/packages/@pnpm.e2e/circular-iterator/2.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/circular-iterator/2.0.1/index.js
  • pnpr/.fixtures/packages/@pnpm.e2e/circular-iterator/2.0.1/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/circular-symbol/3.1.1/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/function-with-clone/4.0.0/index.js
  • pnpr/.fixtures/packages/@pnpm.e2e/function-with-clone/4.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/function-with-clone/4.1.0/index.js
  • pnpr/.fixtures/packages/@pnpm.e2e/function-with-clone/4.1.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/has-build-metadata-dep/0.5.0-alpha.51/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/has-build-metadata/0.5.0-alpha.51/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-a/1.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-a/1.0.1/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-a/2.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-a/2.1.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-b/1.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-b/2.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-b/3.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-b/3.1.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-c/3.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-c/3.1.10/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-c/4.0.0/package.json
  • pnpr/.fixtures/packages/@scoped/exports-function/4.1.1/index.js
  • pnpr/.fixtures/packages/@scoped/exports-function/4.1.1/package.json
  • pnpr/crates/pnpr/config.yaml
  • pnpr/crates/pnpr/src/config/tests.rs
💤 Files with no reviewable changes (1)
  • pnpm11/installing/deps-installer/test/install/multipleImporters.ts
✅ Files skipped from review due to trivial changes (20)
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-b/1.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-c/4.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-c/3.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/circular-symbol/3.1.1/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-a/1.0.1/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-a/2.1.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/has-build-metadata/0.5.0-alpha.51/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/has-build-metadata-dep/0.5.0-alpha.51/package.json
  • pnpr/.fixtures/packages/@scoped/exports-function/4.1.1/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-b/3.1.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-a/2.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-b/3.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/function-with-clone/4.1.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-b/2.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/circular-ext/0.10.31/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/multi-version-c/3.1.10/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/function-with-clone/4.0.0/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/circular-iterator/2.0.1/package.json
  • pnpr/.fixtures/packages/@pnpm.e2e/circular-iterator/2.0.0/package.json
  • pnpm11/registry-access/commands/test/search.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • pnpr/crates/pnpr/config.yaml

Comment thread pnpm11/testing/registry-mock/src/index.ts Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c101fef

…ntegrity retry

The ~public namespace directory is created lazily together with the first
cached packument, so a candidate list built once before the retry loop could
never discover a namespace that appears while the retries are running.
Copilot AI review requested due to automatic review settings July 2, 2026 19:35
@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jul 2, 2026
Comment thread pnpr/crates/pnpr/config.yaml
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f32cb92

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f32cb92

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

Copilot reviewed 37 out of 37 changed files in this pull request and generated 1 comment.

Comment thread pnpm11/testing/registry-mock/src/index.ts
… config

Route the @pnpm and @zkochan fixture packages by exact name in
REGISTRY_MOCK_LOCAL_PATTERNS too, so pacquet's in-process test registry
proxies the rest of those real npm scopes exactly like the bundled
config.yaml does. Also filter the getIntegrity() proxy-cache namespace
enumeration to directories, so a stray file under ~public/ cannot turn a
retryable miss into an ENOTDIR error.

@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 (1)
pnpr/crates/pnpr/src/config.rs (1)

1151-1175: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated allowlist between config.yaml and this Rust constant risks drift.

REGISTRY_MOCK_LOCAL_PATTERNS mirrors the router's local route patterns in pnpr/crates/pnpr/config.yaml, but both lists are maintained by hand independently (and already differ in ordering, e.g. create-touch-file-one-bin is positioned differently in each). Since this very PR exists because a stale/incomplete allowlist broke routing, consider deriving one list from the other (e.g. embed config.yaml's patterns via a build script or shared const) or add a test that asserts the two lists contain the same entries, to prevent future silent drift when new fixture packages are added.

🤖 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 `@pnpr/crates/pnpr/src/config.rs` around lines 1151 - 1175, The allowlist in
REGISTRY_MOCK_LOCAL_PATTERNS is duplicated from the router patterns in
config.yaml, so the two sources can drift and break fixture routing. Update the
pnpr config path by either deriving REGISTRY_MOCK_LOCAL_PATTERNS from the YAML
route list (ideally through a shared source or build-time generation) or adding
a test that compares both sets for exact membership. Keep the list synchronized
around the existing REGISTRY_MOCK_LOCAL_PATTERNS constant and the router’s local
patterns so new fixture packages are only added in one place.

Source: Path instructions

🤖 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 `@pnpr/crates/pnpr/src/config.rs`:
- Around line 1151-1175: The allowlist in REGISTRY_MOCK_LOCAL_PATTERNS is
duplicated from the router patterns in config.yaml, so the two sources can drift
and break fixture routing. Update the pnpr config path by either deriving
REGISTRY_MOCK_LOCAL_PATTERNS from the YAML route list (ideally through a shared
source or build-time generation) or adding a test that compares both sets for
exact membership. Keep the list synchronized around the existing
REGISTRY_MOCK_LOCAL_PATTERNS constant and the router’s local patterns so new
fixture packages are only added in one place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 822c7dbe-1cd5-4bac-adb3-fab6ea409169

📥 Commits

Reviewing files that changed from the base of the PR and between f32cb92 and 6e0186b.

📒 Files selected for processing (2)
  • pnpm11/testing/registry-mock/src/index.ts
  • pnpr/crates/pnpr/src/config.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • pnpm11/testing/registry-mock/src/index.ts

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6e0186b

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6e0186b

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Commit: 6e0186b98ec3

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.216 ± 0.172 3.989 4.572 2.03 ± 0.13
pacquet@main 4.112 ± 0.158 3.920 4.410 1.97 ± 0.12
pnpr@HEAD 2.082 ± 0.099 1.985 2.246 1.00
pnpr@main 2.126 ± 0.115 2.028 2.330 1.02 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.2161433621199995,
      "stddev": 0.17173979587512875,
      "median": 4.2189482925199995,
      "user": 4.037404539999999,
      "system": 3.41662968,
      "min": 3.9888854465200003,
      "max": 4.571850168519999,
      "times": [
        4.571850168519999,
        3.9888854465200003,
        4.0842999215199995,
        4.15652416652,
        4.27589887852,
        4.189414859519999,
        4.018962547519999,
        4.27387168052,
        4.24848172552,
        4.353244226519999
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.1115529650200004,
      "stddev": 0.1581501444362238,
      "median": 4.05722600602,
      "user": 4.01219654,
      "system": 3.40566348,
      "min": 3.92031418452,
      "max": 4.41034236052,
      "times": [
        4.41034236052,
        3.92031418452,
        3.9742064795200003,
        4.19285826752,
        4.01854794252,
        4.095345851519999,
        4.14001129252,
        4.01910616052,
        4.327492020519999,
        4.01730509052
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.08202166342,
      "stddev": 0.09857676324474796,
      "median": 2.0640852070199998,
      "user": 2.77256104,
      "system": 2.9551270799999996,
      "min": 1.98504339752,
      "max": 2.24601554852,
      "times": [
        1.98648527852,
        2.24601554852,
        2.1233070185200003,
        2.09416034252,
        1.9925737025199999,
        2.03961936852,
        1.98504339752,
        2.08855104552,
        2.24469206852,
        2.01976886352
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.1258904965200003,
      "stddev": 0.11471530336006532,
      "median": 2.05297670602,
      "user": 2.7554604399999993,
      "system": 2.9447191799999994,
      "min": 2.02846016752,
      "max": 2.32978775652,
      "times": [
        2.2124219545200003,
        2.0441292795200003,
        2.0436802965200003,
        2.05210820852,
        2.2941950825200004,
        2.32978775652,
        2.02846016752,
        2.0538452035200003,
        2.04467940052,
        2.15559761552
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 651.6 ± 14.7 630.4 670.7 1.00
pacquet@main 677.9 ± 96.8 631.7 950.8 1.04 ± 0.15
pnpr@HEAD 716.6 ± 80.9 676.2 943.9 1.10 ± 0.13
pnpr@main 721.5 ± 35.5 677.3 808.3 1.11 ± 0.06
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6515781029200001,
      "stddev": 0.014698803768933531,
      "median": 0.65091480572,
      "user": 0.3833444599999999,
      "system": 1.3278065000000001,
      "min": 0.6303939207200001,
      "max": 0.67066640972,
      "times": [
        0.6303939207200001,
        0.63987359572,
        0.6638748827200001,
        0.63379787472,
        0.67066640972,
        0.6532068067200001,
        0.6486228047200001,
        0.6435846617200001,
        0.67049769272,
        0.66126237972
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6779075359200002,
      "stddev": 0.09675092357195805,
      "median": 0.6448622392200001,
      "user": 0.3884220599999999,
      "system": 1.3298751,
      "min": 0.6316648897200001,
      "max": 0.9508086317200001,
      "times": [
        0.6370713157200001,
        0.6316648897200001,
        0.6363792137200001,
        0.6610222497200001,
        0.6738020797200001,
        0.64204392172,
        0.6422976397200001,
        0.6474268387200001,
        0.6565585787200001,
        0.9508086317200001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7165576526199999,
      "stddev": 0.08090827598844033,
      "median": 0.6882970927200001,
      "user": 0.3914276599999999,
      "system": 1.3539146000000002,
      "min": 0.67624804672,
      "max": 0.9439082507200001,
      "times": [
        0.7081422337200001,
        0.67624804672,
        0.68008380272,
        0.68691643672,
        0.68484540772,
        0.68090398772,
        0.6985867087200001,
        0.6896777487200001,
        0.9439082507200001,
        0.7162639027200001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7215020099200001,
      "stddev": 0.03545088192613261,
      "median": 0.71896547372,
      "user": 0.3921537599999999,
      "system": 1.346754,
      "min": 0.67734597072,
      "max": 0.80830537272,
      "times": [
        0.72446016272,
        0.72647286172,
        0.7360872337200001,
        0.7287849497200001,
        0.70401623972,
        0.69275962672,
        0.7033168967200001,
        0.80830537272,
        0.67734597072,
        0.71347078472
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.362 ± 0.050 4.288 4.439 1.97 ± 0.10
pacquet@main 4.363 ± 0.035 4.307 4.400 1.97 ± 0.10
pnpr@HEAD 2.212 ± 0.105 2.091 2.361 1.00
pnpr@main 2.230 ± 0.146 2.039 2.465 1.01 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.36243193886,
      "stddev": 0.04965497927533178,
      "median": 4.36505972696,
      "user": 3.907881259999999,
      "system": 3.3608931000000006,
      "min": 4.28756232746,
      "max": 4.43864176646,
      "times": [
        4.43864176646,
        4.36090700646,
        4.36921244746,
        4.37262628646,
        4.28756232746,
        4.43078707246,
        4.33460888646,
        4.3045865044600005,
        4.39296030146,
        4.33242678946
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.363034064160001,
      "stddev": 0.03480531007315911,
      "median": 4.36078615746,
      "user": 3.9005492600000005,
      "system": 3.3726072,
      "min": 4.30720115146,
      "max": 4.40041418946,
      "times": [
        4.40041418946,
        4.35773276746,
        4.34826803346,
        4.30720115146,
        4.39815244746,
        4.38936638146,
        4.30890160146,
        4.3602798934600004,
        4.36129242146,
        4.39873175446
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.2122564905600006,
      "stddev": 0.10520417015922255,
      "median": 2.17573637446,
      "user": 2.55882906,
      "system": 2.8532211000000003,
      "min": 2.09148739946,
      "max": 2.36085744546,
      "times": [
        2.14230357446,
        2.24979660746,
        2.13304729146,
        2.19953899246,
        2.34176800046,
        2.36085744546,
        2.34720094146,
        2.10463089646,
        2.15193375646,
        2.09148739946
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.2297263103600002,
      "stddev": 0.14636470219203962,
      "median": 2.2063743429600002,
      "user": 2.61041556,
      "system": 2.8510334999999998,
      "min": 2.03927384746,
      "max": 2.46542036546,
      "times": [
        2.23980743546,
        2.03927384746,
        2.08123886846,
        2.17294125046,
        2.35938838746,
        2.42560862246,
        2.13084636846,
        2.46542036546,
        2.25343010446,
        2.12930785346
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.455 ± 0.026 1.406 1.508 2.16 ± 0.10
pacquet@main 1.422 ± 0.055 1.392 1.575 2.12 ± 0.12
pnpr@HEAD 0.707 ± 0.083 0.665 0.939 1.05 ± 0.13
pnpr@main 0.672 ± 0.028 0.650 0.749 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.45483915884,
      "stddev": 0.025650730292413475,
      "median": 1.4564400476400001,
      "user": 1.38111308,
      "system": 1.7805081,
      "min": 1.40637304514,
      "max": 1.50816908314,
      "times": [
        1.40637304514,
        1.46530288814,
        1.43688171314,
        1.50816908314,
        1.46231251214,
        1.4470866991400002,
        1.4631029891400003,
        1.4605662831400001,
        1.44628256314,
        1.45231381214
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.4219914627399999,
      "stddev": 0.0550111937765213,
      "median": 1.4047174201400001,
      "user": 1.3652344800000003,
      "system": 1.720702,
      "min": 1.3921840791400002,
      "max": 1.57542258914,
      "times": [
        1.4078040931400002,
        1.3921840791400002,
        1.57542258914,
        1.3959137811400002,
        1.40257447614,
        1.3925779291400002,
        1.41671433314,
        1.4277085291400002,
        1.40215445314,
        1.4068603641400002
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.70676014354,
      "stddev": 0.08263679091983525,
      "median": 0.68046838014,
      "user": 0.34916848,
      "system": 1.3159437,
      "min": 0.66472871514,
      "max": 0.93943221314,
      "times": [
        0.67945433714,
        0.93943221314,
        0.66472871514,
        0.6808354521400001,
        0.68010130814,
        0.7104840811400001,
        0.68411697114,
        0.6787593711400001,
        0.68118437014,
        0.66850461614
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6721618762400001,
      "stddev": 0.027761796462025767,
      "median": 0.66574533014,
      "user": 0.34214468,
      "system": 1.3003594,
      "min": 0.64989098914,
      "max": 0.7490307941400001,
      "times": [
        0.65945096214,
        0.6694260791400001,
        0.66256661914,
        0.64989098914,
        0.6618194151400001,
        0.6601946911400001,
        0.66892404114,
        0.67111002814,
        0.7490307941400001,
        0.66920514314
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.139 ± 0.034 3.111 3.225 4.73 ± 0.08
pacquet@main 3.104 ± 0.047 3.068 3.231 4.68 ± 0.10
pnpr@HEAD 0.683 ± 0.010 0.666 0.701 1.03 ± 0.02
pnpr@main 0.663 ± 0.009 0.653 0.683 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.1386466684200003,
      "stddev": 0.03363097323301493,
      "median": 3.1270839070200003,
      "user": 1.8529125200000003,
      "system": 2.0259212399999997,
      "min": 3.11142530652,
      "max": 3.22465188752,
      "times": [
        3.12720552252,
        3.12190582952,
        3.1179978255200003,
        3.13948591352,
        3.11142530652,
        3.1297968265200002,
        3.16520806852,
        3.22465188752,
        3.12182721252,
        3.12696229152
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.10381860112,
      "stddev": 0.04676559716327111,
      "median": 3.09612432552,
      "user": 1.84984702,
      "system": 2.0055122399999994,
      "min": 3.06843105652,
      "max": 3.23078517252,
      "times": [
        3.0967186675200002,
        3.10231317752,
        3.06974447852,
        3.08743635152,
        3.07673296252,
        3.10855523252,
        3.09552998352,
        3.23078517252,
        3.10193892852,
        3.06843105652
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.68288180812,
      "stddev": 0.010056556397090494,
      "median": 0.6819306735199999,
      "user": 0.34734632,
      "system": 1.32625764,
      "min": 0.66560589852,
      "max": 0.70121384852,
      "times": [
        0.70121384852,
        0.68940063852,
        0.66560589852,
        0.68103427852,
        0.67411539852,
        0.67926006852,
        0.69383828552,
        0.68308209652,
        0.67844049952,
        0.68282706852
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.66339585892,
      "stddev": 0.009041491347776385,
      "median": 0.66010387652,
      "user": 0.33971132000000004,
      "system": 1.2905994399999998,
      "min": 0.65270066652,
      "max": 0.68253772452,
      "times": [
        0.65836295052,
        0.66032940452,
        0.65987834852,
        0.6576868515200001,
        0.65270066652,
        0.66604657252,
        0.68253772452,
        0.65538203852,
        0.67165073152,
        0.6693833005200001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, cold cache + cold store + cold pnpr

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 6.952 ± 0.176 6.737 7.285 1.39 ± 0.04
pacquet@main 6.989 ± 0.172 6.772 7.394 1.39 ± 0.04
pnpr@HEAD 5.016 ± 0.080 4.917 5.172 1.00
pnpr@main 5.055 ± 0.135 4.905 5.244 1.01 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 6.951952639220001,
      "stddev": 0.17606810218578636,
      "median": 6.90829544682,
      "user": 4.21391164,
      "system": 3.6710008199999997,
      "min": 6.73664078132,
      "max": 7.28497584032,
      "times": [
        6.83269770732,
        6.73664078132,
        7.01462555532,
        6.98544596632,
        6.82133161932,
        6.91693971632,
        6.89965117732,
        6.82146520232,
        7.20575282632,
        7.28497584032
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 6.988907087219999,
      "stddev": 0.17205532890849878,
      "median": 6.9431180683200004,
      "user": 4.24378304,
      "system": 3.7128138199999996,
      "min": 6.77229762932,
      "max": 7.39443164232,
      "times": [
        7.39443164232,
        6.77229762932,
        6.89638598332,
        6.90829453432,
        6.94928399632,
        6.97565355632,
        6.93695214032,
        7.15749203832,
        6.99369427032,
        6.9045850813200005
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.01632204732,
      "stddev": 0.07981845633963927,
      "median": 4.98524249482,
      "user": 2.92033634,
      "system": 3.22123682,
      "min": 4.91677001332,
      "max": 5.17200103032,
      "times": [
        4.98227995132,
        4.98693952132,
        5.11993665932,
        5.03824020132,
        4.9345737753200005,
        5.17200103032,
        4.98354546832,
        5.04755592032,
        4.91677001332,
        4.98137793232
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.054585078819999,
      "stddev": 0.13496538478550305,
      "median": 5.04416383782,
      "user": 2.89341964,
      "system": 3.19034452,
      "min": 4.90459994632,
      "max": 5.24410274532,
      "times": [
        4.92092881932,
        5.24225940832,
        5.02028064232,
        5.06804703332,
        4.91984528132,
        5.19007483832,
        5.09490929332,
        4.94080278032,
        4.90459994632,
        5.24410274532
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12769
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
4,362.43 ms
(-4.24%)Baseline: 4,555.61 ms
5,466.73 ms
(79.80%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
3,138.65 ms
(+2.64%)Baseline: 3,057.92 ms
3,669.51 ms
(85.53%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,454.84 ms
(+6.64%)Baseline: 1,364.22 ms
1,637.06 ms
(88.87%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,216.14 ms
(-5.20%)Baseline: 4,447.32 ms
5,336.79 ms
(79.00%)
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
🚷 view threshold
6,951.95 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
651.58 ms
(+3.68%)Baseline: 628.47 ms
754.16 ms
(86.40%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12769
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,212.26 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
682.88 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
706.76 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,082.02 ms
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
⚠️ NO THRESHOLD
5,016.32 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
716.56 ms
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan merged commit a315360 into main Jul 2, 2026
35 checks passed
@zkochan zkochan deleted the fix/registry-mock-upstream-mirror branch July 2, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

product: pnpm@11 product: pnpr reviewed: coderabbit CodeRabbit submitted an approving review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants