From 52db0e112473dbfc4a7ca32d28d124383f45f1ba Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Nov 2026 10:01:19 +0000 Subject: [PATCH 1/2] Keep hatch unit tests off the host's uv path utils::hatch and vendor::pypi_hatch read HATCH_ENV_TYPE_VIRTUAL_UV_PATH from the process: a non-empty value switches every Hatch env to uv, so the vendored-wheel and [tool.uv.sources] refusals fire with no file setting. A developer who points Hatch at uv that way sees 18 socket-patch-core unit tests fail on a clean origin/main checkout (`cargo test -p socket-patch-core --lib`); CI passes only because no job sets the var. Both reads now go through hatch::uv_path_override(). Production builds read the process env as before; unit-test builds read only a value a test holds with an EnvGuard (test_env::guarded_var), as go_env does since #2057. A new test holds the var to cover the uv branch that the unit tests never reached before, and checks a host value stays hidden. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_011f9aeR2MHrmJ9Fh9wmm3X1 --- crates/socket-patch-core/src/utils/hatch.rs | 57 +++++++++++++++++-- .../src/vendor/pypi_hatch.rs | 2 +- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/crates/socket-patch-core/src/utils/hatch.rs b/crates/socket-patch-core/src/utils/hatch.rs index f400c0483..24b54ec54 100644 --- a/crates/socket-patch-core/src/utils/hatch.rs +++ b/crates/socket-patch-core/src/utils/hatch.rs @@ -601,6 +601,25 @@ fn walk( Ok(()) } +/// Whether a non-empty `HATCH_ENV_TYPE_VIRTUAL_UV_PATH` switches every +/// Hatch virtual environment to the uv installer, whatever the files say. +pub(crate) fn uv_path_override() -> bool { + uv_path_env().is_some_and(|path| !path.is_empty()) +} + +#[cfg(not(test))] +fn uv_path_env() -> Option { + std::env::var("HATCH_ENV_TYPE_VIRTUAL_UV_PATH").ok() +} + +/// In the unit tests, only a value a test holds with an +/// [`EnvGuard`](crate::test_env::EnvGuard): a developer who points Hatch at +/// uv would otherwise see every pip-installer fixture refused. +#[cfg(test)] +fn uv_path_env() -> Option { + crate::test_env::guarded_var("HATCH_ENV_TYPE_VIRTUAL_UV_PATH") +} + /// Whether hatch.toml carries `metadata`, which then holds the /// direct-reference permission instead of pyproject. fn external_metadata(documents: &BTreeMap<&'static str, DocumentMut>) -> bool { @@ -620,10 +639,7 @@ fn run_round(documents: &mut BTreeMap<&'static str, DocumentMut>, round: &mut Ro } } } - round.uv_sources = uv_source_names( - documents, - std::env::var("HATCH_ENV_TYPE_VIRTUAL_UV_PATH").is_ok_and(|path| !path.is_empty()), - ); + round.uv_sources = uv_source_names(documents, uv_path_override()); round.apply = false; if let Err(error) = walk(documents, round) { round.fail_rest(&error); @@ -1722,4 +1738,37 @@ mod tests { .unwrap_err() .contains("pip installer")); } + + /// `HATCH_ENV_TYPE_VIRTUAL_UV_PATH` turns on the uv refusals with no file + /// setting, and in the unit tests only a guarded value counts: a + /// developer's own uv path must not refuse the pip-installer fixtures. + #[test] + #[serial_test::serial] + fn uv_path_env_counts_only_when_a_guard_holds_it() { + use crate::test_env::EnvGuard; + const KEY: &str = "HATCH_ENV_TYPE_VIRTUAL_UV_PATH"; + let url = "https://patch.test/six-1.16.0-py2.py3-none-any.whl"; + let inputs = files( + "[project]\ndependencies = [\"six==1.16.0\"]\n[tool.uv.sources]\nsix = { path = \"/abs/six.whl\" }\n", + ); + let host = std::env::var_os(KEY); + std::env::set_var(KEY, "/host/uv"); + assert!(!uv_path_override(), "host value hidden"); + assert!(rewrite(&inputs, "six", "1.16.0", url).is_ok()); + { + let _uv = EnvGuard::set(KEY, "/usr/bin/uv"); + assert!(uv_path_override()); + assert!(rewrite(&inputs, "six", "1.16.0", url) + .unwrap_err() + .contains("[tool.uv.sources]")); + } + { + let _empty = EnvGuard::set(KEY, ""); + assert!(!uv_path_override(), "an empty value is unset"); + } + match host { + Some(value) => std::env::set_var(KEY, value), + None => std::env::remove_var(KEY), + } + } } diff --git a/crates/socket-patch-core/src/vendor/pypi_hatch.rs b/crates/socket-patch-core/src/vendor/pypi_hatch.rs index 8ff804795..af376122f 100644 --- a/crates/socket-patch-core/src/vendor/pypi_hatch.rs +++ b/crates/socket-patch-core/src/vendor/pypi_hatch.rs @@ -177,7 +177,7 @@ async fn require_no_raw_workspace( } fn require_pip_installer() -> Result<(), Failure> { - if std::env::var("HATCH_ENV_TYPE_VIRTUAL_UV_PATH").is_ok_and(|path| !path.is_empty()) { + if crate::utils::hatch::uv_path_override() { return Err(("pypi_hatch_unsupported", "vendored Hatch wheels require the pip installer: uv does not enforce local wheel fragment hashes".into())); } Ok(()) From c76ed80060624b09cf86b652efcc9857677ba8fc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Nov 2026 11:11:23 +0000 Subject: [PATCH 2/2] Don't hold the hatch uv var in a parallel test The first version of uv_path_env_counts_only_when_a_guard_holds_it held HATCH_ENV_TYPE_VIRTUAL_UV_PATH with an EnvGuard. Other utils::hatch tests are not #[serial], so while the guard was live they saw uv switched on: uv_sources_refuse_project_rewrites_under_the_uv_installer failed 2-3 times in 100 loops of `utils::hatch` on a clean env. Even an empty-value guard leaked, because EnvGuard's drop restores the previous value before it releases the key. The test now only checks that an unguarded host value stays hidden; the uv branch itself is covered by the direct uv_source_names(.., true) asserts. utils::hatch now passes 300/300 loops clean and 100/100 with the host var set. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_011f9aeR2MHrmJ9Fh9wmm3X1 --- crates/socket-patch-core/src/utils/hatch.rs | 23 +++++++-------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/crates/socket-patch-core/src/utils/hatch.rs b/crates/socket-patch-core/src/utils/hatch.rs index 24b54ec54..4a07d03fa 100644 --- a/crates/socket-patch-core/src/utils/hatch.rs +++ b/crates/socket-patch-core/src/utils/hatch.rs @@ -1739,13 +1739,15 @@ mod tests { .contains("pip installer")); } - /// `HATCH_ENV_TYPE_VIRTUAL_UV_PATH` turns on the uv refusals with no file - /// setting, and in the unit tests only a guarded value counts: a - /// developer's own uv path must not refuse the pip-installer fixtures. + /// In the unit tests a host `HATCH_ENV_TYPE_VIRTUAL_UV_PATH` is hidden, + /// so a developer's own uv path can't refuse the pip-installer fixtures. + /// The test never holds the key with an `EnvGuard`: the other hatch + /// tests run in parallel and would see uv switched on, even for the + /// instant a guard's drop restores the host value before releasing the + /// key (`uv_source_names(.., true)` covers that branch directly). #[test] #[serial_test::serial] - fn uv_path_env_counts_only_when_a_guard_holds_it() { - use crate::test_env::EnvGuard; + fn uv_path_env_hides_the_host_value() { const KEY: &str = "HATCH_ENV_TYPE_VIRTUAL_UV_PATH"; let url = "https://patch.test/six-1.16.0-py2.py3-none-any.whl"; let inputs = files( @@ -1755,17 +1757,6 @@ mod tests { std::env::set_var(KEY, "/host/uv"); assert!(!uv_path_override(), "host value hidden"); assert!(rewrite(&inputs, "six", "1.16.0", url).is_ok()); - { - let _uv = EnvGuard::set(KEY, "/usr/bin/uv"); - assert!(uv_path_override()); - assert!(rewrite(&inputs, "six", "1.16.0", url) - .unwrap_err() - .contains("[tool.uv.sources]")); - } - { - let _empty = EnvGuard::set(KEY, ""); - assert!(!uv_path_override(), "an empty value is unset"); - } match host { Some(value) => std::env::set_var(KEY, value), None => std::env::remove_var(KEY),