From f5b959b0409b59ebeed364bb0c43791f446b757a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 12:27:57 +0000 Subject: [PATCH 1/4] Start fix for #513 Assisted-by: Claude Code:claude-opus-5-5 From 7ed45ecb53fcfc420c3548bde198c1864f8caaa4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 12:38:03 +0000 Subject: [PATCH 2/4] Refuse a Maven vendor that wires nothing When every reactor declaration of the patched package was left at an uncertain ${property} or another version, and no root could be pinned, vendor still wrote the tree, exited 0 and counted the patch as applied, while the build kept resolving the unpatched jar. vendor --check then reported the artifact as no longer wired. vendor now refuses such a plan (vendor_jvm_shape_unsupported, reason nothing_wired) with the degraded reasons that left it unwired, and writes nothing. Fixes #513 Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-cli/tests/vendor_jvm_cli.rs | 29 +++++-- .../src/vendor/jvm/maven_reactor.rs | 83 +++++++++++++++++++ .../src/vendor/maven_repo.rs | 7 ++ 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs index 18b749a51..b0dd1802b 100644 --- a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs +++ b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs @@ -1156,7 +1156,8 @@ fn gradle_vendor_533_unpatched_classifier_copy_withholds_vex() { /// #513 / #459: a reactor `${prop}` version that can't be pinned with /// certainty (undefined in the checkout, or raised by a profile) is left /// as is with a warning, and `vex` does not attest the package the build -/// still takes unpatched. +/// still takes unpatched. With nothing else to wire, `vendor` refuses +/// rather than counting the patch as applied (#513). #[test] fn maven_reactor_uncertain_property_version_withholds_vex() { let cases = [ @@ -1199,6 +1200,25 @@ fn maven_reactor_uncertain_property_version_withholds_vex() { let b = pristine.replace("a", "b"); write(root, "proj/b/pom.xml", b.as_bytes()); } + let vex = root.join("vex.json"); + if !sibling { + assert_refused(root, "nothing_wired", reason); + let (_, env) = socket( + root, + &[ + "vex", + "-O", + vex.to_str().unwrap(), + "--product", + "pkg:generic/x@1", + ], + ); + if let Ok(bytes) = std::fs::read(&vex) { + let doc = String::from_utf8_lossy(&bytes); + assert!(!doc.contains("not_affected"), "{reason}: {env}"); + } + continue; + } let env = ok(root, &["vendor"]); assert!( env.to_string().contains(&format!("reason: {reason}")), @@ -1208,12 +1228,7 @@ fn maven_reactor_uncertain_property_version_withholds_vex() { std::fs::read_to_string(root.join("proj/a/pom.xml")).unwrap(), a ); - assert_eq!( - wiring_text(root).contains("-socket."), - sibling, - "{reason}: wired" - ); - let vex = root.join("vex.json"); + assert!(wiring_text(root).contains("-socket."), "{reason}: wired"); let (_, env) = socket( root, &[ diff --git a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs index f4a8df996..0490a8694 100644 --- a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs +++ b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs @@ -461,6 +461,45 @@ pub fn wired_checked(read: ReadFn<'_>, c: &Coords<'_>) -> Result, c: &Coords<'_>, plan: &JvmPlan) -> Result<(), JvmRefusal> { + let planned = |rel: &str| match plan.writes.iter().find(|w| w.rel == rel) { + Some(w) => Some(w.bytes.clone()), + None => read(rel), + }; + if wired_checked(&planned, c)? { + return Ok(()); + } + let why: Vec<&str> = plan + .warnings + .iter() + .filter(|w| w.code == DEGRADED) + .map(|w| w.detail.as_str()) + .filter(|d| !d.starts_with("reason: maven_f_outside_root:")) + .filter(|d| !d.starts_with("reason: maven_mirror_of_all:")) + .collect(); + let why = if why.is_empty() { + "no declaration or pin of the reactor can take it".to_string() + } else { + why.join("; ") + }; + Err(JvmRefusal { + code: SHAPE_UNSUPPORTED, + detail: format!( + "reason: nothing_wired: no pom would build {}:{}:{}, so the build keeps \ + resolving {} ({why})", + c.group_id, + c.artifact_id, + c.suffixed_version(), + c.version + ), + }) +} + /// [`wired_checked`], and no declaration of `c`'s g:a is left at a version /// that may still read the base: a `${...}` the checkout does not define /// or that an inheriting pom or a profile moves (#513, #459). Such a @@ -5350,6 +5389,50 @@ mod tests { assert!(unwired(&after)); } + /// #513: a plan that leaves every declaration and root alone wires + /// nothing, so `vendor` refuses it with the reasons instead of + /// counting the patch as applied. + #[test] + fn a_plan_that_wires_nothing_is_refused() { + let parent = "com.corpcorp-parent\ + 3"; + let mut files = external_reactor(parent, ""); + let a = text(&files, "a/pom.xml").replace( + "", + "${ct.version}", + ); + files.insert("a/pom.xml".into(), a.into_bytes()); + let plan = run(&files).unwrap(); + let read = |p: &str| files.get(p).cloned(); + let r = refuse_unwired(&read, &patch().coords(), &plan).unwrap_err(); + assert_eq!(r.code, SHAPE_UNSUPPORTED); + assert_eq!(refusal_reason(&r), "nothing_wired"); + assert!( + r.detail.contains("reason: property_unresolved: "), + "{}", + r.detail + ); + assert!(!r.detail.contains("maven_f_outside_root"), "{}", r.detail); + } + + /// The same check passes a plan that pins the root or rewrites a + /// declaration, and an already vendored reactor whose re-plan writes + /// nothing. + #[test] + fn a_wired_plan_is_not_refused() { + let files = fs(&[ + ("pom.xml", ROOT), + ("a/pom.xml", &module("a", &dep("1.10.0"))), + ("b/pom.xml", &module("b", "")), + ]); + let (plan, after) = vendor(&files); + let read = |p: &str| files.get(p).cloned(); + refuse_unwired(&read, &patch().coords(), &plan).unwrap(); + let again = run(&after).unwrap(); + let read = |p: &str| after.get(p).cloned(); + refuse_unwired(&read, &patch().coords(), &again).unwrap(); + } + /// #513 case 2: Maven 4.1.0 infers an empty `` from /// `../pom.xml`, so the root's property resolves and is rewritten. #[test] diff --git a/crates/socket-patch-core/src/vendor/maven_repo.rs b/crates/socket-patch-core/src/vendor/maven_repo.rs index 1b7afe0f4..8f91bcb1f 100644 --- a/crates/socket-patch-core/src/vendor/maven_repo.rs +++ b/crates/socket-patch-core/src/vendor/maven_repo.rs @@ -975,6 +975,13 @@ async fn vendor_maven_jvm( Ok(plan) => plan, Err(refusal) => return refused(refusal.code, refusal.detail), }; + if matches!(shape, Shape::MavenReactor | Shape::Mixed) { + if let Err(refusal) = + super::jvm::maven_reactor::refuse_unwired(&read, &patch.coords(), &plan) + { + return refused(refusal.code, refusal.detail); + } + } if gradle && reader .read(super::jvm::gradle::VERIFICATION_REL) From 83c7a8a2b33a3c44ef013f7933785cdbf545fe5a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:03:39 +0000 Subject: [PATCH 3/4] Narrow nothing_wired reasons, cover probe run The refusal now lists only the reasons that left a declaration or root unwired, not the repository and config notes. A probe run for a patch without files refuses the same reactor shapes the real vendor does, so the preview and the run agree. Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-cli/tests/vendor_jvm_cli.rs | 1 + .../src/vendor/jvm/maven_reactor.rs | 24 +++++++++++++++---- .../src/vendor/maven_repo.rs | 13 ++++++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs index b0dd1802b..da87561fe 100644 --- a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs +++ b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs @@ -1213,6 +1213,7 @@ fn maven_reactor_uncertain_property_version_withholds_vex() { "pkg:generic/x@1", ], ); + assert!(!env.to_string().contains("not_affected"), "{reason}: {env}"); if let Ok(bytes) = std::fs::read(&vex) { let doc = String::from_utf8_lossy(&bytes); assert!(!doc.contains("not_affected"), "{reason}: {env}"); diff --git a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs index 0490a8694..cdc848eb0 100644 --- a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs +++ b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs @@ -474,13 +474,28 @@ pub fn refuse_unwired(read: ReadFn<'_>, c: &Coords<'_>, plan: &JvmPlan) -> Resul if wired_checked(&planned, c)? { return Ok(()); } + // Only the reasons that leave a declaration or a root alone; the + // repository and config notes (and a mixed root's Gradle warnings) + // say nothing about why the reactor is unwired. + const UNWIRING: [&str; 7] = [ + "property_unresolved", + "conflicting_literal_version", + "conflicting_managed_version", + "management_unresolved", + "range", + "coordinates_overridden", + "classifier_declared", + ]; let why: Vec<&str> = plan .warnings .iter() .filter(|w| w.code == DEGRADED) .map(|w| w.detail.as_str()) - .filter(|d| !d.starts_with("reason: maven_f_outside_root:")) - .filter(|d| !d.starts_with("reason: maven_mirror_of_all:")) + .filter(|d| { + UNWIRING + .iter() + .any(|r| d.starts_with(&format!("reason: {r}: "))) + }) .collect(); let why = if why.is_empty() { "no declaration or pin of the reactor can take it".to_string() @@ -490,8 +505,8 @@ pub fn refuse_unwired(read: ReadFn<'_>, c: &Coords<'_>, plan: &JvmPlan) -> Resul Err(JvmRefusal { code: SHAPE_UNSUPPORTED, detail: format!( - "reason: nothing_wired: no pom would build {}:{}:{}, so the build keeps \ - resolving {} ({why})", + "reason: nothing_wired: no pom of the reactor would build {}:{}:{}, so Maven \ + keeps resolving {} ({why})", c.group_id, c.artifact_id, c.suffixed_version(), @@ -5413,6 +5428,7 @@ mod tests { r.detail ); assert!(!r.detail.contains("maven_f_outside_root"), "{}", r.detail); + assert!(!r.detail.contains("maven_mirror_of_all"), "{}", r.detail); } /// The same check passes a plan that pins the root or rewrites a diff --git a/crates/socket-patch-core/src/vendor/maven_repo.rs b/crates/socket-patch-core/src/vendor/maven_repo.rs index 8f91bcb1f..3bd35c4f2 100644 --- a/crates/socket-patch-core/src/vendor/maven_repo.rs +++ b/crates/socket-patch-core/src/vendor/maven_repo.rs @@ -778,10 +778,19 @@ async fn vendor_maven_jvm( ); } // A declared classifier is sourced only for a real patch. - if let Err(e) = plan { - if !e.detail.starts_with("reason: classifier_unavailable:") { + match plan { + Err(e) if !e.detail.starts_with("reason: classifier_unavailable:") => { return refused(e.code, e.detail); } + Ok(plan) if matches!(shape, Shape::MavenReactor | Shape::Mixed) => { + let read = |rel: &str| reader.read(rel); + if let Err(e) = + super::jvm::maven_reactor::refuse_unwired(&read, &patch.coords(), &plan) + { + return refused(e.code, e.detail); + } + } + _ => {} } return done( synthesized_result(purl, &display_path, Vec::new(), true, None), From 2e242f3eb87e48064975c0bbb45e553b67b7e243 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:11:47 +0000 Subject: [PATCH 4/4] Pass the directory lister to the unwired check main now discovers Maven 4 implicit subprojects by listing directories (#1807), so wired_checked takes a lister. The nothing_wired check passes the same lister the plan used, so it sees the same reactor. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/vendor/jvm/maven_reactor.rs | 15 ++++++++++----- crates/socket-patch-core/src/vendor/maven_repo.rs | 5 +++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs index 0f192f1cc..a6f9ffd6d 100644 --- a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs +++ b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs @@ -483,12 +483,17 @@ pub fn wired_checked( /// root pinned (#513). Vendoring it would count the patch as applied while /// the build keeps resolving the base; the refusal carries the degraded /// reasons that left it unwired. -pub fn refuse_unwired(read: ReadFn<'_>, c: &Coords<'_>, plan: &JvmPlan) -> Result<(), JvmRefusal> { +pub fn refuse_unwired( + read: ReadFn<'_>, + list: ListFn<'_>, + c: &Coords<'_>, + plan: &JvmPlan, +) -> Result<(), JvmRefusal> { let planned = |rel: &str| match plan.writes.iter().find(|w| w.rel == rel) { Some(w) => Some(w.bytes.clone()), None => read(rel), }; - if wired_checked(&planned, c)? { + if wired_checked(&planned, list, c)? { return Ok(()); } // Only the reasons that leave a declaration or a root alone; the @@ -5445,7 +5450,7 @@ mod tests { files.insert("a/pom.xml".into(), a.into_bytes()); let plan = run(&files).unwrap(); let read = |p: &str| files.get(p).cloned(); - let r = refuse_unwired(&read, &patch().coords(), &plan).unwrap_err(); + let r = refuse_unwired(&read, &lister(&files), &patch().coords(), &plan).unwrap_err(); assert_eq!(r.code, SHAPE_UNSUPPORTED); assert_eq!(refusal_reason(&r), "nothing_wired"); assert!( @@ -5469,10 +5474,10 @@ mod tests { ]); let (plan, after) = vendor(&files); let read = |p: &str| files.get(p).cloned(); - refuse_unwired(&read, &patch().coords(), &plan).unwrap(); + refuse_unwired(&read, &lister(&files), &patch().coords(), &plan).unwrap(); let again = run(&after).unwrap(); let read = |p: &str| after.get(p).cloned(); - refuse_unwired(&read, &patch().coords(), &again).unwrap(); + refuse_unwired(&read, &lister(&after), &patch().coords(), &again).unwrap(); } /// #513 case 2: Maven 4.1.0 infers an empty `` from diff --git a/crates/socket-patch-core/src/vendor/maven_repo.rs b/crates/socket-patch-core/src/vendor/maven_repo.rs index c618acbb3..b11e8e432 100644 --- a/crates/socket-patch-core/src/vendor/maven_repo.rs +++ b/crates/socket-patch-core/src/vendor/maven_repo.rs @@ -785,8 +785,9 @@ async fn vendor_maven_jvm( } Ok(plan) if matches!(shape, Shape::MavenReactor | Shape::Mixed) => { let read = |rel: &str| reader.read(rel); + let list = |dir: &str| reader.list(dir); if let Err(e) = - super::jvm::maven_reactor::refuse_unwired(&read, &patch.coords(), &plan) + super::jvm::maven_reactor::refuse_unwired(&read, &list, &patch.coords(), &plan) { return refused(e.code, e.detail); } @@ -987,7 +988,7 @@ async fn vendor_maven_jvm( }; if matches!(shape, Shape::MavenReactor | Shape::Mixed) { if let Err(refusal) = - super::jvm::maven_reactor::refuse_unwired(&read, &patch.coords(), &plan) + super::jvm::maven_reactor::refuse_unwired(&read, &list, &patch.coords(), &plan) { return refused(refusal.code, refusal.detail); }