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

feat(executor): swallow optional-dep build failures and report via pnpm:skipped-optional-dependency (#397)#419

Merged
zkochan merged 8 commits into
mainfrom
fix/optional-dep-build-failure-397
May 12, 2026
Merged

feat(executor): swallow optional-dep build failures and report via pnpm:skipped-optional-dependency (#397)#419
zkochan merged 8 commits into
mainfrom
fix/optional-dep-build-failure-397

Conversation

@zkochan

@zkochan zkochan commented May 12, 2026

Copy link
Copy Markdown
Member

Summary

Closes item #6 of #397. Optional dependencies whose postinstall
hooks fail no longer abort the install — pacquet now reports the
failure through the pnpm:skipped-optional-dependency channel
(reason build_failure) and continues, matching
pnpm v11's behavior at building/during-install/src/index.ts:218-240.

Three commits:

  • feat(lockfile): surface snapshot-level optional flag
    pacquet's lockfile reader previously dropped the
    snapshots[<key>].optional field. Adding it as pub optional: bool
    on SnapshotEntry with #[serde(default, skip_serializing_if = "is_false")]
    so absent ↔ false and false never serializes. The flag is
    pre-computed by pnpm's resolver at install time (ALL-paths-
    optional fold), so pacquet trusts the precomputed value rather
    than re-deriving it — matching lockfileToDepGraph at deps/graph-builder/src/lockfileToDepGraph.ts:315.
  • feat(reporter): add pnpm:skipped-optional-dependency event
    new LogEvent::SkippedOptionalDependency(SkippedOptionalDependencyLog)
    variant + SkippedOptionalPackage + SkippedOptionalReason enum.
    Wire shape mirrors pnpm's SkippedOptionalDependencyMessage.
    All four upstream reasons (BuildFailure, UnsupportedEngine,
    UnsupportedPlatform, ResolutionFailure) are declared even
    though only BuildFailure is emitted today — keeps the enum
    closed so the other emit sites can land without widening it.
  • feat(package-manager): swallow optional-dep build failures
    BuildModules.run reads snapshot.optional. When
    run_postinstall_hooks returns an Err, the dep's optional
    flag decides: optional ⇒ emit a pnpm:skipped-optional-dependency
    event (reason: build_failure, package.id = the dep's pkg dir
    to match upstream's depNode.dir) and continue; non-optional ⇒
    propagate as BuildModulesError::LifecycleScript so the install
    aborts. Two cfg(unix)-gated tests cover both branches.

Test plan

  • just ready (478 tests + clippy clean)
  • taplo format --check (clean — just ready doesn't run this, CI does)
  • CI green on Linux / macOS / Windows

Out of scope

The parents field upstream marks as TODO at building/during-install/src/index.ts:227 is also omitted here. The other three SkippedOptionalReason variants are declared in the enum but not yet emitted — UnsupportedEngine / UnsupportedPlatform come from pacquet's not-yet-built resolution-time platform-check phase, and ResolutionFailure from the registry side.


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

Summary by CodeRabbit

  • New Features

    • Track optional dependencies in lockfile snapshots and propagate optionality into lifecycle events.
  • Bug Fixes

    • Postinstall failures for optional deps are logged and skipped instead of aborting installs; non-optional failures still fail.
  • Logging

    • Added NDJSON event for skipped optional dependencies with structured reason and package info.
  • Tests

    • Added serialization and runtime tests covering optional flag behavior and skipped-optional logging.
  • Tools

    • Updated registry-mock tooling and workspace config for more reliable test runs.

Review Change Stack

zkochan added 4 commits May 12, 2026 11:57
The v9 lockfile spec carries an `optional: true` field on each
snapshot ([upstream type](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/lockfile/types/src/index.ts#L34-L39)),
folded by pnpm's resolver from the importer graph. pacquet's
reader previously dropped it.

Adding it as `pub optional: bool` on `SnapshotEntry` with
`#[serde(default, skip_serializing_if = "is_false")]` so the wire
shape matches upstream: absent ↔ false, and `false` is never
serialized. The flag is the input that the next commit's optional-
aware build-failure handling reads — pacquet trusts the
precomputed flag rather than re-deriving it, matching upstream's
`lockfileToDepGraph` at [`deps/graph-builder/src/lockfileToDepGraph.ts:315`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/deps/graph-builder/src/lockfileToDepGraph.ts#L315).

Tests round-trip an `optional: true` snapshot through deserialize →
serialize, and confirm the field defaults false / omits when false.
New `LogEvent::SkippedOptionalDependency(SkippedOptionalDependencyLog)`
variant + `SkippedOptionalPackage` + `SkippedOptionalReason` enum.
Wire shape mirrors pnpm's `SkippedOptionalDependencyMessage` at
`pnpm/core-loggers/src/skippedOptionalDependencyLogger.ts` (top-
level `details?`, `package: { id, name, version }`, `prefix`,
`reason`); the `parents` field upstream marks as TODO is left off
here too. `SkippedOptionalReason` carries all four upstream reasons
even though pacquet only emits `BuildFailure` today — keeps the
enum closed and means future emit sites for unsupported-engine /
-platform / resolution-failure don't have to widen the type.

Tests cover the wire-shape round-trip, the `details: None`
omit-when-absent path, and the snake_case discriminant for every
reason variant — matching the conventions of the other reporter
event tests.
Ports `building/during-install/src/index.ts:218-240` from pnpm
v11 (`b4f8f47ac2`). When `run_postinstall_hooks` returns an error
for a snapshot whose `optional` flag is set, `BuildModules.run`
now emits the failure through the `pnpm:skipped-optional-dependency`
channel (reason `build_failure`, `package.id` = the dep's pkg dir,
matching upstream's `depNode.dir`) and continues with the rest of
the install. Non-optional failures still propagate as
`BuildModulesError::LifecycleScript` so the install aborts on a
required dep's failure.

Two tests cover the new branches: a `cfg(unix)`-gated test on a
`SnapshotEntry { optional: true, .. }` with a failing postinstall
asserts the install succeeds and the `Reporter` captures a
`SkippedOptionalDependency` event with `reason: BuildFailure` and
the right `name` / `version` / `details`; a symmetric test on a
non-optional snapshot asserts `BuildModules.run` returns
`Err(BuildModulesError::LifecycleScript(_))`.

`SnapshotEntry { optional: false }` is now also set explicitly in
`build_snapshot.rs` (the synthetic-snapshot builder) and the
`build_sequence` tests so the new field is unambiguous at every
construction site.
Copilot AI review requested due to automatic review settings May 12, 2026 09:59
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e3ebe16-9f25-4a3c-aaed-0ee00ad47086

📥 Commits

Reviewing files that changed from the base of the PR and between ec4dcc2 and f8df447.

📒 Files selected for processing (9)
  • crates/executor/src/lifecycle.rs
  • crates/executor/src/lifecycle/tests.rs
  • crates/lockfile/src/snapshot_entry/tests.rs
  • crates/package-manager/src/build_modules.rs
  • crates/package-manager/src/build_modules/tests.rs
  • crates/reporter/src/lib.rs
  • crates/reporter/src/tests.rs
  • tasks/registry-mock/launch.mjs
  • tasks/registry-mock/pnpm-workspace.yaml
✅ Files skipped from review due to trivial changes (1)
  • crates/lockfile/src/snapshot_entry/tests.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • crates/package-manager/src/build_modules.rs
  • tasks/registry-mock/pnpm-workspace.yaml
  • crates/reporter/src/lib.rs
  • crates/package-manager/src/build_modules/tests.rs
  • tasks/registry-mock/launch.mjs
  • crates/reporter/src/tests.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (macos-latest)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Log emissions are part of 'match pnpm'—when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing into plain String or &str to preserve the distinction between validating and non-validating brands
If upstream always validates before construction of a branded string type, implement validation via TryFrom<String> and/or FromStr without providing an infallible public constructor
If upstream never validates a branded string type, expose an infallible From<String> constructor to provide type-safety without runtime validation
When upstream occasionally constructs a branded string type without validation via bare as assertion, expose a from_str_unchecked constructor so callers can opt into the same unchecked path explicitly
Match upstream serde behavior for branded types—use #[serde(try_from = "String")] for deserialization to go through the validator and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls on branded types rather than handwriting impl blocks
Model string-literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers
Template literal types (like ${string}@${string}) should be treated as branded string types with the same validation discipline applied
Follow the code style guide in CODE_STYLE_GUIDE.md covering code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Warnings are errors in cargo clippy --locked -- --deny warnings, and should not be silenced with `#[allow(....

Files:

  • crates/executor/src/lifecycle.rs
  • crates/executor/src/lifecycle/tests.rs
🧠 Learnings (2)
📚 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/executor/src/lifecycle.rs
  • crates/executor/src/lifecycle/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 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/executor/src/lifecycle/tests.rs
🔇 Additional comments (2)
crates/executor/src/lifecycle.rs (1)

114-123: Optional flag propagation in lifecycle events looks correct.

RunPostinstallHooks.optional is threaded into both Script and Exit lifecycle messages while preserving emit ordering and existing failure behavior. This matches the intended pnpm wire-shape parity.

Also applies to: 217-226, 341-349

crates/executor/src/lifecycle/tests.rs (1)

59-59: Test updates are aligned and complete for the new optional field.

Good coverage: existing fixtures now include explicit optional: false, and the new unix test validates optional: true is emitted on both Script and Exit lifecycle events.

Also applies to: 122-198, 241-241, 291-291, 325-325, 398-398, 453-453


📝 Walkthrough

Walkthrough

Adds an optional: bool to v9 snapshot entries and propagates it through executor events; introduces pnpm:skipped-optional-dependency reporter types/tests; makes postinstall failures non-fatal for optional deps (emitting the new log); wires explicit optional: false into snapshot generation/tests; and adds a registry-mock launch wrapper plus related wiring and tests.

Changes

Optional Dependency Lifecycle

Layer / File(s) Summary
Snapshot Entry Optional Field
crates/lockfile/src/snapshot_entry.rs, crates/lockfile/src/snapshot_entry/tests.rs
Add pub optional: bool to SnapshotEntry with #[serde(default, skip_serializing_if = "is_false")] and YAML round-trip tests for present/absent cases.
Skipped Optional Dependency Log Event
crates/reporter/src/lib.rs, crates/reporter/src/tests.rs
Add LogEvent::SkippedOptionalDependency and payloads SkippedOptionalDependencyLog, SkippedOptionalPackage, and SkippedOptionalReason (BuildFailure, UnsupportedEngine, UnsupportedPlatform, ResolutionFailure); include serialization tests matching pnpm wire shape.
Conditional Postinstall Failure Handling
crates/package-manager/src/build_modules.rs, crates/package-manager/src/build_modules/tests.rs
BuildModules::run reads snapshot optional; on postinstall failure, optional deps emit SkippedOptionalDependency (BuildFailure) and are skipped, non-optional failures return BuildModulesError::LifecycleScript. Tests cover both paths and add a failing-postinstall fixture helper.
Executor lifecycle events
crates/executor/src/lifecycle.rs, crates/executor/src/lifecycle/tests.rs
Add optional: bool to RunPostinstallHooks and propagate it into LifecycleMessage::Script and LifecycleMessage::Exit; tests validate propagation and set explicit optional:false where applicable.
Snapshot Generation with Optional Field
crates/package-manager/src/build_snapshot.rs, crates/package-manager/src/build_sequence/tests.rs
Set optional: false explicitly when building SnapshotEntry and update test helpers to emit the field.

Registry Mock Wrapper & Wiring

Layer / File(s) Summary
Registry-mock launch script & deps
tasks/registry-mock/launch.mjs, tasks/registry-mock/package.json, tasks/registry-mock/pnpm-workspace.yaml
Add launch.mjs wrapper for @pnpm/registry-mock, bump @pnpm/registry-mock devDependency, and set nodeLinker: hoisted in workspace config.
Node registry-mock Command wiring
tasks/registry-mock/src/node_registry_mock.rs, tasks/registry-mock/src/mock_instance.rs
Change node_registry_mock() to return a Command that runs node <launch.mjs> with cached launch script resolution; remove PATH-based binary discovery and adjust mock_instance call sites to use the Command.
Integration test using registry mock
crates/package-manager/src/install/tests.rs
Add integration test that installs an optional package whose transitive postinstall fails via the registry mock and asserts install succeeds with expected virtual-store extraction.

Sequence Diagram

sequenceDiagram
  participant BM as BuildModules::run
  participant PH as run_postinstall_hooks
  participant Reporter as Reporter
  participant Err as Caller/Error
  BM->>BM: read `optional` from snapshot
  BM->>PH: run_postinstall_hooks(opts)
  alt postinstall succeeds
    PH-->>BM: Ok
  else postinstall fails
    PH-->>BM: Err
    alt opts.optional == true
      BM->>Reporter: emit pnpm:skipped-optional-dependency (BuildFailure)
      BM-->>Err: continue (no error)
    else opts.optional == false
      BM-->>Err: return BuildModulesError::LifecycleScript
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pacquet#349: Related reporter API changes; prior reporter extension that this PR builds on.
  • pnpm/pacquet#418: Modifies RunPostinstallHooks-related wiring; overlaps with executor lifecycle changes here.
  • pnpm/pacquet#391: Modifies lifecycle/build_modules and reporter channels; closely related functionality.

Suggested reviewers

  • KSXGitHub
  • anthonyshew

Poem

🐰 A little flag tucked in a snapshot line,
Optional by default, resting snugly fine.
If postinstall tumbles and stumbles in fright,
A skipped log whispers, and we carry on light.
Hooray — registry mocks clap in the night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: optional dependency build failures are now swallowed and reported via a new pnpm event channel, matching the core feature implemented across all modified files.
Description check ✅ Passed The description provides comprehensive coverage: Summary explains the feature, Linked issue references #397, Upstream reference links pnpm behavior, and Checklist items are marked. All required template sections are present and substantively 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/optional-dep-build-failure-397

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

@codecov

codecov Bot commented May 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.42857% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 85.21%. Comparing base (ff6edea) to head (f8df447).

Files with missing lines Patch % Lines
crates/package-manager/src/build_modules.rs 95.65% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #419      +/-   ##
==========================================
+ Coverage   84.70%   85.21%   +0.50%     
==========================================
  Files          78       79       +1     
  Lines        5415     5439      +24     
==========================================
+ Hits         4587     4635      +48     
+ Misses        828      804      -24     

☔ 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 12, 2026

Copy link
Copy Markdown

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00     16.3±0.12ms   266.7 KB/sec    1.02     16.6±0.39ms   261.4 KB/sec

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
crates/reporter/src/tests.rs (1)

617-637: 💤 Low value

Consider adding diagnostic logging closer to the assertion.

The dbg! call is several lines before the assertion that checks details absence. Adding a focused log immediately before line 636 would improve test debuggability per CODE_STYLE_GUIDE.

Suggested addition
     let json: Value = envelope
         .pipe_ref(serde_json::to_string)
         .expect("serialize envelope")
         .pipe_as_ref(serde_json::from_str)
         .expect("parse JSON");
+    eprintln!("json.get(\"details\") = {:?}", json.get("details"));
     assert!(json.get("details").is_none(), "details must be omitted when absent, got {json:?}",);
🤖 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 `@crates/reporter/src/tests.rs` around lines 617 - 637, In the test
skipped_optional_omits_absent_details, add a focused diagnostic print
immediately before the assert that checks json.get("details").is_none() so
failures show the exact JSON under test; e.g., log the `json` Value or
`json.get("details")` right before the assertion (where `envelope` is serialized
into `json`) to improve debuggability of the failing assertion in this test.
crates/lockfile/src/snapshot_entry/tests.rs (2)

27-38: ⚡ Quick win

Add diagnostic logging before assertions.

Same issue as the previous test—add explicit logging before assert! calls per CODE_STYLE_GUIDE. Based on learnings, 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).

Suggested logging additions
 #[test]
 fn optional_defaults_false_and_omits_when_false() {
     let yaml = text_block! {
         "dependencies:"
         "  bar: 1.0.0"
     };
     let entry: SnapshotEntry = serde_saphyr::from_str(yaml).expect("parse");
+    eprintln!("entry.optional = {}", entry.optional);
     assert!(!entry.optional, "default must be false when absent");
 
     let out = serialize_yaml::to_string(&entry).expect("serialize");
+    eprintln!("serialized output:\n{}", out);
     assert!(!out.contains("optional"), "false must not be serialized:\n{out}");
 }
🤖 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 `@crates/lockfile/src/snapshot_entry/tests.rs` around lines 27 - 38, Add
diagnostic logging in the test optional_defaults_false_and_omits_when_false
before each assert: print the parsed SnapshotEntry optional flag (e.g.,
eprintln!("entry.optional = {:?}", entry.optional)) before the
assert!(!entry.optional...) and print the serialized YAML string (e.g.,
eprintln!("serialized out = {}", out)) before the
assert!(!out.contains("optional")...) so failing assertions emit useful
diagnostics; keep logs simple and use eprintln! to match the other tests' style.

10-22: ⚡ Quick win

Add diagnostic logging before assertions.

Per the CODE_STYLE_GUIDE learning, tests should include eprintln! or dbg! to log actual values before assert! calls (not just in the assertion message) so failures can be diagnosed without rerunning. Based on learnings, 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).

Suggested logging additions
 #[test]
 fn optional_true_round_trips() {
     let yaml = text_block! {
         "dependencies:"
         "  foo: 1.2.3"
         "optional: true"
     };
     let entry: SnapshotEntry = serde_saphyr::from_str(yaml).expect("parse");
+    eprintln!("entry.optional = {}", entry.optional);
     assert!(entry.optional, "deserialize must capture optional: true");
 
     let out = serialize_yaml::to_string(&entry).expect("serialize");
+    eprintln!("serialized output:\n{}", out);
     assert!(out.contains("optional: true"), "serialize must round-trip optional: true:\n{out}");
 }
🤖 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 `@crates/lockfile/src/snapshot_entry/tests.rs` around lines 10 - 22, The test
optional_true_round_trips lacks diagnostic logging before assertions; add
eprintln! (or dbg!) calls to print the deserialized SnapshotEntry state and the
serialized output so failures are easier to diagnose: log entry.optional (or the
entire entry) right after let entry: SnapshotEntry = ... and log the serialized
out right after serialize_yaml::to_string(&entry).expect(...), keeping the
existing asserts unchanged.
🤖 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 `@crates/lockfile/src/snapshot_entry/tests.rs`:
- Around line 27-38: Add diagnostic logging in the test
optional_defaults_false_and_omits_when_false before each assert: print the
parsed SnapshotEntry optional flag (e.g., eprintln!("entry.optional = {:?}",
entry.optional)) before the assert!(!entry.optional...) and print the serialized
YAML string (e.g., eprintln!("serialized out = {}", out)) before the
assert!(!out.contains("optional")...) so failing assertions emit useful
diagnostics; keep logs simple and use eprintln! to match the other tests' style.
- Around line 10-22: The test optional_true_round_trips lacks diagnostic logging
before assertions; add eprintln! (or dbg!) calls to print the deserialized
SnapshotEntry state and the serialized output so failures are easier to
diagnose: log entry.optional (or the entire entry) right after let entry:
SnapshotEntry = ... and log the serialized out right after
serialize_yaml::to_string(&entry).expect(...), keeping the existing asserts
unchanged.

In `@crates/reporter/src/tests.rs`:
- Around line 617-637: In the test skipped_optional_omits_absent_details, add a
focused diagnostic print immediately before the assert that checks
json.get("details").is_none() so failures show the exact JSON under test; e.g.,
log the `json` Value or `json.get("details")` right before the assertion (where
`envelope` is serialized into `json`) to improve debuggability of the failing
assertion in this test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f4a7a9f1-7e4e-4d48-8833-de27c0bd2c65

📥 Commits

Reviewing files that changed from the base of the PR and between ff6edea and 719bc7d.

📒 Files selected for processing (8)
  • crates/lockfile/src/snapshot_entry.rs
  • crates/lockfile/src/snapshot_entry/tests.rs
  • crates/package-manager/src/build_modules.rs
  • crates/package-manager/src/build_modules/tests.rs
  • crates/package-manager/src/build_sequence/tests.rs
  • crates/package-manager/src/build_snapshot.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: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (macos-latest)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Log emissions are part of 'match pnpm'—when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing into plain String or &str to preserve the distinction between validating and non-validating brands
If upstream always validates before construction of a branded string type, implement validation via TryFrom<String> and/or FromStr without providing an infallible public constructor
If upstream never validates a branded string type, expose an infallible From<String> constructor to provide type-safety without runtime validation
When upstream occasionally constructs a branded string type without validation via bare as assertion, expose a from_str_unchecked constructor so callers can opt into the same unchecked path explicitly
Match upstream serde behavior for branded types—use #[serde(try_from = "String")] for deserialization to go through the validator and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls on branded types rather than handwriting impl blocks
Model string-literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers
Template literal types (like ${string}@${string}) should be treated as branded string types with the same validation discipline applied
Follow the code style guide in CODE_STYLE_GUIDE.md covering code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Warnings are errors in cargo clippy --locked -- --deny warnings, and should not be silenced with `#[allow(....

Files:

  • crates/package-manager/src/build_snapshot.rs
  • crates/package-manager/src/build_sequence/tests.rs
  • crates/lockfile/src/snapshot_entry/tests.rs
  • crates/lockfile/src/snapshot_entry.rs
  • crates/reporter/src/lib.rs
  • crates/package-manager/src/build_modules.rs
  • crates/package-manager/src/build_modules/tests.rs
  • crates/reporter/src/tests.rs
🧠 Learnings (3)
📚 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_snapshot.rs
  • crates/package-manager/src/build_sequence/tests.rs
  • crates/lockfile/src/snapshot_entry/tests.rs
  • crates/lockfile/src/snapshot_entry.rs
  • crates/reporter/src/lib.rs
  • crates/package-manager/src/build_modules.rs
  • crates/package-manager/src/build_modules/tests.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 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_sequence/tests.rs
  • crates/lockfile/src/snapshot_entry/tests.rs
  • crates/package-manager/src/build_modules/tests.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 (11)
crates/lockfile/src/snapshot_entry.rs (1)

5-49: LGTM!

The optional field addition is well-structured:

  • Comprehensive documentation linking to upstream behavior
  • Correct serde configuration for default + skip-when-false semantics
  • Simple, focused helper function

The implementation correctly mirrors pnpm's v9 lockfile spec where optional is precomputed by the resolver and written to the snapshot.

crates/package-manager/src/build_snapshot.rs (1)

110-110: LGTM!

Explicitly setting optional: false ensures the snapshot construction always populates the field, which aligns with the serde skip_serializing_if = "is_false" behavior (the field will be omitted from YAML output).

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

37-37: LGTM!

The test helper now constructs SnapshotEntry with the new optional field, maintaining consistency with the updated struct definition and production code in build_snapshot.rs.

crates/reporter/src/lib.rs (2)

153-164: LGTM!

The SkippedOptionalDependency event variant is correctly integrated:

  • Proper serde rename to pnpm:skipped-optional-dependency for wire compatibility
  • Comprehensive documentation linking to upstream emit site
  • Clear scope note that only build_failure is emitted today

533-573: LGTM!

The new reporter types correctly mirror pnpm's wire format:

  • SkippedOptionalDependencyLog with optional details field properly configured
  • SkippedOptionalPackage matches upstream's non-resolution-failure shape
  • SkippedOptionalReason enum with snake_case serialization for pnpm compatibility
  • All four upstream reasons declared for forward compatibility

The serde configuration ensures @pnpm/cli.default-reporter will parse these events correctly.

crates/reporter/src/tests.rs (2)

580-613: LGTM!

The test comprehensively verifies the pnpm:skipped-optional-dependency wire format:

  • Channel name, level, and reason fields
  • Nested package structure
  • details field presence
  • Uses dbg!(&json) for diagnostic logging

641-653: LGTM!

The parameterized test correctly verifies all four SkippedOptionalReason variants serialize to the snake_case strings pnpm's reporter expects.

crates/package-manager/src/build_modules.rs (1)

223-270: LGTM!

The optional-dependency build-failure handling correctly mirrors pnpm's behavior:

  • Reads the precomputed optional flag from the v9 lockfile snapshot
  • On postinstall failure, branches to either:
    • Emit pnpm:skipped-optional-dependency with BuildFailure reason and continue (optional)
    • Return BuildModulesError::LifecycleScript and abort (non-optional)
  • Populates the event with package identification (id, name, version), error details, and prefix

The use of pkg_dir.to_string_lossy() for the id field matches upstream's depNode.dir and is appropriate for paths that need serialization.

crates/package-manager/src/build_modules/tests.rs (3)

264-326: LGTM!

This test correctly verifies that an optional dependency's postinstall failure is swallowed and logged as pnpm:skipped-optional-dependency with:

  • Reason BuildFailure
  • Populated details field
  • Correct package name and version

The Unix gating is appropriate given the spawn/shell semantics differences, and the use of dangerouslyAllowAllBuilds correctly exercises the build-failure path rather than the policy gate.


328-357: LGTM!

The symmetric test correctly verifies that a non-optional dependency's postinstall failure propagates as BuildModulesError::LifecycleScript, matching upstream's throw err path.


359-377: LGTM!

The create_failing_pkg helper cleanly mirrors create_buildable_pkg with a failing "exit 1" postinstall script, allowing the tests to exercise the error path.

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

This PR aligns pacquet’s install/build behavior and reporter wire format with pnpm v11 for optional dependencies whose lifecycle scripts fail, by continuing the install and emitting a pnpm:skipped-optional-dependency event instead of aborting.

Changes:

  • Surface snapshots[<key>].optional in the lockfile model (SnapshotEntry.optional) with correct defaulting/serialization behavior.
  • Add reporter support for pnpm:skipped-optional-dependency, including SkippedOptionalReason serialization and wire-shape tests.
  • Update BuildModules to swallow lifecycle failures for snapshot-marked optional deps and emit the skipped-optional event; add tests for optional vs non-optional failure behavior.

Reviewed changes

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

Show a summary per file
File Description
crates/reporter/src/lib.rs Adds the new pnpm:skipped-optional-dependency log event types and reason enum.
crates/reporter/src/tests.rs Adds wire-shape/serialization tests for the new skipped-optional event and reasons.
crates/lockfile/src/snapshot_entry.rs Adds SnapshotEntry.optional with serde defaulting and omission when false.
crates/lockfile/src/snapshot_entry/tests.rs Adds YAML round-trip tests for optional: true and omission when absent/false.
crates/package-manager/src/build_modules.rs Swallows optional-dep lifecycle failures and emits pnpm:skipped-optional-dependency.
crates/package-manager/src/build_modules/tests.rs Adds tests ensuring optional failures are swallowed+reported and non-optional failures propagate.
crates/package-manager/src/build_snapshot.rs Initializes the new SnapshotEntry.optional field when constructing snapshots.
crates/package-manager/src/build_sequence/tests.rs Updates helper snapshot construction to include optional: false.

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

Comment thread crates/package-manager/src/build_modules.rs
Comment thread crates/reporter/src/lib.rs Outdated
Comment thread crates/package-manager/src/build_modules/tests.rs Outdated
Comment thread crates/lockfile/src/snapshot_entry/tests.rs Outdated
@github-actions

github-actions Bot commented May 12, 2026

Copy link
Copy Markdown

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.485 ± 0.069 2.378 2.593 1.00 ± 0.04
pacquet@main 2.474 ± 0.066 2.373 2.586 1.00
pnpm 6.331 ± 0.084 6.166 6.438 2.56 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.4848017133200004,
      "stddev": 0.06907687884485912,
      "median": 2.49404528482,
      "user": 2.4575992399999995,
      "system": 3.4635440799999997,
      "min": 2.3779451208199998,
      "max": 2.59274864282,
      "times": [
        2.52154781982,
        2.53616557782,
        2.38677455782,
        2.45490747982,
        2.52926637982,
        2.45237257782,
        2.52974622682,
        2.46654274982,
        2.59274864282,
        2.3779451208199998
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.4743788276200003,
      "stddev": 0.06559854009645909,
      "median": 2.46812412682,
      "user": 2.49985134,
      "system": 3.46317088,
      "min": 2.37267978782,
      "max": 2.58598184482,
      "times": [
        2.42213938682,
        2.48337624182,
        2.45250379282,
        2.58598184482,
        2.45287201182,
        2.41281283782,
        2.5543671248199997,
        2.37267978782,
        2.49144847482,
        2.51560677282
      ]
    },
    {
      "command": "pnpm",
      "mean": 6.33137536802,
      "stddev": 0.08417253664739424,
      "median": 6.33468592332,
      "user": 9.43948484,
      "system": 4.53789378,
      "min": 6.16596284482,
      "max": 6.43819772382,
      "times": [
        6.43335162182,
        6.37493580882,
        6.33111235482,
        6.33268822382,
        6.26962364182,
        6.38023295682,
        6.33668362282,
        6.25096488082,
        6.43819772382,
        6.16596284482
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 682.9 ± 58.9 651.6 844.5 1.00
pacquet@main 684.0 ± 45.0 645.1 764.6 1.00 ± 0.11
pnpm 2664.1 ± 82.3 2537.1 2779.8 3.90 ± 0.36
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6829253511600001,
      "stddev": 0.05891428408173842,
      "median": 0.65879246386,
      "user": 0.35924528,
      "system": 1.4768329599999999,
      "min": 0.6515822248600001,
      "max": 0.84446150486,
      "times": [
        0.84446150486,
        0.6793471848600001,
        0.65364347986,
        0.67624851486,
        0.6539902898600001,
        0.7001044728600001,
        0.6611675988600001,
        0.6515822248600001,
        0.65641732886,
        0.6522909118600001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.68398827296,
      "stddev": 0.04499486798505863,
      "median": 0.6625674183600001,
      "user": 0.36068068,
      "system": 1.4753041599999999,
      "min": 0.6451331988600001,
      "max": 0.7645867838600001,
      "times": [
        0.7645867838600001,
        0.65420370386,
        0.7538578468600001,
        0.6630001968600001,
        0.65054664386,
        0.6621346398600001,
        0.6451331988600001,
        0.71962859586,
        0.6513259038600001,
        0.6754652158600001
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.6641386669600005,
      "stddev": 0.08230622631575653,
      "median": 2.6558912998600004,
      "user": 3.22523238,
      "system": 2.21655336,
      "min": 2.53712671886,
      "max": 2.77983913686,
      "times": [
        2.58576947486,
        2.53712671886,
        2.67391816186,
        2.6378644378600002,
        2.73355683186,
        2.63639855086,
        2.5826190128600004,
        2.76567928686,
        2.7086150568600003,
        2.77983913686
      ]
    }
  ]
}

…#397)

Per review feedback. The two BuildModules tests for the optional-
build-failure path used synthetic identifiers (`flaky@1.0.0`,
`required@1.0.0`) and a stripped-down `exit 1` script body. They
now use the upstream fixture identity verbatim —
`@pnpm.e2e/failing-postinstall@1.0.0` from
`/Volumes/src/pnpm/registry-mock/packages/failing-postinstall/`
(pinned at the same commit pnpm v11's `b4f8f47ac2` consumes) — and
the same `postinstall` body (`echo hello && echo world && exit 1`).
Mirrors the test cases at
`installing/deps-installer/test/install/optionalDependencies.ts:563`
and `:574`.

The full e2e port (resolve + extract through the live
registry-mock then run scripts) is deferred: pacquet's pinned
`@pnpm/registry-mock@^3.16.0` predates the `failing-postinstall`
fixture (introduced in v5.x). Upgrading that dep is its own
follow-up.

Renames:
- `optional_dep_build_failure_is_swallowed_and_reported`
  → `do_not_fail_on_optional_dep_with_failing_postinstall`
- `non_optional_dep_build_failure_propagates`
  → `fail_when_failing_postinstall_is_required`
- `create_failing_pkg` → `create_failing_postinstall_fixture`

@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)
crates/package-manager/src/build_modules/tests.rs (1)

320-335: ⚡ Quick win

Strengthen the optional-failure assertions to catch silent regressions.

This test currently logs ignored but does not assert it; it also accepts empty details as long as it is Some. Tightening both checks will make the behavior contract explicit.

Proposed test hardening
-    dbg!(&ignored);
+    assert!(
+        ignored.is_empty(),
+        "optional build failure should not add ignored scripts: {ignored:?}",
+    );

@@
-    assert!(skipped_event.details.is_some(), "details must carry the error toString");
+    let details = skipped_event.details.as_deref().unwrap_or("");
+    assert!(
+        !details.trim().is_empty(),
+        "details must carry non-empty lifecycle error context: {skipped_event:?}",
+    );
🤖 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 `@crates/package-manager/src/build_modules/tests.rs` around lines 320 - 335,
The test currently prints `ignored` and only checks `details.is_some()`, which
allows silent regressions; update the assertions to (1) assert that the
`ignored` collection (variable `ignored`) contains the expected entry(s) for the
failing-postinstall package (i.e., not empty and/or contains the package name),
and (2) assert that `skipped_event.details` is a non-empty string (not just
Some) so it actually carries the error text; locate the test where `ignored` is
created and the `skipped_event` is extracted from `EVENTS` (matching
`LogEvent::SkippedOptionalDependency`) and replace the weak checks with concrete
assertions that `ignored` includes the failing package and
`skipped_event.details.as_ref().map(|s| !s.trim().is_empty()).unwrap_or(false)`
is true while keeping the existing checks for
`SkippedOptionalReason::BuildFailure` and package name/version.
🤖 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 `@crates/package-manager/src/build_modules/tests.rs`:
- Around line 320-335: The test currently prints `ignored` and only checks
`details.is_some()`, which allows silent regressions; update the assertions to
(1) assert that the `ignored` collection (variable `ignored`) contains the
expected entry(s) for the failing-postinstall package (i.e., not empty and/or
contains the package name), and (2) assert that `skipped_event.details` is a
non-empty string (not just Some) so it actually carries the error text; locate
the test where `ignored` is created and the `skipped_event` is extracted from
`EVENTS` (matching `LogEvent::SkippedOptionalDependency`) and replace the weak
checks with concrete assertions that `ignored` includes the failing package and
`skipped_event.details.as_ref().map(|s| !s.trim().is_empty()).unwrap_or(false)`
is true while keeping the existing checks for
`SkippedOptionalReason::BuildFailure` and package name/version.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d5ab006-4fb0-43e2-9b59-17d7bef94812

📥 Commits

Reviewing files that changed from the base of the PR and between 719bc7d and 68318b4.

📒 Files selected for processing (1)
  • crates/package-manager/src/build_modules/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: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (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: Log emissions are part of 'match pnpm'—when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing into plain String or &str to preserve the distinction between validating and non-validating brands
If upstream always validates before construction of a branded string type, implement validation via TryFrom<String> and/or FromStr without providing an infallible public constructor
If upstream never validates a branded string type, expose an infallible From<String> constructor to provide type-safety without runtime validation
When upstream occasionally constructs a branded string type without validation via bare as assertion, expose a from_str_unchecked constructor so callers can opt into the same unchecked path explicitly
Match upstream serde behavior for branded types—use #[serde(try_from = "String")] for deserialization to go through the validator and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls on branded types rather than handwriting impl blocks
Model string-literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers
Template literal types (like ${string}@${string}) should be treated as branded string types with the same validation discipline applied
Follow the code style guide in CODE_STYLE_GUIDE.md covering code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Warnings are errors in cargo clippy --locked -- --deny warnings, and should not be silenced with `#[allow(....

Files:

  • crates/package-manager/src/build_modules/tests.rs
🧠 Learnings (2)
📚 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
📚 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
🔇 Additional comments (1)
crates/package-manager/src/build_modules/tests.rs (1)

350-378: Nice branch coverage for required dependency failures.

This cleanly pins the optional: false path to BuildModulesError::LifecycleScript, which complements the optional-skip test well.

zkochan added 2 commits May 12, 2026 12:32
Bumps the mock from `^3.16.0` (resolves to 3.50.0) to `^6.0.0`.
v3 predates the `@pnpm.e2e/failing-postinstall` and
`@pnpm.e2e/has-failing-postinstall-dep` fixtures (added in v5.x);
v6 ships them along with ~250 other `@pnpm.e2e/*` fixtures used by
upstream pnpm's tests, which unlocks faithful test parity for
ports against pnpm v11.

Three follow-on adjustments needed for v6 to actually launch and
for existing tests to keep passing:

- **Launch via the programmatic API.** v6's default CLI export
  invokes the bundled verdaccio 5.33 directly with the host
  Node, but verdaccio 5.33 rejects its own auto-generated
  64-character storage secret on Node 22+
  (`Invalid storage secret key length, must be 32 characters
  long but is 64`). Pnpm's own jest `globalSetup`
  (`pnpm/__utils__/jest-config/with-registry/globalSetup.js`)
  works around this by calling the programmatic
  `start({ useNodeVersion: '20.16.0', listen, … })` so verdaccio
  runs under a Node version where the 32-char enforcement
  isn't there. Added a tiny wrapper `tasks/registry-mock/launch.mjs`
  that mirrors that shape; the Rust launcher
  (`node_registry_mock.rs`) now returns a pre-populated
  `Command` of `node <launch.mjs>` instead of pointing at the
  bare `registry-mock` CLI binary in `node_modules/.bin`.
- **`nodeLinker: hoisted` in `pnpm-workspace.yaml`.** Verdaccio's
  CJS deps (notably `@verdaccio/signature` requiring
  `@verdaccio/config`) don't resolve cleanly under pnpm's strict
  isolation when running from the nested `node_modules/.bin`. A
  flat node_modules makes the resolution predictable.
- **Snapshot refresh for three CLI install tests** —
  `should_install_dependencies` / `should_install_index_files` /
  `should_install_exec_files`. v6's `@pnpm.e2e/hello-world-js-bin`
  and `…-js-bin-parent` fixtures now ship MIT LICENSE files and
  slightly different `package.json` content, so the recursive
  on-disk-listing snapshots had to be regenerated. Reviewed and
  accepted — the changes are consistent with "fixture content
  was refreshed upstream," nothing structural.
… install test (#397)

Adds an integration test that drives `Install::run` end-to-end
through the live mocked registry (now serving the
`@pnpm.e2e/failing-postinstall` and `@pnpm.e2e/has-failing-postinstall-dep`
fixtures after the previous commit's `@pnpm/registry-mock` upgrade).
Ports `'do not fail on an optional dependency that has a
non-optional dependency with a failing postinstall script'` at
`installing/deps-installer/test/install/optionalDependencies.ts:563`
in pnpm v11 (`b4f8f47ac2`).

The test resolves `@pnpm.e2e/has-failing-postinstall-dep@1.0.0` as
an `optionalDependencies` entry, runs install, and asserts both
the wrapper and the transitive `@pnpm.e2e/failing-postinstall`
land in the virtual store without the install aborting. Pacquet's
`frozen_lockfile=false` path doesn't run scripts (script execution
is `BuildModules`'s job in the frozen path), so this test pins the
fetch + extract behavior on the optional edge. The unit-level
`BuildModules` test for the same fixture
(`crate::build_modules::tests::do_not_fail_on_optional_dep_with_failing_postinstall`)
covers the script-failure swallow.
Copilot AI review requested due to automatic review settings May 12, 2026 10:33

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 17 out of 18 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • tasks/registry-mock/pnpm-lock.yaml: Language not supported

Comment thread tasks/registry-mock/launch.mjs
Comment thread tasks/registry-mock/launch.mjs Outdated
Comment thread tasks/registry-mock/pnpm-workspace.yaml Outdated

@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)
crates/package-manager/src/install/tests.rs (1)

708-724: ⚡ Quick win

Add diagnostic logs around assert! checks in the new test.

Lines 708-724 use assert! checks without printing the evaluated runtime state, which makes CI failures harder to triage.

Suggested patch
-    assert!(
-        is_symlink_or_junction(
-            &project_root.join("node_modules/@pnpm.e2e/has-failing-postinstall-dep"),
-        )
-        .unwrap(),
-        "wrapper symlink missing",
-    );
+    let wrapper_link = project_root.join("node_modules/@pnpm.e2e/has-failing-postinstall-dep");
+    eprintln!(
+        "wrapper_link={wrapper_link:?} symlink_or_junction={:?}",
+        is_symlink_or_junction(&wrapper_link)
+    );
+    assert!(
+        is_symlink_or_junction(&wrapper_link).unwrap(),
+        "wrapper symlink missing",
+    );
-    assert!(
-        project_root
-            .join("node_modules/.pacquet/@pnpm.e2e+has-failing-postinstall-dep@1.0.0")
-            .is_dir(),
-        "wrapper virtual-store dir missing",
-    );
+    let wrapper_store_dir =
+        project_root.join("node_modules/.pacquet/@pnpm.e2e+has-failing-postinstall-dep@1.0.0");
+    eprintln!(
+        "wrapper_store_dir={wrapper_store_dir:?} is_dir={}",
+        wrapper_store_dir.is_dir()
+    );
+    assert!(wrapper_store_dir.is_dir(), "wrapper virtual-store dir missing");
-    assert!(
-        project_root.join("node_modules/.pacquet/@pnpm.e2e+failing-postinstall@1.0.0").is_dir(),
-        "transitive `failing-postinstall` must be extracted to the virtual store",
-    );
+    let transitive_store_dir =
+        project_root.join("node_modules/.pacquet/@pnpm.e2e+failing-postinstall@1.0.0");
+    eprintln!(
+        "transitive_store_dir={transitive_store_dir:?} is_dir={}",
+        transitive_store_dir.is_dir()
+    );
+    assert!(
+        transitive_store_dir.is_dir(),
+        "transitive `failing-postinstall` must be extracted to the virtual store",
+    );

Based on learnings: “In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., eprintln!) for assert!/assert_ne!-style assertions so failures are diagnosable.”

🤖 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 `@crates/package-manager/src/install/tests.rs` around lines 708 - 724, The
three assert! checks around is_symlink_or_junction(...) and the
project_root.join(...) path existence checks lack diagnostic output; before each
assert!, print the evaluated runtime state (e.g., the boolean result of
is_symlink_or_junction, the full path strings, and whether .is_dir() returned
true) using eprintln! so CI failures show actual values; specifically, for the
is_symlink_or_junction call store its Result/boolean and eprintln! the path and
result before asserting, and for the two .is_dir() checks eprintln! the joined
path and the .is_dir() boolean before the assert!.
🤖 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 `@crates/package-manager/src/install/tests.rs`:
- Around line 708-724: The three assert! checks around
is_symlink_or_junction(...) and the project_root.join(...) path existence checks
lack diagnostic output; before each assert!, print the evaluated runtime state
(e.g., the boolean result of is_symlink_or_junction, the full path strings, and
whether .is_dir() returned true) using eprintln! so CI failures show actual
values; specifically, for the is_symlink_or_junction call store its
Result/boolean and eprintln! the path and result before asserting, and for the
two .is_dir() checks eprintln! the joined path and the .is_dir() boolean before
the assert!.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 17408aa9-5b8f-4f6f-9d22-093317f8a0df

📥 Commits

Reviewing files that changed from the base of the PR and between 68318b4 and ec4dcc2.

⛔ Files ignored due to path filters (4)
  • crates/cli/tests/snapshots/install__should_install_dependencies.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/install__should_install_exec_files.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/install__should_install_index_files.snap is excluded by !**/*.snap
  • tasks/registry-mock/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • crates/package-manager/src/install/tests.rs
  • tasks/registry-mock/launch.mjs
  • tasks/registry-mock/package.json
  • tasks/registry-mock/pnpm-workspace.yaml
  • tasks/registry-mock/src/mock_instance.rs
  • tasks/registry-mock/src/node_registry_mock.rs
✅ Files skipped from review due to trivial changes (3)
  • tasks/registry-mock/package.json
  • tasks/registry-mock/pnpm-workspace.yaml
  • tasks/registry-mock/launch.mjs
📜 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). (5)
  • GitHub Check: Agent
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Log emissions are part of 'match pnpm'—when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing into plain String or &str to preserve the distinction between validating and non-validating brands
If upstream always validates before construction of a branded string type, implement validation via TryFrom<String> and/or FromStr without providing an infallible public constructor
If upstream never validates a branded string type, expose an infallible From<String> constructor to provide type-safety without runtime validation
When upstream occasionally constructs a branded string type without validation via bare as assertion, expose a from_str_unchecked constructor so callers can opt into the same unchecked path explicitly
Match upstream serde behavior for branded types—use #[serde(try_from = "String")] for deserialization to go through the validator and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls on branded types rather than handwriting impl blocks
Model string-literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers
Template literal types (like ${string}@${string}) should be treated as branded string types with the same validation discipline applied
Follow the code style guide in CODE_STYLE_GUIDE.md covering code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Warnings are errors in cargo clippy --locked -- --deny warnings, and should not be silenced with `#[allow(....

Files:

  • crates/package-manager/src/install/tests.rs
  • tasks/registry-mock/src/node_registry_mock.rs
  • tasks/registry-mock/src/mock_instance.rs
🧠 Learnings (2)
📚 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/install/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/install/tests.rs
  • tasks/registry-mock/src/node_registry_mock.rs
  • tasks/registry-mock/src/mock_instance.rs
🔇 Additional comments (2)
tasks/registry-mock/src/node_registry_mock.rs (1)

18-24: Clean launch-command refactor.

Using a preconfigured Command for node <launch.mjs> keeps the call sites simple and avoids PATH-based binary lookup complexity.

tasks/registry-mock/src/mock_instance.rs (1)

81-102: Prepare/spawn wiring looks solid.

The updated prepare and spawn flows align well with the new node_registry_mock() command API and keep process setup readable.

Dylint blocker + seven Copilot threads:

- `crates/reporter/src/tests.rs:636` — `assert!` collapsed to a
  single-line trailing-comma form by `cargo fmt`, which dylint's
  `perfectionist::macro-trailing-comma` rejects. Remove the
  trailing comma. (Same conflict we hit on #418.)
- `pnpm:lifecycle` events now carry the dep's `optional` flag.
  Previously `LifecycleMessage::Script` / `Exit` were hard-coded
  to `optional: false`, so an optional dep's lifecycle records
  diverged from upstream's `lifecycleLogger.debug({ optional, … })`
  payloads at `pnpm/exec/lifecycle/src/runLifecycleHook.ts:102`
  and `:165`. Added `RunPostinstallHooks.optional`, threaded
  through to both emit sites, and added a `cfg(unix)` test
  asserting the flag survives the spawn-and-capture round-trip.
  `BuildModules` reads the value from `SnapshotEntry.optional`
  (already in the lockfile, now in the wire too).
- Tightened the `SkippedOptionalDependencyLog` doc comment to
  explain that `ResolutionFailure` has a distinct `package` shape
  upstream (`bareSpecifier`, no `id`) and will need a sibling
  struct / untagged enum when an emit site lands — not just a new
  `reason` value. Refactoring deferred until then per YAGNI.
- `crates/lockfile/src/snapshot_entry/tests.rs` — `!out.contains("optional")`
  would falsely fire on a future fixture containing
  `optionalDependencies:`. Tightened to check the exact key
  spelling (`optional: ` or `optional:\n`).
- `crates/package-manager/src/build_modules/tests.rs` — stale
  comment pointed at `crate::executor::lifecycle::tests` which
  is not a valid path from this crate. Now references
  `pacquet_executor::select_shell` and the executor crate's
  `shell::tests` module.
- `tasks/registry-mock/launch.mjs` — signal-forwarding logic
  installed `process.on('SIGINT'|'SIGTERM'|'SIGHUP', …)` handlers
  that overrode Node's default termination behavior, while the
  child-exit handler tried to re-raise via
  `process.kill(process.pid, signal)` — that hits our own
  listener in a loop. Switched to forwarding signals to the
  child only, and exiting with the shell-convention `128 + signo`
  when the child reports it was killed by a signal.
- `tasks/registry-mock/launch.mjs` — stale path
  `crates/tasks/registry-mock/...` → `tasks/registry-mock/...`.
- `tasks/registry-mock/pnpm-workspace.yaml` — comment said "Rust
  calls the CLI binary" but the Rust launcher now spawns
  `node launch.mjs`. Updated.
@zkochan zkochan merged commit 98c9886 into main May 12, 2026
16 checks passed
@zkochan zkochan deleted the fix/optional-dep-build-failure-397 branch May 12, 2026 10:59
zkochan added a commit that referenced this pull request May 12, 2026
The `install_optional_failing_postinstall_dep_via_registry_mock_succeeds`
test came in via the merge of `main` (commit 98c9886, PR #419 landed
after this branch forked) and still constructed the config with
`Npmrc::new()`. The rest of `pacquet-config` is `Config` now, so CI was
failing to compile `pacquet-package-manager` (lib test) on both
windows-latest and macos-latest.

---
Written by an agent (Claude Code, claude-opus-4-7).
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.

2 participants