Skip to content

Works on remaining missing tests#306

Merged
arcanis merged 34 commits into
mainfrom
mael/sage-bee
Jun 1, 2026
Merged

Works on remaining missing tests#306
arcanis merged 34 commits into
mainfrom
mael/sage-bee

Conversation

@arcanis

@arcanis arcanis commented May 30, 2026

Copy link
Copy Markdown
Member

Note

High Risk
Large cross-cutting changes to resolution, virtual peer deduplication, exec script execution, cache/refetch paths, and HTTP client selection affect core install/link behavior.

Overview
This PR closes gaps against upstream Yarn behavior and acceptance tests across resolution, caching, config, CLI output, and CI.

Package protocols and resolution: Adds jsr: semver/tag ranges (mapped to @jsr/... npm-style ids), exec: ranges/references with workspace-only parents and script-gated builds, and optional content hashes on file:/folder:/tarball:/exec: locators. Virtual peer instance deduplication is reworked to key on peer sets rather than per-descriptor consolidation.

Install, cache, and network: enableMirror, cacheFolder (replacing localCacheFolderName), selective global cache cleanup, refetch_blob_data, and offline-aware npm tag resolution that prefers cached tarballs. Per-host TLS overrides in networkSettings, logFilters, install --json NDJSON reporting, and stricter peer-warning visibility when messages are filtered.

PnP, binaries, and tooling: PnP fallback pool is populated from installed packages; visible binaries resolve via node_modules layout. yarn run list, daemon --open quiet stdout, switch PM install export, git prepare via real yarn binaries, and CI pnpm@10 install.

Config: Merges user/project rc with onConflict extend/reset metadata before deserialization.

Reviewed by Cursor Bugbot for commit 4236bb8. Bugbot is set up for automated code reviews on this repo. Configure here.

@arcanis

arcanis commented May 30, 2026

Copy link
Copy Markdown
Member Author

@cursor review

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Immutable cache refetch breaks
    • Folder, tarball, and exec fetchers now reuse cached archives normally and only force refetch when checksum checking is enabled.
  • ✅ Fixed: JSR loose resolution omits locator
    • JSR semver and tag loose resolutions now return the concrete resolved locator when registry resolution succeeds.

Create PR

Or push these changes by commenting:

@cursor push 227b15e7a4
Preview (227b15e7a4)
diff --git a/packages/zpm/src/descriptor_loose.rs b/packages/zpm/src/descriptor_loose.rs
--- a/packages/zpm/src/descriptor_loose.rs
+++ b/packages/zpm/src/descriptor_loose.rs
@@ -404,7 +404,7 @@
 
         Ok(LooseResolution {
             descriptor,
-            locator: None,
+            locator: Some(resolution_result.resolution.locator),
         })
     }
 
@@ -445,7 +445,7 @@
         if resolution_check_result.resolution.version == resolution_result.resolution.version {
             Ok(LooseResolution {
                 descriptor,
-                locator: None,
+                locator: Some(resolution_check_result.resolution.locator),
             })
         } else {
             let fixed_range = resolution_result.resolution.version
@@ -456,7 +456,7 @@
 
             Ok(LooseResolution {
                 descriptor,
-                locator: None,
+                locator: Some(resolution_result.resolution.locator),
             })
         }
     }

diff --git a/packages/zpm/src/fetchers/exec.rs b/packages/zpm/src/fetchers/exec.rs
--- a/packages/zpm/src/fetchers/exec.rs
+++ b/packages/zpm/src/fetchers/exec.rs
@@ -54,7 +54,7 @@
     let locator_str
         = locator.to_file_string();
 
-    let pkg_blob = package_cache.refetch_blob_data(locator.clone(), ".zip", || async {
+    let fetch_archive = || async {
         let temp_dir
             = Path::temp_dir_pattern("exec-<>")?;
         let build_dir
@@ -81,8 +81,14 @@
         }).await??;
 
         Ok(archive)
-    }).await?;
+    };
 
