Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.

feat: fetch-failure swallow for optional snapshots + ResolutionFailure payload (#434 slice 4)#474

Merged
zkochan merged 6 commits into
mainfrom
feat-fetch-failure-swallow
May 13, 2026
Merged

feat: fetch-failure swallow for optional snapshots + ResolutionFailure payload (#434 slice 4)#474
zkochan merged 6 commits into
mainfrom
feat-fetch-failure-swallow

Conversation

@zkochan

@zkochan zkochan commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Slice 4 of the #434 umbrella (Proper optionalDependencies support). Slices 1–3 (#439, #456, #467) close the installability-driven skip path; this slice adds the second skip pathway upstream handles in the frozen install: an optional: true snapshot whose fetch / extract step fails at install time must not abort the install. Local-materialization errors (CreateVirtualDir) and config-shape errors still abort even for optional snapshots — matching upstream's linkPkg path which sits outside the catch.

Mirrors the silent catch sites at deps/graph-builder/src/lockfileToDepGraph.ts:294-298 and installing/deps-restorer/src/index.ts:912-921:

try { ... fetch ... }
catch (err) { if (pkgSnapshot.optional) return; throw err }

Changes

  • Reporter: SkippedOptionalPackage becomes a #[serde(untagged)] enum with Installed { id, name, version } and ResolutionFailure { name?, version?, bareSpecifier } variants. Models upstream's discriminated union at core-loggers/src/skippedOptionalDependencyLogger.ts:10-31. The new variant is wire-shape-only in slice 4 — pacquet has no resolver yet, but a future resolver port at the upstream emit site (resolveDependencies.ts:1376-1383) lands without re-touching this type.
  • SkippedSnapshots: gains a fetch_failed subset disjoint from the existing installability subset. contains / iter return the union (downstream consumers treat both as absent); iter_installability returns only the persistent subset that .modules.yaml.skipped records, matching upstream's behavior of never updating opts.skipped at the fetch-failure catch sites.
  • CreateVirtualStore: cold-batch dispatch swallows per-snapshot errors when snapshot.optional is true and the error is fetch-side. A new is_fetch_side_failure helper restricts the swallow to InstallPackageBySnapshotError::DownloadTarball and ::GitFetch — the same surface upstream wraps in storeController.fetchPackage. Local-materialization (CreateVirtualDir) and config-shape errors (MissingTarballIntegrity / UnsupportedResolution) propagate even for optional snapshots, matching upstream's linkPkg path which sits outside the catch. Side benefit: the warm-batch path (which only surfaces CreateVirtualDir errors) stays consistently abort-on-error without a parallel swallow site. The per-install fetch_failed: HashSet<PackageKey> rides out on CreateVirtualStoreOutput.
  • InstallFrozenLockfile::run: folds the returned fetch_failed into its mutable SkippedSnapshots so downstream phases (SymlinkDirectDependencies, LinkVirtualStoreBins, BuildModules, hoist) treat the dropped snapshots as absent through the same gate they already use for installability skips.

Test plan

  • reporter::tests::skipped_optional_resolution_failure_event_matches_pnpm_wire_shape — full payload (bareSpecifier camelCase, no id).
  • reporter::tests::skipped_optional_resolution_failure_omits_absent_name_and_version — optional-field shape.
  • install::tests::frozen_install_silently_swallows_unreachable_optional_tarball — ports installing/deps-restorer/test/index.ts:340-360: unreachable optional tarball (http://127.0.0.1:1/...), install succeeds, slot not created, .modules.yaml.skipped left empty. Test-the-test verified by inverting the optional guard in the cold-batch match arm. Runs with enable_global_virtual_store = false so the slot-path assertion targets the legacy layout.
  • install::tests::frozen_install_propagates_non_optional_fetch_failure — pins the polarity: same fixture, non-optional snapshot, install must error.
  • just ready (typos + fmt + check + nextest + clippy).
  • taplo format --check.
  • just dylint (perfectionist).
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace.

Out of scope

  • Resolver-side resolution_failure emit site — needs a resolver, tracked separately.
  • --no-optional / --omit=optional plumbing — umbrella slice 5.
  • Surfacing fetch failures to the reporter on the frozen path — upstream itself is silent here; only the resolver-side emit fires pnpm:skipped-optional-dependency.

Closes #471.


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

Summary by CodeRabbit

  • New Features

    • Frozen installs now silently succeed when optional dependency tarballs are unreachable.
  • Bug Fixes

    • Optional dependency fetch failures are recorded for the run but not persisted to .modules.yaml, avoiding repeated handling on subsequent installs.
    • Non-optional fetch failures continue to fail the install as expected.
    • Skipped-optional dependency events now include richer serialized details for resolution failures.
  • Tests

    • Added integration and unit tests covering frozen-install behavior and skipped-optional reporting.

Review Change Stack

…e payload (#434 slice 4)

Slices 1–3 (#439, #456, #467) close the installability-driven skip
path end to end. This slice adds the second skip pathway upstream
handles in the frozen install: an `optional: true` snapshot whose
tarball / metadata / extract step fails at install time must not
abort the install. Mirrors pnpm/pnpm@94240bc046's silent
catch sites at
`deps/graph-builder/src/lockfileToDepGraph.ts:294-298` and
`installing/deps-restorer/src/index.ts:912-921`:

  try { ... fetch ... }
  catch (err) { if (pkgSnapshot.optional) return; throw err; }

The reporter side gains the `ResolutionFailure` sibling payload
the doc comment at `crates/reporter/src/lib.rs:546-562` had been
flagging. Defined for the resolver-side emit at
`installing/deps-resolver/src/resolveDependencies.ts:1376-1383`;
pacquet has no resolver so the variant is wire-shape-only until a
resolver port lands. Models upstream's discriminated union at
`core-loggers/src/skippedOptionalDependencyLogger.ts:10-31`.

## Changes

- `SkippedOptionalPackage` becomes a `#[serde(untagged)]` enum with
  `Installed { id, name, version }` and
  `ResolutionFailure { name?, version?, bareSpecifier }` variants.
  Two new wire-shape tests pin the resolution-failure payload.
- `SkippedSnapshots` gains a `fetch_failed` subset disjoint from
  the `installability` subset:
    - `contains` / `iter` return the union (downstream consumers
      should treat both as absent).
    - `iter_installability` returns only the persistent subset —
      `.modules.yaml.skipped` writes that, matching upstream's
      behavior of never updating `opts.skipped` at fetch-failure
      catch sites (a future install retries the URL).
- `CreateVirtualStoreOutput` carries the per-install
  `fetch_failed: HashSet<PackageKey>`; the cold-batch dispatch in
  `CreateVirtualStore::run` swallows the per-snapshot error when
  `snapshot.optional` is true. A non-optional failure still
  propagates.
- `InstallFrozenLockfile::run` folds the returned `fetch_failed`
  into its mutable `SkippedSnapshots`, so the downstream phases
  (`SymlinkDirectDependencies`, `LinkVirtualStoreBins`,
  `BuildModules`, hoist) skip the dropped snapshots through the
  same gate they already use for installability skips.

## Tests

- `reporter::tests::skipped_optional_resolution_failure_event_matches_pnpm_wire_shape`
  pins the full payload (`bareSpecifier` camelCase, no `id`).
- `reporter::tests::skipped_optional_resolution_failure_omits_absent_name_and_version`
  pins the optional-field shape.
- `install::tests::frozen_install_silently_swallows_unreachable_optional_tarball`
  ports `installing/deps-restorer/test/index.ts:340` —
  unreachable optional tarball, install succeeds, slot not
  created, `.modules.yaml.skipped` left empty. Test-the-test
  verified by removing the `optional`-guard in the cold-batch
  match arm.
- `install::tests::frozen_install_propagates_non_optional_fetch_failure`
  pins the polarity: same fixture, non-optional snapshot, install
  must error.

Closes #471.
Copilot AI review requested due to automatic review settings May 13, 2026 17:50
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR swallows fetch/extract failures for optional snapshots during CreateVirtualStore, surfaces those keys as a transient fetch_failed set (not persisted), splits SkippedSnapshots into persisted installability vs transient fetch-failed sets, and models reporter payloads as an untagged SkippedOptionalPackage enum with two wire shapes.

Changes

Optional Fetch Failures and SkippedOptionalPackage Redesign

Layer / File(s) Summary
SkippedOptionalPackage untagged enum
crates/reporter/src/lib.rs, crates/reporter/src/tests.rs
SkippedOptionalPackage transitions from a single struct to a #[serde(untagged)] enum with Installed { id, name, version } (for build/unsupported failures) and ResolutionFailure { name?, version?, bareSpecifier } (for resolution failures). Wire-shape tests validate both variants and optional field serialization.
SkippedSnapshots dual-set tracking
crates/package-manager/src/installability.rs, crates/package-manager/src/installability/tests.rs
SkippedSnapshots splits from a single set into two tracked subsets: installability (persisted to .modules.yaml.skipped) and fetch_failed (transient, recorded during current install). New APIs include add_fetch_failed, iter_installability (subset-only), and union-based contains/len/is_empty. Assertions updated to destructure the Installed variant.
CreateVirtualStore optional fetch-failure swallowing
crates/package-manager/src/create_virtual_store.rs
Cold-batch error handling now distinguishes optional vs non-optional snapshot failures: optional failures are logged and swallowed while accumulating keys into a new fetch_failed: HashSet<PackageKey> output field; non-optional failures propagate. Early-return and final-output paths initialize/return fetch_failed.
Install pipeline fetch-failed integration
crates/package-manager/src/install_frozen_lockfile.rs, crates/package-manager/src/install.rs
InstallFrozenLockfile::run makes skipped mutable after installability computation, destructures fetch_failed from CreateVirtualStore output, and adds each key via skipped.add_fetch_failed(). build_modules_manifest serializes only skipped.iter_installability() to prevent transient fetch failures from persisting to .modules.yaml.
Build modules and frozen-install test coverage
crates/package-manager/src/build_modules.rs, crates/package-manager/src/build_modules/tests.rs, crates/package-manager/src/install/tests.rs
Build-module logging and test assertions updated to use SkippedOptionalPackage::Installed { ... }. Two new frozen-lockfile integration tests: frozen_install_silently_swallows_unreachable_optional_tarball (optional fetch fails, install succeeds, not persisted) and frozen_install_propagates_non_optional_fetch_failure (non-optional fetch fails, install aborts).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pacquet#419: Related to pnpm:skipped-optional-dependency logging payload and tests; this PR extends the payload to an untagged enum and updates emitters/tests.
  • pnpm/pacquet#439: Related installability/optional-skip infrastructure that this PR builds upon.
  • pnpm/pacquet#467: Overlaps on SkippedSnapshots/installability pipeline changes; both modify skipped-state handling.

Poem

🐰 Fetch fails? Hop right along!
Optional snapshots skip with a song,
Installability persists through the night,
While fetch-failed vanish from sight—
Non-optional errors still ring loud and strong! 🌙

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the two main changes: fetch-failure swallowing for optional snapshots and the ResolutionFailure payload variant, matching the PR's primary objectives.
Description check ✅ Passed The description is comprehensive, covering summary, linked issues, upstream references, test plan details, scope boundaries, and completed checklist items. All required sections are present and well-filled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-fetch-failure-swallow

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.22222% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.12%. Comparing base (e3e9f51) to head (89c27a0).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
crates/package-manager/src/create_virtual_store.rs 90.90% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #474      +/-   ##
==========================================
+ Coverage   89.18%   90.12%   +0.93%     
==========================================
  Files         116      119       +3     
  Lines       10472    11134     +662     
==========================================
+ Hits         9339    10034     +695     
+ Misses       1133     1100      -33     

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

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

@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.01     16.6±0.96ms   262.0 KB/sec    1.00     16.4±0.49ms   264.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.

Actionable comments posted: 2

🤖 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 `@crates/package-manager/src/create_virtual_store.rs`:
- Around line 657-674: The current match swallows every
InstallPackageBySnapshotError when snapshot.optional is true; change it to only
swallow the fetch/metadata/extract failure variants and propagate all other
InstallPackageBySnapshotError variants. Concretely, keep the tracing::warn!
branch but guard it with a pattern that checks snapshot.optional && matches the
error to the fetch/metadata/extract enum variants of
InstallPackageBySnapshotError (e.g. InstallPackageBySnapshotError::Fetch(..) |
InstallPackageBySnapshotError::Metadata(..) |
InstallPackageBySnapshotError::Extract(..)); for all other Err(err) cases return
Err(CreateVirtualStoreError::InstallPackageBySnapshot(err)) as before. Ensure
the tracing::warn! still logs snapshot_key and err when those specific variants
are swallowed.

In `@crates/reporter/src/tests.rs`:
- Around line 700-720: The test
skipped_optional_resolution_failure_omits_absent_name_and_version should print
the serialized JSON when an assertion fails; update the version absence check
(or add a debug print before asserts) so failures include the payload (e.g.,
change the second assertion to include the JSON in its message or call
eprintln!/dbg! on json) referencing the local variable json and the assert on
json["package"].get("version") so nextest shows the diagnostic output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 716d534d-38e8-4f70-98c9-5d9abc31e61b

📥 Commits

Reviewing files that changed from the base of the PR and between e3e9f51 and 7565a66.

📒 Files selected for processing (10)
  • crates/package-manager/src/build_modules.rs
  • crates/package-manager/src/build_modules/tests.rs
  • crates/package-manager/src/create_virtual_store.rs
  • crates/package-manager/src/install.rs
  • crates/package-manager/src/install/tests.rs
  • crates/package-manager/src/install_frozen_lockfile.rs
  • crates/package-manager/src/installability.rs
  • crates/package-manager/src/installability/tests.rs
  • crates/reporter/src/lib.rs
  • crates/reporter/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). (6)
  • GitHub Check: Agent
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Preserve existing method chains and pipe-trait chains; do not break them into intermediate let bindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Path over &PathBuf, &str over &String) when it does not force extra copies.
Prefer Arc::clone(&x) and Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*; for any glob whose target is a module you control. External-crate preludes (e.g., use rayon::prelude::*;) and root-of-module re-exports (e.g., pub use submodule::*; in lib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plain String or &str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only via TryFrom<String> and/or FromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallible From<String> and From<&str> constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bare as assertion), add a from_str_unchecked (or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...

Files:

  • crates/package-manager/src/build_modules/tests.rs
  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/build_modules.rs
  • crates/reporter/src/lib.rs
  • crates/package-manager/src/install_frozen_lockfile.rs
  • crates/package-manager/src/install.rs
  • crates/package-manager/src/install/tests.rs
  • crates/package-manager/src/create_virtual_store.rs
  • crates/package-manager/src/installability.rs
  • crates/reporter/src/tests.rs
🧠 Learnings (3)
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.

Applied to files:

  • crates/package-manager/src/build_modules/tests.rs
  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/install/tests.rs
  • crates/reporter/src/tests.rs
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/package-manager/src/build_modules/tests.rs
  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/build_modules.rs
  • crates/reporter/src/lib.rs
  • crates/package-manager/src/install_frozen_lockfile.rs
  • crates/package-manager/src/install.rs
  • crates/package-manager/src/install/tests.rs
  • crates/package-manager/src/create_virtual_store.rs
  • crates/package-manager/src/installability.rs
  • crates/reporter/src/tests.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In this repo’s Rust tests (run via `cargo nextest`), stdout/stderr from passing tests is captured and is only shown when a test fails. As a result, `eprintln!` (and similar stderr logging) in these tests should generally not be flagged as CI “noise” on the happy path; CI output should only appear for failing tests.

Applied to files:

  • crates/reporter/src/tests.rs
🔇 Additional comments (3)
crates/package-manager/src/install_frozen_lockfile.rs (1)

281-281: LGTM!

Also applies to: 375-408

crates/package-manager/src/installability/tests.rs (1)

10-10: LGTM!

Also applies to: 123-127

crates/package-manager/src/install/tests.rs (1)

1709-1879: LGTM!

Comment thread crates/package-manager/src/create_virtual_store.rs
Comment thread crates/reporter/src/tests.rs

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

Implements pnpm slice #434 (slice 4) behavior for frozen installs by treating fetch/extract failures of optional: true snapshots as non-fatal, and extends reporter wire compatibility to support the upstream resolution_failure skipped-optional payload shape.

Changes:

  • Update pnpm:skipped-optional-dependency payload modeling to support both upstream package shapes via a #[serde(untagged)] enum (Installed vs ResolutionFailure) and add serialization tests for the new variant.
  • Split SkippedSnapshots into persistent installability vs transient fetch_failed, ensuring only installability skips are persisted to .modules.yaml.skipped.
  • Swallow optional snapshot failures during CreateVirtualStore cold-batch install and thread the resulting fetch_failed set through InstallFrozenLockfile so downstream phases treat those snapshots as absent.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crates/reporter/src/lib.rs Refactors skipped-optional package payload into an untagged enum to support resolution_failure wire shape.
crates/reporter/src/tests.rs Updates existing skipped-optional tests and adds tests covering resolution_failure payload serialization.
crates/package-manager/src/installability.rs Splits skipped snapshot tracking into persistent vs transient subsets and updates insert/iterate semantics.
crates/package-manager/src/installability/tests.rs Adjusts assertions for the new SkippedOptionalPackage enum shape.
crates/package-manager/src/create_virtual_store.rs Adds optional fetch/extract swallow path (cold batch) and returns a per-install fetch_failed set.
crates/package-manager/src/install_frozen_lockfile.rs Folds fetch_failed into the live skipped set so downstream phases skip those snapshots.
crates/package-manager/src/install.rs Persists only installability skips to .modules.yaml.skipped via iter_installability().
crates/package-manager/src/install/tests.rs Adds frozen-install integration tests for optional swallow vs non-optional failure propagation.
crates/package-manager/src/build_modules.rs Updates skipped-optional emission to construct SkippedOptionalPackage::Installed.
crates/package-manager/src/build_modules/tests.rs Updates assertions for the new skipped-optional package enum shape.

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

Comment on lines +657 to +661
match result {
Ok(()) => Ok(None),
Err(err) if snapshot.optional => {
// Silent swallow, matching upstream. `tracing::warn!`
// gives operator visibility without polluting

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved by the narrower swallow in 8c6db09. The warm batch only surfaces CreateVirtualDir errors (the tarball was already in CAFS, so the only thing left is the per-snapshot import/link), and the new is_fetch_side_failure helper excludes that variant from the swallow set. So both batches now consistently abort on local materialization errors — matching upstream's linkPkg path which sits outside the catch.


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

Comment on lines +666 to +670
tracing::warn!(
target: "pacquet::install",
snapshot = %snapshot_key,
error = %err,
"optional snapshot fetch/extract failed; dropping from install",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved by the narrower swallow in 8c6db09. The CreateVirtualDir variant — the only one that runs after CAS import and could leave a partial slot dir — is no longer in the swallow set, so it propagates and aborts the install. The remaining swallowed variants (DownloadTarball, GitFetch) all fail before the per-snapshot slot dir is created (the slot is built by CreateVirtualDirBySnapshot which runs after a successful fetch), so there's nothing left to clean up.


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

config.lockfile = false;
config.store_dir = store_dir.clone().into();
config.modules_dir = modules_dir.clone();
config.virtual_store_dir = virtual_store_dir.clone();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Right — fixed in 8c6db09. Both frozen_install_silently_swallows_unreachable_optional_tarball and the sister frozen_install_propagates_non_optional_fetch_failure test now explicitly set config.enable_global_virtual_store = false, matching the convention from warm_reinstall_skips_snapshot_when_current_lockfile_matches.


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

manifest.save().unwrap();

let mut config = Config::new();
config.lockfile = false;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 8c6db09 — same comment as on the sister test above.


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

@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.692 ± 0.044 2.628 2.758 1.00
pacquet@main 2.703 ± 0.069 2.600 2.792 1.00 ± 0.03
pnpm 6.713 ± 0.246 6.342 7.039 2.49 ± 0.10
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.69242605686,
      "stddev": 0.04421309524416897,
      "median": 2.69639942186,
      "user": 2.60316174,
      "system": 3.761784759999999,
      "min": 2.6277389853599997,
      "max": 2.75775343436,
      "times": [
        2.73675734036,
        2.6965023173600002,
        2.75775343436,
        2.64705354936,
        2.6277389853599997,
        2.7057123713599998,
        2.69629652636,
        2.74088281136,
        2.64882295136,
        2.66674028136
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.70294241846,
      "stddev": 0.06891256222439981,
      "median": 2.70062786936,
      "user": 2.5877858399999996,
      "system": 3.7477670599999997,
      "min": 2.59976533236,
      "max": 2.79196796436,
      "times": [
        2.71071371236,
        2.7866951693599997,
        2.77411830536,
        2.65772707436,
        2.59976533236,
        2.73120620536,
        2.67438895836,
        2.79196796436,
        2.69054202636,
        2.61229943636
      ]
    },
    {
      "command": "pnpm",
      "mean": 6.712586934159999,
      "stddev": 0.24632152218047748,
      "median": 6.81846635486,
      "user": 10.002544539999999,
      "system": 4.72426626,
      "min": 6.34196919436,
      "max": 7.03872955136,
      "times": [
        6.34196919436,
        6.40478599536,
        6.55390987836,
        6.95814282736,
        7.03872955136,
        6.88357106636,
        6.82132465136,
        6.47563511636,
        6.81560805836,
        6.8321930023599995
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 802.6 ± 82.3 718.5 963.0 1.00
pacquet@main 809.0 ± 81.0 706.8 939.3 1.01 ± 0.14
pnpm 2727.8 ± 114.5 2594.7 2929.5 3.40 ± 0.38
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.80258750818,
      "stddev": 0.0823209288781923,
      "median": 0.77855658208,
      "user": 0.3945681,
      "system": 1.61587514,
      "min": 0.7185039210799999,
      "max": 0.96295146008,
      "times": [
        0.96295146008,
        0.75089581708,
        0.86075152908,
        0.86780551008,
        0.73936001708,
        0.80621734708,
        0.73324405808,
        0.7185039210799999,
        0.85976240708,
        0.72638301508
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.80897441478,
      "stddev": 0.08099274468725157,
      "median": 0.78552473908,
      "user": 0.3854587,
      "system": 1.63726744,
      "min": 0.70676124208,
      "max": 0.93926033808,
      "times": [
        0.93926033808,
        0.79582459808,
        0.84104761908,
        0.76453746308,
        0.91796897808,
        0.77522488008,
        0.74251042308,
        0.70676124208,
        0.87726242808,
        0.72934617808
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.7278477937800005,
      "stddev": 0.11452067981143348,
      "median": 2.7290162440800003,
      "user": 3.2348706999999997,
      "system": 2.28872974,
      "min": 2.59470890408,
      "max": 2.92952713308,
      "times": [
        2.8892663050800005,
        2.92952713308,
        2.60598325108,
        2.6106354030800003,
        2.7568137520800002,
        2.59470890408,
        2.7536175930800004,
        2.7533392400800003,
        2.6798931080800004,
        2.7046932480800003
      ]
    }
  ]
}

…iants + GVS-off in tests

Addresses PR #474 review comments:

1. **Swallow scoping** (CodeRabbit major): `InstallPackageBySnapshotError`
   carries both fetch-side variants (`DownloadTarball`, `GitFetch`)
   and post-fetch variants (`CreateVirtualDir`, plus the
   config-shape `MissingTarballIntegrity` / `UnsupportedResolution`).
   Upstream's catch at
   `deps/graph-builder/src/lockfileToDepGraph.ts:286-298` wraps
   `storeController.fetchPackage` only — the local materialization
   (`linkPkg`) sits outside. Restrict pacquet's swallow accordingly
   via `is_fetch_side_failure`, so a local link error on an
   optional snapshot still aborts the install. Side benefit: the
   warm-batch path (which only surfaces `CreateVirtualDir` errors)
   stays consistently abort-on-error without a parallel swallow
   site.

2. **Test diagnostic** (CodeRabbit minor): add `dbg!(&json)` and
   include `{json:?}` in the `version` assertion message on
   `skipped_optional_resolution_failure_omits_absent_name_and_version`
   so failures self-diagnose under nextest.

3. **GVS-off in tests** (Copilot): both new
   `frozen_install_{silently_swallows,propagates_non_optional}`
   tests now explicitly `config.enable_global_virtual_store = false`.
   The swallow test's path assertion targets the legacy
   `<virtual_store_dir>/<flat-name>` slot — with GVS on the slot
   lives elsewhere and the assertion would always pass regardless
   of the swallow firing. Same pattern as
   `warm_reinstall_skips_snapshot_when_current_lockfile_matches`.
Copilot AI review requested due to automatic review settings May 13, 2026 18:29

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 10 out of 10 changed files in this pull request and generated 1 comment.

Comment thread crates/reporter/src/lib.rs Outdated
Comment on lines +558 to +563
/// that picks the right shape depending on which variant the emit
/// site constructs. The pairing is not type-enforced against
/// `reason` (a `BuildFailure` reason with a
/// `ResolutionFailure` package is constructible in Rust), but every
/// emit site is centralized inside this crate, so the constructor
/// discipline lives there rather than in the type.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good correction — the doc was misleading. Fixed in 6fdf5c5: the comment now names the actual emit sites (installability.rs, build_modules.rs, create_virtual_store.rs for slice 4 — all in pacquet-package-manager) and acknowledges the reason/package pairing is convention-driven rather than type-enforced.


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

zkochan added 2 commits May 13, 2026 20:43
…site locality

Copilot review on PR #474 pointed out that the doc comment claimed
emit sites are "centralized inside this crate" but the actual
emitters live in `pacquet-package-manager` — `installability.rs`
(slice 1), `build_modules.rs` (build-failure), and now
`create_virtual_store.rs` (slice 4 fetch-failure swallow). Reword
to name those sites explicitly and acknowledge the
reason/package pairing is convention-driven, not type-enforced.
Copilot AI review requested due to automatic review settings May 13, 2026 18:49

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 10 out of 10 changed files in this pull request and generated 1 comment.

Comment thread crates/reporter/src/lib.rs Outdated
Comment on lines +562 to +564
/// sites live in `pacquet-package-manager`
/// (`installability.rs`, `build_modules.rs`,
/// `create_virtual_store.rs` for slice 4) and must keep the

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — CreateVirtualStore's slice 4 path is intentionally silent on the wire (the swallow just drops the error; the per-snapshot pnpm:skipped-optional-dependency event upstream-equivalent fires from the resolver site, which pacquet doesn't have yet). Reworded in 89c27a0 to list only the actual constructor sites (installability.rs, build_modules.rs) and explicitly call out that the slice 4 path is silent.


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

…e list

Copilot follow-up on the previous fix: `CreateVirtualStore`'s
slice 4 fetch-failure path is silent on the reporter wire — it
swallows the error without emitting `pnpm:skipped-optional-dependency`
(matches upstream's `lockfileToDepGraph.ts:294-298` catch). So
it's not a constructor site for this log type. Reword the doc
comment to list only the actual emit sites (`installability.rs`
and `build_modules.rs`) and explicitly note that the slice 4 path
is silent.
@zkochan zkochan merged commit bd99486 into main May 13, 2026
23 of 24 checks passed
@zkochan zkochan deleted the feat-fetch-failure-swallow branch May 13, 2026 19:15
zkochan added a commit that referenced this pull request May 13, 2026
…#485)

Slice 5 of the [#434 umbrella](#434) (`Proper optionalDependencies support`). Slices 1–4 (#439, #456, #467, #474) handle installability + the fetch-failure swallow. This slice closes the user-facing `--no-optional` flag: the CLI flag already exists and threads through `Install::dependency_groups`, but only `SymlinkDirectDependencies` and the on-disk `included` field actually honored it. The rest of the install pipeline walked `optional_dependencies` unconditionally, so the optional subtree was still extracted, linked, and built.

Mirrors upstream's depNode filter at [`installing/deps-installer/src/install/link.ts:109-111`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/src/install/link.ts#L109-L111):

```ts
if (!opts.include.optionalDependencies) {
  depNodes = depNodes.filter(({ optional }) => !optional)
}
```

## Changes

- **`SkippedSnapshots`**: gains a third disjoint subset `optional_excluded` alongside `installability` (slice 1) and `fetch_failed` (slice 4). The existing `contains` / `iter` already return the union, so every downstream consumer that filters by skip-set (`CreateVirtualStore`, `SymlinkDirectDependencies`, `BuildModules`, hoist, `link_bins`) now respects `--no-optional` through the same gate. `iter_installability` still returns only the persistent subset for `.modules.yaml.skipped` writes — `--no-optional` exclusions are transient (matching upstream's behavior of never updating `opts.skipped` at the filter site).
- **`InstallFrozenLockfile::run`**: iterates the lockfile snapshots once and inserts every `snap.optional == true` entry into the new subset when the dispatch list doesn't contain `DependencyGroup::Optional`. The `SnapshotEntry::optional` flag is set by the resolver only when the snapshot is reachable **exclusively** through optional edges, so a snapshot reachable through any non-optional edge carries `optional: false` and survives the filter — covers [`optionalDependencies.ts:712`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/test/install/optionalDependencies.ts#L712) (`dependency that is both optional and non-optional is installed`).
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fetch-failure swallow for optional snapshots + ResolutionFailure payload (#434 slice 4)

2 participants