diff --git a/crates/socket-patch-cli/tests/apply/apply_invariants.rs b/crates/socket-patch-cli/tests/apply/apply_invariants.rs index 39fb88ce2..b2d8ca66b 100644 --- a/crates/socket-patch-cli/tests/apply/apply_invariants.rs +++ b/crates/socket-patch-cli/tests/apply/apply_invariants.rs @@ -15,10 +15,9 @@ //! crawler won't match, which trips the "no packages found / offline" //! branches and exercises the invariants without needing a real fixture. -use crate::common::{binary, git_sha256}; +use crate::common::{binary, git_sha256, hermetic_command}; use std::path::{Path, PathBuf}; -use std::process::Command; use sha2::{Digest, Sha256}; @@ -113,11 +112,9 @@ fn collect_files(root: &Path, dir: &Path, out: &mut Vec<(PathBuf, Vec)>) { fn run_apply(cwd: &Path, extra: &[&str]) -> (i32, String) { let mut args = vec!["apply", "--json"]; args.extend_from_slice(extra); - let out = Command::new(binary()) + let out = hermetic_command(&binary()) .args(&args) .current_dir(cwd) - .env_remove("SOCKET_API_TOKEN") - .env_remove("SOCKET_CLI_API_TOKEN") .output() .expect("run socket-patch"); ( @@ -455,11 +452,9 @@ fn apply_with_no_socket_dir_emits_no_manifest_envelope() { #[test] fn apply_with_no_socket_dir_silent_emits_nothing() { let tmp = tempfile::tempdir().expect("tempdir"); - let out = Command::new(binary()) + let out = hermetic_command(&binary()) .args(["apply", "--silent"]) .current_dir(tmp.path()) - .env_remove("SOCKET_API_TOKEN") - .env_remove("SOCKET_CLI_API_TOKEN") .output() .expect("run socket-patch"); assert_eq!(out.status.code(), Some(0)); @@ -475,11 +470,9 @@ fn apply_with_no_socket_dir_silent_emits_nothing() { // the message simply never existed — i.e. it would not actually prove // the silent-mode short-circuit suppresses anything. let tmp2 = tempfile::tempdir().expect("tempdir"); - let loud = Command::new(binary()) + let loud = hermetic_command(&binary()) .args(["apply"]) .current_dir(tmp2.path()) - .env_remove("SOCKET_API_TOKEN") - .env_remove("SOCKET_CLI_API_TOKEN") .output() .expect("run socket-patch"); assert_eq!(loud.status.code(), Some(0)); diff --git a/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs b/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs index 15639f137..d47c732b3 100644 --- a/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs +++ b/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs @@ -26,10 +26,10 @@ //! modified"). When NO variant matches, the base still fails with //! "no matching variant found" and the file stays untouched. -use crate::common::{binary, git_sha256}; +use crate::common::hermetic::{self, Extra}; +use crate::common::{binary, git_sha256, hermetic_command}; use std::path::{Path, PathBuf}; -use std::process::Command; const SINGLETON_PURL: &str = "pkg:gem/rack@3.1.0"; const UUID_SINGLETON: &str = "31313131-3131-4131-8131-313131313131"; @@ -55,23 +55,15 @@ fn with_marker(base: &[u8], marker: &[u8]) -> Vec { } /// Run `socket-patch apply --offline --ecosystems gem ` in `cwd` -/// with the ambient `SOCKET_*` environment scrubbed (prefix scrub, keeping -/// `SOCKET_NO_CONFIG`) so only the argv decides behavior, and telemetry -/// disabled. +/// with the hermetic `SOCKET_*` environment (`hermetic_command`) and the +/// ambient `BUNDLE_*` scrubbed, so only the argv decides behavior. fn run_apply(cwd: &Path, extra: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.arg("apply") .args(["--offline", "--ecosystems", "gem"]) .args(extra) .current_dir(cwd); - for (key, _) in std::env::vars_os() { - if key.to_string_lossy().starts_with("SOCKET_") - && key.to_string_lossy() != "SOCKET_NO_CONFIG" - { - cmd.env_remove(&key); - } - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); + hermetic::scrub_extra(&mut cmd, &[Extra::Bundle]); let out = cmd.output().expect("run socket-patch apply"); ( out.status.code().unwrap_or(-1), diff --git a/crates/socket-patch-cli/tests/apply/covgap_commands_apply.rs b/crates/socket-patch-cli/tests/apply/covgap_commands_apply.rs index f4c60e0bd..55947a163 100644 --- a/crates/socket-patch-cli/tests/apply/covgap_commands_apply.rs +++ b/crates/socket-patch-cli/tests/apply/covgap_commands_apply.rs @@ -39,15 +39,13 @@ use common::{git_sha256, parse_json_envelope, run_with_env}; // ───────────────────────── shared fixture helpers ───────────────────────── -/// Run `socket-patch apply ` in `cwd` with the ambient `SOCKET_*` -/// environment scrubbed (see `common::run_with_env`), telemetry disabled, -/// and `extra_env` landing last (so tests can inject API URLs / TMPDIR). +/// Run `socket-patch apply ` in `cwd` with the hermetic `SOCKET_*` +/// environment (see `common::run_with_env`) and `extra_env` landing last +/// (so tests can inject API URLs / TMPDIR). fn run_apply(cwd: &Path, args: &[&str], extra_env: &[(&str, &str)]) -> (i32, String, String) { let mut argv: Vec<&str> = vec!["apply"]; argv.extend_from_slice(args); - let mut env: Vec<(&str, &str)> = vec![("SOCKET_TELEMETRY_DISABLED", "1")]; - env.extend_from_slice(extra_env); - run_with_env(cwd, &argv, &env) + run_with_env(cwd, &argv, extra_env) } /// One camelCase manifest record (the TS-compatible on-disk schema). @@ -714,7 +712,6 @@ fn corrupt_manifest_under_pnp_layout_reports_manifest_error_not_refusal() { #[cfg(unix)] mod gem_fallback_home_human { use super::*; - use std::process::Command; const QUALIFIED_PURL: &str = "pkg:gem/rack@3.1.0?platform=ruby"; const ORIGINAL: &[u8] = b"module Rack\n VERSION = 'VULNERABLE'\nend\n"; @@ -753,19 +750,14 @@ mod gem_fallback_home_human { file } - /// Human-mode apply with SOCKET_*/BUNDLE_* scrubbed, PATH = the - /// fake-gem bin dir only, BUNDLE_PATH = the store root. + /// Human-mode apply with the hermetic SOCKET_* env and BUNDLE_* + /// scrubbed, PATH = the fake-gem bin dir only, BUNDLE_PATH = the store + /// root. fn run_apply_human(project: &Path, bin_dir: &Path, store_root: &Path) -> (i32, String, String) { - let mut cmd = Command::new(common::binary()); + let mut cmd = common::hermetic_command(&common::binary()); cmd.args(["apply", "--offline", "--ecosystems", "gem", "--cwd"]) .arg(project); - for (key, _) in std::env::vars_os() { - let k = key.to_string_lossy(); - if (k.starts_with("SOCKET_") && k != "SOCKET_NO_CONFIG") || k.starts_with("BUNDLE_") { - cmd.env_remove(&key); - } - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); + common::hermetic::scrub_extra(&mut cmd, &[common::hermetic::Extra::Bundle]); cmd.env("PATH", bin_dir); cmd.env("BUNDLE_PATH", store_root); let out = cmd.output().expect("run socket-patch apply"); diff --git a/crates/socket-patch-cli/tests/apply/in_process_gem_config_warning.rs b/crates/socket-patch-cli/tests/apply/in_process_gem_config_warning.rs index d267e26ad..fe4147d98 100644 --- a/crates/socket-patch-cli/tests/apply/in_process_gem_config_warning.rs +++ b/crates/socket-patch-cli/tests/apply/in_process_gem_config_warning.rs @@ -10,10 +10,10 @@ //! * non-JSON runs print ONE stderr warning, gated on `!--silent` //! (`--silent` = errors only — a bare crawler eprintln violated that). -use crate::common::{binary, git_sha256}; +use crate::common::hermetic::{self, Extra}; +use crate::common::{binary, git_sha256, hermetic_command}; use std::path::Path; -use std::process::Command; const PURL: &str = "pkg:gem/rack@3.1.0"; const CODE: &str = "gem_bundle_config_path_ignored"; @@ -76,21 +76,15 @@ fn build_project(root: &Path, outside: &Path, with_store: bool) { } } -/// Run the binary with SOCKET_*/BUNDLE_* scrubbed and PATH pointed at an -/// empty dir, so no ambient bundler config and no real `gem` binary can -/// perturb discovery. +/// Run the binary with the hermetic SOCKET_* env, BUNDLE_* scrubbed and +/// PATH pointed at an empty dir, so no ambient bundler config and no real +/// `gem` binary can perturb discovery. fn run(root: &Path, args: &[&str]) -> (i32, String, String) { let empty_path = root.join("empty-bin"); std::fs::create_dir_all(&empty_path).unwrap(); - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.args(args).arg("--cwd").arg(root); - for (key, _) in std::env::vars_os() { - let k = key.to_string_lossy(); - if (k.starts_with("SOCKET_") && k != "SOCKET_NO_CONFIG") || k.starts_with("BUNDLE_") { - cmd.env_remove(&key); - } - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); + hermetic::scrub_extra(&mut cmd, &[Extra::Bundle]); cmd.env("PATH", &empty_path); let out = cmd.output().expect("run socket-patch"); ( diff --git a/crates/socket-patch-cli/tests/apply/in_process_gem_fallback_home.rs b/crates/socket-patch-cli/tests/apply/in_process_gem_fallback_home.rs index f941c627e..d81914f5e 100644 --- a/crates/socket-patch-cli/tests/apply/in_process_gem_fallback_home.rs +++ b/crates/socket-patch-cli/tests/apply/in_process_gem_fallback_home.rs @@ -20,10 +20,10 @@ #![cfg(unix)] -use crate::common::{binary, git_sha256}; +use crate::common::hermetic::{self, Extra}; +use crate::common::{binary, git_sha256, hermetic_command}; use std::path::{Path, PathBuf}; -use std::process::Command; const BASE_PURL: &str = "pkg:gem/rack@3.1.0"; const QUALIFIED_PURL: &str = "pkg:gem/rack@3.1.0?platform=ruby"; @@ -126,20 +126,15 @@ fn build_fixture(with_store: bool, home_bytes: &[u8], purl: &str) -> Fixture { } } -/// Run apply with SOCKET_*/BUNDLE_* scrubbed, PATH = the fake-gem bin dir -/// only, and BUNDLE_PATH set to the store root when present. +/// Run apply with the hermetic SOCKET_* env and BUNDLE_* scrubbed, PATH = +/// the fake-gem bin dir only, and BUNDLE_PATH set to the store root when +/// present. fn run_apply(fx: &Fixture, extra: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.args(["apply", "--offline", "--ecosystems", "gem", "--cwd"]) .arg(fx.project.path()) .args(extra); - for (key, _) in std::env::vars_os() { - let k = key.to_string_lossy(); - if (k.starts_with("SOCKET_") && k != "SOCKET_NO_CONFIG") || k.starts_with("BUNDLE_") { - cmd.env_remove(&key); - } - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); + hermetic::scrub_extra(&mut cmd, &[Extra::Bundle]); cmd.env("PATH", &fx.bin_dir); if let Some(store_root) = &fx.store_root { cmd.env("BUNDLE_PATH", store_root); diff --git a/crates/socket-patch-cli/tests/apply/in_process_gem_multicopy.rs b/crates/socket-patch-cli/tests/apply/in_process_gem_multicopy.rs index b3a8629cc..abceea6b0 100644 --- a/crates/socket-patch-cli/tests/apply/in_process_gem_multicopy.rs +++ b/crates/socket-patch-cli/tests/apply/in_process_gem_multicopy.rs @@ -17,10 +17,10 @@ //! physical copy is patched (and later restored) AND that the JSON summary //! counts every copy. -use crate::common::{binary, git_sha256}; +use crate::common::hermetic::{self, Extra}; +use crate::common::{binary, git_sha256, hermetic_command}; use std::path::{Path, PathBuf}; -use std::process::Command; const PURL: &str = "pkg:gem/rack@3.1.0"; @@ -94,21 +94,15 @@ fn build_two_store_project(tmp: &Path) -> (PathBuf, PathBuf, PathBuf, String, St ) } -/// Run the given subcommand with the ambient `SOCKET_*` and `BUNDLE_*` -/// environment scrubbed (a developer's `BUNDLE_PATH` export or -/// `BUNDLE_APP_CONFIG` must not steer discovery), telemetry disabled. +/// Run the given subcommand with the hermetic `SOCKET_*` environment and +/// the ambient `BUNDLE_*` scrubbed (a developer's `BUNDLE_PATH` export or +/// `BUNDLE_APP_CONFIG` must not steer discovery). fn run_json(root: &Path, args: &[&str]) -> (i32, serde_json::Value) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.args(args) .args(["--json", "--offline", "--ecosystems", "gem", "--cwd"]) .arg(root); - for (key, _) in std::env::vars_os() { - let k = key.to_string_lossy(); - if (k.starts_with("SOCKET_") && k != "SOCKET_NO_CONFIG") || k.starts_with("BUNDLE_") { - cmd.env_remove(&key); - } - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); + hermetic::scrub_extra(&mut cmd, &[Extra::Bundle]); let out = cmd.output().expect("run socket-patch"); let code = out.status.code().unwrap_or(-1); let stdout = String::from_utf8_lossy(&out.stdout).to_string(); @@ -269,17 +263,11 @@ fn build_platform_variant_project(tmp: &Path) -> (PathBuf, PathBuf, PathBuf, Vec /// `run_json` without `--json`, for `vex`, whose stdout is the document. fn run_raw(root: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.args(args) .args(["--offline", "--ecosystems", "gem", "--cwd"]) .arg(root); - for (key, _) in std::env::vars_os() { - let k = key.to_string_lossy(); - if (k.starts_with("SOCKET_") && k != "SOCKET_NO_CONFIG") || k.starts_with("BUNDLE_") { - cmd.env_remove(&key); - } - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); + hermetic::scrub_extra(&mut cmd, &[Extra::Bundle]); let out = cmd.output().expect("run socket-patch"); ( out.status.code().unwrap_or(-1), diff --git a/crates/socket-patch-cli/tests/apply/in_process_npm_multicopy.rs b/crates/socket-patch-cli/tests/apply/in_process_npm_multicopy.rs index 1f3158237..6b6f02919 100644 --- a/crates/socket-patch-cli/tests/apply/in_process_npm_multicopy.rs +++ b/crates/socket-patch-cli/tests/apply/in_process_npm_multicopy.rs @@ -12,11 +12,10 @@ //! assert EVERY physical copy is patched (and later restored) AND that the //! JSON summary counts every copy. -use crate::common::{binary, git_sha256}; +use crate::common::{binary, git_sha256, hermetic_command}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use std::process::Command; /// Write `node_modules/...//{package.json,index.js}` at the given /// `pkg_dir` (which already encodes the nesting), returning the index.js path. @@ -124,7 +123,7 @@ fn run_apply(root: &Path) -> (i32, serde_json::Value) { /// `run_apply` plus `extra` arguments (flags or path targets). fn run_apply_with(root: &Path, extra: &[&OsStr]) -> (i32, serde_json::Value) { - let out = Command::new(binary()) + let out = hermetic_command(&binary()) .args([ "apply", "--json", @@ -228,7 +227,7 @@ fn rollback_restores_every_on_disk_copy_of_a_duplicated_package() { } // Now roll back and assert EVERY copy is restored to pristine bytes. - let out = Command::new(binary()) + let out = hermetic_command(&binary()) .args([ "rollback", "--json", @@ -324,7 +323,7 @@ fn run_rollback(root: &Path) -> (i32, serde_json::Value) { /// `run_rollback` plus `extra` arguments (flags or path targets). fn run_rollback_with(root: &Path, extra: &[&OsStr]) -> (i32, serde_json::Value) { - let out = Command::new(binary()) + let out = hermetic_command(&binary()) .args([ "rollback", "--json", @@ -662,7 +661,7 @@ fn apply_and_rollback_refuse_a_node_modules_link_to_first_party_source() { ); }; - let out = Command::new(binary()) + let out = hermetic_command(&binary()) .args([ "apply", "--json", diff --git a/crates/socket-patch-cli/tests/apply/in_process_variant_apply_failure.rs b/crates/socket-patch-cli/tests/apply/in_process_variant_apply_failure.rs index 05e5efb4f..b6eb191b7 100644 --- a/crates/socket-patch-cli/tests/apply/in_process_variant_apply_failure.rs +++ b/crates/socket-patch-cli/tests/apply/in_process_variant_apply_failure.rs @@ -19,7 +19,8 @@ //! Requires: `python3` with `venv` and `pip` on PATH. Skipped (visibly) //! when python3 is missing — same contract as `in_process_pypi_apply`. -use crate::common::{binary, git_sha256}; +use crate::common::hermetic::{self, Extra}; +use crate::common::{binary, git_sha256, hermetic_command}; use std::path::{Path, PathBuf}; use std::process::Command; @@ -28,43 +29,21 @@ const PYPI_PACKAGE: &str = "six"; const PYPI_VERSION: &str = "1.16.0"; const UUID: &str = "12121212-1212-4121-8121-121212121212"; -/// Spawn the CLI with the ambient environment scrubbed, so the flags each -/// test passes are the only thing deciding behaviour. +/// Spawn the CLI with the hermetic `SOCKET_*` environment +/// (`hermetic_command`), so the flags each test passes are the only thing +/// deciding behaviour: an ambient `SOCKET_DRY_RUN=true` would turn both real +/// applies here into no-op dry runs, and `SOCKET_GLOBAL` / +/// `SOCKET_GLOBAL_PREFIX` would aim the crawl (and the patch writes) at the +/// host's real site-packages. /// -/// The binary binds a wide `SOCKET_*` env surface: an ambient -/// `SOCKET_DRY_RUN=true` turns both real applies here into no-op dry runs -/// (every variant `verified`, exit 0 — both tests red), and -/// `SOCKET_GLOBAL` / `SOCKET_GLOBAL_PREFIX` aim the crawl — and the patch -/// WRITES — at the host's real site-packages. Seed-then-scrub (mirrors -/// `common::run_with_env`): the highest-risk vars are seeded with hostile -/// values so a dropped scrub line turns the tests red immediately; -/// telemetry opt-outs are deliberately kept. -/// -/// `VIRTUAL_ENV` must go too: the PyPI crawler early-returns on it, so an -/// activated ambient venv hijacks discovery away from the tmp venv (six is -/// "not installed" — both tests red) or, if that venv holds six@1.16.0, -/// aims the patch at the developer's own environment. A hostile seed is -/// impossible here (only a *real* venv path triggers the early return), so -/// it is plain-removed. +/// `VIRTUAL_ENV` must go too (`Extra::Venv`): the PyPI crawler early-returns +/// on it, so an activated ambient venv hijacks discovery away from the tmp +/// venv (six is "not installed", both tests red) or, if that venv holds +/// six@1.16.0, aims the patch at the developer's own environment. fn run_apply_scrubbed(args: &[&str]) -> std::process::Output { - let mut cmd = Command::new(binary()); - cmd.args(args) - .env("SOCKET_DRY_RUN", "true") - .env("SOCKET_GLOBAL", "true") - .env("SOCKET_GLOBAL_PREFIX", "/nonexistent") - .env("SOCKET_MANIFEST_PATH", "/nonexistent/manifest.json") - .env_remove("SOCKET_DRY_RUN") - .env_remove("SOCKET_GLOBAL") - .env_remove("SOCKET_GLOBAL_PREFIX") - .env_remove("SOCKET_MANIFEST_PATH") - .env_remove("VIRTUAL_ENV"); - for (key, _) in std::env::vars_os() { - let name = key.to_string_lossy(); - if name.starts_with("SOCKET_") && !name.contains("TELEMETRY") && name != "SOCKET_NO_CONFIG" - { - cmd.env_remove(&key); - } - } + let mut cmd = hermetic_command(&binary()); + cmd.args(args); + hermetic::scrub_extra(&mut cmd, &[Extra::Venv]); cmd.output().expect("run socket-patch apply") } diff --git a/crates/socket-patch-cli/tests/common/hermetic.rs b/crates/socket-patch-cli/tests/common/hermetic.rs index 8c7859040..bf1f0b2f5 100644 --- a/crates/socket-patch-cli/tests/common/hermetic.rs +++ b/crates/socket-patch-cli/tests/common/hermetic.rs @@ -160,6 +160,13 @@ pub enum Extra { /// old private scrub plus the cache-folder spellings; [`Extra::Yarn`] is /// the full `YARN_*` sweep. YarnClassic, + /// Every `BUNDLE_*` var. Bundler lets any `.bundle/config` setting be + /// overridden by env, and the gem crawler reads `BUNDLE_PATH`, + /// `BUNDLE_APP_CONFIG` and friends, so an ambient value moves the gem + /// store the suite points it at. Seeded with a bogus + /// `BUNDLE_APP_CONFIG` and then scrubbed; run it before the suite sets + /// its own `BUNDLE_*` values. + Bundle, } /// Apply each of `extras` to `cmd` (see [`Extra`]). Run it before @@ -206,6 +213,15 @@ pub fn scrub_extra(cmd: &mut Command, extras: &[Extra]) { } cmd.env_remove("PREFIX"); } + Extra::Bundle => { + cmd.env("BUNDLE_APP_CONFIG", "/nonexistent/.bundle"); + for (key, _) in std::env::vars_os() { + if key.to_string_lossy().starts_with("BUNDLE_") { + cmd.env_remove(&key); + } + } + cmd.env_remove("BUNDLE_APP_CONFIG"); + } } } } diff --git a/crates/socket-patch-cli/tests/get/get_edge_cases_e2e.rs b/crates/socket-patch-cli/tests/get/get_edge_cases_e2e.rs index d10d32833..647e9ce61 100644 --- a/crates/socket-patch-cli/tests/get/get_edge_cases_e2e.rs +++ b/crates/socket-patch-cli/tests/get/get_edge_cases_e2e.rs @@ -8,10 +8,10 @@ use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; // Every invocation must go through `common::run`/`run_with_env`: the binary -// binds a wide `SOCKET_*` env surface, and a raw `Command::new(binary())` +// binds a wide `SOCKET_*` env surface, and a bare spawn of the binary // inherits the developer's shell — `SOCKET_PROXY_URL` outranks the proxy -// these tests pin, and `SOCKET_MANIFEST_PATH` makes a *passing* test write its manifest and -// blobs into whatever real project the variable points at. +// these tests pin, and `SOCKET_MANIFEST_PATH` makes a *passing* test write +// its manifest and blobs into whatever real project the variable points at. use crate::common; const ORG_SLUG: &str = "test-org"; diff --git a/crates/socket-patch-cli/tests/get/global_packages_e2e.rs b/crates/socket-patch-cli/tests/get/global_packages_e2e.rs index 8be886344..9d63055d9 100644 --- a/crates/socket-patch-cli/tests/get/global_packages_e2e.rs +++ b/crates/socket-patch-cli/tests/get/global_packages_e2e.rs @@ -25,36 +25,17 @@ use std::path::Path; use std::process::Command; use crate::cache_env; -use crate::common::binary; - -/// Build a `socket-patch` `Command` with the ambient `SOCKET_*` env -/// surface scrubbed (mirrors `common::run_with_env`). The binary binds -/// ~20 `SOCKET_*` vars via clap, so an ambient value silently changes -/// what these tests exercise — `SOCKET_DRY_RUN=true` flips every apply -/// envelope's `dryRun` field, and `SOCKET_GLOBAL_PREFIX` bypasses the -/// npm/yarn/pnpm resolution chain this file exists to cover. The -/// highest-risk vars are seeded with hostile values and then scrubbed — -/// `env_remove` clears the seed too, so the child never sees it, but if -/// a scrub line is ever dropped the seed (rather than a developer's -/// ambient shell, which the suite can't rely on) turns the tests red -/// immediately. Telemetry opt-outs are kept so an opted-out dev stays -/// opted out. +use crate::common::{binary, hermetic_command}; + +/// Build a `socket-patch` `Command` with the hermetic `SOCKET_*` +/// environment (`hermetic_command`). The binary binds ~20 `SOCKET_*` vars +/// via clap, so an ambient value silently changes what these tests +/// exercise: `SOCKET_DRY_RUN=true` flips every apply envelope's `dryRun` +/// field, and `SOCKET_GLOBAL_PREFIX` bypasses the npm/yarn/pnpm resolution +/// chain this file exists to cover. fn cli(cwd: &Path) -> Command { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.current_dir(cwd); - cmd.env("SOCKET_DRY_RUN", "true") - .env("SOCKET_GLOBAL_PREFIX", "/nonexistent") - .env("SOCKET_MANIFEST_PATH", "/nonexistent/manifest.json") - .env_remove("SOCKET_DRY_RUN") - .env_remove("SOCKET_GLOBAL_PREFIX") - .env_remove("SOCKET_MANIFEST_PATH"); - for (key, _) in std::env::vars_os() { - let name = key.to_string_lossy(); - if name.starts_with("SOCKET_") && !name.contains("TELEMETRY") && name != "SOCKET_NO_CONFIG" - { - cmd.env_remove(&key); - } - } // Download caches only — NOT the full `cache_env::isolate`. Resolving the // real global prefixes is the whole point of this file, and those come out // of `$HOME`/`PNPM_HOME`, so redirecting either would change the answer the diff --git a/crates/socket-patch-cli/tests/remove/remove_network.rs b/crates/socket-patch-cli/tests/remove/remove_network.rs index bfb0e433b..8d70b3c17 100644 --- a/crates/socket-patch-cli/tests/remove/remove_network.rs +++ b/crates/socket-patch-cli/tests/remove/remove_network.rs @@ -16,10 +16,9 @@ //! `rollback_patches` (the binary would contact the mock, succeed, and //! delete the entry). -use crate::common::{binary, git_sha256}; +use crate::common::{binary, git_sha256, hermetic_command}; use std::path::Path; -use std::process::Command; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -99,34 +98,19 @@ async fn mount_before_blob(mock: &MockServer, before: &[u8], before_hash: &str) fn run_remove(cwd: &Path, api_url: &str, extra: &[&str]) -> (i32, String) { let mut argv: Vec<&str> = vec!["remove", PURL, "--json", "--yes"]; argv.extend_from_slice(extra); - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.args(&argv).current_dir(cwd); - // Hermeticity: drop every ambient SOCKET_* var by prefix before - // re-setting only the four this test controls. The spawned child - // inherits the parent environment, and the binary binds a wide - // `SOCKET_*` env surface (clap-flattened `GlobalArgs`, per-command - // flags, runtime toggles) — an ambient value satisfies (or defeats) - // the behaviour under test *instead of* the argv. An ambient - // `SOCKET_OFFLINE` would make the `--offline` test pass even if the - // *flag* handling regressed; `SOCKET_MANIFEST_PATH`/`SOCKET_CWD` - // could aim the binary at the wrong manifest. Scrub by prefix, not - // an enumerated list: a list drifts from the binary's env surface — - // it missed `SOCKET_SKIP_ROLLBACK` (remove's own env-bound flag), - // under which `remove --offline` skipped rollback entirely, exited - // 0, and deleted the entry, failing both tests for the wrong - // reason. The controlled set must be seeded *after* scrubbing. - for (key, _) in std::env::vars_os() { - if key.to_string_lossy().starts_with("SOCKET_") - && key.to_string_lossy() != "SOCKET_NO_CONFIG" - { - cmd.env_remove(&key); - } - } + // Hermeticity (`hermetic_command`): every ambient SOCKET_* var is + // dropped before the three this test controls are set. An ambient + // value would satisfy (or defeat) the behaviour under test instead of + // the argv: `SOCKET_OFFLINE` would make the `--offline` test pass even + // if the flag handling regressed, and `SOCKET_SKIP_ROLLBACK` (remove's + // own env-bound flag) once made `remove --offline` skip rollback, exit + // 0 and delete the entry, failing both tests for the wrong reason. let out = cmd .env("SOCKET_API_URL", api_url) .env("SOCKET_API_TOKEN", "fake-token-for-test") .env("SOCKET_ORG_SLUG", ORG_SLUG) - .env("SOCKET_TELEMETRY_DISABLED", "1") .output() .expect("run socket-patch"); ( diff --git a/crates/socket-patch-cli/tests/rollback/cli_rollback_silent.rs b/crates/socket-patch-cli/tests/rollback/cli_rollback_silent.rs index c8d776612..d38724b85 100644 --- a/crates/socket-patch-cli/tests/rollback/cli_rollback_silent.rs +++ b/crates/socket-patch-cli/tests/rollback/cli_rollback_silent.rs @@ -19,26 +19,16 @@ //! command (offline runs suppress it) and is out of scope for `rollback`'s //! `--silent` gating. -use crate::common::{binary, git_sha256}; +use crate::common::{binary, git_sha256, hermetic_command}; use std::path::Path; -use std::process::Command; -/// Run `socket-patch rollback` in `cwd` with the entire `SOCKET_*` ambient -/// environment scrubbed (prefix scrub — ambient tokens, silent toggles, or -/// manifest redirects must not change the branch under test) and telemetry -/// disabled. +/// Run `socket-patch rollback` in `cwd` with the hermetic `SOCKET_*` +/// environment (`hermetic_command`: ambient tokens, silent toggles, or +/// manifest redirects must not change the branch under test). fn run_rollback(cwd: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.arg("rollback").args(args).current_dir(cwd); - for (key, _) in std::env::vars_os() { - if key.to_string_lossy().starts_with("SOCKET_") - && key.to_string_lossy() != "SOCKET_NO_CONFIG" - { - cmd.env_remove(&key); - } - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); let out = cmd.output().expect("run socket-patch rollback"); ( out.status.code().unwrap_or(-1), diff --git a/crates/socket-patch-cli/tests/rollback/rollback_duality_invariants.rs b/crates/socket-patch-cli/tests/rollback/rollback_duality_invariants.rs index 835be053e..f58b0c3c3 100644 --- a/crates/socket-patch-cli/tests/rollback/rollback_duality_invariants.rs +++ b/crates/socket-patch-cli/tests/rollback/rollback_duality_invariants.rs @@ -4,30 +4,23 @@ //! blob-pinning rules that keep revert data alive for out-of-scope and //! not-installed entries. //! -//! Same shape as `rollback_invariants.rs`: binary-driven, SOCKET_*-scrubbed +//! Same shape as `rollback_invariants.rs`: binary-driven, hermetic //! child processes, hand-written camelCase manifests, git-sha256 oracle, //! `--offline` throughout (before-blobs are staged, so nothing fetches). -use crate::common::{binary, git_sha256}; +use crate::common::{binary, git_sha256, hermetic_command}; use std::path::{Path, PathBuf}; use std::process::Command; -/// A `rollback` command with the full `SOCKET_*` environment scrubbed and -/// the working directory pinned (same rationale as the twin helper in -/// `rollback_invariants.rs`: an ambient `SOCKET_OFFLINE`/`SOCKET_DRY_RUN`/ +/// A `rollback` command with the hermetic `SOCKET_*` environment +/// (`hermetic_command`) and the working directory pinned (same rationale as +/// the twin helper in `rollback_invariants.rs`: an ambient `SOCKET_OFFLINE`/`SOCKET_DRY_RUN`/ /// `SOCKET_PRESERVE_STATE` must never satisfy — or sabotage — a test that -/// is named after the flag's real code path). Scrub by prefix, not list. +/// is named after the flag's real code path). fn rollback_cmd(cwd: &Path) -> Command { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.arg("rollback").current_dir(cwd); - for (key, _) in std::env::vars_os() { - if key.to_string_lossy().starts_with("SOCKET_") - && key.to_string_lossy() != "SOCKET_NO_CONFIG" - { - cmd.env_remove(&key); - } - } cmd } diff --git a/crates/socket-patch-cli/tests/rollback/rollback_invariants.rs b/crates/socket-patch-cli/tests/rollback/rollback_invariants.rs index 8170f3121..3f85e4993 100644 --- a/crates/socket-patch-cli/tests/rollback/rollback_invariants.rs +++ b/crates/socket-patch-cli/tests/rollback/rollback_invariants.rs @@ -5,34 +5,29 @@ //! unroutable localhost port. use crate::common::envelope::{codes_in, events}; -use crate::common::{binary, git_sha256}; +use crate::common::{binary, git_sha256, hermetic_command}; use std::path::{Path, PathBuf}; use std::process::Command; -/// A `rollback` command with the full `SOCKET_*` environment scrubbed and the -/// working directory pinned. All tests build their child process through here -/// so none can be satisfied by ambient environment instead of the code path. +/// A `rollback` command with the hermetic `SOCKET_*` environment +/// (`hermetic_command`) and the working directory pinned. All tests build +/// their child process through here so none can be satisfied by ambient +/// environment instead of the code path. /// /// The child process inherits the parent's environment, so an ambient value /// would let a test pass via the environment instead of via the flag (and the /// real code path) it is named after — e.g. an ambient `SOCKET_OFFLINE=true` /// would satisfy the `--offline` tests even if `--offline` were broken, and /// `SOCKET_MANIFEST_PATH` would silently redirect the manifest out from under -/// the no-manifest / override tests. Scrub by prefix, not by list: an explicit -/// list rots as flags are added (it missed `SOCKET_VENDOR_SOURCE`, whose -/// ambient garbage aborted every invocation with a clap usage error). Tests -/// that need a `SOCKET_*` var seed it AFTER this scrub via `.env()`. +/// the no-manifest / override tests. The shared builder scrubs by prefix, +/// not by list: an explicit list rots as flags are added (one missed +/// `SOCKET_VENDOR_SOURCE`, whose ambient garbage aborted every invocation +/// with a clap usage error). Tests that need a `SOCKET_*` var seed it AFTER +/// this scrub via `.env()`. fn rollback_cmd(cwd: &Path) -> Command { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.arg("rollback").current_dir(cwd); - for (key, _) in std::env::vars_os() { - if key.to_string_lossy().starts_with("SOCKET_") - && key.to_string_lossy() != "SOCKET_NO_CONFIG" - { - cmd.env_remove(&key); - } - } cmd } @@ -296,7 +291,6 @@ fn rollback_undownloadable_blob_envelope_names_blob_and_remedy() { make_socket_dir(tmp.path()); install_manifest_package(tmp.path()); let out = rollback_cmd(tmp.path()) - .env("SOCKET_TELEMETRY_DISABLED", "1") .args([ "--json", "--api-url", @@ -344,16 +338,15 @@ fn rollback_undownloadable_blob_envelope_names_blob_and_remedy() { ); // The env is scrubbed (no SOCKET_API_TOKEN) and socket-cli config is - // vetoed by the workspace-pinned SOCKET_NO_CONFIG=1, so every client - // build wants the no-token notice. Under --json it is muted entirely; - // in human mode it must appear exactly once per invocation, not once - // per internal phase. + // vetoed by the SOCKET_NO_CONFIG=1 that `hermetic_command` forces, so + // every client build wants the no-token notice. Under --json it is + // muted entirely; in human mode it must appear exactly once per + // invocation, not once per internal phase. assert!( !stderr.contains("No SOCKET_API_TOKEN set"), "--json must mute the no-token notice; stderr=\n{stderr}" ); let human = rollback_cmd(tmp.path()) - .env("SOCKET_TELEMETRY_DISABLED", "1") .args([ "--api-url", "http://127.0.0.1:1/", @@ -479,7 +472,6 @@ fn rollback_not_installed_entry_triggers_no_blob_download() { make_socket_dir(tmp.path()); let out = rollback_cmd(tmp.path()) - .env("SOCKET_TELEMETRY_DISABLED", "1") .args([ "--json", "--api-url", diff --git a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs index fce596335..007776399 100644 --- a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs +++ b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs @@ -29,15 +29,7 @@ use serial_test::serial; /// children see whatever `SOCKET_*` the ambient shell exports unless the file /// scrubs by hand. const PENDING_RAW_SPAWNS: &[&str] = &[ - "apply/apply_invariants.rs", "apply/apply_network.rs", - "apply/cli_gem_variant_mismatch_policy.rs", - "apply/covgap_commands_apply.rs", - "apply/in_process_gem_config_warning.rs", - "apply/in_process_gem_fallback_home.rs", - "apply/in_process_gem_multicopy.rs", - "apply/in_process_npm_multicopy.rs", - "apply/in_process_variant_apply_failure.rs", "cli/covgap_api_client.rs", "cli/covgap_commands_list.rs", "cli/telemetry_e2e.rs", @@ -109,8 +101,6 @@ const PENDING_RAW_SPAWNS: &[&str] = &[ "e2e_vex_lockfile/yarn.rs", "e2e_vex_redirect.rs", "e2e_vex_vendor.rs", - "get/get_edge_cases_e2e.rs", - "get/global_packages_e2e.rs", "hosted_memory_common/mod.rs", "hosted_memory_engine.rs", "hosted_superseding_pypi.rs", @@ -126,11 +116,7 @@ const PENDING_RAW_SPAWNS: &[&str] = &[ "mode_migration_bun.rs", "mode_migration_cargo.rs", "mode_migration_pypi.rs", - "remove/remove_network.rs", "remove_rollback_api_overrides.rs", - "rollback/cli_rollback_silent.rs", - "rollback/rollback_duality_invariants.rs", - "rollback/rollback_invariants.rs", "rollback/rollback_multicopy_blob_gate.rs", "scan_api_retry_e2e.rs", "scan_pnpm_relocated_store_cwd_e2e.rs", @@ -497,14 +483,41 @@ fn yarn_classic_extra_sweeps_mirror_rc_and_prefix_overrides() { ); } +#[test] +#[serial] +fn bundle_extra_sweeps_every_bundler_override() { + with_ambient( + &[ + ("BUNDLE_PATH", "/ambient/bundle"), + ("BUNDLE_APP_CONFIG", "/ambient/.bundle"), + ("BUNDLE_GEMFILE", "/ambient/Gemfile"), + ("GEM_HOME", "/ambient/gems"), + ], + || { + let mut cmd = Command::new("bundle"); + hermetic::scrub_extra(&mut cmd, &[Extra::Bundle]); + let env = effective_env(&cmd); + assert_eq!(get(&env, "BUNDLE_PATH"), None); + assert_eq!(get(&env, "BUNDLE_APP_CONFIG"), None); + assert_eq!(get(&env, "BUNDLE_GEMFILE"), None); + assert_eq!(get(&env, "GEM_HOME"), Some("/ambient/gems")); + + // A suite's own BUNDLE_PATH, set after the scrub, survives. + cmd.env("BUNDLE_PATH", "/store"); + assert_eq!(get(&effective_env(&cmd), "BUNDLE_PATH"), Some("/store")); + }, + ); +} + #[test] #[serial] fn extra_seeds_are_scrubbed_whatever_the_ambient_value() { let mut cmd = Command::new("tool"); - hermetic::scrub_extra(&mut cmd, &[Extra::Yarn, Extra::Pnpm]); + hermetic::scrub_extra(&mut cmd, &[Extra::Yarn, Extra::Pnpm, Extra::Bundle]); let env = effective_env(&cmd); assert_eq!(get(&env, "YARN_NODE_LINKER"), None); assert_eq!(get(&env, "npm_config_node_linker"), None); + assert_eq!(get(&env, "BUNDLE_APP_CONFIG"), None); } /// Every `.rs` file under `tests/`, as `/`-separated paths relative to it.