+    let pkg_blob = if context.check_checksums {
+        package_cache.refetch_blob_data(locator.clone(), ".zip", fetch_archive).await?
+    } else {
+        package_cache.upsert_blob(locator.clone(), ".zip", fetch_archive).await?
+    };
+
     let first_entry
         = zpm_formats::zip::first_entry_from_zip(&pkg_blob.data)?;
 

diff --git a/packages/zpm/src/fetchers/folder.rs b/packages/zpm/src/fetchers/folder.rs
--- a/packages/zpm/src/fetchers/folder.rs
+++ b/packages/zpm/src/fetchers/folder.rs
@@ -50,7 +50,7 @@
     let context_directory_for_entries
         = context_directory.clone();
 
-    let pkg_blob = package_cache.refetch_blob_data(locator.clone(), ".zip", || async {
+    let fetch_archive = || async {
         let archive = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, Error> {
             let entries
                 = zpm_formats::entries_from_folder(&context_directory_for_entries)?
@@ -61,8 +61,14 @@
         }).await??;
 
         Ok(archive)
-    }).await?;
+    };
 
+    let pkg_blob = if context.check_checksums {
+        package_cache.refetch_blob_data(locator.clone(), ".zip", fetch_archive).await?
+    } else {
+        package_cache.upsert_blob(locator.clone(), ".zip", fetch_archive).await?
+    };
+
     let first_entry
         = zpm_formats::zip::first_entry_from_zip(&pkg_blob.data)?;
 

diff --git a/packages/zpm/src/fetchers/tarball.rs b/packages/zpm/src/fetchers/tarball.rs
--- a/packages/zpm/src/fetchers/tarball.rs
+++ b/packages/zpm/src/fetchers/tarball.rs
@@ -48,7 +48,7 @@
     let package_subdir_for_entries
         = package_subdir.clone();
 
-    let cached_blob = package_cache.refetch_blob_data(locator.clone(), ".zip", || async {
+    let fetch_archive = || async {
         let tgz_data
             = tarball_path.fs_read()?;
 
@@ -66,8 +66,14 @@
         }).await??;
 
         Ok(archive)
-    }).await?;
+    };
 
+    let cached_blob = if context.check_checksums {
+        package_cache.refetch_blob_data(locator.clone(), ".zip", fetch_archive).await?
+    } else {
+        package_cache.upsert_blob(locator.clone(), ".zip", fetch_archive).await?
+    };
+
     let first_entry
         = zpm_formats::zip::first_entry_from_zip(&cached_blob.data)?;

You can send follow-ups to the cloud agent here.

Comment thread packages/zpm/src/fetchers/folder.rs Outdated
Comment thread packages/zpm/src/descriptor_loose.rs
@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown

⏱️ Benchmark Results

gatsby install-full-cold

Metric Base Head Difference
Mean 4.365s 4.347s -0.41% ✅
Median 4.362s 4.347s -0.34% ✅
Min 4.292s 4.270s
Max 4.444s 4.454s
Std Dev 0.039s 0.048s
📊 Raw benchmark data (gatsby install-full-cold)

Base times: 4.338s, 4.373s, 4.440s, 4.358s, 4.404s, 4.352s, 4.330s, 4.426s, 4.338s, 4.319s, 4.369s, 4.363s, 4.336s, 4.353s, 4.360s, 4.386s, 4.385s, 4.419s, 4.407s, 4.375s, 4.444s, 4.339s, 4.385s, 4.330s, 4.306s, 4.308s, 4.353s, 4.369s, 4.292s, 4.405s

Head times: 4.307s, 4.278s, 4.297s, 4.392s, 4.425s, 4.304s, 4.347s, 4.358s, 4.313s, 4.400s, 4.279s, 4.406s, 4.333s, 4.384s, 4.393s, 4.346s, 4.343s, 4.274s, 4.356s, 4.331s, 4.421s, 4.361s, 4.311s, 4.354s, 4.371s, 4.372s, 4.270s, 4.454s, 4.301s, 4.344s


gatsby install-cache-only

