diff --git a/crates/socket-patch-cli/tests/cli/covgap_api_client.rs b/crates/socket-patch-cli/tests/cli/covgap_api_client.rs index f956f4018..4272de32c 100644 --- a/crates/socket-patch-cli/tests/cli/covgap_api_client.rs +++ b/crates/socket-patch-cli/tests/cli/covgap_api_client.rs @@ -6,9 +6,7 @@ //! stderr *text* — the "you configured the sha512- storage hash, not the //! token" hint — which only a spawned process can observe. -use crate::common::binary; - -use std::process::Command; +use crate::common::{binary, hermetic_command}; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -73,14 +71,10 @@ async fn run_get_with_hash_shaped_token(extra: &[&str]) -> std::process::Output "sha512-deadbeefdeadbeef", ]); // `mock` is dropped (and its `.expect(1)`s verified) on return. - Command::new(binary()) + // Hermetic env: ambient state must not short-circuit auto-resolution + // (no env slug, no offline gate, no socket-cli config). + hermetic_command(&binary()) .args(&args) - // Ambient state must not short-circuit auto-resolution: no env - // slug, no offline gate, no socket-cli config (`socket login`). - .env_remove("SOCKET_ORG_SLUG") - .env_remove("SOCKET_OFFLINE") - .env_remove("SOCKET_API_TOKEN") - .env("SOCKET_NO_CONFIG", "1") .current_dir(tmp.path()) .output() .expect("run socket-patch get") @@ -244,12 +238,7 @@ async fn unresolved_org_reaches_get_uuid_dry_run_json_warnings() { let cwd = tmp.path(); let uri = mock.uri(); let token = format!("sktsec_{}_api", "x".repeat(44)); - let mut cmd = Command::new(binary()); - for (key, _) in std::env::vars() { - if key.starts_with("SOCKET_") { - cmd.env_remove(key); - } - } + let mut cmd = hermetic_command(&binary()); let out = cmd .args([ "get", @@ -268,7 +257,6 @@ async fn unresolved_org_reaches_get_uuid_dry_run_json_warnings() { "--api-token", &token, ]) - .env("SOCKET_NO_CONFIG", "1") .current_dir(cwd) .output() .expect("run socket-patch get"); @@ -377,12 +365,7 @@ async fn unresolved_org_routes_the_whole_scan_vex_run_to_the_proxy_once() { let vex_path = cwd.join("out.vex.json"); let uri = mock.uri(); let token = format!("sktsec_{}_api", "x".repeat(44)); - let mut cmd = Command::new(binary()); - for (key, _) in std::env::vars() { - if key.starts_with("SOCKET_") { - cmd.env_remove(key); - } - } + let mut cmd = hermetic_command(&binary()); let out = cmd .args([ "scan", @@ -400,7 +383,6 @@ async fn unresolved_org_routes_the_whole_scan_vex_run_to_the_proxy_once() { "--api-token", &token, ]) - .env("SOCKET_NO_CONFIG", "1") .current_dir(cwd) .output() .expect("run socket-patch scan"); @@ -485,12 +467,7 @@ async fn unresolved_org_reaches_apply_and_vendor_json_warnings() { std::fs::write(cwd.join(".socket/manifest.json"), r#"{"patches":{}}"#).unwrap(); let uri = mock.uri(); let token = format!("sktsec_{}_api", "x".repeat(44)); - let mut cmd = Command::new(binary()); - for (key, _) in std::env::vars() { - if key.starts_with("SOCKET_") { - cmd.env_remove(key); - } - } + let mut cmd = hermetic_command(&binary()); let out = cmd .args([ command, @@ -504,7 +481,6 @@ async fn unresolved_org_reaches_apply_and_vendor_json_warnings() { "--api-token", &token, ]) - .env("SOCKET_NO_CONFIG", "1") .current_dir(cwd) .output() .unwrap_or_else(|e| panic!("run socket-patch {command}: {e}")); diff --git a/crates/socket-patch-cli/tests/cli/covgap_commands_list.rs b/crates/socket-patch-cli/tests/cli/covgap_commands_list.rs index 2e337c01e..4924a223d 100644 --- a/crates/socket-patch-cli/tests/cli/covgap_commands_list.rs +++ b/crates/socket-patch-cli/tests/cli/covgap_commands_list.rs @@ -28,8 +28,6 @@ use crate::vlt_vendored; // `--debug` provenance echoes (GlobalArgs::telemetry_credentials, config layer) // --------------------------------------------------------------------------- -const BINARY: &str = env!("CARGO_BIN_EXE_socket-patch"); - /// The platform env var that positions the socket-cli data dir. const DATA_DIR_VAR: &str = if cfg!(windows) { "LOCALAPPDATA" @@ -60,28 +58,20 @@ fn write_empty_manifest(root: &Path) { std::fs::write(dir.join("manifest.json"), r#"{ "patches": {} }"#).unwrap(); } -/// `socket-patch list` against `project`, hermetic like -/// cli_config_fallback.rs's `list_cmd`: every ambient `SOCKET_*` scrubbed, -/// the data dir pointed at the fixture, the config layer re-enabled, -/// telemetry off (the provenance test re-enables it), the update notifier -/// off. `debug` adds the `--debug` flag under test. +/// `socket-patch list` against `project` in the shared hermetic env (every +/// ambient `SOCKET_*` scrubbed, telemetry and the update notifier off; the +/// provenance test re-enables telemetry), with the data dir pointed at the +/// fixture and the config layer re-enabled. `debug` adds the `--debug` +/// flag under test. fn list_cmd(project: &Path, data_dir: &Path, debug: bool) -> Command { - let mut cmd = Command::new(BINARY); + let mut cmd = common::hermetic_command(&common::binary()); cmd.arg("list"); if debug { cmd.arg("--debug"); } cmd.arg("--cwd").arg(project); - for (key, _) in std::env::vars_os() { - let name = key.to_string_lossy(); - if name.starts_with("SOCKET_") { - cmd.env_remove(&key); - } - } cmd.env(DATA_DIR_VAR, data_dir); cmd.env("SOCKET_NO_CONFIG", "0"); - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); - cmd.env("SOCKET_NO_UPDATE_CHECK", "1"); cmd } diff --git a/crates/socket-patch-cli/tests/cli/telemetry_e2e.rs b/crates/socket-patch-cli/tests/cli/telemetry_e2e.rs index 3116c3985..4406e1607 100644 --- a/crates/socket-patch-cli/tests/cli/telemetry_e2e.rs +++ b/crates/socket-patch-cli/tests/cli/telemetry_e2e.rs @@ -8,7 +8,7 @@ //! count). Each test runs the released binary in a tempdir against //! the mock URI. -use crate::common::binary; +use crate::common::{binary, hermetic_command}; use std::path::Path; use std::process::Command; @@ -97,45 +97,20 @@ fn build_cmd_with_token( ORG_SLUG, ]; args.extend_from_slice(extra_args); - let mut cmd = Command::new(binary()); - cmd.args(&args).current_dir(cwd); // The binary binds a wide `SOCKET_*` env surface; an ambient value // silently changes what these tests exercise — SOCKET_ECOSYSTEMS=cargo // makes scan skip the npm fixture (scannedPackages 0), SOCKET_CWD aims - // the crawl outside the tempdir, SOCKET_GLOBAL flips to global mode. - // 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 this suite can't rely on) turns the - // tests red immediately. Same pattern as `common::run_with_env`. - cmd.env("SOCKET_CWD", "/nonexistent") - .env("SOCKET_ECOSYSTEMS", "cargo") - .env("SOCKET_GLOBAL", "true") - .env("SOCKET_GLOBAL_PREFIX", "/nonexistent") - .env("SOCKET_DRY_RUN", "true") - .env("SOCKET_MANIFEST_PATH", "/nonexistent/manifest.json") - .env_remove("SOCKET_CWD") - .env_remove("SOCKET_ECOSYSTEMS") - .env_remove("SOCKET_GLOBAL") - .env_remove("SOCKET_GLOBAL_PREFIX") - .env_remove("SOCKET_DRY_RUN") - .env_remove("SOCKET_MANIFEST_PATH"); - // Prefix-scrub whatever else the ambient shell carries (SOCKET_YES, - // SOCKET_STRICT, SOCKET_VENDOR_SOURCE, ...). Unlike common/mod.rs this - // scrub covers the telemetry opt-outs too: this suite's entire point is - // asserting telemetry POSTs against a local wiremock, so an opted-out - // dev shell must not vacuously green the count-0 asserts. - for (key, _) in std::env::vars_os() { - let name = key.to_string_lossy(); - if name.starts_with("SOCKET_") && name != "SOCKET_NO_CONFIG" { - cmd.env_remove(&key); - } - } - // Default: disable the test-environment short-circuit - // (`is_telemetry_disabled()` flips on `VITEST=true`). + // the crawl outside the tempdir, SOCKET_GLOBAL flips to global mode. The + // hermetic builder scrubs all of them. + let mut cmd = hermetic_command(&binary()); + cmd.args(&args).current_dir(cwd); + // This suite's entire point is asserting telemetry POSTs against a + // local wiremock, so undo the builder's forced opt-out (an opted-out + // dev shell must not vacuously green the count-0 asserts), and disable + // the test-environment short-circuit (`is_telemetry_disabled()` flips + // on `VITEST=true`). cmd.env_remove("VITEST"); cmd.env_remove("SOCKET_TELEMETRY_DISABLED"); - cmd.env_remove("SOCKET_OFFLINE"); // An ambient VIRTUAL_ENV hijacks the python crawler (its site-packages // get crawled as project packages), breaking the exact // `scannedPackages` oracles below. diff --git a/crates/socket-patch-cli/tests/cli_apply_silent.rs b/crates/socket-patch-cli/tests/cli_apply_silent.rs index c78b2e07c..bd39b7efa 100644 --- a/crates/socket-patch-cli/tests/cli_apply_silent.rs +++ b/crates/socket-patch-cli/tests/cli_apply_silent.rs @@ -25,23 +25,16 @@ #[path = "common/mod.rs"] mod common; -use common::binary; +use common::{binary, hermetic_command}; use std::path::Path; -use std::process::Command; - -use socket_patch_cli::args::GLOBAL_ARG_ENV_VARS; /// Run `socket-patch apply` in `cwd` with a scrubbed SOCKET_* environment /// so ambient developer/CI configuration (tokens, silent toggles) can't /// change the branch under test. fn run_apply(cwd: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.arg("apply").args(args).current_dir(cwd); - for var in GLOBAL_ARG_ENV_VARS { - cmd.env_remove(var); - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); let out = cmd.output().expect("run socket-patch apply"); ( out.status.code().unwrap_or(-1), diff --git a/crates/socket-patch-cli/tests/cli_argv_non_utf8.rs b/crates/socket-patch-cli/tests/cli_argv_non_utf8.rs index 08cecb3ff..0d7ea160a 100644 --- a/crates/socket-patch-cli/tests/cli_argv_non_utf8.rs +++ b/crates/socket-patch-cli/tests/cli_argv_non_utf8.rs @@ -17,9 +17,9 @@ use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; -use std::process::Command; -const BINARY: &str = env!("CARGO_BIN_EXE_socket-patch"); +#[path = "common/hermetic.rs"] +mod hermetic; /// An argument that is valid on the OS level but not valid UTF-8. fn non_utf8_arg() -> &'static OsStr { @@ -28,16 +28,13 @@ fn non_utf8_arg() -> &'static OsStr { /// Run the binary with the given args in a hermetic env and capture output. fn run(args: &[&OsStr]) -> (Option, String, String) { - let mut cmd = Command::new(BINARY); + // Hermetic env: ambient SOCKET_* vars can never perturb where the + // invocation fails (the assertion is about the argv path, not env + // handling). + let mut cmd = hermetic::binary_command(); for a in args { cmd.arg(a); } - // Scrub the global env-var surface so ambient SOCKET_* vars can never - // perturb where the invocation fails (the assertion is about the argv - // path, not env handling). - for var in socket_patch_cli::args::GLOBAL_ARG_ENV_VARS { - cmd.env_remove(var); - } let out = cmd.output().expect("spawn socket-patch"); ( out.status.code(), diff --git a/crates/socket-patch-cli/tests/cli_config_fallback.rs b/crates/socket-patch-cli/tests/cli_config_fallback.rs index 530a53c5f..bc7d78b81 100644 --- a/crates/socket-patch-cli/tests/cli_config_fallback.rs +++ b/crates/socket-patch-cli/tests/cli_config_fallback.rs @@ -25,7 +25,8 @@ use base64::Engine as _; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; -const BINARY: &str = env!("CARGO_BIN_EXE_socket-patch"); +#[path = "common/hermetic.rs"] +mod hermetic; /// The platform env var that positions the socket-cli data dir. const DATA_DIR_VAR: &str = if cfg!(windows) { @@ -50,34 +51,26 @@ fn write_config(data_dir: &Path, json: &serde_json::Value) { std::fs::write(dir.join("config.json"), encoded).unwrap(); } -/// Build a hermetic human-mode `socket-patch scan -e npm --cwd ` command: every -/// ambient `SOCKET_*` var is scrubbed (including the inherited -/// `SOCKET_NO_CONFIG=1` guard — tests re-add exactly what they need), the -/// data dir points at `data_dir`, and the project dir is an empty npm -/// project so the crawl finds nothing and no batch request fires. +/// Build a hermetic human-mode `socket-patch scan -e npm --cwd ` +/// command: the shared hermetic env (every ambient `SOCKET_*` scrubbed, +/// telemetry off) with the config layer re-enabled, the data dir pointed +/// at `data_dir`, and the project dir an empty npm project so the crawl +/// finds nothing and no batch request fires. fn scan_cmd(project: &Path, data_dir: &Path) -> Command { - let mut cmd = Command::new(BINARY); + let mut cmd = hermetic::binary_command(); // Human mode: core's proxy advisory (the oracle below) is muted under // `--json`/`--silent`. cmd.args(["scan", "-e", "npm", "--cwd"]).arg(project); - for (key, _) in std::env::vars_os() { - let name = key.to_string_lossy(); - if name.starts_with("SOCKET_") { - cmd.env_remove(&key); - } - } // Ambient VIRTUAL_ENV would be harmless under `-e npm`, but scrub it // anyway to mirror the other scan harnesses. cmd.env_remove("VIRTUAL_ENV"); cmd.env(DATA_DIR_VAR, data_dir); - // Re-enable the config layer (the workspace-level SOCKET_NO_CONFIG=1 - // guard was scrubbed above; set an explicit falsy value so the intent - // is visible). Gate tests override this with "1". + // Re-enable the config layer (the hermetic builder forces + // SOCKET_NO_CONFIG=1; caller env lands last). Gate tests override this + // with "1". Telemetry stays off (it is fire-and-forget to the *real* + // proxy when a run is unauthenticated); the one test that exercises + // telemetry-follows-config re-enables it explicitly. cmd.env("SOCKET_NO_CONFIG", "0"); - // Telemetry is fire-and-forget to the *real* proxy when a run is - // unauthenticated — keep these tests off the network. The one test - // that exercises telemetry-follows-config re-enables it explicitly. - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); cmd } @@ -441,23 +434,14 @@ fn write_empty_manifest(root: &Path) { /// `socket-patch list` against `project`, with the same hermetic env as /// [`scan_cmd`]: every ambient `SOCKET_*` scrubbed, the data dir pointed at -/// the fixture, the config layer re-enabled, telemetry off by default. +/// the fixture, the config layer re-enabled, telemetry off by default, and +/// the passive update check off so the only request a run can make is the +/// telemetry POST under test. fn list_cmd(project: &Path, data_dir: &Path) -> Command { - let mut cmd = Command::new(BINARY); + let mut cmd = hermetic::binary_command(); cmd.arg("list").arg("--cwd").arg(project); - for (key, _) in std::env::vars_os() { - let name = key.to_string_lossy(); - if name.starts_with("SOCKET_") { - cmd.env_remove(&key); - } - } cmd.env(DATA_DIR_VAR, data_dir); cmd.env("SOCKET_NO_CONFIG", "0"); - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); - // The scrub above also dropped the workspace-level notifier guard; keep - // the passive update check off so the only request a run can make is the - // telemetry POST under test. - cmd.env("SOCKET_NO_UPDATE_CHECK", "1"); cmd } diff --git a/crates/socket-patch-cli/tests/cli_get_silent.rs b/crates/socket-patch-cli/tests/cli_get_silent.rs index e5c3e6681..169fcdd55 100644 --- a/crates/socket-patch-cli/tests/cli_get_silent.rs +++ b/crates/socket-patch-cli/tests/cli_get_silent.rs @@ -9,26 +9,16 @@ #[path = "common/mod.rs"] mod common; -use common::binary; +use common::{binary, hermetic_command}; use std::path::Path; -use std::process::Command; - -use socket_patch_cli::args::GLOBAL_ARG_ENV_VARS; /// Run `socket-patch get` in `cwd` with a scrubbed SOCKET_* environment /// so ambient developer/CI configuration (tokens, org slugs, silent /// toggles) can't change the branch under test. fn run_get(cwd: &Path, args: &[&str]) -> (i32, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.arg("get").args(args).current_dir(cwd); - for var in GLOBAL_ARG_ENV_VARS { - cmd.env_remove(var); - } - for var in ["SOCKET_SAVE_ONLY", "SOCKET_ALL_RELEASES"] { - cmd.env_remove(var); - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); let out = cmd.output().expect("run socket-patch get"); ( out.status.code().unwrap_or(-1), diff --git a/crates/socket-patch-cli/tests/cli_global_args.rs b/crates/socket-patch-cli/tests/cli_global_args.rs index b4a37f678..c506e5f4e 100644 --- a/crates/socket-patch-cli/tests/cli_global_args.rs +++ b/crates/socket-patch-cli/tests/cli_global_args.rs @@ -20,6 +20,9 @@ use clap::Parser; use socket_patch_cli::args::GlobalArgs; use socket_patch_cli::Cli; +#[path = "common/hermetic.rs"] +mod hermetic; + /// Subcommands under test. `rollback` is omitted because its only positional /// is optional — covered by the no-positional variant. Setup is exercised /// even though most globals are no-ops there; the point is to lock in that @@ -718,12 +721,10 @@ use socket_patch_cli::args::GLOBAL_ARG_ENV_VARS as GLOBAL_ENV_VARS; #[serial_test::serial] fn empty_nonbool_env_vars_do_not_crash_the_binary() { let tmp = tempfile::tempdir().expect("tempdir"); - let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_socket-patch")); + // Start from the hermetic env (no ambient SOCKET_* bleed into the + // child, telemetry off)… + let mut cmd = hermetic::binary_command(); cmd.current_dir(tmp.path()); - // Start from a clean slate (no ambient SOCKET_* bleed into the child)… - for var in GLOBAL_ENV_VARS { - cmd.env_remove(var); - } // …then export every non-bool global blank, the way `VAR=` does. for var in [ "SOCKET_CWD", @@ -739,8 +740,6 @@ fn empty_nonbool_env_vars_do_not_crash_the_binary() { ] { cmd.env(var, ""); } - // Keep the spawned process from attempting telemetry network calls. - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); let out = cmd .args(["list", "--json"]) diff --git a/crates/socket-patch-cli/tests/cli_parse_list.rs b/crates/socket-patch-cli/tests/cli_parse_list.rs index 93cbaa6e9..741db07d7 100644 --- a/crates/socket-patch-cli/tests/cli_parse_list.rs +++ b/crates/socket-patch-cli/tests/cli_parse_list.rs @@ -12,13 +12,13 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::process::Command; use clap::Parser; use socket_patch_cli::commands::list::{run, ListArgs}; #[path = "common/mod.rs"] mod common; +use common::hermetic::binary_command; use socket_patch_cli::{Cli, Commands}; use socket_patch_core::manifest::schema::{ PatchFileInfo, PatchManifest, PatchRecord, VulnerabilityInfo, @@ -273,7 +273,7 @@ fn missing_manifest_json_is_an_empty_success_via_binary() { // No manifest and no ledger is an empty project (normal for hosted // mode): `list --json` emits the success envelope with no events. let tmp = tempfile::tempdir().unwrap(); - let out = Command::new(env!("CARGO_BIN_EXE_socket-patch")) + let out = binary_command() .args(["list", "--cwd", tmp.path().to_str().unwrap(), "--json"]) .output() .expect("failed to execute socket-patch binary"); @@ -424,9 +424,12 @@ fn write_manifest_in(dir: &Path, manifest: &PatchManifest) { .unwrap(); } -/// Run `list` against the compiled binary with `--cwd ` plus extra args. +/// Run `list` against the compiled binary with `--cwd ` plus extra +/// args, in the hermetic env: ambient developer/CI configuration +/// (SOCKET_SILENT, SOCKET_JSON, tokens…) can't change the branch under +/// test, and telemetry stays off so the test stays offline. fn run_list_binary(cwd: &Path, extra: &[&str]) -> std::process::Output { - Command::new(env!("CARGO_BIN_EXE_socket-patch")) + binary_command() .arg("list") .arg("--cwd") .arg(cwd) @@ -852,26 +855,12 @@ fn absolute_manifest_path_content_wins_over_cwd_via_binary() { // found in manifest." line for an empty manifest. // --------------------------------------------------------------------------- -/// Like [`run_list_binary`] but with every `GlobalArgs` env var scrubbed, -/// so ambient developer/CI configuration (SOCKET_SILENT, SOCKET_JSON, -/// tokens…) can't change the branch under test, and telemetry disabled so -/// the test stays offline. -fn run_list_binary_scrubbed(cwd: &Path, extra: &[&str]) -> std::process::Output { - let mut cmd = Command::new(env!("CARGO_BIN_EXE_socket-patch")); - cmd.arg("list").arg("--cwd").arg(cwd).args(extra); - for var in socket_patch_cli::args::GLOBAL_ARG_ENV_VARS { - cmd.env_remove(var); - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); - cmd.output().expect("failed to execute socket-patch binary") -} - #[test] fn silent_suppresses_human_listing_via_binary() { let tmp = tempfile::tempdir().unwrap(); write_manifest_in(tmp.path(), &populated_manifest()); - let out = run_list_binary_scrubbed(tmp.path(), &["--silent"]); + let out = run_list_binary(tmp.path(), &["--silent"]); let stdout = String::from_utf8_lossy(&out.stdout); assert_eq!( out.status.code(), @@ -885,7 +874,7 @@ fn silent_suppresses_human_listing_via_binary() { // Control run: the same manifest WITHOUT --silent must print the table — // otherwise the assertion above passes vacuously. - let loud = run_list_binary_scrubbed(tmp.path(), &[]); + let loud = run_list_binary(tmp.path(), &[]); assert_eq!(loud.status.code(), Some(0)); assert!( String::from_utf8_lossy(&loud.stdout).contains("Package: pkg:npm/test-pkg@1.0.0"), @@ -898,7 +887,7 @@ fn silent_suppresses_no_patches_message_via_binary() { let tmp = tempfile::tempdir().unwrap(); write_manifest_in(tmp.path(), &PatchManifest::new()); - let out = run_list_binary_scrubbed(tmp.path(), &["--silent"]); + let out = run_list_binary(tmp.path(), &["--silent"]); let stdout = String::from_utf8_lossy(&out.stdout); assert_eq!( out.status.code(), @@ -918,7 +907,7 @@ fn silent_does_not_mute_json_envelope_via_binary() { let tmp = tempfile::tempdir().unwrap(); write_manifest_in(tmp.path(), &populated_manifest()); - let out = run_list_binary_scrubbed(tmp.path(), &["--silent", "--json"]); + let out = run_list_binary(tmp.path(), &["--silent", "--json"]); assert_eq!(out.status.code(), Some(0)); let v: serde_json::Value = serde_json::from_str(String::from_utf8_lossy(&out.stdout).trim()) .expect("--silent --json must still print the JSON envelope"); @@ -932,7 +921,7 @@ fn silent_empty_project_prints_nothing_via_binary() { // nothing and exits 0. let tmp = tempfile::tempdir().unwrap(); - let out = run_list_binary_scrubbed(tmp.path(), &["--silent"]); + let out = run_list_binary(tmp.path(), &["--silent"]); assert_eq!(out.status.code(), Some(0), "an empty project exits 0"); assert!(out.stdout.is_empty() && out.stderr.is_empty(), "{out:?}"); } @@ -1385,7 +1374,7 @@ fn silent_suppresses_hosted_listing_via_binary() { let tmp = tempfile::tempdir().unwrap(); write_hosted_lock(tmp.path(), &[(HOSTED_PURL, HOSTED_UUID)]); - let out = run_list_binary_scrubbed(tmp.path(), &["--silent"]); + let out = run_list_binary(tmp.path(), &["--silent"]); assert_eq!( out.status.code(), Some(0), @@ -1398,7 +1387,7 @@ fn silent_suppresses_hosted_listing_via_binary() { "--silent must suppress the hosted listing; got {stdout:?}" ); // Control: the same project without --silent prints the pin. - let loud = run_list_binary_scrubbed(tmp.path(), &[]); + let loud = run_list_binary(tmp.path(), &[]); assert!( String::from_utf8_lossy(&loud.stdout).contains(&format!("Package: {HOSTED_PURL}")), "non-silent run must print the listing" @@ -1418,7 +1407,7 @@ fn silent_gates_the_malformed_ledger_warning_via_binary() { std::fs::write(vendor_dir.join("redirect-state.json"), "{ torn ledger").unwrap(); // Control: without --silent the corruption is surfaced. - let loud = run_list_binary_scrubbed(tmp.path(), &[]); + let loud = run_list_binary(tmp.path(), &[]); assert_eq!(loud.status.code(), Some(0), "manifest still lists"); assert!( String::from_utf8_lossy(&loud.stderr).contains("malformed"), @@ -1428,7 +1417,7 @@ fn silent_gates_the_malformed_ledger_warning_via_binary() { ); // --silent: the warning is muted, the exit code unchanged. - let out = run_list_binary_scrubbed(tmp.path(), &["--silent"]); + let out = run_list_binary(tmp.path(), &["--silent"]); assert_eq!(out.status.code(), Some(0)); assert!( String::from_utf8_lossy(&out.stderr).trim().is_empty(), @@ -1693,7 +1682,7 @@ fn silent_gates_the_malformed_vendor_ledger_warning_via_binary() { std::fs::create_dir_all(&vendor_dir).unwrap(); std::fs::write(vendor_dir.join("state.json"), "{ torn ledger").unwrap(); - let loud = run_list_binary_scrubbed(tmp.path(), &[]); + let loud = run_list_binary(tmp.path(), &[]); assert_eq!(loud.status.code(), Some(0), "manifest still lists"); assert!( String::from_utf8_lossy(&loud.stderr).contains("unreadable vendor ledger"), @@ -1702,7 +1691,7 @@ fn silent_gates_the_malformed_vendor_ledger_warning_via_binary() { String::from_utf8_lossy(&loud.stderr) ); - let out = run_list_binary_scrubbed(tmp.path(), &["--silent"]); + let out = run_list_binary(tmp.path(), &["--silent"]); assert_eq!(out.status.code(), Some(0)); assert!( String::from_utf8_lossy(&out.stderr).trim().is_empty(), @@ -1785,7 +1774,7 @@ async fn list_telemetry_counts_manifest_patches_only_via_binary() { ], ); - let mut cmd = Command::new(env!("CARGO_BIN_EXE_socket-patch")); + let mut cmd = binary_command(); cmd.arg("list") .arg("--cwd") .arg(tmp.path()) @@ -1794,12 +1783,8 @@ async fn list_telemetry_counts_manifest_patches_only_via_binary() { .arg("sktsec_telemetry_test") .arg("--org") .arg("test-org"); - for var in socket_patch_cli::args::GLOBAL_ARG_ENV_VARS { - cmd.env_remove(var); - } cmd.env("SOCKET_TELEMETRY_DISABLED", "0"); cmd.env("SOCKET_API_URL", server.uri()); - cmd.env("SOCKET_NO_UPDATE_CHECK", "1"); let out = cmd.output().expect("run socket-patch binary"); assert_eq!( out.status.code(), diff --git a/crates/socket-patch-cli/tests/cli_remove_silent.rs b/crates/socket-patch-cli/tests/cli_remove_silent.rs index 6adbdbcba..0edb4f268 100644 --- a/crates/socket-patch-cli/tests/cli_remove_silent.rs +++ b/crates/socket-patch-cli/tests/cli_remove_silent.rs @@ -14,12 +14,9 @@ #[path = "common/mod.rs"] mod common; -use common::binary; +use common::{binary, hermetic_command}; use std::path::{Path, PathBuf}; -use std::process::Command; - -use socket_patch_cli::args::GLOBAL_ARG_ENV_VARS; const ONE_PATCH_MANIFEST: &str = r#"{ "patches": { @@ -46,13 +43,8 @@ fn make_socket_dir(root: &Path) -> PathBuf { /// so ambient developer/CI configuration (tokens, silent toggles) can't /// change the branch under test. fn run_remove(cwd: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.arg("remove").args(args).current_dir(cwd); - for var in GLOBAL_ARG_ENV_VARS { - cmd.env_remove(var); - } - cmd.env_remove("SOCKET_SKIP_ROLLBACK"); - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); let out = cmd.output().expect("run socket-patch remove"); ( out.status.code().unwrap_or(-1), diff --git a/crates/socket-patch-cli/tests/cli_scan_silent.rs b/crates/socket-patch-cli/tests/cli_scan_silent.rs index c0dc62c0a..2b9e0b31e 100644 --- a/crates/socket-patch-cli/tests/cli_scan_silent.rs +++ b/crates/socket-patch-cli/tests/cli_scan_silent.rs @@ -17,19 +17,16 @@ #[path = "common/mod.rs"] mod common; -use common::{binary, git_sha256}; +use common::{binary, git_sha256, hermetic_command}; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; use std::path::Path; -use std::process::Command; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; -use socket_patch_cli::args::GLOBAL_ARG_ENV_VARS; - const ORG_SLUG: &str = "test-org"; const UUID: &str = "11111111-1111-4111-8111-111111111111"; @@ -56,14 +53,8 @@ fn write_npm_package(root: &Path, name: &str, version: &str, content: &[u8]) { /// so ambient developer/CI configuration (tokens, silent toggles) can't /// change the branch under test. fn run_scan(cwd: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.arg("scan").args(args).current_dir(cwd); - for var in GLOBAL_ARG_ENV_VARS { - cmd.env_remove(var); - } - cmd.env_remove("SOCKET_BATCH_SIZE"); - cmd.env_remove("SOCKET_ALL_RELEASES"); - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); let out = cmd.output().expect("run socket-patch scan"); ( out.status.code().unwrap_or(-1), diff --git a/crates/socket-patch-cli/tests/cli_sigpipe.rs b/crates/socket-patch-cli/tests/cli_sigpipe.rs index cba356cda..b0a9cf16e 100644 --- a/crates/socket-patch-cli/tests/cli_sigpipe.rs +++ b/crates/socket-patch-cli/tests/cli_sigpipe.rs @@ -16,9 +16,11 @@ #![cfg(unix)] use std::os::unix::process::ExitStatusExt; -use std::process::{Command, Stdio}; +use std::process::Stdio; + +#[path = "common/hermetic.rs"] +mod hermetic; -const BINARY: &str = env!("CARGO_BIN_EXE_socket-patch"); /// `libc::SIGPIPE`, inlined so the test crate needs no libc dependency. const SIGPIPE: i32 = 13; @@ -38,16 +40,13 @@ fn closed_stdout_pipe_is_not_a_panic() { let (reader, writer) = std::io::pipe().expect("pipe"); drop(reader); - let mut cmd = Command::new(BINARY); + // Hermetic env: ambient SOCKET_* vars can never perturb the invocation + // (the assertion is about stdout plumbing). + let mut cmd = hermetic::binary_command(); cmd.arg("list") .current_dir(dir.path()) .stdout(Stdio::from(writer)) .stderr(Stdio::piped()); - // Scrub the global env-var surface so ambient SOCKET_* vars can never - // perturb the invocation (the assertion is about stdout plumbing). - for var in socket_patch_cli::args::GLOBAL_ARG_ENV_VARS { - cmd.env_remove(var); - } let out = cmd.output().expect("spawn socket-patch"); let stderr = String::from_utf8_lossy(&out.stderr); diff --git a/crates/socket-patch-cli/tests/common/hermetic.rs b/crates/socket-patch-cli/tests/common/hermetic.rs index cbc15b1f8..b0e0b9538 100644 --- a/crates/socket-patch-cli/tests/common/hermetic.rs +++ b/crates/socket-patch-cli/tests/common/hermetic.rs @@ -35,6 +35,7 @@ use std::process::Command; /// developer's ambient shell, which the suites can't rely on) turns the tests /// red immediately. const HOSTILE_SEEDS: &[(&str, &str)] = &[ + ("SOCKET_CWD", "/nonexistent"), ("SOCKET_GLOBAL", "true"), ("SOCKET_GLOBAL_PREFIX", "/nonexistent"), ("SOCKET_DRY_RUN", "true"), @@ -48,11 +49,12 @@ const HOSTILE_SEEDS: &[(&str, &str)] = &[ ]; /// A `Command` for `bin` with the hermetic `SOCKET_*` environment: the -/// hostile seeds scrubbed, every other ambient `SOCKET_*` removed (removing -/// `SOCKET_API_TOKEN` also forces the public proxy), and the three opt-outs -/// (config layer, update notifier, telemetry) forced on. Callers add args, -/// cwd and their own env afterwards; caller env lands last, so explicit -/// injections survive the scrub. +/// hostile seeds scrubbed, every flag-bound `SOCKET_*` and every other +/// ambient `SOCKET_*` removed (removing `SOCKET_API_TOKEN` also forces the +/// public proxy), and the three opt-outs (config layer, update notifier, +/// telemetry) forced on. Callers add args, cwd and their own env +/// afterwards; caller env lands last, so explicit injections survive the +/// scrub. pub fn command(bin: &Path) -> Command { let mut cmd = Command::new(bin); for (k, v) in HOSTILE_SEEDS { @@ -61,6 +63,16 @@ pub fn command(bin: &Path) -> Command { for (k, _) in HOSTILE_SEEDS { cmd.env_remove(k); } + // The flag-bound vars are removed by name, not only when the sweep below + // finds them: in-process tests in the same binary set some of them + // (`apply_env_toggles`, env save/restore helpers), and one set between + // this sweep and the spawn would otherwise reach the child. + for k in socket_patch_cli::args::GLOBAL_ARG_ENV_VARS + .iter() + .chain(socket_patch_cli::args::LOCAL_ARG_ENV_VARS) + { + cmd.env_remove(k); + } cmd.env_remove("SOCKET_API_TOKEN"); scrub_socket_vars(&mut cmd); // Belt-and-braces on top of the `.cargo/config.toml` `[env]` default: diff --git a/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs b/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs index 9261d673f..aa56f3799 100644 --- a/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs +++ b/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs @@ -22,23 +22,16 @@ #[path = "common/mod.rs"] mod common; -use common::binary; +use common::{binary, hermetic_command}; use std::path::Path; -use std::process::Command; - -use socket_patch_cli::args::GLOBAL_ARG_ENV_VARS; /// Run `socket-patch apply` in `cwd` with a scrubbed SOCKET_* environment /// so ambient developer/CI configuration (tokens, silent toggles) can't /// change the branch under test. fn run_apply(cwd: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.arg("apply").args(args).current_dir(cwd); - for var in GLOBAL_ARG_ENV_VARS { - cmd.env_remove(var); - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); let out = cmd.output().expect("run socket-patch apply"); ( out.status.code().unwrap_or(-1), diff --git a/crates/socket-patch-cli/tests/coverage_fix_scan_hosted_dryrun_vendored.rs b/crates/socket-patch-cli/tests/coverage_fix_scan_hosted_dryrun_vendored.rs index e5ead69e6..b530d7569 100644 --- a/crates/socket-patch-cli/tests/coverage_fix_scan_hosted_dryrun_vendored.rs +++ b/crates/socket-patch-cli/tests/coverage_fix_scan_hosted_dryrun_vendored.rs @@ -12,6 +12,8 @@ #[path = "common/envelope.rs"] mod envelope; +#[path = "common/hermetic.rs"] +mod hermetic; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; use envelope::codes_in; @@ -216,10 +218,10 @@ fn seed_manifest_and_blob(root: &Path) { std::fs::write(socket.join("blobs").join(after_hash), PATCHED_INDEX).unwrap(); } -/// Run the built binary with ambient `SOCKET_*` scrubbed; `(code, stdout, -/// stderr)`. +/// Run the built binary in the hermetic env (ambient `SOCKET_*` scrubbed, +/// telemetry off); `(code, stdout, stderr)`. fn run_cli(cwd: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_socket-patch")); + let mut cmd = hermetic::binary_command(); let fixture = (args.first() == Some(&"vendor")).then(|| prebuilt_common::Server::project(cwd)); let args: Vec<_> = args .iter() @@ -227,12 +229,6 @@ fn run_cli(cwd: &Path, args: &[&str]) -> (i32, String, String) { .filter(|arg| fixture.is_none() || *arg != "--offline") .collect(); 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("SOCKET_TELEMETRY_DISABLED", "1"); // Isolate the npm config layers the hosted `.npmrc` auto-config // consults (an ambient explicit `allow-remote` would change the plan). let absent = cwd.join(".absent-npm-config"); diff --git a/crates/socket-patch-cli/tests/coverage_fix_vendor_silent_mute_exit.rs b/crates/socket-patch-cli/tests/coverage_fix_vendor_silent_mute_exit.rs index eec277448..7a3e20860 100644 --- a/crates/socket-patch-cli/tests/coverage_fix_vendor_silent_mute_exit.rs +++ b/crates/socket-patch-cli/tests/coverage_fix_vendor_silent_mute_exit.rs @@ -22,23 +22,16 @@ #[path = "common/mod.rs"] mod common; -use common::binary; +use common::{binary, hermetic_command}; use std::path::Path; -use std::process::Command; - -use socket_patch_cli::args::GLOBAL_ARG_ENV_VARS; /// Run `socket-patch vendor` in `cwd` with a scrubbed SOCKET_* environment /// so ambient developer/CI configuration (tokens, silent toggles) can't /// change the branch under test. fn run_vendor(cwd: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.arg("vendor").args(args).current_dir(cwd); - for var in GLOBAL_ARG_ENV_VARS { - cmd.env_remove(var); - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); let out = cmd.output().expect("run socket-patch vendor"); ( out.status.code().unwrap_or(-1), diff --git a/crates/socket-patch-cli/tests/covgap_commands_rollback.rs b/crates/socket-patch-cli/tests/covgap_commands_rollback.rs index 5cfae05cc..4aa75591d 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_rollback.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_rollback.rs @@ -248,37 +248,11 @@ fn readonly_dir_enforced(dir: &Path) -> bool { /// Spawn the binary WITHOUT waiting (the lock-choreography tests mutate /// the fixture while the child is blocked on the apply lock), with the -/// exact same seed-then-scrub environment as `common::run`. +/// same hermetic `SOCKET_*` environment as `common::run` (not its JVM +/// isolation; no test here spawns a JVM fixture this way). fn spawn_scrubbed(cwd: &Path, args: &[&str]) -> std::process::Child { - let mut cmd = std::process::Command::new(common::binary()); + let mut cmd = common::hermetic_command(&common::binary()); cmd.args(args).current_dir(cwd); - cmd.env("SOCKET_GLOBAL", "true") - .env("SOCKET_GLOBAL_PREFIX", "/nonexistent") - .env("SOCKET_DRY_RUN", "true") - .env("SOCKET_MANIFEST_PATH", "/nonexistent/manifest.json") - .env("SOCKET_JSON", "true") - .env("SOCKET_SILENT", "true") - .env("SOCKET_VERBOSE", "true") - .env_remove("SOCKET_GLOBAL") - .env_remove("SOCKET_GLOBAL_PREFIX") - .env_remove("SOCKET_DRY_RUN") - .env_remove("SOCKET_MANIFEST_PATH") - .env_remove("SOCKET_JSON") - .env_remove("SOCKET_SILENT") - .env_remove("SOCKET_VERBOSE") - .env_remove("SOCKET_API_TOKEN"); - 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" - && name != "SOCKET_NO_UPDATE_CHECK" - { - cmd.env_remove(&key); - } - } - cmd.env("SOCKET_NO_CONFIG", "1"); - cmd.env("SOCKET_NO_UPDATE_CHECK", "1"); cmd.stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()); cmd.spawn().expect("spawn socket-patch binary") diff --git a/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs b/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs index 432b11996..e041a3b48 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs @@ -34,7 +34,7 @@ #[path = "common/mod.rs"] mod common; -use common::{binary, git_sha256}; +use common::{binary, git_sha256, hermetic_command}; #[path = "common/pty_io.rs"] mod pty_io; @@ -43,13 +43,10 @@ mod vlt_hosted_common; #[path = "vlt_hosted_common/vendored.rs"] mod vlt_vendored; use std::path::Path; -use std::process::Command; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; -use socket_patch_cli::args::{GLOBAL_ARG_ENV_VARS, LOCAL_ARG_ENV_VARS}; - const ORG_SLUG: &str = "test-org"; const UUID: &str = "11111111-1111-4111-8111-111111111111"; const OLD_UUID: &str = "99999999-9999-4999-8999-999999999999"; @@ -73,19 +70,15 @@ fn write_npm_package(root: &Path, name: &str, version: &str, content: &[u8]) { std::fs::write(pkg_dir.join("index.js"), content).unwrap(); } -/// Run `socket-patch scan` in `cwd` with the whole flag-bound `SOCKET_*` -/// environment scrubbed, so the CLI arguments are the sole source of truth -/// (telemetry stays disabled so no run phones home). +/// Run `socket-patch scan` in `cwd` in the hermetic env, so the CLI +/// arguments are the sole source of truth (telemetry stays disabled so no +/// run phones home). fn run_scan(cwd: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = Command::new(binary()); + let mut cmd = hermetic_command(&binary()); cmd.arg("scan").args(args).current_dir(cwd); - for var in GLOBAL_ARG_ENV_VARS.iter().chain(LOCAL_ARG_ENV_VARS.iter()) { - cmd.env_remove(var); - } // The unconditional python crawl consults VIRTUAL_ENV first, so an // activated host virtualenv would make "empty project" scans non-empty. cmd.env_remove("VIRTUAL_ENV"); - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); let out = cmd.output().expect("run socket-patch scan"); ( out.status.code().unwrap_or(-1), diff --git a/crates/socket-patch-cli/tests/covgap_commands_vendor.rs b/crates/socket-patch-cli/tests/covgap_commands_vendor.rs index 9f8b51f65..751668fdb 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_vendor.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_vendor.rs @@ -13,6 +13,8 @@ #[path = "common/envelope.rs"] mod envelope; +#[path = "common/hermetic.rs"] +mod hermetic; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; use envelope::{events, find_event}; @@ -182,10 +184,10 @@ fn vendor_args(cwd: &Path) -> VendorArgs { // ───────────────────────── subprocess runner ───────────────────────── -/// Run the built `socket-patch` binary with every ambient `SOCKET_*` env var -/// scrubbed from the child and telemetry hard-disabled. +/// Run the built `socket-patch` binary in the hermetic env (every ambient +/// `SOCKET_*` env var scrubbed from the child, telemetry hard-disabled). fn run_cli(cwd: &Path, args: &[&str], extra_env: &[(&str, &str)]) -> (i32, String, String) { - let mut cmd = Command::new(env!("CARGO_BIN_EXE_socket-patch")); + let mut cmd = hermetic::binary_command(); let fixture = (!args .iter() .any(|a| matches!(*a, "--api-url" | "--vendor-url")) @@ -203,24 +205,6 @@ fn run_cli(cwd: &Path, args: &[&str], extra_env: &[(&str, &str)]) -> (i32, Strin } } cmd.args(&filtered).current_dir(cwd); - for (key, _) in std::env::vars() { - if key.starts_with("SOCKET_") && key != "SOCKET_NO_CONFIG" { - cmd.env_remove(key); - } - } - // In-process tests in this binary `std::env::set_var` these via - // `apply_env_toggles`; one set by a parallel test between the scan - // above and the spawn would be inherited, so remove them - // unconditionally (see in_process_vendor.rs `run_cli`). - for key in [ - "SOCKET_OFFLINE", - "SOCKET_DEBUG", - "SOCKET_API_URL", - "SOCKET_PROXY_URL", - ] { - cmd.env_remove(key); - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); for (k, v) in extra_env { cmd.env(k, v); } @@ -763,13 +747,10 @@ fn npm_only_human_vendor_does_not_read_composer_lock() { .unwrap() .success()); let fixture = prebuilt_common::Server::project(fx.root()); - let mut child = Command::new(env!("CARGO_BIN_EXE_socket-patch")) + let mut child = hermetic::binary_command() .env("SOCKET_VENDOR_URL", &fixture.uri) .args(["vendor", "--cwd", fx.root().to_str().unwrap()]) .current_dir(fx.root()) - .env("SOCKET_TELEMETRY_DISABLED", "1") - .env_remove("SOCKET_OFFLINE") - .env_remove("SOCKET_API_URL") .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) diff --git a/crates/socket-patch-cli/tests/covgap_commands_vex.rs b/crates/socket-patch-cli/tests/covgap_commands_vex.rs index c4235aecc..d00716687 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_vex.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_vex.rs @@ -12,7 +12,7 @@ #[path = "common/mod.rs"] mod common; -use common::binary; +use common::{binary, hermetic_command}; use std::collections::HashMap; use std::path::Path; @@ -37,20 +37,13 @@ const UUID: &str = "9f6b2c4e-1d3a-4f6b-8c2d-7e5a9b1c3d5f"; /// `e2e_vex_vendor.rs`. const STALE_OPENVEX_DOC: &str = r#"{"@context":"https://openvex.dev/ns/v0.2.0","@id":"urn:uuid:stale","author":"Socket","timestamp":"2020-01-01T00:00:00Z","version":1,"statements":[]}"#; -/// CLI invocation with the ambient `SOCKET_*` environment scrubbed (same +/// CLI invocation in the hermetic env: ambient `SOCKET_*` scrubbed (same /// rationale as `e2e_vex.rs`: explicit flags must be the sole source of -/// truth; the parent env is never mutated so tests need no serialization). +/// truth; the parent env is never mutated so tests need no serialization) +/// and telemetry off (`vex` failures otherwise POST a real event to the +/// production proxy endpoint on every run). fn cli() -> Command { - let mut cmd = Command::new(binary()); - for (key, _) in std::env::vars() { - if key.starts_with("SOCKET_") && key != "SOCKET_NO_CONFIG" { - cmd.env_remove(key); - } - } - // Keep telemetry off: `vex` failures otherwise POST a real event to the - // production proxy endpoint on every run. - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); - cmd + hermetic_command(&binary()) } /// Write `manifest` to `/.socket/manifest.json`. diff --git a/crates/socket-patch-cli/tests/covgap_utils_socket_cli_config.rs b/crates/socket-patch-cli/tests/covgap_utils_socket_cli_config.rs index cb93b7246..15a6ec773 100644 --- a/crates/socket-patch-cli/tests/covgap_utils_socket_cli_config.rs +++ b/crates/socket-patch-cli/tests/covgap_utils_socket_cli_config.rs @@ -19,7 +19,8 @@ use base64::Engine as _; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; -const BINARY: &str = env!("CARGO_BIN_EXE_socket-patch"); +#[path = "common/hermetic.rs"] +mod hermetic; /// The platform env var that positions the socket-cli data dir. const DATA_DIR_VAR: &str = if cfg!(windows) { @@ -43,24 +44,17 @@ fn write_config(data_dir: &Path, json: &serde_json::Value) { std::fs::write(dir.join("config.json"), encoded).unwrap(); } -/// Build a hermetic `socket-patch scan --json -e npm` command: every -/// ambient `SOCKET_*` var is scrubbed (including the workspace-level -/// `SOCKET_NO_CONFIG=1` guard — re-enabled with an explicit falsy value), -/// the data dir points at `data_dir`, telemetry stays off. +/// Build a hermetic `socket-patch scan --json -e npm` command: the shared +/// hermetic env (every ambient `SOCKET_*` scrubbed, telemetry off) with the +/// config layer re-enabled by an explicit falsy `SOCKET_NO_CONFIG`, and +/// the data dir pointed at `data_dir`. fn scan_cmd(project: &Path, data_dir: &Path) -> Command { - let mut cmd = Command::new(BINARY); + let mut cmd = hermetic::binary_command(); cmd.args(["scan", "--json", "-e", "npm", "--cwd"]) .arg(project); - for (key, _) in std::env::vars_os() { - let name = key.to_string_lossy(); - if name.starts_with("SOCKET_") { - cmd.env_remove(&key); - } - } cmd.env_remove("VIRTUAL_ENV"); cmd.env(DATA_DIR_VAR, data_dir); cmd.env("SOCKET_NO_CONFIG", "0"); - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); cmd } diff --git a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs index 007776399..234371a09 100644 --- a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs +++ b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs @@ -30,26 +30,6 @@ use serial_test::serial; /// scrubs by hand. const PENDING_RAW_SPAWNS: &[&str] = &[ "apply/apply_network.rs", - "cli/covgap_api_client.rs", - "cli/covgap_commands_list.rs", - "cli/telemetry_e2e.rs", - "cli_apply_silent.rs", - "cli_argv_non_utf8.rs", - "cli_config_fallback.rs", - "cli_get_silent.rs", - "cli_global_args.rs", - "cli_parse_list.rs", - "cli_remove_silent.rs", - "cli_scan_silent.rs", - "cli_sigpipe.rs", - "coverage_fix_apply_silent_mute_exit.rs", - "coverage_fix_scan_hosted_dryrun_vendored.rs", - "coverage_fix_vendor_silent_mute_exit.rs", - "covgap_commands_rollback.rs", - "covgap_commands_scan_mod.rs", - "covgap_commands_vendor.rs", - "covgap_commands_vex.rs", - "covgap_utils_socket_cli_config.rs", "e2e_cargo.rs", "e2e_composer.rs", "e2e_composer_version_identity.rs", @@ -362,6 +342,7 @@ fn workflow_env_copies_carry_every_opt_out() { fn command_never_leaks_its_hostile_seeds() { let env = effective_env(&hermetic::command(Path::new("socket-patch"))); for key in [ + "SOCKET_CWD", "SOCKET_GLOBAL", "SOCKET_GLOBAL_PREFIX", "SOCKET_DRY_RUN", @@ -377,6 +358,34 @@ fn command_never_leaks_its_hostile_seeds() { } } +/// In-process tests set flag-bound vars in the test binary's own +/// environment (`apply_env_toggles`, save/restore helpers). One set after +/// the builder's sweep but before the spawn must still not reach the child. +#[test] +#[serial] +fn command_drops_flag_bound_vars_set_after_it_is_built() { + let cmd = hermetic::command(Path::new("socket-patch")); + with_ambient( + &[ + ("SOCKET_OFFLINE", "true"), + ("SOCKET_DEBUG", "1"), + ("SOCKET_PROXY_URL", "http://ambient.invalid"), + ("SOCKET_SKIP_ROLLBACK", "true"), + ], + || { + let env = effective_env(&cmd); + for key in [ + "SOCKET_OFFLINE", + "SOCKET_DEBUG", + "SOCKET_PROXY_URL", + "SOCKET_SKIP_ROLLBACK", + ] { + assert_eq!(get(&env, key), None, "{key} set after build leaked"); + } + }, + ); +} + #[test] #[serial] fn caller_env_set_after_command_survives_the_scrub() {