From 5b42c27d833f021474f07945b695786343fac4ef Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 23:52:32 +0000 Subject: [PATCH] Scrub ambient Go env from spawned test children A developer or runner that exports GOWORK, GOFLAGS=-modfile=..., GOMODCACHE or GOPATH (or sets them with `go env -w`) leaks them into every socket-patch and `go` child the CLI tests spawn. With GOWORK= exported, every `go mod download` in the Go suites' staging fails; with GOFLAGS=-modfile=... Go vendor refuses to write its replace. e2e_golang_build failed 21/21 and e2e_vendor_golang_build 11/13 that way on a clean checkout. Add hermetic::Extra::Go (remove GOFLAGS, GOWORK, GOPATH, GOMODCACHE; set GOENV=off) and apply it to CRAWLER_EXTRAS, to every child of common::run_bin_with_env, and to the go/socket-patch spawn helpers of e2e_golang_build, e2e_golang_hosted_build and e2e_vendor_golang_build. Tests that need a value still set it after the scrub. A spawn_env_hygiene self-test pins the scrub. Part of #1981 (items 1-2). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_011f9aeR7CFpkqZDdd9hfYmM --- .../socket-patch-cli/tests/common/hermetic.rs | 25 ++++++++++++---- crates/socket-patch-cli/tests/common/mod.rs | 4 +++ .../tests/e2e_golang_build.rs | 3 ++ .../tests/e2e_golang_hosted_build.rs | 3 ++ .../tests/e2e_hosted_production.rs | 2 +- .../tests/e2e_vendor_golang_build.rs | 5 +++- .../tests/e2e_vendored_production.rs | 4 +-- .../tests/spawn_env_hygiene.rs | 30 +++++++++++++++++++ .../tests/vex_e2e_common/mod.rs | 3 +- 9 files changed, 69 insertions(+), 10 deletions(-) diff --git a/crates/socket-patch-cli/tests/common/hermetic.rs b/crates/socket-patch-cli/tests/common/hermetic.rs index 4961880e8..9260696e4 100644 --- a/crates/socket-patch-cli/tests/common/hermetic.rs +++ b/crates/socket-patch-cli/tests/common/hermetic.rs @@ -267,15 +267,24 @@ pub enum Extra { /// fixture's `vendor/`. The pin outranks `XDG_CONFIG_HOME` and holds /// no config. Run it before the suite sets its own `COMPOSER_*` values. Composer, + /// `GOFLAGS`, `GOWORK`, `GOPATH` and `GOMODCACHE` removed, then + /// `GOENV=off`. The go crawler finds installed modules through + /// `GOMODCACHE`/`GOPATH`, and Go apply, `vex` and `vendor` honor + /// `GOFLAGS=-modfile` and `GOWORK` (`utils::go_env`), so an ambient + /// value moves the crawl off the fixture or turns a replace into a + /// refusal. `GOENV=off` stops the same keys arriving from the user's + /// `go env -w` file. Tests that need a value set it after the scrub. + Go, } /// The [`Extra`] scrubs for a `socket-patch` child that crawls a real /// project tree: the binary's own crawlers read `VIRTUAL_ENV`, bundler's -/// `BUNDLE_PATH`/`BUNDLE_APP_CONFIG`/`BUNDLE_GEMFILE` and Composer's vendor -/// dir and home, so an ambient value moves discovery (and the installed -/// evidence `vex` hashes) off the fixture. Any env the test sets after the -/// scrub still wins. Apply it with [`scrub_crawler_inputs`]. -pub const CRAWLER_EXTRAS: [Extra; 3] = [Extra::Venv, Extra::Bundle, Extra::Composer]; +/// `BUNDLE_PATH`/`BUNDLE_APP_CONFIG`/`BUNDLE_GEMFILE`, Composer's vendor +/// dir and home, and Go's module cache and flags, so an ambient value +/// moves discovery (and the installed evidence `vex` hashes) off the +/// fixture. Any env the test sets after the scrub still wins. Apply it +/// with [`scrub_crawler_inputs`]. +pub const CRAWLER_EXTRAS: [Extra; 4] = [Extra::Venv, Extra::Bundle, Extra::Composer, Extra::Go]; /// Where [`scrub_crawler_inputs`] points `BUNDLE_USER_CONFIG`. pub const NO_BUNDLE_USER_CONFIG: &str = "/nonexistent/bundle-user-config"; @@ -365,6 +374,12 @@ pub fn scrub_extra(cmd: &mut Command, extras: &[Extra]) { } cmd.env("COMPOSER_HOME", "/nonexistent/composer-home"); } + Extra::Go => { + for key in ["GOFLAGS", "GOWORK", "GOPATH", "GOMODCACHE"] { + cmd.env_remove(key); + } + cmd.env("GOENV", "off"); + } } } } diff --git a/crates/socket-patch-cli/tests/common/mod.rs b/crates/socket-patch-cli/tests/common/mod.rs index 2c9fac5d4..466fdba6b 100644 --- a/crates/socket-patch-cli/tests/common/mod.rs +++ b/crates/socket-patch-cli/tests/common/mod.rs @@ -107,6 +107,10 @@ pub fn run_bin_with_env( // would otherwise read the developer's `~/.gradle` / `~/.m2` (and any // `GRADLE_USER_HOME` / `GRADLE_RO_DEP_CACHE`) into every test. jvm_env::isolate_cli(&mut cmd); + // No ambient Go module cache, flags or `go env -w` file either: an + // exported `GOFLAGS=-modfile=...` or `GOMODCACHE` would move the go + // crawl and Go apply off the fixture. + hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Go]); // Caller-supplied env lands last so explicit injections (runtime // gates, discovery roots) survive the scrub. for (k, v) in env { diff --git a/crates/socket-patch-cli/tests/e2e_golang_build.rs b/crates/socket-patch-cli/tests/e2e_golang_build.rs index e98aab90c..3eba075c3 100644 --- a/crates/socket-patch-cli/tests/e2e_golang_build.rs +++ b/crates/socket-patch-cli/tests/e2e_golang_build.rs @@ -88,6 +88,9 @@ fn run_socket(cwd: &Path, args: &[&str], modcache: &Path) -> (i32, String, Strin fn go(dir: &Path, args: &[&str], env: &[(&str, &str)]) -> std::process::Output { let mut cmd = Command::new("go"); cmd.args(args).current_dir(dir); + // No ambient `GOWORK`/`GOFLAGS`/`go env -w` file: an exported + // `GOWORK=` fails every `go mod download` in `stage`. + common::hermetic::scrub_extra(&mut cmd, &[common::hermetic::Extra::Go]); cache_env::isolate(&mut cmd); for (k, v) in env { cmd.env(k, v); diff --git a/crates/socket-patch-cli/tests/e2e_golang_hosted_build.rs b/crates/socket-patch-cli/tests/e2e_golang_hosted_build.rs index 1deae0f5f..06b16a817 100644 --- a/crates/socket-patch-cli/tests/e2e_golang_hosted_build.rs +++ b/crates/socket-patch-cli/tests/e2e_golang_hosted_build.rs @@ -97,6 +97,9 @@ impl Machine { fn go(dir: &Path, machine: &Machine, args: &[&str], env: &[(&str, &str)]) -> std::process::Output { let mut cmd = Command::new("go"); cmd.args(args).current_dir(dir); + // No ambient `GOWORK`/`GOFLAGS`: an exported `GOWORK=` fails + // every staging `go` command. + common::hermetic::scrub_extra(&mut cmd, &[common::hermetic::Extra::Go]); cache_env::isolate(&mut cmd); cmd.env("GOMODCACHE", &machine.modcache); cmd.env("GOCACHE", &machine.gocache); diff --git a/crates/socket-patch-cli/tests/e2e_hosted_production.rs b/crates/socket-patch-cli/tests/e2e_hosted_production.rs index 5593d236a..a5b79fa49 100644 --- a/crates/socket-patch-cli/tests/e2e_hosted_production.rs +++ b/crates/socket-patch-cli/tests/e2e_hosted_production.rs @@ -305,7 +305,7 @@ fn has_command(cmd: &str) -> bool { /// immediately instead of letting a developer's ambient shell decide what got /// tested. The crawler's own ambient inputs are scrubbed too /// (`hermetic::CRAWLER_EXTRAS`: an activated venv, `BUNDLE_*`, -/// `COMPOSER_*`): the hosted scan discovers packages unscoped under the cwd. +/// `COMPOSER_*`, `GOFLAGS`/`GOMODCACHE` and friends): the hosted scan discovers packages unscoped under the cwd. fn run(cwd: &Path, args: &[&str]) -> (i32, String, String) { let mut cmd = hermetic::binary_command(); hermetic::scrub_crawler_inputs(&mut cmd); diff --git a/crates/socket-patch-cli/tests/e2e_vendor_golang_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_golang_build.rs index 8f72f7dbf..41200150e 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_golang_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_golang_build.rs @@ -64,7 +64,7 @@ const PATCHED_LIB: &str = "package upstream\n\nfunc Greeting() string { return \ /// through it). fn run_socket(cwd: &Path, args: &[&str], modcache: &Path) -> (i32, String, String) { let mut cmd = hermetic_command(&binary()); - hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Venv]); + hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Venv, hermetic::Extra::Go]); cmd.current_dir(cwd); cmd.env("GOMODCACHE", modcache); let _fixture = prebuilt_common::prepare_command( @@ -103,6 +103,9 @@ fn go_env<'a>(modcache: &'a str, proxy: &'a str) -> Vec<(&'a str, &'a str)> { fn go(dir: &Path, args: &[&str], env: &[(&str, &str)]) -> Output { let mut cmd = Command::new("go"); cmd.args(args).current_dir(dir); + // No ambient `GOWORK`/`GOFLAGS`/`go env -w` file: an exported + // `GOWORK=` fails every `go mod download` in `stage`. + hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Go]); cache_env::isolate(&mut cmd); for (k, v) in env { cmd.env(k, v); diff --git a/crates/socket-patch-cli/tests/e2e_vendored_production.rs b/crates/socket-patch-cli/tests/e2e_vendored_production.rs index 1530f601f..eaea1a5e1 100644 --- a/crates/socket-patch-cli/tests/e2e_vendored_production.rs +++ b/crates/socket-patch-cli/tests/e2e_vendored_production.rs @@ -325,8 +325,8 @@ fn has_corepack_pm(pm: &str) -> bool { /// test process and fails if one reaches the child), and forces /// `SOCKET_NO_CONFIG=1` to block the socket-cli /// config.json. The crawler's own ambient inputs are scrubbed too -/// (`hermetic::CRAWLER_EXTRAS`: an activated venv, `BUNDLE_*`, `COMPOSER_*`), -/// so an ambient BUNDLE_PATH can't move the gem leg's crawl off +/// (`hermetic::CRAWLER_EXTRAS`: an activated venv, `BUNDLE_*`, `COMPOSER_*`, +/// `GOFLAGS`/`GOMODCACHE` and friends), so an ambient BUNDLE_PATH can't move the gem leg's crawl off /// `vendor/bundle`. fn run_socket(cwd: &Path, args: &[&str]) -> (i32, String, String) { let mut cmd = hermetic::binary_command(); diff --git a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs index cd43ab272..5c2d19982 100644 --- a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs +++ b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs @@ -708,6 +708,36 @@ fn crawler_inputs_sweep_bundler_and_composer_overrides() { ); } +#[test] +#[serial] +fn crawler_inputs_sweep_go_module_cache_and_flags() { + with_ambient( + &[ + ("GOFLAGS", "-modfile=/ambient/alt.mod"), + ("GOWORK", "/ambient/go.work"), + ("GOPATH", "/ambient/gopath"), + ("GOMODCACHE", "/ambient/modcache"), + ("GOENV", "/ambient/go-env"), + ], + || { + let mut cmd = hermetic::binary_command(); + hermetic::scrub_crawler_inputs(&mut cmd); + let env = effective_env(&cmd); + for key in ["GOFLAGS", "GOWORK", "GOPATH", "GOMODCACHE"] { + assert_eq!(get(&env, key), None, "{key}"); + } + // The user's `go env -w` file is switched off, not left to + // supply the same keys. + assert_eq!(get(&env, "GOENV"), Some("off")); + + // A test's own pin, set after the scrub, survives. + cmd.env("GOMODCACHE", "/fixture/modcache"); + let env = effective_env(&cmd); + assert_eq!(get(&env, "GOMODCACHE"), Some("/fixture/modcache")); + }, + ); +} + #[test] fn no_private_bundler_sweeps() { let found: Vec = test_sources() diff --git a/crates/socket-patch-cli/tests/vex_e2e_common/mod.rs b/crates/socket-patch-cli/tests/vex_e2e_common/mod.rs index caa3c3599..0c87844c9 100644 --- a/crates/socket-patch-cli/tests/vex_e2e_common/mod.rs +++ b/crates/socket-patch-cli/tests/vex_e2e_common/mod.rs @@ -52,7 +52,8 @@ //! - Every run is hermetic: ambient `SOCKET_*` is scrubbed, telemetry and //! the socket-cli config are off, ambient API tokens are vetoed (only //! [`VexRun::api_token`] authenticates) and the crawler's ambient inputs -//! (`VIRTUAL_ENV`, `BUNDLE_*`, `COMPOSER_*`; `hermetic::CRAWLER_EXTRAS`) +//! (`VIRTUAL_ENV`, `BUNDLE_*`, `COMPOSER_*`, Go's `GO*` module/flag vars; +//! `hermetic::CRAWLER_EXTRAS`) //! are removed. Package-manager homes/caches the installed-tree lookups need go in //! [`VexRun::envs`]. //! - `--json` is the default ([`VexRun::human`] opts out), so