From 2978aa71b0320dbc6a999889861a430cf05ba463 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 12:59:45 +0000 Subject: [PATCH 1/4] Start refactor for #832 Assisted-by: Claude Code:claude-opus-5-5 From a7082f49bd5ee34491a86297efdaec05b60aeacc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 13:13:18 +0000 Subject: [PATCH 2/4] Scan every vendored-write file for references The vendored-reference scan behind repair, the orphan sweeps (vendor --revert, the vendored gc), the vendor stranded-reference gate and rollback's ledger-less gate read only the registry's VENDORED rows, while the files NuGet, Maven, Hatch and pnpm vendoring also write sat in a second list, VENDORED_WRITES_UNMARKED, that only the dry run's symlink check used. NuGet's feed and Maven's repository also name the uuid directory itself, which parse_vendor_path rejects. So with a missing ledger entry, a NuGet or Maven unit, or a wheel a hatch.toml environment installs, was deleted by the orphan sweep and never reported by repair while the project still pointed at it. The eight rows now carry the VENDORED role and the second list is gone, so the scan and wiring_paths read one notion of "a file a vendored run writes". parse_vendor_reference is parse_vendor_path's grammar plus the bare uuid dir, and the scan reads through it, with `<` ending a reference inside XML text. Fixes #832 and #958. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/commands/vendored_backend/repair.rs | 99 +++++++++++++++-- .../tests/repair/repair_vendor_e2e.rs | 100 ++++++++++++++++++ .../socket-patch-core/src/formats/registry.rs | 98 +++++++++++------ crates/socket-patch-core/src/vendor/path.rs | 40 ++++++- 4 files changed, 296 insertions(+), 41 deletions(-) diff --git a/crates/socket-patch-cli/src/commands/vendored_backend/repair.rs b/crates/socket-patch-cli/src/commands/vendored_backend/repair.rs index 268540822..8444883a4 100644 --- a/crates/socket-patch-cli/src/commands/vendored_backend/repair.rs +++ b/crates/socket-patch-cli/src/commands/vendored_backend/repair.rs @@ -12,8 +12,8 @@ use socket_patch_core::manifest::schema::{PatchManifest, PatchRecord}; use socket_patch_core::utils::fs::read_regular_to_string; use socket_patch_core::utils::purl::normalize_purl; use socket_patch_core::vendor::{ - self, artifact_is_file_shaped, check_vendored_artifact, parse_vendor_path, ArtifactHealth, - VendorEntry, VendorState, VendorWarning, + self, artifact_is_file_shaped, check_vendored_artifact, parse_vendor_path, + path::parse_vendor_reference, ArtifactHealth, VendorEntry, VendorState, VendorWarning, }; use super::VendoredBackend; @@ -33,7 +33,8 @@ struct Candidate { } /// Scan the wiring-bearing files for vendored-artifact references, -/// returning deduped `(ecosystem, uuid, artifact relpath)` triples. Pure +/// returning deduped `(ecosystem, uuid, artifact relpath)` triples (the +/// relpath is the uuid dir itself for a directory-wired unit). Pure /// text scan plus native binary Bun resolution records and the canonical /// path parser. Used by repair (references the ledger does not cover), by /// the orphan sweeps (`vendor --revert`, `scan --prune`: a dir a lockfile @@ -69,19 +70,24 @@ pub(crate) async fn scan_vendor_references(project_root: &Path) -> Vec<(String, let slice = &rest[idx..]; // `:` ends a reference too: pnpm snapshot keys are // `name@file::` and yaml mappings suffix the path with a - // colon — npm names/versions never contain one. + // colon — npm names/versions never contain one. `<` ends an XML + // element's text (Maven's `…/`). let end = slice .find([ - '"', '\'', '`', ' ', '\t', '\n', '\r', ',', ')', ']', '}', ';', ':', + '"', '\'', '`', ' ', '\t', '\n', '\r', ',', ')', ']', '}', ';', ':', '<', ]) .unwrap_or(slice.len()); let candidate = slice[..end].replace('\\', "/"); - if let Some(parts) = parse_vendor_path(&candidate) { + // NuGet's feed and Maven's repository name the uuid dir itself. + if let Some(parts) = parse_vendor_reference(&candidate) { if seen.insert((parts.eco.to_string(), parts.uuid.clone())) { out.push(( parts.eco.to_string(), parts.uuid.clone(), - candidate.trim_start_matches("./").to_string(), + candidate + .trim_start_matches("./") + .trim_end_matches('/') + .to_string(), )); } } @@ -93,8 +99,9 @@ pub(crate) async fn scan_vendor_references(project_root: &Path) -> Vec<(String, } /// Every wiring-bearing file name the vendor backends may rewrite, relative -/// to `project_root`: the registry's vendored wiring files -/// ([`registry::VENDORED`]), vlt importer manifests, the Python +/// to `project_root`: every file the registry says a vendored run writes +/// ([`registry::VENDORED`]: `nuget.config`, `pom.xml` and `hatch.toml` +/// included), vlt importer manifests, the Python /// locks the root lists (and their scripts) and the requirements `-r` /// include tree. Sorted and deduplicated; entries need not exist. async fn wiring_files(project_root: &Path) -> Vec { @@ -1015,6 +1022,80 @@ mod tests { ); } + /// #832, #958: every file a vendored run writes is scanned, and a + /// reference to the uuid dir itself counts. NuGet's feed and Maven's + /// repository name the dir (Windows backslashes and a trailing slash + /// included); a Hatch environment in `hatch.toml` names a wheel. Every + /// caller (repair, the orphan sweeps, the `vendor` stranded-reference + /// gate, rollback's ledger-less gate) reads this one scan. + #[tokio::test] + async fn scan_recovers_unit_dir_and_hatch_toml_references() { + let tmp = tempfile::tempdir().unwrap(); + let nuget = "22222222-2222-4222-8222-222222222222"; + let nuget_win = "55555555-5555-4555-8555-555555555555"; + let maven = "33333333-3333-4333-8333-333333333333"; + let pypi = "44444444-4444-4444-8444-444444444444"; + let wheel = "six-1.16.0-py2.py3-none-any.whl"; + for (file, text) in [ + ( + "nuget.config", + format!(""), + ), + ( + "NuGet.Config", + format!(""), + ), + ( + "pom.xml", + format!("file://${{project.basedir}}/.socket/vendor/maven/{maven}"), + ), + ( + "hatch.toml", + format!("[envs.default]\ndependencies = [\"six @ {{root:uri}}/.socket/vendor/pypi/{pypi}/{wheel}\"]\n"), + ), + ] { + tokio::fs::write(tmp.path().join(file), text).await.unwrap(); + } + let refs = scan_vendor_references(tmp.path()).await; + assert_eq!( + refs, + vec![ + ( + "maven".to_string(), + maven.to_string(), + format!(".socket/vendor/maven/{maven}") + ), + ( + "nuget".to_string(), + nuget.to_string(), + format!(".socket/vendor/nuget/{nuget}") + ), + ( + "nuget".to_string(), + nuget_win.to_string(), + format!(".socket/vendor/nuget/{nuget_win}") + ), + ( + "pypi".to_string(), + pypi.to_string(), + format!(".socket/vendor/pypi/{pypi}/{wheel}") + ), + ] + ); + + // A bare eco dir or a non-uuid dir is still no reference. + tokio::fs::write( + tmp.path().join("pom.xml"), + "file://${project.basedir}/.socket/vendor/maven\n\ + file://${maven.multiModuleProjectDirectory}/.socket/vendor/maven2\n\ + file://${project.basedir}/.socket/vendor/maven/not-a-uuid", + ) + .await + .unwrap(); + let refs = scan_vendor_references(tmp.path()).await; + assert!(refs.iter().all(|(eco, _, _)| eco != "maven"), "{refs:?}"); + } + /// pnpm writes vendored paths in THREE spellings — override values, /// `tarball:` fields, and snapshot KEYS with a trailing colon. The /// scanner must yield the clean relpath whichever form it meets first. diff --git a/crates/socket-patch-cli/tests/repair/repair_vendor_e2e.rs b/crates/socket-patch-cli/tests/repair/repair_vendor_e2e.rs index 88fd159f6..21bb9c422 100644 --- a/crates/socket-patch-cli/tests/repair/repair_vendor_e2e.rs +++ b/crates/socket-patch-cli/tests/repair/repair_vendor_e2e.rs @@ -535,6 +535,106 @@ async fn repair_rebuilds_detached_entry_without_manifest() { assert_socket_dir_lean(tmp.path()); } +/// 7b. #832, #958: NuGet's vendored feed (`nuget.config`), Maven's vendored +/// repository (`pom.xml`) and a Hatch environment (`hatch.toml`) wire a +/// unit too. With the ledger gone, repair reports each one as +/// `vendor_ledger_missing` instead of seeing no vendored traces at all. +/// NuGet and Maven name the uuid dir itself, not a file inside it. +#[tokio::test] +async fn repair_reports_missing_ledger_for_nuget_maven_and_hatch_wiring() { + let mock = MockServer::start().await; + mount_patch_api(&mock).await; + let tmp = tempfile::tempdir().unwrap(); + let nuget = "22222222-2222-4222-8222-222222222222"; + let maven = "33333333-3333-4333-8333-333333333333"; + let pypi = "44444444-4444-4444-8444-444444444444"; + let wheel = "six-1.16.0-py2.py3-none-any.whl"; + let files = [ + ( + "nuget.config".to_string(), + format!( + "\n\n \n \ + \n \ + \n\n" + ), + ), + ( + "pom.xml".to_string(), + format!( + "\n \n \n \ + socket-patch-vendor-{maven}\n \ + file://${{project.basedir}}/.socket/vendor/maven/{maven}\n \ + \n \n\n" + ), + ), + ( + "hatch.toml".to_string(), + format!( + "[envs.default]\ndependencies = [\n \"six @ {{root:uri}}/.socket/vendor/pypi/{pypi}/{wheel}#sha256={}\",\n]\n", + "0".repeat(64) + ), + ), + ]; + for (name, text) in &files { + std::fs::write(tmp.path().join(name), text).unwrap(); + } + for (eco, uuid, leaf) in [ + ("nuget", nuget, "x.nupkg"), + ("maven", maven, "x.pom"), + ("pypi", pypi, wheel), + ] { + let dir = tmp.path().join(format!(".socket/vendor/{eco}/{uuid}")); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join(leaf), b"artifact").unwrap(); + } + + let (code, stdout, stderr) = run_cli(tmp.path(), &mock.uri(), &["repair"]); + assert_eq!(code, 1, "stdout={stdout} stderr={stderr}"); + let v = parse_env(&stdout); + let mut missing: Vec<(String, String, String)> = events_of(&v) + .into_iter() + .filter(|e| e["errorCode"] == "vendor_ledger_missing") + .map(|e| { + ( + e["details"]["ecosystem"].as_str().unwrap_or("").to_string(), + e["uuid"].as_str().unwrap_or("").to_string(), + e["details"]["path"].as_str().unwrap_or("").to_string(), + ) + }) + .collect(); + missing.sort(); + assert_eq!( + missing, + vec![ + ( + "maven".to_string(), + maven.to_string(), + format!(".socket/vendor/maven/{maven}") + ), + ( + "nuget".to_string(), + nuget.to_string(), + format!(".socket/vendor/nuget/{nuget}") + ), + ( + "pypi".to_string(), + pypi.to_string(), + format!( + ".socket/vendor/pypi/{pypi}/{wheel}#sha256={}", + "0".repeat(64) + ) + ), + ], + "envelope={v}" + ); + for (name, text) in &files { + assert_eq!( + &std::fs::read_to_string(tmp.path().join(name)).unwrap(), + text + ); + } +} + /// G6 for a manifest-free vendored project: after the run, `.socket/` holds /// exactly `vendor/` — no `apply.lock` outlives it, no blobs/diffs/packages /// are conjured by a repair that rebuilds from the ledger's embedded record. diff --git a/crates/socket-patch-core/src/formats/registry.rs b/crates/socket-patch-core/src/formats/registry.rs index 03bff38b4..26551b4da 100644 --- a/crates/socket-patch-core/src/formats/registry.rs +++ b/crates/socket-patch-core/src/formats/registry.rs @@ -16,9 +16,10 @@ /// Read by the hosted planners (`scan --mode hosted`, the in-memory /// engine's candidate reads). pub const HOSTED: u8 = 1 << 0; -/// Rewired by a vendored planner: the search space for -/// `.socket/vendor///` references when the vendor ledger -/// is gone (`repair`). +/// Written by a vendored run: the search space for +/// `.socket/vendor//[/]` references (`repair`, the orphan +/// sweeps, rollback's ledger-less gate) and the files a vendored dry run +/// checks for symbolic links ([`wiring_paths`]). pub const VENDORED: u8 = 1 << 1; /// A lock (or wiring config) a vendored artifact is consumed through — the /// liveness probe of a ledger entry whose recorded wiring files are gone @@ -77,7 +78,7 @@ const REGISTRY: &[FormatFile] = &[ // Package-manager detection only: the vendor probe and the hosted // planners have never accepted these spellings. row("pnpm-lock.yml", "npm", PNPM_MARKER), - row("pnpm-workspace.yaml", "npm", PNPM_MARKER), + row("pnpm-workspace.yaml", "npm", VENDORED | PNPM_MARKER), // pnpm <= 2 uses the same package identities under the old filename. row("shrinkwrap.yaml", "npm", HOSTED), row("node_modules/.modules.yaml", "npm", HOSTED), @@ -107,7 +108,7 @@ const REGISTRY: &[FormatFile] = &[ // than abandoned. row("Pipfile", "pypi", HOSTED | PRESENCE_ONLY), row("pyproject.toml", "pypi", HOSTED | VENDORED | PROBE), - row("hatch.toml", "pypi", HOSTED | PROBE), + row("hatch.toml", "pypi", HOSTED | VENDORED | PROBE), // ── cargo ── row("Cargo.toml", "cargo", HOSTED | VENDORED | PROBE), row("Cargo.lock", "cargo", HOSTED | VENDORED | ROOT), @@ -121,10 +122,10 @@ const REGISTRY: &[FormatFile] = &[ row("composer.json", "composer", VENDORED), row("composer.lock", "composer", HOSTED | VENDORED | PROBE | ROOT), // ── nuget ── - row("nuget.config", "nuget", HOSTED | PROBE), - row("NuGet.config", "nuget", HOSTED | PROBE), - row("NuGet.Config", "nuget", HOSTED | PROBE), - row("packages.lock.json", "nuget", HOSTED), + row("nuget.config", "nuget", HOSTED | VENDORED | PROBE), + row("NuGet.config", "nuget", HOSTED | VENDORED | PROBE), + row("NuGet.Config", "nuget", HOSTED | VENDORED | PROBE), + row("packages.lock.json", "nuget", HOSTED | VENDORED), // ── gem ── row("Gemfile", "gem", HOSTED | VENDORED), row("Gemfile.lock", "gem", HOSTED | VENDORED | PROBE | ROOT), @@ -140,10 +141,10 @@ const REGISTRY: &[FormatFile] = &[ row("go.mod", "golang", HOSTED | VENDORED | PROBE | ROOT), row("go.sum", "golang", HOSTED | ROOT), // ── maven ── - row("pom.xml", "maven", HOSTED | PROBE), + row("pom.xml", "maven", HOSTED | VENDORED | PROBE), // Maven Trusted Checksums files the fail-closed maven planner merges // into (read so an existing user config / checksum set is preserved). - row(".mvn/maven.config", "maven", HOSTED), + row(".mvn/maven.config", "maven", HOSTED | VENDORED), row(".mvn/checksums/checksums.sha256", "maven", HOSTED), // Never edited: its `distributionUrl` names the project's Maven, which // the maven planner checks against the Trusted Checksums floor (3.9.4) @@ -241,21 +242,6 @@ pub fn hosted_file_ecosystem(rel: &str) -> Option<&'static str> { .map(|f| f.ecosystem) } -/// Files a vendored run writes that carry no [`VENDORED`] role (that role -/// also scopes `repair`'s fingerprint): pnpm's workspace file, NuGet's -/// config and lock (the vendored feed), the root `pom.xml` and -/// `.mvn/maven.config` (vendored Maven), and `hatch.toml` (vendored Hatch). -const VENDORED_WRITES_UNMARKED: &[&str] = &[ - "pnpm-workspace.yaml", - "nuget.config", - "NuGet.config", - "NuGet.Config", - "packages.lock.json", - "pom.xml", - ".mvn/maven.config", - "hatch.toml", -]; - /// The project-relative paths of `ecosystem` that a vendored run may /// rewrite: the files a vendored dry run checks for symbolic links, since /// the wet run's commit refuses to rename over one. Files a vendored run @@ -264,10 +250,7 @@ const VENDORED_WRITES_UNMARKED: &[&str] = &[ pub fn wiring_paths(ecosystem: &str) -> Vec<&'static str> { REGISTRY .iter() - .filter(|f| { - f.ecosystem == ecosystem - && (f.has(VENDORED) || VENDORED_WRITES_UNMARKED.contains(&f.path)) - }) + .filter(|f| f.ecosystem == ecosystem && f.has(VENDORED)) .map(|f| f.path) .collect() } @@ -325,6 +308,61 @@ mod tests { } } + /// One notion of "a file a vendored run writes" (#832, #958): the + /// [`VENDORED`] role. The vendored dry run's symlink check + /// ([`wiring_paths`]) and the vendored-reference scan + /// (`paths_with(VENDORED)`) read the same rows, so every file a vendored + /// backend rewires (NuGet's config and lock, Maven's pom and + /// `.mvn/maven.config`, Hatch's `hatch.toml`, pnpm's workspace file) is + /// searched for references. + #[test] + fn the_reference_scan_and_the_symlink_check_read_the_same_files() { + let vendored = paths_with(VENDORED); + let mut union: Vec<&str> = [ + "npm", "pypi", "cargo", "composer", "nuget", "gem", "golang", "maven", + ] + .iter() + .flat_map(|eco| wiring_paths(eco)) + .collect(); + union.sort_unstable(); + let mut sorted = vendored.clone(); + sorted.sort_unstable(); + assert_eq!(sorted, union); + for p in [ + "pnpm-workspace.yaml", + "nuget.config", + "NuGet.config", + "NuGet.Config", + "packages.lock.json", + "pom.xml", + ".mvn/maven.config", + "hatch.toml", + ] { + assert!(vendored.contains(&p), "{p}"); + } + assert_eq!( + wiring_paths("nuget"), + [ + "nuget.config", + "NuGet.config", + "NuGet.Config", + "packages.lock.json" + ] + ); + assert_eq!( + wiring_paths("pypi"), + [ + "requirements.txt", + "uv.lock", + "poetry.lock", + "pdm.lock", + "Pipfile.lock", + "pyproject.toml", + "hatch.toml" + ] + ); + } + #[test] fn hosted_file_ecosystem_matches_basenames_of_edited_files_only() { assert_eq!(hosted_file_ecosystem("package-lock.json"), Some("npm")); diff --git a/crates/socket-patch-core/src/vendor/path.rs b/crates/socket-patch-core/src/vendor/path.rs index e0c3426cb..eead69e1e 100644 --- a/crates/socket-patch-core/src/vendor/path.rs +++ b/crates/socket-patch-core/src/vendor/path.rs @@ -137,6 +137,16 @@ pub struct VendorPathParts { /// This is the documented external-tool recovery rule; `None` means the /// string is not a Socket-vendored path. pub fn parse_vendor_path(s: &str) -> Option { + parse_vendor_reference(s).filter(|parts| !parts.leaf.is_empty()) +} + +/// [`parse_vendor_path`]'s grammar, also accepting a reference to the uuid +/// directory itself (`leaf` empty): NuGet's vendored feed +/// (`value=".socket/vendor/nuget/"`) and Maven's vendored repository +/// (`file://${project.basedir}/.socket/vendor/maven/`) +/// name the unit, not a file inside it. The vendored-reference scan reads +/// wiring through this, so a directory-wired unit counts as still wired. +pub fn parse_vendor_reference(s: &str) -> Option { let norm = s.replace('\\', "/"); let norm = norm.strip_prefix("file:").unwrap_or(&norm); let norm = norm.strip_prefix("./").unwrap_or(norm); @@ -152,8 +162,8 @@ pub fn parse_vendor_path(s: &str) -> Option { let mut it = rest.splitn(3, '/'); let eco = it.next()?; let uuid = it.next()?; - let leaf = it.next()?.trim_end_matches('/'); - if !ECOSYSTEM_DIRS.contains(&eco) || !is_canonical_uuid(uuid) || leaf.is_empty() { + let leaf = it.next().unwrap_or("").trim_end_matches('/'); + if !ECOSYSTEM_DIRS.contains(&eco) || !is_canonical_uuid(uuid) { return None; } Some(VendorPathParts { @@ -636,6 +646,32 @@ mod tests { assert!(parse_vendor_path(&format!("x.socket/vendor/npm/{UUID}/y.tgz")).is_none()); } + /// `parse_vendor_reference` is `parse_vendor_path`'s grammar plus the + /// bare uuid dir (NuGet's feed, Maven's repository); `parse_vendor_path` + /// itself still needs a leaf. + #[test] + fn parse_vendor_reference_accepts_the_uuid_dir_itself() { + for s in [ + format!(".socket/vendor/nuget/{UUID}"), + format!(".socket/vendor/nuget/{UUID}/"), + format!(".socket\\vendor\\nuget\\{UUID}"), + format!("file://${{project.basedir}}/.socket/vendor/nuget/{UUID}"), + ] { + let p = parse_vendor_reference(&s).unwrap_or_else(|| panic!("{s}")); + assert_eq!( + (p.eco.as_str(), p.uuid.as_str(), p.leaf.as_str()), + ("nuget", UUID, "") + ); + assert!(parse_vendor_path(&s).is_none(), "{s}"); + } + let p = parse_vendor_reference(&format!(".socket/vendor/npm/{UUID}/a/b.tgz")).unwrap(); + assert_eq!(p.leaf, "a/b.tgz"); + assert!(parse_vendor_reference(".socket/vendor/nuget").is_none()); + assert!(parse_vendor_reference(".socket/vendor/nuget/not-a-uuid").is_none()); + assert!(parse_vendor_reference(&format!(".socket/vendor/maven2/{UUID}")).is_none()); + assert!(parse_vendor_reference(&format!("x.socket/vendor/nuget/{UUID}")).is_none()); + } + /// The re-vendor carry-forward matches wiring keys ACROSS a patch-uuid /// change when the key embeds the vendored path (berry's `file:` locator /// key), and only byte-equal otherwise. From 803f92af4dbd1316e24b809d58cd134c0f129ece Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 13:18:39 +0000 Subject: [PATCH 3/4] Drop three migrated files from digest ratchet main is red on production_digests_go_through_the_helpers: #690 moved gradle_cache.rs, jvm_jar.rs and sidecars/maven.rs onto the utils::digest helpers, but PENDING_INLINE_DIGESTS still lists them, and the ratchet fails on a stale entry. Same three-line change as #889 and #980; it no-ops once main carries it. Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-core/src/utils/digest.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/socket-patch-core/src/utils/digest.rs b/crates/socket-patch-core/src/utils/digest.rs index 105225e5e..630adefa1 100644 --- a/crates/socket-patch-core/src/utils/digest.rs +++ b/crates/socket-patch-core/src/utils/digest.rs @@ -135,9 +135,6 @@ mod tests { /// when you move it onto the helpers above; the test fails on a stale /// entry as well as on a new inline copy. const PENDING_INLINE_DIGESTS: &[&str] = &[ - "crawlers/gradle_cache.rs", - "patch/jvm_jar.rs", - "patch/sidecars/maven.rs", "utils/group_commit.rs", "vendor/jvm/mod.rs", "vendor/maven_repo.rs", From 82185b88643977f60e0b201abdaec1cf10a352c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 13:29:15 +0000 Subject: [PATCH 4/4] Split NuGet.Config scan case into its own project nuget.config and NuGet.Config are one file on a case-insensitive file system (Windows, default macOS), so the second fixture write replaced the first and the scan test could not see both uuids. The backslashed NuGet.Config case now runs in its own temp project. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/commands/vendored_backend/repair.rs | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/socket-patch-cli/src/commands/vendored_backend/repair.rs b/crates/socket-patch-cli/src/commands/vendored_backend/repair.rs index 8444883a4..464feec93 100644 --- a/crates/socket-patch-cli/src/commands/vendored_backend/repair.rs +++ b/crates/socket-patch-cli/src/commands/vendored_backend/repair.rs @@ -1041,10 +1041,6 @@ mod tests { "nuget.config", format!(""), ), - ( - "NuGet.Config", - format!(""), - ), ( "pom.xml", format!("file://${{project.basedir}}/.socket/vendor/maven/{maven}"), @@ -1070,11 +1066,6 @@ mod tests { nuget.to_string(), format!(".socket/vendor/nuget/{nuget}") ), - ( - "nuget".to_string(), - nuget_win.to_string(), - format!(".socket/vendor/nuget/{nuget_win}") - ), ( "pypi".to_string(), pypi.to_string(), @@ -1083,6 +1074,27 @@ mod tests { ] ); + // The backslashed Windows spelling, under another config spelling. + // Its own project: on a case-insensitive file system `NuGet.Config` + // and `nuget.config` are one file. + let win = tempfile::tempdir().unwrap(); + tokio::fs::write( + win.path().join("NuGet.Config"), + format!( + "" + ), + ) + .await + .unwrap(); + assert_eq!( + scan_vendor_references(win.path()).await, + vec![( + "nuget".to_string(), + nuget_win.to_string(), + format!(".socket/vendor/nuget/{nuget_win}") + )] + ); + // A bare eco dir or a non-uuid dir is still no reference. tokio::fs::write( tmp.path().join("pom.xml"),