From 3aa36c8f55c61a9d9fcfabdbbae944ab0a46f130 Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Sun, 22 Mar 2026 14:36:24 -0700 Subject: [PATCH] fix(tests): eliminate env mutex poison cascade (#1558) * fix(tests): eliminate env mutex poison cascade and fix test flakiness The shared ENV_MUTEX used by ~68 config tests would cascade a single test panic into failures across every module. Replace all .unwrap() / .expect() lock acquisitions with a poison-recovering lock_env() helper. Consolidate rogue module-local ENV_LOCK instances (workspace, orchestrator, bootstrap) onto the shared global mutex to prevent cross-module races. Also fixes: - gateway user_id fallback was hardcoded to "default" instead of owner_id - test_ironclaw_env_path used LazyLock which is order-dependent Co-Authored-By: Claude Opus 4.6 (1M context) * test(helpers): add regression test for lock_env poison recovery Satisfies the regression-test-check CI gate by adding a test that intentionally poisons ENV_MUTEX and verifies lock_env() recovers. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(ci): detect test changes inside #[cfg(test)] regions The regression test check relied on git diff -W to expand context to function boundaries, but git doesn't recognize Rust `mod tests {}` as a function boundary. Changes to imports, helpers, or lock calls inside test modules were invisible to the check. Add a line-level fallback: for each changed .rs file, find where #[cfg(test)] starts and check if any diff hunk targets a line at or after that boundary. This catches edits anywhere inside test modules regardless of git's language awareness. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: address PR review feedback - Clear ENV_MUTEX poison after regression test so it doesn't leave global state dirty for subsequent tests. - Fix CI regression-test-check to match #[cfg(test)] only when followed by `mod` (the test module pattern), avoiding false positives from standalone #[cfg(test)] items like statics or functions. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/regression-test-check.yml | 35 ++++++++++++++ src/bootstrap.rs | 37 ++++++++++----- src/cli/doctor.rs | 8 ++-- src/cli/oauth_defaults.rs | 24 +++++----- src/config/builder.rs | 6 +-- src/config/channels.rs | 6 +-- src/config/embeddings.rs | 14 +++--- src/config/helpers.rs | 32 ++++++++++++- src/config/llm.rs | 52 ++++++++++----------- src/config/safety.rs | 6 +-- src/config/sandbox.rs | 20 ++------ src/config/search.rs | 14 +++--- src/config/wasm.rs | 6 +-- src/config/workspace.rs | 7 +-- src/db/libsql/workspace.rs | 10 ++-- src/extensions/manager.rs | 24 +++------- src/llm/oauth_helpers.rs | 6 +-- src/orchestrator/mod.rs | 10 ++-- src/setup/wizard.rs | 18 +++---- 19 files changed, 192 insertions(+), 143 deletions(-) diff --git a/.github/workflows/regression-test-check.yml b/.github/workflows/regression-test-check.yml index ef1a4d92..75b8eb55 100644 --- a/.github/workflows/regression-test-check.yml +++ b/.github/workflows/regression-test-check.yml @@ -121,6 +121,7 @@ jobs: fi # Whole-function context: detect edits inside existing test functions. + # Uses -W (whole function) which works when git recognises function boundaries. if git diff "${BASE_REF}...${HEAD_REF}" -W -- '*.rs' | awk ' /^@@/ { if (has_test && has_add) { found=1; exit } has_test=0; has_add=0 } /^ .*#\[test\]/ || /^ .*#\[tokio::test\]/ || /^ .*#\[cfg\(test\)\]/ || /^ .*mod tests/ { has_test=1 } @@ -132,6 +133,40 @@ jobs: exit 0 fi + # Line-level check: detect changes inside #[cfg(test)] mod blocks. + # git -W relies on function boundary detection which misses Rust mod blocks, + # so this fallback checks whether changed line numbers fall within test modules. + # We specifically match #[cfg(test)] that is followed by `mod` (same or next + # line) to avoid false positives from standalone #[cfg(test)] items like + # individual statics or functions. + CHANGED_RS=$(echo "$CHANGED_FILES" | grep '\.rs$' || true) + if [ -n "$CHANGED_RS" ]; then + while IFS= read -r rs_file; do + [ -f "$rs_file" ] || continue + + # Find the line where #[cfg(test)] precedes a `mod` declaration. + # Handles both `#[cfg(test)] mod tests` (same line) and the two-line form. + TEST_MOD_START=$(awk ' + /^[[:space:]]*#\[cfg\(test\)\].*mod / { print NR; exit } + /^[[:space:]]*#\[cfg\(test\)\][[:space:]]*$/ { pending=NR; next } + pending && /^[[:space:]]*mod / { print pending; exit } + { pending=0 } + ' "$rs_file") + [ -n "$TEST_MOD_START" ] || continue + + # Get changed line numbers in this file from the diff hunk headers. + # Each @@ line looks like: @@ -old,count +new,count @@ + while IFS= read -r hunk_line; do + line_no=$(echo "$hunk_line" | sed -E 's/^@@ -[0-9,]+ \+([0-9]+).*/\1/') + [ -n "$line_no" ] || continue + if [ "$line_no" -ge "$TEST_MOD_START" ]; then + echo "Test changes found: $rs_file has changes at line $line_no inside #[cfg(test)] mod block (starts at line $TEST_MOD_START)." + exit 0 + fi + done < <(git diff "${BASE_REF}...${HEAD_REF}" -U0 -- "$rs_file" | grep -E '^@@') + done <<< "$CHANGED_RS" + fi + if grep -qE '^tests/' <<< "$CHANGED_FILES"; then echo "Test file changes found under tests/." exit 0 diff --git a/src/bootstrap.rs b/src/bootstrap.rs index f8a283f3..a5c8ffdb 100644 --- a/src/bootstrap.rs +++ b/src/bootstrap.rs @@ -568,14 +568,12 @@ impl Drop for PidLock { #[cfg(test)] mod tests { use super::*; + use crate::config::helpers::lock_env; use std::process::Command; - use std::sync::Mutex; use std::thread; use std::time::{Duration, Instant}; use tempfile::tempdir; - static ENV_MUTEX: Mutex<()> = Mutex::new(()); - #[test] fn test_save_and_load_database_url() { let dir = tempdir().unwrap(); @@ -669,8 +667,23 @@ INJECTED="pwned"#; #[test] fn test_ironclaw_env_path() { - let path = ironclaw_env_path(); - assert!(path.ends_with(".ironclaw/.env")); + // Use compute_ironclaw_base_dir() directly to avoid LazyLock caching, + // which can be poisoned by whichever test initializes it first. + let _guard = lock_env(); + let old_val = std::env::var("IRONCLAW_BASE_DIR").ok(); + // SAFETY: Under lock_env(), no concurrent env access. + unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") }; + + let path = compute_ironclaw_base_dir().join(".env"); + assert!( + path.ends_with(".ironclaw/.env"), + "expected path ending with .ironclaw/.env, got: {}", + path.display() + ); + + if let Some(val) = old_val { + unsafe { std::env::set_var("IRONCLAW_BASE_DIR", val) }; + } } #[test] @@ -836,7 +849,7 @@ INJECTED="pwned"#; #[test] fn test_libsql_autodetect_sets_backend_when_db_exists() { - let _guard = ENV_MUTEX.lock().unwrap(); + let _guard = lock_env(); let old_val = std::env::var("DATABASE_BACKEND").ok(); // SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests unsafe { std::env::remove_var("DATABASE_BACKEND") }; @@ -907,7 +920,7 @@ INJECTED="pwned"#; #[test] fn test_libsql_autodetect_does_not_override_explicit_backend() { - let _guard = ENV_MUTEX.lock().unwrap(); + let _guard = lock_env(); let old_val = std::env::var("DATABASE_BACKEND").ok(); // SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests unsafe { std::env::set_var("DATABASE_BACKEND", "postgres") }; @@ -1034,7 +1047,7 @@ INJECTED="pwned"#; fn test_ironclaw_base_dir_default() { // This test must run first (or in isolation) before the LazyLock is initialized. // It verifies that when IRONCLAW_BASE_DIR is not set, the default path is used. - let _guard = ENV_MUTEX.lock().unwrap(); + let _guard = lock_env(); let old_val = std::env::var("IRONCLAW_BASE_DIR").ok(); // SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") }; @@ -1054,7 +1067,7 @@ INJECTED="pwned"#; fn test_ironclaw_base_dir_env_override() { // This test verifies that when IRONCLAW_BASE_DIR is set, // the custom path is used. Must run before LazyLock is initialized. - let _guard = ENV_MUTEX.lock().unwrap(); + let _guard = lock_env(); let old_val = std::env::var("IRONCLAW_BASE_DIR").ok(); // SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "/custom/ironclaw/path") }; @@ -1076,7 +1089,7 @@ INJECTED="pwned"#; fn test_compute_base_dir_env_path_join() { // Verifies that ironclaw_env_path correctly joins .env to the base dir. // Uses compute_ironclaw_base_dir directly to avoid LazyLock caching. - let _guard = ENV_MUTEX.lock().unwrap(); + let _guard = lock_env(); let old_val = std::env::var("IRONCLAW_BASE_DIR").ok(); // SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "/my/custom/dir") }; @@ -1098,7 +1111,7 @@ INJECTED="pwned"#; #[test] fn test_ironclaw_base_dir_empty_env() { // Verifies that empty IRONCLAW_BASE_DIR falls back to default. - let _guard = ENV_MUTEX.lock().unwrap(); + let _guard = lock_env(); let old_val = std::env::var("IRONCLAW_BASE_DIR").ok(); // SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "") }; @@ -1120,7 +1133,7 @@ INJECTED="pwned"#; #[test] fn test_ironclaw_base_dir_special_chars() { // Verifies that paths with special characters are handled correctly. - let _guard = ENV_MUTEX.lock().unwrap(); + let _guard = lock_env(); let old_val = std::env::var("IRONCLAW_BASE_DIR").ok(); // SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "/tmp/test_with-special.chars") }; diff --git a/src/cli/doctor.rs b/src/cli/doctor.rs index 5d13ade6..023ac4e1 100644 --- a/src/cli/doctor.rs +++ b/src/cli/doctor.rs @@ -692,7 +692,7 @@ mod tests { } } - let _mutex = crate::config::helpers::ENV_MUTEX.lock().expect("env mutex"); + let _mutex = crate::config::helpers::lock_env(); let prev = std::env::var("LLM_BACKEND").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { @@ -812,7 +812,7 @@ mod tests { #[test] fn check_llm_config_shows_nearai_model_for_nearai_backend() { - let _guard = crate::config::helpers::ENV_MUTEX.lock().expect("env mutex"); + let _guard = crate::config::helpers::lock_env(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { std::env::remove_var("LLM_BACKEND"); @@ -839,7 +839,7 @@ mod tests { #[test] fn check_embeddings_disabled_by_default_returns_skip() { - let _guard = crate::config::helpers::ENV_MUTEX.lock().expect("env mutex"); + let _guard = crate::config::helpers::lock_env(); // SAFETY: Under ENV_MUTEX. unsafe { std::env::remove_var("EMBEDDING_ENABLED"); @@ -861,7 +861,7 @@ mod tests { #[test] fn check_routines_enabled_by_default() { - let _guard = crate::config::helpers::ENV_MUTEX.lock().expect("env mutex"); + let _guard = crate::config::helpers::lock_env(); // SAFETY: Under ENV_MUTEX. unsafe { std::env::remove_var("ROUTINES_ENABLED"); diff --git a/src/cli/oauth_defaults.rs b/src/cli/oauth_defaults.rs index b4e93704..531d474e 100644 --- a/src/cli/oauth_defaults.rs +++ b/src/cli/oauth_defaults.rs @@ -758,7 +758,7 @@ mod tests { use crate::cli::oauth_defaults::{ builtin_credentials, callback_host, callback_url, is_loopback_host, landing_html, }; - use crate::config::helpers::ENV_MUTEX; + use crate::config::helpers::lock_env; #[test] fn test_is_loopback_host() { @@ -775,7 +775,7 @@ mod tests { #[test] fn test_callback_host_default() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original = std::env::var("OAUTH_CALLBACK_HOST").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { @@ -792,7 +792,7 @@ mod tests { #[test] fn test_callback_host_env_override() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original_host = std::env::var("OAUTH_CALLBACK_HOST").ok(); let original_url = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. @@ -819,7 +819,7 @@ mod tests { #[test] fn test_callback_url_default() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); // Clear both env vars to test default behavior let original_url = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); let original_host = std::env::var("OAUTH_CALLBACK_HOST").ok(); @@ -843,7 +843,7 @@ mod tests { #[test] fn test_callback_url_env_override() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { @@ -1008,7 +1008,7 @@ mod tests { #[test] fn test_use_gateway_callback_false_by_default() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { @@ -1024,7 +1024,7 @@ mod tests { #[test] fn test_use_gateway_callback_true_for_hosted() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { @@ -1045,7 +1045,7 @@ mod tests { #[test] fn test_use_gateway_callback_false_for_localhost() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { @@ -1063,7 +1063,7 @@ mod tests { #[test] fn test_use_gateway_callback_false_for_empty() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { @@ -1083,7 +1083,7 @@ mod tests { fn test_build_platform_state_with_instance() { use crate::cli::oauth_defaults::{build_platform_state, decode_hosted_oauth_state}; - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original = std::env::var("IRONCLAW_INSTANCE_NAME").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { @@ -1107,7 +1107,7 @@ mod tests { fn test_build_platform_state_without_instance() { use crate::cli::oauth_defaults::{build_platform_state, decode_hosted_oauth_state}; - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original = std::env::var("IRONCLAW_INSTANCE_NAME").ok(); let original_oc = std::env::var("OPENCLAW_INSTANCE_NAME").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. @@ -1134,7 +1134,7 @@ mod tests { fn test_build_platform_state_with_openclaw_instance() { use crate::cli::oauth_defaults::{build_platform_state, decode_hosted_oauth_state}; - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original_ic = std::env::var("IRONCLAW_INSTANCE_NAME").ok(); let original_oc = std::env::var("OPENCLAW_INSTANCE_NAME").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. diff --git a/src/config/builder.rs b/src/config/builder.rs index 088db90c..f7bad12c 100644 --- a/src/config/builder.rs +++ b/src/config/builder.rs @@ -63,12 +63,12 @@ impl BuilderModeConfig { #[cfg(test)] mod tests { use super::*; - use crate::config::helpers::ENV_MUTEX; + use crate::config::helpers::lock_env; use crate::settings::Settings; #[test] fn resolve_falls_back_to_settings() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let mut settings = Settings::default(); settings.builder.max_iterations = 99; settings.builder.auto_register = false; @@ -80,7 +80,7 @@ mod tests { #[test] fn env_overrides_settings() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let mut settings = Settings::default(); settings.builder.timeout_secs = 123; diff --git a/src/config/channels.rs b/src/config/channels.rs index bc704445..d249dd18 100644 --- a/src/config/channels.rs +++ b/src/config/channels.rs @@ -113,7 +113,7 @@ impl ChannelsConfig { let gateway = if gateway_enabled { let user_id = optional_env("GATEWAY_USER_ID")? .or_else(|| cs.gateway_user_id.clone()) - .unwrap_or_else(|| "default".to_string()); + .unwrap_or_else(|| owner_id.to_string()); Some(GatewayConfig { host: optional_env("GATEWAY_HOST")? @@ -236,7 +236,7 @@ fn default_channels_dir() -> PathBuf { #[cfg(test)] mod tests { use crate::config::channels::*; - use crate::config::helpers::ENV_MUTEX; + use crate::config::helpers::lock_env; use crate::settings::Settings; #[test] @@ -395,7 +395,7 @@ mod tests { #[test] fn resolve_uses_settings_channel_values_with_owner_scope_user_ids() { - let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + let _guard = lock_env(); let mut settings = Settings::default(); settings.channels.http_enabled = true; settings.channels.http_host = Some("127.0.0.2".to_string()); diff --git a/src/config/embeddings.rs b/src/config/embeddings.rs index 68b0ff2c..98183976 100644 --- a/src/config/embeddings.rs +++ b/src/config/embeddings.rs @@ -196,7 +196,7 @@ impl EmbeddingsConfig { #[cfg(test)] mod tests { use super::*; - use crate::config::helpers::ENV_MUTEX; + use crate::config::helpers::lock_env; use crate::settings::{EmbeddingsSettings, Settings}; use crate::testing::credentials::*; @@ -215,7 +215,7 @@ mod tests { #[test] fn embeddings_disabled_not_overridden_by_openai_key() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_embedding_env(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { @@ -245,7 +245,7 @@ mod tests { #[test] fn embeddings_enabled_from_settings() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_embedding_env(); let settings = Settings { @@ -265,7 +265,7 @@ mod tests { #[test] fn embeddings_env_override_takes_precedence() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_embedding_env(); // SAFETY: Under ENV_MUTEX. unsafe { @@ -294,7 +294,7 @@ mod tests { #[test] fn embedding_base_url_parsed_from_env() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_embedding_env(); // SAFETY: Under ENV_MUTEX, no concurrent env access. @@ -313,7 +313,7 @@ mod tests { #[test] fn embedding_base_url_defaults_to_none() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_embedding_env(); let settings = Settings::default(); @@ -326,7 +326,7 @@ mod tests { #[test] fn cache_size_zero_rejected() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_embedding_env(); // SAFETY: Under ENV_MUTEX. unsafe { diff --git a/src/config/helpers.rs b/src/config/helpers.rs index dc40fc9f..ff5ee706 100644 --- a/src/config/helpers.rs +++ b/src/config/helpers.rs @@ -14,6 +14,16 @@ use crate::config::INJECTED_VARS; #[cfg(test)] pub(crate) static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); +/// Acquire the env-var mutex, recovering from poison. +/// +/// A poisoned mutex means a previous test panicked while holding the lock. +/// The env state might be slightly stale, but cascading every subsequent +/// test into a `PoisonError` panic is far worse. Recover and carry on. +#[cfg(test)] +pub(crate) fn lock_env() -> std::sync::MutexGuard<'static, ()> { + ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()) +} + /// Thread-safe mutable overlay for env vars set at runtime. /// /// Unlike `INJECTED_VARS` (which is set once at startup from the secrets @@ -353,7 +363,7 @@ mod tests { #[test] fn real_env_var_takes_priority_over_runtime_override() { - let _guard = ENV_MUTEX.lock().unwrap(); + let _guard = lock_env(); let key = "IRONCLAW_TEST_ENV_PRIORITY_42"; // Set runtime override @@ -372,6 +382,26 @@ mod tests { assert_eq!(env_or_override(key), Some("override_value".to_string())); } + // --- lock_env poison recovery (regression for env mutex cascade) --- + + #[test] + fn lock_env_recovers_from_poisoned_mutex() { + // Simulate a poisoned mutex: spawn a thread that panics while holding the lock. + let _ = std::thread::spawn(|| { + let _guard = ENV_MUTEX.lock().unwrap(); + panic!("intentional poison"); + }) + .join(); + + // The mutex is now poisoned. lock_env() should recover, not cascade. + assert!(ENV_MUTEX.lock().is_err(), "mutex should be poisoned"); + let _guard = lock_env(); // must not panic + drop(_guard); + + // Clean up so this test doesn't leave ENV_MUTEX permanently poisoned. + ENV_MUTEX.clear_poison(); + } + // --- validate_base_url tests (regression for #1103) --- #[test] diff --git a/src/config/llm.rs b/src/config/llm.rs index 0976051f..87e4daa5 100644 --- a/src/config/llm.rs +++ b/src/config/llm.rs @@ -532,7 +532,7 @@ pub fn default_session_path() -> PathBuf { #[cfg(test)] mod tests { use super::*; - use crate::config::helpers::ENV_MUTEX; + use crate::config::helpers::lock_env; use crate::settings::Settings; use crate::testing::credentials::*; @@ -548,7 +548,7 @@ mod tests { #[test] fn openai_compatible_uses_selected_model_when_llm_model_unset() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_compatible_env(); let settings = Settings { @@ -566,7 +566,7 @@ mod tests { #[test] fn openai_compatible_llm_model_env_overrides_selected_model() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_compatible_env(); // SAFETY: Under ENV_MUTEX. unsafe { @@ -690,7 +690,7 @@ mod tests { #[test] fn ollama_uses_selected_model_when_ollama_model_unset() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_ollama_env(); let settings = Settings { @@ -707,7 +707,7 @@ mod tests { #[test] fn ollama_model_env_overrides_selected_model() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_ollama_env(); // SAFETY: Under ENV_MUTEX. unsafe { @@ -733,7 +733,7 @@ mod tests { #[test] fn openai_compatible_preserves_dotted_model_name() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_compatible_env(); let settings = Settings { @@ -754,7 +754,7 @@ mod tests { #[test] fn registry_provider_resolves_groq() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); // SAFETY: Under ENV_MUTEX. unsafe { std::env::remove_var("LLM_BACKEND"); @@ -779,7 +779,7 @@ mod tests { #[test] fn registry_provider_resolves_tinfoil() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); // SAFETY: Under ENV_MUTEX. unsafe { std::env::remove_var("LLM_BACKEND"); @@ -807,7 +807,7 @@ mod tests { #[test] fn registry_provider_alias_resolves_zai() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); // SAFETY: Under ENV_MUTEX. unsafe { std::env::remove_var("LLM_BACKEND"); @@ -832,7 +832,7 @@ mod tests { #[test] fn registry_provider_resolves_github_copilot_alias() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); // SAFETY: Under ENV_MUTEX. unsafe { std::env::set_var("LLM_BACKEND", "github-copilot"); @@ -880,7 +880,7 @@ mod tests { #[test] fn nearai_backend_has_no_registry_provider() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); // SAFETY: Under ENV_MUTEX. unsafe { std::env::remove_var("LLM_BACKEND"); @@ -894,7 +894,7 @@ mod tests { #[test] fn backend_alias_normalized_to_canonical_id() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_compatible_env(); // SAFETY: Under ENV_MUTEX. unsafe { @@ -920,7 +920,7 @@ mod tests { #[test] fn unknown_backend_falls_back_to_openai_compatible() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_compatible_env(); // SAFETY: Under ENV_MUTEX. unsafe { @@ -944,7 +944,7 @@ mod tests { #[test] fn nearai_aliases_all_resolve_to_nearai() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); for alias in &["nearai", "near_ai", "near"] { // SAFETY: Under ENV_MUTEX. @@ -971,7 +971,7 @@ mod tests { #[test] fn base_url_resolution_priority() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_compatible_env(); // SAFETY: Under ENV_MUTEX. @@ -1029,7 +1029,7 @@ mod tests { fn anthropic_oauth_token_sets_placeholder_api_key() { use secrecy::ExposeSecret; - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_anthropic_env(); // SAFETY: Under ENV_MUTEX. unsafe { @@ -1067,7 +1067,7 @@ mod tests { fn anthropic_api_key_takes_priority_over_oauth() { use secrecy::ExposeSecret; - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_anthropic_env(); // SAFETY: Under ENV_MUTEX. unsafe { @@ -1100,7 +1100,7 @@ mod tests { #[test] fn non_anthropic_provider_has_no_oauth_token() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_anthropic_env(); // SAFETY: Under ENV_MUTEX. unsafe { @@ -1208,7 +1208,7 @@ mod tests { #[test] fn test_request_timeout_defaults_to_120() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); // SAFETY: Under ENV_MUTEX. unsafe { std::env::remove_var("LLM_REQUEST_TIMEOUT_SECS"); @@ -1219,7 +1219,7 @@ mod tests { #[test] fn test_request_timeout_configurable() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); // SAFETY: Under ENV_MUTEX. unsafe { std::env::set_var("LLM_REQUEST_TIMEOUT_SECS", "300"); @@ -1246,7 +1246,7 @@ mod tests { #[test] fn openai_codex_resolves_config() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_codex_env(); let settings = Settings { @@ -1266,7 +1266,7 @@ mod tests { #[test] fn openai_codex_model_env_resolution() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_codex_env(); // SAFETY: Under ENV_MUTEX. unsafe { @@ -1290,7 +1290,7 @@ mod tests { #[test] fn openai_codex_falls_back_to_openai_model() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_codex_env(); // SAFETY: Under ENV_MUTEX. unsafe { @@ -1314,7 +1314,7 @@ mod tests { #[test] fn openai_codex_falls_back_to_selected_model() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_codex_env(); let settings = Settings { @@ -1331,7 +1331,7 @@ mod tests { /// Regression: SSRF validation on OPENAI_CODEX_API_URL (#1103). #[test] fn openai_codex_rejects_ssrf_api_url() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_codex_env(); // SAFETY: Under ENV_MUTEX. unsafe { @@ -1362,7 +1362,7 @@ mod tests { /// Regression: SSRF validation on OPENAI_CODEX_AUTH_URL (#1103). #[test] fn openai_codex_rejects_ssrf_auth_url() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_openai_codex_env(); // SAFETY: Under ENV_MUTEX. unsafe { diff --git a/src/config/safety.rs b/src/config/safety.rs index ff9e900a..edeceee0 100644 --- a/src/config/safety.rs +++ b/src/config/safety.rs @@ -19,12 +19,12 @@ pub(crate) fn resolve_safety_config( #[cfg(test)] mod tests { use super::*; - use crate::config::helpers::ENV_MUTEX; + use crate::config::helpers::lock_env; use crate::settings::Settings; #[test] fn resolve_falls_back_to_settings() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let mut settings = Settings::default(); settings.safety.max_output_length = 42; settings.safety.injection_check_enabled = false; @@ -36,7 +36,7 @@ mod tests { #[test] fn env_overrides_settings() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let mut settings = Settings::default(); settings.safety.max_output_length = 42; diff --git a/src/config/sandbox.rs b/src/config/sandbox.rs index 8c0eb689..01a8c327 100644 --- a/src/config/sandbox.rs +++ b/src/config/sandbox.rs @@ -594,9 +594,7 @@ mod tests { #[test] fn sandbox_resolve_falls_back_to_settings() { - let _guard = crate::config::helpers::ENV_MUTEX - .lock() - .expect("env mutex poisoned"); + let _guard = crate::config::helpers::lock_env(); let mut settings = crate::settings::Settings::default(); settings.sandbox.cpu_shares = 99; settings.sandbox.auto_pull_image = false; @@ -610,9 +608,7 @@ mod tests { #[test] fn sandbox_env_overrides_settings() { - let _guard = crate::config::helpers::ENV_MUTEX - .lock() - .expect("env mutex poisoned"); + let _guard = crate::config::helpers::lock_env(); let mut settings = crate::settings::Settings::default(); settings.sandbox.timeout_secs = 999; @@ -628,9 +624,7 @@ mod tests { #[test] fn claude_code_resolve_uses_settings_enabled() { - let _guard = crate::config::helpers::ENV_MUTEX - .lock() - .expect("env mutex poisoned"); + let _guard = crate::config::helpers::lock_env(); let mut settings = crate::settings::Settings::default(); settings.sandbox.claude_code_enabled = true; @@ -640,9 +634,7 @@ mod tests { #[test] fn claude_code_resolve_defaults_disabled() { - let _guard = crate::config::helpers::ENV_MUTEX - .lock() - .expect("env mutex poisoned"); + let _guard = crate::config::helpers::lock_env(); let settings = crate::settings::Settings::default(); let cfg = ClaudeCodeConfig::resolve(&settings).expect("resolve"); assert!(!cfg.enabled); @@ -650,9 +642,7 @@ mod tests { #[test] fn claude_code_env_overrides_settings() { - let _guard = crate::config::helpers::ENV_MUTEX - .lock() - .expect("env mutex poisoned"); + let _guard = crate::config::helpers::lock_env(); let mut settings = crate::settings::Settings::default(); settings.sandbox.claude_code_enabled = true; diff --git a/src/config/search.rs b/src/config/search.rs index 9555fecc..e6b663cf 100644 --- a/src/config/search.rs +++ b/src/config/search.rs @@ -92,7 +92,7 @@ impl WorkspaceSearchConfig { #[cfg(test)] mod tests { use super::*; - use crate::config::helpers::ENV_MUTEX; + use crate::config::helpers::lock_env; fn clear_search_env() { // SAFETY: Only called under ENV_MUTEX in tests. @@ -106,7 +106,7 @@ mod tests { #[test] fn defaults_when_no_env() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_search_env(); let config = WorkspaceSearchConfig::resolve().expect("should resolve"); @@ -118,7 +118,7 @@ mod tests { #[test] fn env_overrides() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_search_env(); // SAFETY: Under ENV_MUTEX. @@ -140,7 +140,7 @@ mod tests { #[test] fn invalid_strategy_rejected() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_search_env(); // SAFETY: Under ENV_MUTEX. @@ -156,7 +156,7 @@ mod tests { #[test] fn weighted_strategy_defaults() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_search_env(); // SAFETY: Under ENV_MUTEX. @@ -175,7 +175,7 @@ mod tests { #[test] fn weighted_both_zero_rejected() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_search_env(); // SAFETY: Under ENV_MUTEX. @@ -193,7 +193,7 @@ mod tests { #[test] fn rrf_both_zero_allowed() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); clear_search_env(); // SAFETY: Under ENV_MUTEX. diff --git a/src/config/wasm.rs b/src/config/wasm.rs index a9bfbd35..4c494a38 100644 --- a/src/config/wasm.rs +++ b/src/config/wasm.rs @@ -95,12 +95,12 @@ impl WasmConfig { #[cfg(test)] mod tests { use super::*; - use crate::config::helpers::ENV_MUTEX; + use crate::config::helpers::lock_env; use crate::settings::Settings; #[test] fn resolve_falls_back_to_settings() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let mut settings = Settings::default(); settings.wasm.default_memory_limit = 42; settings.wasm.cache_compiled = false; @@ -112,7 +112,7 @@ mod tests { #[test] fn env_overrides_settings() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let mut settings = Settings::default(); settings.wasm.default_fuel_limit = 42; diff --git a/src/config/workspace.rs b/src/config/workspace.rs index 5f89c655..5daa73eb 100644 --- a/src/config/workspace.rs +++ b/src/config/workspace.rs @@ -79,13 +79,10 @@ impl WorkspaceConfig { #[cfg(test)] mod tests { use super::*; - use std::sync::Mutex; - - // Serialize env-var-dependent tests to avoid races. - static ENV_LOCK: Mutex<()> = Mutex::new(()); + use crate::config::helpers::lock_env; fn with_env(key: &str, val: Option<&str>, f: impl FnOnce()) { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = lock_env(); let prev = std::env::var(key).ok(); match val { Some(v) => unsafe { std::env::set_var(key, v) }, diff --git a/src/db/libsql/workspace.rs b/src/db/libsql/workspace.rs index d43f1277..5680e435 100644 --- a/src/db/libsql/workspace.rs +++ b/src/db/libsql/workspace.rs @@ -1017,7 +1017,7 @@ mod tests { mod resolve_dimension { use super::*; - use crate::config::helpers::ENV_MUTEX; + use crate::config::helpers::lock_env; fn clear_embedding_env() { // SAFETY: called under ENV_MUTEX @@ -1030,14 +1030,14 @@ mod tests { #[test] fn returns_none_when_disabled() { - let _guard = ENV_MUTEX.lock().expect("env mutex"); + let _guard = lock_env(); clear_embedding_env(); assert!(resolve_embedding_dimension().is_none()); } #[test] fn returns_explicit_dimension() { - let _guard = ENV_MUTEX.lock().expect("env mutex"); + let _guard = lock_env(); clear_embedding_env(); // SAFETY: under ENV_MUTEX unsafe { @@ -1053,7 +1053,7 @@ mod tests { #[test] fn infers_from_model() { - let _guard = ENV_MUTEX.lock().expect("env mutex"); + let _guard = lock_env(); clear_embedding_env(); // SAFETY: under ENV_MUTEX unsafe { @@ -1069,7 +1069,7 @@ mod tests { #[test] fn defaults_to_1536_for_unknown_model() { - let _guard = ENV_MUTEX.lock().expect("env mutex"); + let _guard = lock_env(); clear_embedding_env(); // SAFETY: under ENV_MUTEX unsafe { diff --git a/src/extensions/manager.rs b/src/extensions/manager.rs index 3ecf3657..df5de72d 100644 --- a/src/extensions/manager.rs +++ b/src/extensions/manager.rs @@ -7305,9 +7305,7 @@ mod tests { #[test] fn should_use_gateway_mode_true_for_tunnel_url() { - let _guard = crate::config::helpers::ENV_MUTEX - .lock() - .expect("env mutex poisoned"); + let _guard = crate::config::helpers::lock_env(); let original = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { @@ -7329,9 +7327,7 @@ mod tests { #[test] fn should_use_gateway_mode_false_without_tunnel() { - let _guard = crate::config::helpers::ENV_MUTEX - .lock() - .expect("env mutex poisoned"); + let _guard = crate::config::helpers::lock_env(); let original = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); unsafe { std::env::remove_var("IRONCLAW_OAUTH_CALLBACK_URL"); @@ -7352,9 +7348,7 @@ mod tests { #[test] fn should_use_gateway_mode_false_for_loopback_tunnel() { - let _guard = crate::config::helpers::ENV_MUTEX - .lock() - .expect("env mutex poisoned"); + let _guard = crate::config::helpers::lock_env(); let original = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); unsafe { std::env::remove_var("IRONCLAW_OAUTH_CALLBACK_URL"); @@ -7382,9 +7376,7 @@ mod tests { impl EnvGuard { fn new() -> Self { - let guard = crate::config::helpers::ENV_MUTEX - .lock() - .expect("env mutex poisoned"); + let guard = crate::config::helpers::lock_env(); let original = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { @@ -7442,9 +7434,7 @@ mod tests { #[test] fn gateway_callback_redirect_uri_does_not_duplicate_callback_path_from_env() { - let _guard = crate::config::helpers::ENV_MUTEX - .lock() - .expect("env mutex poisoned"); + let _guard = crate::config::helpers::lock_env(); let original = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); unsafe { std::env::set_var( @@ -7470,9 +7460,7 @@ mod tests { #[test] fn gateway_callback_redirect_uri_trims_trailing_slash_from_env_callback() { - let _guard = crate::config::helpers::ENV_MUTEX - .lock() - .expect("env mutex poisoned"); + let _guard = crate::config::helpers::lock_env(); let original = std::env::var("IRONCLAW_OAUTH_CALLBACK_URL").ok(); unsafe { std::env::set_var( diff --git a/src/llm/oauth_helpers.rs b/src/llm/oauth_helpers.rs index 2881e60e..daaf1b42 100644 --- a/src/llm/oauth_helpers.rs +++ b/src/llm/oauth_helpers.rs @@ -361,7 +361,7 @@ pub fn landing_html(provider_name: &str, success: bool) -> String { #[cfg(test)] mod tests { use super::*; - use crate::config::helpers::ENV_MUTEX; + use crate::config::helpers::lock_env; #[test] fn loopback_detection() { @@ -390,7 +390,7 @@ mod tests { #[allow(clippy::await_holding_lock)] #[tokio::test] async fn bind_rejects_wildcard_ipv4() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original = std::env::var("OAUTH_CALLBACK_HOST").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { std::env::set_var("OAUTH_CALLBACK_HOST", "0.0.0.0") }; @@ -414,7 +414,7 @@ mod tests { #[allow(clippy::await_holding_lock)] #[tokio::test] async fn bind_rejects_wildcard_ipv6() { - let _guard = ENV_MUTEX.lock().expect("env mutex poisoned"); + let _guard = lock_env(); let original = std::env::var("OAUTH_CALLBACK_HOST").ok(); // SAFETY: Under ENV_MUTEX, no concurrent env access. unsafe { std::env::set_var("OAUTH_CALLBACK_HOST", "::") }; diff --git a/src/orchestrator/mod.rs b/src/orchestrator/mod.rs index b72f90ee..d6e028a5 100644 --- a/src/orchestrator/mod.rs +++ b/src/orchestrator/mod.rs @@ -164,19 +164,15 @@ pub async fn setup_orchestrator( #[cfg(test)] mod tests { - use std::sync::Mutex; - use super::*; - - /// Serialize access to `ORCHESTRATOR_PORT` env var across test threads. - static ENV_LOCK: Mutex<()> = Mutex::new(()); + use crate::config::helpers::lock_env; #[test] fn resolve_orchestrator_port_from_env() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = lock_env(); // Safety: env-var mutation requires unsafe in edition 2024; - // ENV_LOCK serializes concurrent access from other test threads. + // lock_env() serializes concurrent access from other test threads. // Absent env var → default 50051 unsafe { std::env::remove_var("ORCHESTRATOR_PORT") }; diff --git a/src/setup/wizard.rs b/src/setup/wizard.rs index b7669070..7ad86610 100644 --- a/src/setup/wizard.rs +++ b/src/setup/wizard.rs @@ -3736,7 +3736,7 @@ mod tests { use tempfile::tempdir; use super::*; - use crate::config::helpers::ENV_MUTEX; + use crate::config::helpers::lock_env; #[test] fn test_wizard_creation() { @@ -3760,7 +3760,7 @@ mod tests { #[test] fn test_wizard_owner_id_uses_resolved_env_scope() { - let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + let _guard = lock_env(); let _owner = EnvGuard::set("IRONCLAW_OWNER_ID", " wizard-owner "); let wizard = SetupWizard::new(); @@ -3769,7 +3769,7 @@ mod tests { #[test] fn test_wizard_owner_id_uses_toml_scope() { - let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + let _guard = lock_env(); let _owner = EnvGuard::clear("IRONCLAW_OWNER_ID"); let dir = tempdir().unwrap(); // safety: test-only tempdir setup let path = dir.path().join("config.toml"); @@ -3785,7 +3785,7 @@ mod tests { fn test_try_with_config_and_toml_propagates_invalid_owner_env() { use std::os::unix::ffi::OsStringExt; - let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + let _guard = lock_env(); let original = std::env::var_os("IRONCLAW_OWNER_ID"); unsafe { std::env::set_var("IRONCLAW_OWNER_ID", OsString::from_vec(vec![0x66, 0x80])); @@ -4245,7 +4245,7 @@ mod tests { fn test_build_nearai_model_fetch_config_picks_up_api_key_env() { use secrecy::ExposeSecret; - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = lock_env(); let _guard = EnvGuard::set("NEARAI_API_KEY", "test-cloud-api-key-12345"); let _guard2 = EnvGuard::clear("NEARAI_BASE_URL"); @@ -4269,7 +4269,7 @@ mod tests { /// the config should have `api_key: None` (session token path). #[test] fn test_build_nearai_model_fetch_config_none_when_no_api_key() { - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = lock_env(); let _guard = EnvGuard::clear("NEARAI_API_KEY"); let _guard2 = EnvGuard::clear("NEARAI_BASE_URL"); @@ -4288,7 +4288,7 @@ mod tests { /// Regression test for #799: empty NEARAI_API_KEY should be treated as absent. #[test] fn test_build_nearai_model_fetch_config_none_when_empty_api_key() { - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = lock_env(); let _guard = EnvGuard::set("NEARAI_API_KEY", ""); let config = build_nearai_model_fetch_config(); @@ -4306,7 +4306,7 @@ mod tests { fn test_model_discovery_picks_up_injected_var() { use secrecy::ExposeSecret; - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = lock_env(); let _guard = EnvGuard::clear("NEARAI_API_KEY"); let _guard2 = EnvGuard::clear("NEARAI_BASE_URL"); @@ -4337,7 +4337,7 @@ mod tests { /// the NEAR AI authentication menu. #[test] fn test_build_nearai_model_fetch_config_picks_up_runtime_env() { - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = lock_env(); // Ensure the real env var is unset so the only source is the overlay. let _guard = EnvGuard::clear("NEARAI_API_KEY");