diff --git a/crates/socket-patch-cli/tests/common/yarn_classic_vex.rs b/crates/socket-patch-cli/tests/common/yarn_classic_vex.rs index fc6d912fe..83010cbc2 100644 --- a/crates/socket-patch-cli/tests/common/yarn_classic_vex.rs +++ b/crates/socket-patch-cli/tests/common/yarn_classic_vex.rs @@ -160,6 +160,29 @@ pub fn require_yarn_classic(suite: &str, isolate: impl Fn(&mut Command)) -> bool false } +/// Run `cmd` (a suite's `corepack ...` invocation), re-running it after 2 s +/// and then 5 s while it dies on a transient DNS failure (`EAI_AGAIN`). +/// +/// yarn 1 re-queues a registry request on `ENOTFOUND`, `ECONNRESET` and +/// `ETIMEDOUT` (`RequestManager.isPossibleOfflineError` in 1.22.22) but not +/// on `EAI_AGAIN`, so one resolver blip on a CI host fails the install +/// outright, and under [`REQUIRED_ENV`]`=1` the whole leg with it (#1734's +/// CI, 2026-10-26: `getaddrinfo EAI_AGAIN registry.yarnpkg.com`). A failed +/// lookup writes nothing, so the re-run starts from the same tree. Any +/// other failure returns on the first attempt. +pub fn output_retrying_dns(cmd: &mut Command) -> std::process::Output { + let mut out = cmd.output().expect("failed to run corepack"); + for backoff in [2, 5] { + if out.status.success() || !String::from_utf8_lossy(&out.stderr).contains("EAI_AGAIN") { + break; + } + eprintln!("yarn hit a transient DNS failure (EAI_AGAIN); retrying in {backoff} s"); + std::thread::sleep(std::time::Duration::from_secs(backoff)); + out = cmd.output().expect("failed to run corepack"); + } + out +} + // ── manifest-less VEX ───────────────────────────────────────────────── /// Which wiring the flow produced. diff --git a/crates/socket-patch-cli/tests/e2e_redirect_yarn_classic_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_yarn_classic_build.rs index ce38670ef..fc7b1b5eb 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_yarn_classic_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_yarn_classic_build.rs @@ -111,7 +111,7 @@ fn corepack(cwd: &Path, pm: &str, args: &[&str], extra_env: &[(&str, &str)]) -> for (k, v) in extra_env { cmd.env(k, v); } - cmd.output().expect("failed to run corepack") + yarn_classic_vex::output_retrying_dns(&mut cmd) } fn run_socket(cwd: &Path, args: &[&str]) -> (i32, String, String) { diff --git a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs index b388f5b9f..5293c8ebe 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs @@ -112,7 +112,7 @@ fn corepack(cwd: &Path, pm: &str, args: &[&str], extra_env: &[(&str, &str)]) -> for (k, v) in extra_env { cmd.env(k, v); } - cmd.output().expect("failed to run corepack") + yarn_classic_vex::output_retrying_dns(&mut cmd) } /// Run the socket-patch binary with a scrubbed environment. diff --git a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs index 0f51f0748..01bb85ff0 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs @@ -132,7 +132,7 @@ fn corepack(cwd: &Path, pm: &str, args: &[&str], extra_env: &[(&str, &str)]) -> for (k, v) in extra_env { cmd.env(k, v); } - cmd.output().expect("failed to run corepack") + yarn_classic_vex::output_retrying_dns(&mut cmd) } /// Run the socket-patch binary with a scrubbed environment. diff --git a/crates/socket-patch-cli/tests/e2e_vex_build/pdm.rs b/crates/socket-patch-cli/tests/e2e_vex_build/pdm.rs index 7331301bd..16efed366 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_build/pdm.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_build/pdm.rs @@ -137,6 +137,12 @@ fn pins_for(v: &str) -> Vec { if ((2, 21, 0)..(2, 26, 1)).contains(&t) { pins.push("hishel<1"); } + // 2.x declares an unbounded `unearth`, and unearth 0.19 (2026-10-27) + // moved to `httpx2`: PDM's PyPI client is no longer the `httpx` + // client unearth expects, so `pdm lock` dies with "PDMPyPIClient is + // not an instance or subtype of type (Client)" (seen on 2.29.2). + // `<0.19` resolves the 0.18 line every 2.x ran with before. + pins.push("unearth<0.19"); } let mut out = vec![format!("pdm=={v}")]; out.extend(pins.into_iter().map(String::from)); diff --git a/crates/socket-patch-cli/tests/mode_migration_npm.rs b/crates/socket-patch-cli/tests/mode_migration_npm.rs index c8091f68e..78fac9495 100644 --- a/crates/socket-patch-cli/tests/mode_migration_npm.rs +++ b/crates/socket-patch-cli/tests/mode_migration_npm.rs @@ -93,7 +93,7 @@ fn corepack(cwd: &Path, pm: &str, args: &[&str], extra_env: &[(&str, &str)]) -> for (k, v) in extra_env { cmd.env(k, v); } - cmd.output().expect("failed to run corepack") + yarn_classic_vex::output_retrying_dns(&mut cmd) } fn run_socket(cwd: &Path, args: &[&str]) -> (i32, String, String) { diff --git a/scripts/backtest-pdm.py b/scripts/backtest-pdm.py index e7f4db789..d403cb545 100644 --- a/scripts/backtest-pdm.py +++ b/scripts/backtest-pdm.py @@ -204,6 +204,11 @@ def pins_for(version): # ModuleNotFoundError before locking. if (2, 21) <= v < (2, 26, 1): pins.append("hishel<1") + # 2.x declares an unbounded `unearth`; 0.19 (2026-10-27) moved to + # httpx2 and breaks PDM's PyPI client ("PDMPyPIClient is not an + # instance or subtype of type (Client)" on `pdm lock`, seen on + # 2.29.2). `<0.19` resolves the 0.18 line 2.x ran with before. + pins.append("unearth<0.19") return pins