From a1d819bb0df2993d0b1e9235437103aafbd8f951 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 17:10:21 +0000 Subject: [PATCH] Stop npm oracle trees symlinking into a cycle The randomized npm-crawler oracle generator gave some pnpm store entries a node_modules symlink to nm_dirs.first(), which is usually the importer node_modules that holds the store. Both crawlers follow an entry's node_modules link, so they walked .pnpm//node_modules/.pnpm//... until the OS refused the path. Both treat every I/O error as an empty dir, so the oracle comparison came down to where each walker gave up, not what it found. Seeds 5, 7, 12 and 41 drew this shape; seed 12 failed CI once with the sequential walker stopping 6 levels short of the new one. Point the link at a fresh node_modules outside the tree instead, the same way the vlt store case and the symlinked-scope case already do. The followed-symlink shape is still generated (16 of 64 seeds), and the two oracle tests run about 30% faster. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/crawlers/npm_crawler/oracle.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs b/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs index 95e04093e..29d2eecc9 100644 --- a/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs +++ b/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs @@ -1336,12 +1336,18 @@ mod tests { let _ = std::fs::write(entry.join("node_modules"), "file"); } 76..=79 => { - // An entry whose node_modules is a symlink. - if let Some(target) = self.nm_dirs.first().cloned() { - let entry = store.join(self.store_entry_name(&name, &version)); - let _ = std::fs::create_dir_all(&entry); - Self::symlink(&target, &entry.join("node_modules")); - } + // An entry whose node_modules is a symlink (to a + // dir outside the tree, so the followed walk cannot + // cycle: a link back to an ancestor `node_modules` + // is walked until the OS refuses the path, which + // compares where two walkers gave up, not what + // they found). + let entry = store.join(self.store_entry_name(&name, &version)); + let _ = std::fs::create_dir_all(&entry); + let id = self.uniq(); + let elsewhere = self.scratch.join(format!("pnpm-nm{id}")); + self.package_json(&elsewhere.join(&name), &name, &version); + Self::symlink(&elsewhere, &entry.join("node_modules")); } 80..=83 => { // The entry itself is a symlink (skipped).