Metric Base Head Difference
Mean 1.248s 1.255s +0.54% ⚠️
Median 1.248s 1.256s +0.64% ⚠️
Min 1.227s 1.234s
Max 1.280s 1.287s
Std Dev 0.011s 0.011s
📊 Raw benchmark data (gatsby install-cache-only)

Base times: 1.263s, 1.246s, 1.245s, 1.238s, 1.255s, 1.261s, 1.280s, 1.227s, 1.250s, 1.254s, 1.255s, 1.245s, 1.239s, 1.259s, 1.240s, 1.248s, 1.264s, 1.249s, 1.242s, 1.255s, 1.230s, 1.254s, 1.239s, 1.248s, 1.242s, 1.247s, 1.227s, 1.250s, 1.243s, 1.250s

Head times: 1.244s, 1.246s, 1.251s, 1.259s, 1.251s, 1.258s, 1.258s, 1.248s, 1.269s, 1.248s, 1.244s, 1.248s, 1.257s, 1.238s, 1.265s, 1.258s, 1.263s, 1.251s, 1.272s, 1.287s, 1.251s, 1.261s, 1.255s, 1.245s, 1.244s, 1.259s, 1.263s, 1.261s, 1.234s, 1.257s


gatsby install-cache-and-lock (warm, with lockfile)

Metric Base Head Difference
Mean 0.335s 0.340s +1.60% ⚠️
Median 0.334s 0.340s +1.85% ⚠️
Min 0.330s 0.334s
Max 0.358s 0.355s
Std Dev 0.005s 0.004s
📊 Raw benchmark data (gatsby install-cache-and-lock (warm, with lockfile))

Base times: 0.333s, 0.338s, 0.332s, 0.331s, 0.331s, 0.334s, 0.331s, 0.333s, 0.333s, 0.332s, 0.336s, 0.337s, 0.330s, 0.336s, 0.336s, 0.338s, 0.334s, 0.342s, 0.336s, 0.334s, 0.331s, 0.335s, 0.335s, 0.335s, 0.334s, 0.358s, 0.332s, 0.333s, 0.334s, 0.334s

Head times: 0.335s, 0.343s, 0.336s, 0.335s, 0.334s, 0.337s, 0.355s, 0.350s, 0.339s, 0.345s, 0.337s, 0.338s, 0.340s, 0.341s, 0.341s, 0.335s, 0.340s, 0.341s, 0.340s, 0.343s, 0.340s, 0.340s, 0.339s, 0.340s, 0.342s, 0.340s, 0.341s, 0.338s, 0.340s, 0.342s

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Symlink folder sync check changed
    • Restored folder checks to use symlink metadata so symlinked directories are replaced instead of treated as valid folders.

Create PR

Or push these changes by commenting:

