diff --git a/crates/sharecli-core/src/speculation.rs b/crates/sharecli-core/src/speculation.rs index 399c12cd..a314ec34 100644 --- a/crates/sharecli-core/src/speculation.rs +++ b/crates/sharecli-core/src/speculation.rs @@ -57,6 +57,7 @@ pub struct SpeculationCandidate { /// /// Wrapped in `Arc>` so the background task can drain candidates /// without blocking the hot `Hypervisor::run` path. +#[derive(Default)] struct Inner { /// CommandKey → (hit count, first-seen instant). hits: HashMap, @@ -64,6 +65,7 @@ struct Inner { requests: HashMap, } +#[derive(Default)] pub struct SpeculationTracker { inner: Mutex, } @@ -71,7 +73,7 @@ pub struct SpeculationTracker { impl SpeculationTracker { /// Create a new, empty tracker. pub fn new() -> Self { - Self { inner: Mutex::new(Inner { hits: HashMap::new(), requests: HashMap::new() }) } + Self::default() } /// Record one cache hit for `key`. @@ -121,8 +123,10 @@ impl SpeculationTracker { .map(|(key, (count, _))| (*count, key.clone())) .collect(); - // Highest frequency first. - scored.sort_by(|a, b| b.0.cmp(&a.0)); + // Highest frequency first, then stable key order for deterministic truncation. + scored.sort_by(|(left_count, left_key), (right_count, right_key)| { + right_count.cmp(left_count).then_with(|| left_key.cmp(right_key)) + }); scored.truncate(SPECULATION_MAX_CANDIDATES); let mut candidates = Vec::new(); @@ -320,7 +324,11 @@ mod tests { } let candidates = tracker.drain_candidates().await; - assert!(candidates.len() <= SPECULATION_MAX_CANDIDATES, "must not exceed max candidates"); + assert_eq!(candidates.len(), SPECULATION_MAX_CANDIDATES, "must cap candidates"); + let keys: Vec<_> = candidates.into_iter().map(|candidate| candidate.key.0).collect(); + let expected: Vec<_> = + (0..SPECULATION_MAX_CANDIDATES).map(|i| format!("key-{i:04}")).collect(); + assert_eq!(keys, expected, "equal counts must use key order before truncation"); } #[tokio::test] diff --git a/crates/sharecli-mesh/src/worktree_pool.rs b/crates/sharecli-mesh/src/worktree_pool.rs index eb900ac2..5b94553e 100644 --- a/crates/sharecli-mesh/src/worktree_pool.rs +++ b/crates/sharecli-mesh/src/worktree_pool.rs @@ -9,6 +9,32 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::Mutex; +/// Git variables that bind a subprocess to the caller's repository. +/// +/// Git exports these to hooks. Every command in this module intentionally +/// operates on an explicit foreign checkout, so inheriting them would let a +/// hook test inspect or mutate the hook's repository instead. +const GIT_LOCAL_ENV_VARS: &[&str] = &[ + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_CONFIG", + "GIT_CONFIG_PARAMETERS", + "GIT_CONFIG_COUNT", + "GIT_OBJECT_DIRECTORY", + "GIT_DIR", + "GIT_WORK_TREE", + "GIT_IMPLICIT_WORK_TREE", + "GIT_GRAFT_FILE", + "GIT_INDEX_FILE", + "GIT_NO_REPLACE_OBJECTS", + "GIT_REPLACE_REF_BASE", + "GIT_PREFIX", + "GIT_SHALLOW_FILE", + "GIT_COMMON_DIR", + // `git rev-parse --local-env-vars` does not report this ref namespace, + // but foreign worktree operations must never inherit the caller's scope. + "GIT_NAMESPACE", +]; + /// Error from worktree pool operations. #[derive(Debug, thiserror::Error)] pub enum WorktreePoolError { @@ -150,9 +176,8 @@ impl WorktreePool { } fn ensure_git_repo(path: &Path) -> Result<(), WorktreePoolError> { - let status = Command::new("git") + let status = git_command(path) .args(["rev-parse", "--git-dir"]) - .current_dir(path) .output() .map_err(|e| WorktreePoolError::Git(e.to_string()))?; if status.status.success() { @@ -163,11 +188,8 @@ fn ensure_git_repo(path: &Path) -> Result<(), WorktreePoolError> { } fn git(cwd: &Path, args: &[&str]) -> Result { - let out = Command::new("git") - .args(args) - .current_dir(cwd) - .output() - .map_err(|e| WorktreePoolError::Git(e.to_string()))?; + let out = + git_command(cwd).args(args).output().map_err(|e| WorktreePoolError::Git(e.to_string()))?; if out.status.success() { Ok(String::from_utf8_lossy(&out.stdout).into_owned()) } else { @@ -181,6 +203,15 @@ fn git(cwd: &Path, args: &[&str]) -> Result { } } +fn git_command(cwd: &Path) -> Command { + let mut command = Command::new("git"); + command.current_dir(cwd); + for variable in GIT_LOCAL_ENV_VARS { + command.env_remove(variable); + } + command +} + #[cfg(test)] mod tests { use super::*; @@ -221,4 +252,19 @@ mod tests { let err = WorktreePool::open(dir.path(), pool_dir.path()).expect_err("must fail"); assert!(matches!(err, WorktreePoolError::NotGitRepo(_))); } + + #[test] + fn git_command_clears_hook_repository_context() { + let command = git_command(Path::new("/tmp")); + for variable in GIT_LOCAL_ENV_VARS { + assert!( + command.get_envs().any(|(key, value)| key == *variable && value.is_none()), + "{variable} must not leak into a foreign git command" + ); + } + assert!( + command.get_envs().any(|(key, value)| key == "GIT_NAMESPACE" && value.is_none()), + "GIT_NAMESPACE must not leak into a foreign git command" + ); + } } diff --git a/src/main.rs b/src/main.rs index 991bb267..0eb26f8b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1512,6 +1512,7 @@ fn cli_version() -> Result<()> { println!("{splash}"); println!("sharecli {version}"); println!("shared CLI process manager"); + println!("Backbone-2 family (ASCII palette disabled)"); println!("(NO_COLOR set — ASCII palette disabled)"); } else { let splash = r#" diff --git a/src/runtime.rs b/src/runtime.rs index 0d2545c7..6100c8f8 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -848,7 +848,7 @@ mod tests { ); let info = pool.spawn(cmd, &args, None, None, None).await; - assert!(info.is_ok()); + assert!(info.is_ok(), "process-pool spawn failed: {info:?}"); let list = pool.list().await; assert!(!list.is_empty()); diff --git a/tests/fr006_proc_ndjson.rs b/tests/fr006_proc_ndjson.rs index 313e9fb6..22f4e8fe 100644 --- a/tests/fr006_proc_ndjson.rs +++ b/tests/fr006_proc_ndjson.rs @@ -5,20 +5,96 @@ //! AC-006.37 NDJSON agent rows include `state` (parity with flat `--json`, AC-006.32) use std::io::Read; -use std::process::{Command, Stdio}; +use std::process::{Child, Command, Stdio}; +use std::sync::{Arc, Mutex}; use std::thread; -use std::time::Duration; +use std::time::{Duration, Instant}; -fn watch_grace(normal: Duration, profiled: Duration) -> Duration { - matches!(std::env::var("SHARECLI_DHAT_PROFILE"), Ok(value) if value == "1") - .then_some(profiled) - .unwrap_or(normal) -} +const WATCH_DEADLINE: Duration = Duration::from_secs(45); fn bin() -> Command { Command::new(env!("CARGO_BIN_EXE_sharecli")) } +fn complete_ndjson_line_count(output: &str) -> usize { + output + .split_inclusive('\n') + .filter(|line| line.ends_with('\n') && !line.trim().is_empty()) + .count() +} + +fn drain_watch_until( + child: &mut Child, + mut ready: impl FnMut(&str, &str) -> bool, +) -> (String, String) { + let stdout = child.stdout.take().expect("piped stdout"); + let stderr = child.stderr.take().expect("piped stderr"); + let stdout_buf = Arc::new(Mutex::new(String::new())); + let stderr_buf = Arc::new(Mutex::new(String::new())); + let out_arc = Arc::clone(&stdout_buf); + let err_arc = Arc::clone(&stderr_buf); + let stdout_reader = thread::spawn(move || { + let mut chunk = [0u8; 16_384]; + let mut out = stdout; + loop { + match out.read(&mut chunk) { + Ok(0) | Err(_) => break, + Ok(n) => out_arc + .lock() + .expect("stdout lock") + .push_str(&String::from_utf8_lossy(&chunk[..n])), + } + } + }); + let stderr_reader = thread::spawn(move || { + let mut chunk = [0u8; 4096]; + let mut err = stderr; + loop { + match err.read(&mut chunk) { + Ok(0) | Err(_) => break, + Ok(n) => err_arc + .lock() + .expect("stderr lock") + .push_str(&String::from_utf8_lossy(&chunk[..n])), + } + } + }); + + let deadline = Instant::now() + WATCH_DEADLINE; + let mut early_exit = None; + while Instant::now() < deadline { + let stdout = stdout_buf.lock().expect("stdout lock").clone(); + let stderr = stderr_buf.lock().expect("stderr lock").clone(); + if ready(&stdout, &stderr) { + break; + } + if let Some(status) = child.try_wait().expect("check watch child") { + early_exit = Some(status); + break; + } + thread::sleep(Duration::from_millis(100)); + } + + if early_exit.is_none() { + let _ = child.kill(); + let _ = child.wait(); + } + let _ = stdout_reader.join(); + let _ = stderr_reader.join(); + let stdout = stdout_buf.lock().expect("stdout lock").clone(); + let stderr = stderr_buf.lock().expect("stderr lock").clone(); + if let Some(status) = early_exit { + panic!("watch child exited before readiness: {status}; stdout: {stdout}; stderr: {stderr}"); + } + (stdout, stderr) +} + +#[test] +fn complete_ndjson_line_count_requires_newline_delimited_objects() { + assert_eq!(complete_ndjson_line_count("{\"ts\":1}\n{\"ts\":2}\n"), 2); + assert_eq!(complete_ndjson_line_count("{\"ts\":1}\n{\"ts\":2}"), 1); +} + /// FR-006 / AC-006.18 — each watch refresh is a single parseable NDJSON line with `ts`. #[test] #[serial_test::serial] @@ -30,14 +106,8 @@ fn fr006_proc_watch_ndjson_one_line_per_refresh() { .spawn() .expect("spawn sharecli proc --json --watch 1"); - thread::sleep(watch_grace(Duration::from_secs(8), Duration::from_secs(25))); - let _ = child.kill(); - - let mut stdout = String::new(); - if let Some(mut out) = child.stdout.take() { - let _ = out.read_to_string(&mut stdout); - } - let _ = child.wait(); + let (stdout, _) = + drain_watch_until(&mut child, |stdout, _| complete_ndjson_line_count(stdout) >= 2); let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect(); assert!( @@ -63,18 +133,9 @@ fn fr006_proc_watch_ndjson_stdout_is_pipe_clean() { .spawn() .expect("spawn sharecli proc --json --watch 1"); - thread::sleep(watch_grace(Duration::from_secs(5), Duration::from_secs(12))); - let _ = child.kill(); - - let mut stdout = String::new(); - let mut stderr = String::new(); - if let Some(mut out) = child.stdout.take() { - let _ = out.read_to_string(&mut stdout); - } - if let Some(mut err) = child.stderr.take() { - let _ = err.read_to_string(&mut stderr); - } - let _ = child.wait(); + let (stdout, stderr) = drain_watch_until(&mut child, |stdout, stderr| { + complete_ndjson_line_count(stdout) >= 1 && stderr.contains("[watch]") + }); assert!( !stdout.contains("[watch]"), @@ -98,14 +159,8 @@ fn fr006_proc_watch_ndjson_agent_rows_include_state_key() { .spawn() .expect("spawn sharecli proc --json --watch 1"); - thread::sleep(watch_grace(Duration::from_millis(2_500), Duration::from_secs(12))); - let _ = child.kill(); - - let mut stdout = String::new(); - if let Some(mut out) = child.stdout.take() { - let _ = out.read_to_string(&mut stdout); - } - let _ = child.wait(); + let (stdout, _) = + drain_watch_until(&mut child, |stdout, _| complete_ndjson_line_count(stdout) >= 1); let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect(); let line = lines.first().copied().unwrap_or_else(|| { diff --git a/tests/fr007_health_pool_status_csv.rs b/tests/fr007_health_pool_status_csv.rs index f77acf14..9eb148cd 100644 --- a/tests/fr007_health_pool_status_csv.rs +++ b/tests/fr007_health_pool_status_csv.rs @@ -115,7 +115,7 @@ fn fr007_render_health_csv_body() { healthy: true, issues: vec![], gate: gate.clone(), - host_watch: host_watch.clone(), + host_watch, status: None, }; let health = HealthJson { diff --git a/tests/fr007_ipc_health_pool_status.rs b/tests/fr007_ipc_health_pool_status.rs index 4b6073f7..2edc2237 100644 --- a/tests/fr007_ipc_health_pool_status.rs +++ b/tests/fr007_ipc_health_pool_status.rs @@ -158,7 +158,7 @@ fn fr007_ipc_health_snapshot_pool_status_order() { total_memory_mb: 16384, healthy: true, gate: gate.clone(), - host_watch: host_watch.clone(), + host_watch, pool: PoolSnapshot { node_total: 2, node_idle: 1, @@ -168,7 +168,7 @@ fn fr007_ipc_health_snapshot_pool_status_order() { healthy: true, issues: vec![], gate: gate.clone(), - host_watch: host_watch.clone(), + host_watch, status: None, }, status: StatusSnapshot { diff --git a/tests/fr007_ipc_health_status_gate_host_watch.rs b/tests/fr007_ipc_health_status_gate_host_watch.rs index 6fdbb6b3..31119426 100644 --- a/tests/fr007_ipc_health_status_gate_host_watch.rs +++ b/tests/fr007_ipc_health_status_gate_host_watch.rs @@ -139,24 +139,23 @@ fn fr007_ipc_health_snapshot_gate_before_host_watch() { fn fr007_ipc_health_snapshot_wire_roundtrip() { use sharecli_ipc::handler::HealthSnapshot; - let raw = format!( - r#"{{"managed_processes":3,"used_memory_mb":2048,"total_memory_mb":16384, - "healthy":true,"gate":{{"thermal_pressure":"GREEN","detected_agents":0, - "agent_total_rss_bytes":0,"agent_contention":"OK","gate_decision":"ADMIT"}}, - "host_watch":{{"fd_count":1,"net_rx_bytes":2,"net_tx_bytes":3, - "mem_rss_bytes":4,"load_1m":0.5}}, - "pool":{{"node_total":0,"node_idle":0,"bun_total":0,"bun_idle":0,"max_per_type":4, + let raw = r#"{"managed_processes":3,"used_memory_mb":2048,"total_memory_mb":16384, + "healthy":true,"gate":{"thermal_pressure":"GREEN","detected_agents":0, + "agent_total_rss_bytes":0,"agent_contention":"OK","gate_decision":"ADMIT"}, + "host_watch":{"fd_count":1,"net_rx_bytes":2,"net_tx_bytes":3, + "mem_rss_bytes":4,"load_1m":0.5}, + "pool":{"node_total":0,"node_idle":0,"bun_total":0,"bun_idle":0,"max_per_type":4, "healthy":true,"issues":[], - "gate":{{"thermal_pressure":"GREEN","detected_agents":0, - "agent_total_rss_bytes":0,"agent_contention":"OK","gate_decision":"ADMIT"}}, - "host_watch":{{"fd_count":0,"net_rx_bytes":0,"net_tx_bytes":0, - "mem_rss_bytes":0,"load_1m":0.0}}}}, - "status":{{"total_processes":0,"agents":[],"scanned":0,"watched":0, - "gate":{{"thermal_pressure":"GREEN","detected_agents":0, - "agent_total_rss_bytes":0,"agent_contention":"OK","gate_decision":"ADMIT"}}, - "host_watch":{{"fd_count":0,"net_rx_bytes":0,"net_tx_bytes":0, - "mem_rss_bytes":0,"load_1m":0.0}}}}}}"# - ); + "gate":{"thermal_pressure":"GREEN","detected_agents":0, + "agent_total_rss_bytes":0,"agent_contention":"OK","gate_decision":"ADMIT"}, + "host_watch":{"fd_count":0,"net_rx_bytes":0,"net_tx_bytes":0, + "mem_rss_bytes":0,"load_1m":0.0}}, + "status":{"total_processes":0,"agents":[],"scanned":0,"watched":0, + "gate":{"thermal_pressure":"GREEN","detected_agents":0, + "agent_total_rss_bytes":0,"agent_contention":"OK","gate_decision":"ADMIT"}, + "host_watch":{"fd_count":0,"net_rx_bytes":0,"net_tx_bytes":0, + "mem_rss_bytes":0,"load_1m":0.0}}}"# + .to_string(); let h: HealthSnapshot = serde_json::from_str(&raw).expect("decode HealthSnapshot wire JSON"); assert_eq!(h.managed_processes, 3); assert_eq!(h.used_memory_mb, 2048); diff --git a/tests/fr007_ipc_monitoring_report_gate_host_watch.rs b/tests/fr007_ipc_monitoring_report_gate_host_watch.rs index 3f8de679..2658c7c5 100644 --- a/tests/fr007_ipc_monitoring_report_gate_host_watch.rs +++ b/tests/fr007_ipc_monitoring_report_gate_host_watch.rs @@ -110,7 +110,7 @@ fn fr007_ipc_monitoring_report_snapshot_gate_before_host_watch() { thread_count: None, }], gate: gate.clone(), - host_watch: host_watch.clone(), + host_watch, pool: PoolSnapshot { node_total: 2, node_idle: 1, @@ -120,7 +120,7 @@ fn fr007_ipc_monitoring_report_snapshot_gate_before_host_watch() { healthy: true, issues: vec![], gate: gate.clone(), - host_watch: host_watch.clone(), + host_watch, status: None, }, status: StatusSnapshot { diff --git a/tests/fr007_ipc_monitoring_report_pool_status.rs b/tests/fr007_ipc_monitoring_report_pool_status.rs index 50c1516f..553ea22e 100644 --- a/tests/fr007_ipc_monitoring_report_pool_status.rs +++ b/tests/fr007_ipc_monitoring_report_pool_status.rs @@ -110,7 +110,7 @@ fn fr007_ipc_monitoring_report_snapshot_pool_status_order() { thread_count: None, }], gate: gate.clone(), - host_watch: host_watch.clone(), + host_watch, pool: PoolSnapshot { node_total: 2, node_idle: 1, @@ -120,7 +120,7 @@ fn fr007_ipc_monitoring_report_snapshot_pool_status_order() { healthy: true, issues: vec![], gate: gate.clone(), - host_watch: host_watch.clone(), + host_watch, status: None, }, status: StatusSnapshot { diff --git a/tests/fr007_proc_csv_watch.rs b/tests/fr007_proc_csv_watch.rs index 7370522c..c9594fb9 100644 --- a/tests/fr007_proc_csv_watch.rs +++ b/tests/fr007_proc_csv_watch.rs @@ -24,27 +24,6 @@ const HOST_CSV_HEADER: &str = "record,fd_count,net_rx_bytes,net_tx_bytes,mem_rss const POOL_CSV_HEADER: &str = "record,node_total,node_idle,bun_total,bun_idle,max_per_type,healthy"; const STATUS_CSV_HEADER: &str = "record,scanned,watched,total_processes,agent_rows"; -fn drain_watch_pipes(child: &mut Child, dwell: Duration) -> (String, String) { - let stdout = child.stdout.take().expect("piped stdout"); - let stderr = child.stderr.take().expect("piped stderr"); - let stdout_reader = thread::spawn(move || { - let mut buf = String::new(); - let mut out = stdout; - let _ = out.read_to_string(&mut buf); - buf - }); - let stderr_reader = thread::spawn(move || { - let mut buf = String::new(); - let mut err = stderr; - let _ = err.read_to_string(&mut buf); - buf - }); - thread::sleep(dwell); - let _ = child.kill(); - let _ = child.wait(); - (stdout_reader.join().expect("stdout drain"), stderr_reader.join().expect("stderr drain")) -} - fn assert_csv_envelope(frame: &str, body_header: &str, context: &str) { let body = frame .find(body_header) @@ -78,7 +57,9 @@ fn fr007_proc_csv_watch_stderr_silent_and_envelope() { .spawn() .expect("spawn proc --csv --watch 1"); - let (stdout, stderr) = drain_watch_pipes(&mut child, Duration::from_millis(10_000)); + let (stdout, stderr) = drain_watch_until(&mut child, Duration::from_secs(45), |buf| { + complete_csv_frame_count(buf, FLAT_CSV_HEADER) >= 2 + }); assert!( stderr.is_empty(), @@ -91,7 +72,7 @@ fn fr007_proc_csv_watch_stderr_silent_and_envelope() { let complete_frames: Vec<&str> = stdout .split(FRAME_MARKER) .skip(1) - .filter(|frame| frame.contains(FLAT_CSV_HEADER)) + .filter(|frame| frame.contains(FLAT_CSV_HEADER) && frame.contains("[watch]")) .collect(); assert!( complete_frames.len() >= 2, @@ -118,7 +99,9 @@ fn fr007_proc_tree_csv_watch_stderr_silent_and_envelope() { .spawn() .expect("spawn proc --tree --csv --watch 1"); - let (stdout, stderr) = drain_watch_pipes(&mut child, Duration::from_millis(10_000)); + let (stdout, stderr) = drain_watch_until(&mut child, Duration::from_secs(45), |buf| { + complete_csv_frame_count(buf, TREE_CSV_HEADER) >= 2 + }); assert!( stderr.is_empty(), @@ -127,7 +110,7 @@ fn fr007_proc_tree_csv_watch_stderr_silent_and_envelope() { let complete_frames: Vec<&str> = stdout .split(FRAME_MARKER) .skip(1) - .filter(|frame| frame.contains(TREE_CSV_HEADER)) + .filter(|frame| frame.contains(TREE_CSV_HEADER) && frame.contains("[watch]")) .collect(); assert!( complete_frames.len() >= 2, @@ -189,22 +172,48 @@ fn drain_watch_until( } }); let deadline = Instant::now() + max_dwell; + let mut early_exit = None; while Instant::now() < deadline { let snapshot = stdout_buf.lock().expect("stdout lock").clone(); if ready(&snapshot) { break; } + if let Some(status) = child.try_wait().expect("check watch child") { + early_exit = Some(status); + break; + } thread::sleep(Duration::from_millis(100)); } - let _ = child.kill(); - let _ = child.wait(); + if early_exit.is_none() { + let _ = child.kill(); + let _ = child.wait(); + } let _ = stdout_reader.join(); let _ = stderr_reader.join(); let stdout = stdout_buf.lock().expect("stdout lock").clone(); let stderr = stderr_buf.lock().expect("stderr lock").clone(); + if let Some(status) = early_exit { + panic!("watch child exited before readiness: {status}; stdout: {stdout}; stderr: {stderr}"); + } (stdout, stderr) } +fn complete_csv_frame_count(output: &str, body_header: &str) -> usize { + output + .split(FRAME_MARKER) + .skip(1) + .filter(|frame| frame.contains(body_header) && frame.contains("[watch]")) + .count() +} + +#[test] +fn complete_csv_frame_count_requires_watch_footer() { + let partial = format!("{FRAME_MARKER}\n{FLAT_CSV_HEADER}\n"); + let complete = format!("{partial}# [watch] tick=1\n"); + assert_eq!(complete_csv_frame_count(&partial, FLAT_CSV_HEADER), 0); + assert_eq!(complete_csv_frame_count(&complete, FLAT_CSV_HEADER), 1); +} + /// FR-007 / AC-007.94 — `# [watch]` footer must flush in the same tick as the CSV body. #[test] #[serial_test::serial] diff --git a/tests/fr010_mesh_substrate.rs b/tests/fr010_mesh_substrate.rs index 47e62005..a59f3389 100644 --- a/tests/fr010_mesh_substrate.rs +++ b/tests/fr010_mesh_substrate.rs @@ -20,6 +20,24 @@ use std::path::Path; use std::process::Command; use tempfile::TempDir; +const GIT_LOCAL_ENV_VARS: &[&str] = &[ + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_CONFIG", + "GIT_CONFIG_PARAMETERS", + "GIT_CONFIG_COUNT", + "GIT_OBJECT_DIRECTORY", + "GIT_DIR", + "GIT_WORK_TREE", + "GIT_IMPLICIT_WORK_TREE", + "GIT_GRAFT_FILE", + "GIT_INDEX_FILE", + "GIT_NO_REPLACE_OBJECTS", + "GIT_REPLACE_REF_BASE", + "GIT_PREFIX", + "GIT_SHALLOW_FILE", + "GIT_COMMON_DIR", +]; + /// FR-010 / AC-010.1 — disconnected registry uses default mesh prefix. #[test] fn fr010_default_subject_prefix() { @@ -146,7 +164,12 @@ fn fr010_smart_merge_git_fallback_conflict() { fn git_init_with_commit(dir: &Path) { let run = |args: &[&str]| { - let st = Command::new("git").args(args).current_dir(dir).output().expect("git"); + let mut command = Command::new("git"); + command.args(args).current_dir(dir); + for variable in GIT_LOCAL_ENV_VARS { + command.env_remove(variable); + } + let st = command.output().expect("git"); assert!( st.status.success(), "git {:?} failed: {}",