diff --git a/crates/socket-patch-cli/tests/apply/apply_network.rs b/crates/socket-patch-cli/tests/apply/apply_network.rs index 3c5724157..379af2b57 100644 --- a/crates/socket-patch-cli/tests/apply/apply_network.rs +++ b/crates/socket-patch-cli/tests/apply/apply_network.rs @@ -11,12 +11,11 @@ //! node_modules and leaves `.socket/` byte-identical. use std::path::Path; -use std::process::Command; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; -use crate::common::{binary, git_sha256}; +use crate::common::{binary, git_sha256, hermetic}; const ORG_SLUG: &str = "test-org"; @@ -79,7 +78,7 @@ fn run_apply(cwd: &Path, api_url: &str, extra: &[&str]) -> (i32, String, String) // --api-url/--api-token/--org flags). let mut argv: Vec<&str> = vec!["apply", "--json"]; argv.extend_from_slice(extra); - let out = Command::new(binary()) + let out = hermetic::command(&binary()) .args(&argv) .current_dir(cwd) .env("SOCKET_API_URL", api_url) @@ -323,10 +322,9 @@ async fn apply_dry_run_emits_verified_event_without_writing() { std::fs::write(blobs.join(&after_hash), after).unwrap(); // No mock needed — apply finds everything locally. - let out = Command::new(binary()) + let out = hermetic::command(&binary()) .args(["apply", "--json", "--dry-run", "--offline"]) .current_dir(tmp.path()) - .env_remove("SOCKET_API_TOKEN") .output() .expect("run socket-patch"); let code = out.status.code().unwrap_or(-1); @@ -403,10 +401,9 @@ async fn apply_with_force_overrides_hash_mismatch() { // Without --force apply should fail (hash mismatch). With --force it // should bypass the verification and write the patched content. - let out = Command::new(binary()) + let out = hermetic::command(&binary()) .args(["apply", "--json", "--offline", "--force"]) .current_dir(tmp.path()) - .env_remove("SOCKET_API_TOKEN") .output() .expect("run socket-patch"); let code = out.status.code().unwrap_or(-1); @@ -467,10 +464,9 @@ async fn apply_hash_mismatch_default_warns_and_applies_strict_fails() { // DEFAULT: the mismatch is overwritten with the full verified patched // content (the blob is hash-gated to afterHash) and surfaced as a warning event — exit 0. let tmp = fixture(); - let out = Command::new(binary()) + let out = hermetic::command(&binary()) .args(["apply", "--json", "--offline"]) .current_dir(tmp.path()) - .env_remove("SOCKET_API_TOKEN") .output() .expect("run socket-patch"); let stdout = String::from_utf8_lossy(&out.stdout).to_string(); @@ -500,10 +496,9 @@ async fn apply_hash_mismatch_default_warns_and_applies_strict_fails() { // The human run logs the warning to stderr. let tmp = fixture(); - let out = Command::new(binary()) + let out = hermetic::command(&binary()) .args(["apply", "--offline", "--yes"]) .current_dir(tmp.path()) - .env_remove("SOCKET_API_TOKEN") .output() .expect("run socket-patch"); let stderr = String::from_utf8_lossy(&out.stderr); @@ -515,10 +510,9 @@ async fn apply_hash_mismatch_default_warns_and_applies_strict_fails() { // --strict: fail closed — exit 1, failed event, file untouched. let tmp = fixture(); - let out = Command::new(binary()) + let out = hermetic::command(&binary()) .args(["apply", "--json", "--offline", "--strict"]) .current_dir(tmp.path()) - .env_remove("SOCKET_API_TOKEN") .output() .expect("run socket-patch"); let stdout = String::from_utf8_lossy(&out.stdout).to_string(); @@ -591,15 +585,13 @@ async fn apply_pypi_package_uses_python_crawler() { // Run apply restricted to pypi. With the venv staged on disk and the // after-blob pre-cached, this must locate the package via the python // crawler and patch it — exercising the pypi dispatch branch end to - // end, not just "without panicking". `VIRTUAL_ENV` is cleared so an - // ambient venv in CI can't redirect discovery away from our `.venv`. - let out = Command::new(binary()) - .args(["apply", "--json", "--offline", "--ecosystems", "pypi"]) - .current_dir(tmp.path()) - .env_remove("SOCKET_API_TOKEN") - .env_remove("VIRTUAL_ENV") - .output() - .expect("run socket-patch"); + // end, not just "without panicking". An ambient venv is scrubbed so CI + // can't redirect discovery away from our `.venv`. + let mut cmd = hermetic::command(&binary()); + cmd.args(["apply", "--json", "--offline", "--ecosystems", "pypi"]) + .current_dir(tmp.path()); + hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Venv]); + 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(); let stderr = String::from_utf8_lossy(&out.stderr).to_string(); @@ -655,15 +647,14 @@ async fn apply_uses_locally_cached_blob_without_fetching() { // No mock server. If apply tries to hit the network, the test will // fail (connection refused) — proving the local-blob fast path is - // taken when sources are already on disk. - let out = Command::new(binary()) + // taken when sources are already on disk. Both URLs are pinned: with + // no token the child takes the public-proxy path. + let out = hermetic::command(&binary()) .args(["apply", "--json"]) .current_dir(tmp.path()) - .env_remove("SOCKET_API_TOKEN") - .env( - "SOCKET_API_URL", - "http://127.0.0.1:1", // unreachable port — should never be contacted - ) + // Unreachable port: should never be contacted. + .env("SOCKET_API_URL", "http://127.0.0.1:1") + .env("SOCKET_PROXY_URL", "http://127.0.0.1:1") .output() .expect("run socket-patch"); let code = out.status.code().unwrap_or(-1); @@ -819,19 +810,18 @@ async fn offline_apply_with_token_makes_zero_network_requests() { std::fs::create_dir_all(&blobs).expect("create blobs"); std::fs::write(blobs.join(&after_hash), after).expect("stage blob"); - let mut cmd = Command::new(binary()); + let mut cmd = hermetic::command(&binary()); cmd.args(["apply", "--json", "--offline"]) .current_dir(tmp.path()); - // Scrub ambient SOCKET_* (an ambient SOCKET_ORG_SLUG would skip the - // auto-resolution and make this test vacuously green), then seed only - // what the scenario needs: token set, org absent, both URLs pinned to - // the mock so any traffic — API or proxy path — is captured. - for (k, _) in std::env::vars_os() { - if k.to_string_lossy().starts_with("SOCKET_") && k.to_string_lossy() != "SOCKET_NO_CONFIG" { - cmd.env_remove(&k); - } - } - cmd.env("SOCKET_API_URL", mock.uri()) + // The builder scrubs ambient SOCKET_* (an ambient SOCKET_ORG_SLUG would + // skip the auto-resolution and make this test vacuously green) and + // keeps the config layer off; seed only what the scenario needs: token + // set, org absent, both URLs pinned to the mock so any traffic — API or + // proxy path — is captured. Telemetry is switched back on (the builder + // forces it off) so only `--offline` can keep the telemetry POST away + // from the mock. + cmd.env("SOCKET_TELEMETRY_DISABLED", "0") + .env("SOCKET_API_URL", mock.uri()) .env("SOCKET_PROXY_URL", mock.uri()) .env("SOCKET_API_TOKEN", "fake-token-for-test"); let out = cmd.output().expect("run socket-patch"); diff --git a/crates/socket-patch-cli/tests/e2e_scan.rs b/crates/socket-patch-cli/tests/e2e_scan.rs index 0487bccb0..c1a64d548 100644 --- a/crates/socket-patch-cli/tests/e2e_scan.rs +++ b/crates/socket-patch-cli/tests/e2e_scan.rs @@ -29,7 +29,7 @@ #[path = "common/mod.rs"] mod common; -use common::{binary, git_sha256}; +use common::{binary, git_sha256, hermetic}; use std::path::Path; use std::process::{Command, Output}; @@ -93,24 +93,14 @@ fn git_sha256_file(path: &Path) -> String { } fn run(cwd: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); + // The shared builder scrubs every ambient `SOCKET_*` (an ambient + // SOCKET_DRY_RUN would turn every `scan --mode agent` into a preview, + // SOCKET_GLOBAL would aim mutations at the host's real global + // node_modules) and forces the config layer, update check and telemetry + // off; removing SOCKET_API_TOKEN also forces the public proxy + // (free-tier). + let mut cmd = hermetic::command(&binary()); cmd.args(args).current_dir(cwd); - // The binary binds a wide `SOCKET_*` env surface (SOCKET_CWD, - // SOCKET_DRY_RUN, SOCKET_GLOBAL, SOCKET_GLOBAL_PREFIX, SOCKET_PROXY_URL, - // SOCKET_MANIFEST_PATH, ...). An ambient value silently changes what - // these tests exercise — SOCKET_DRY_RUN=true turns every `scan --mode agent` - // into a no-op preview, and SOCKET_GLOBAL aims mutations at the host's - // *real* global node_modules. Scrub the whole prefix so only the flags - // each test passes are in effect; removing SOCKET_API_TOKEN also forces - // the public proxy (free-tier). Telemetry opt-outs are deliberately kept - // so an opted-out dev stays opted out. - 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 out: Output = cmd.output().expect("failed to execute socket-patch binary"); let code = out.status.code().unwrap_or(-1); let stdout = String::from_utf8_lossy(&out.stdout).to_string(); @@ -130,6 +120,7 @@ fn assert_run_ok(cwd: &Path, args: &[&str], context: &str) -> (String, String) { fn npm_run(cwd: &Path, args: &[&str]) { let mut cmd = Command::new("npm"); cmd.args(args).current_dir(cwd); + hermetic::scrub_socket_vars(&mut cmd); cache_env::isolate(&mut cmd); let out = cmd.output().expect("failed to run npm"); assert!( diff --git a/crates/socket-patch-cli/tests/e2e_socket_yml_policy.rs b/crates/socket-patch-cli/tests/e2e_socket_yml_policy.rs index 200c40de4..2f9460ece 100644 --- a/crates/socket-patch-cli/tests/e2e_socket_yml_policy.rs +++ b/crates/socket-patch-cli/tests/e2e_socket_yml_policy.rs @@ -3,6 +3,8 @@ //! scanned through the real binary in hosted, agent and vendored mode //! against a mock patch API that serves a small catalog. +#[path = "common/hermetic.rs"] +mod hermetic; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; @@ -403,18 +405,14 @@ impl Repo { } } -/// Run the binary with ambient `SOCKET_*` scrubbed; `(code, stdout, stderr)`. +/// Run the binary with the hermetic `SOCKET_*` environment (ambient values +/// scrubbed, config layer, update check and telemetry forced off); +/// `(code, stdout, stderr)`. fn run_cli(cwd: &Path, args: &[&str], env: &[(&str, &str)]) -> (i32, String, String) { - let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_socket-patch")); + let mut cmd = hermetic::binary_command(); cmd.args(args).current_dir(cwd); - for (key, _) in std::env::vars() { - if key.starts_with("SOCKET_") && key != "SOCKET_NO_CONFIG" { - cmd.env_remove(key); - } - } - cmd.env_remove("GIT_CEILING_DIRECTORIES") - .env_remove("VIRTUAL_ENV"); - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); + hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Venv, hermetic::Extra::Bundle]); + cmd.env_remove("GIT_CEILING_DIRECTORIES"); // The fixture's hosted pins name this origin; it makes them recorded. cmd.env("SOCKET_PATCH_SERVER_URL", "http://patch.test"); let absent = cwd.join(".absent-npm-config"); diff --git a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs index d2a76309c..00a33bd4a 100644 --- a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs +++ b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs @@ -29,14 +29,11 @@ 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_network.rs", "e2e_hosted_production.rs", "e2e_maven.rs", "e2e_npm.rs", "e2e_redirect_bun_build.rs", "e2e_redirect_cargo_shapes.rs", - "e2e_scan.rs", - "e2e_socket_yml_policy.rs", "e2e_vendored_production.rs", "e2e_vex_vendor.rs", "rollback/rollback_multicopy_blob_gate.rs",