@cursor push 3b6d53d580
Preview (3b6d53d580)
diff --git a/packages/zpm-sync/src/lib.rs b/packages/zpm-sync/src/lib.rs
--- a/packages/zpm-sync/src/lib.rs
+++ b/packages/zpm-sync/src/lib.rs
@@ -280,7 +280,7 @@
 
             SyncNode::Folder {template, ..} => {
                 let is_dir
-                    = path.fs_is_dir();
+                    = metadata.is_dir();
 
                 Ok(SyncCheck {
                     must_remove: !is_dir,

You can send follow-ups to the cloud agent here.

must_remove: !metadata.is_dir(),
must_create: !metadata.is_dir() && template.is_none(),
must_remove: !is_dir,
must_create: !is_dir && template.is_none(),

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.

Symlink folder sync check changed

Low Severity

Folder sync checks now use path.fs_is_dir() instead of metadata.is_dir() from fs_symlink_metadata(). For a symlink that points at a directory, those calls can disagree, so the sync step may treat the path as a directory when it previously required replacement, or the reverse.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6556d7a. Configure here.

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Empty config field after onConflict
    • Fields containing only onConflict metadata are now removed after recording the conflict mode so deserialization treats them as missing values.
  • ✅ Fixed: Deduped virtuals skip peer links
    • Peer link registration now resolves deduped virtual operations through their final master locator instead of skipping removed duplicate locators.

Create PR

Or push these changes by commenting:

@cursor push b4dae504b1
Preview (b4dae504b1)
diff --git a/packages/zpm-config/src/lib.rs b/packages/zpm-config/src/lib.rs
--- a/packages/zpm-config/src/lib.rs
+++ b/packages/zpm-config/src/lib.rs
@@ -935,6 +935,8 @@
 
     let mut field_modes
         = BTreeMap::new();
+    let mut fields_to_remove
+        = Vec::new();
 
     for (key, field_value) in mapping.iter_mut() {
         let serde_yaml::Value::String(key) = key else {
@@ -956,9 +958,15 @@
 
         if let Some(value) = field_mapping.remove(&value_key) {
             *field_value = value;
+        } else if field_mapping.is_empty() {
+            fields_to_remove.push(key.clone());
         }
     }
 
+    for key in fields_to_remove {
+        mapping.remove(serde_yaml::Value::String(key));
+    }
+
     (root_mode, field_modes)
 }
 

diff --git a/packages/zpm/src/tree_resolver.rs b/packages/zpm/src/tree_resolver.rs
--- a/packages/zpm/src/tree_resolver.rs
+++ b/packages/zpm/src/tree_resolver.rs
@@ -464,27 +464,30 @@
                 .entry(final_resolution.clone()).or_default()
                 .insert(parent_locator.clone());
 
-            if !self.resolution_tree.locator_resolutions.contains_key(&operation.virtualized_locator) {
+            let Some(virtualized_resolution) = self.resolution_tree.locator_resolutions.get(&final_resolution) else {
                 continue;
-            }
+            };
 
-            let peer_dependencies = &self.resolution_tree.locator_resolutions
-                .get(&operation.virtualized_locator).unwrap()
-                .peer_dependencies;
+            let peer_dependencies = virtualized_resolution.peer_dependencies.keys().cloned().sorted().collect_vec();
 
-            for peer_ident in peer_dependencies.keys().sorted() {
-                let root = operation.next_peer_slots.get(peer_ident)
+            for peer_ident in peer_dependencies {
+                let root = operation.next_peer_slots.get(&peer_ident)
                     .expect("Expected the peer dependency ident to be listed in the next peer slots");
+                let root = if root == &operation.virtualized_locator {
+                    &final_resolution
+                } else {
+                    root
+                };
 
                 self.peer_dependency_links
                     .entry(root.clone())
                     .or_default()
-                    .entry(peer_ident.clone())
+                    .entry(peer_ident)
                     .or_default();
             }
 
             let virtualized_resolution = self.resolution_tree.locator_resolutions
-                .get_mut(&operation.virtualized_locator).unwrap();
+                .get_mut(&final_resolution).unwrap();
 
             for missing_peer_dependency in &virtualized_resolution.missing_peer_dependencies {
                 virtualized_resolution.dependencies.remove(missing_peer_dependency);

You can send follow-ups to the cloud agent here.

Comment thread packages/zpm-config/src/lib.rs
Comment thread packages/zpm/src/tree_resolver.rs
@arcanis arcanis marked this pull request as ready for review June 1, 2026 18:15
@arcanis

arcanis commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

@cursor review

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Empty sync root deletes files
    • Restored filtering for empty root folder nodes so an empty sync tree is skipped instead of deleting directory contents.

Create PR

Or push these changes by commenting:

@cursor push 1ce49431ee

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 4236bb8. Configure here.

pub fn is_node_filtered_out(&self, node_idx: usize) -> bool {
if node_idx == 0 {
return false;
}

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.

Empty sync root deletes files

Medium Severity

Exempting node index 0 from is_node_filtered_out means a SyncTree whose root folder has no registered children is no longer skipped. process_node then treats an existing root directory as an empty folder template and can enqueue deletes for every directory entry not in the (empty) child map.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4236bb8. Configure here.

@arcanis arcanis merged commit 2c56033 into main Jun 1, 2026
21 checks passed
@arcanis arcanis deleted the mael/sage-bee branch June 1, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant