//! Test-only: the crate's single lock for tests that mutate process //! environment variables. //! //! Environment variables are one process-global, and every `#[test]` in this //! crate lands in the *same* test binary, run on parallel threads. A test that //! sets a variable, asserts, then restores it is only safe against a *concurrent* //! test if both take the same lock — so any test here that touches the //! environment must route through [`lock`] rather than rolling its own //! save/restore. //! //! ⚠️ **The lock has to be crate-wide, not per-module.** `hive-bash-mcp`'s //! sibling helper records why: two separate per-module mutexes serialise //! nothing against each other, and that produced a CI-only flake. The pair that //! motivated this one is `meta::tests` (which renders a flake from //! `HYPERHIVE_OTEL_*`) and `stats::otel_metrics::tests` (which asserts which //! variable enables the exporter) — different modules, same variable, same //! binary. //! //! ⚠️ **A green local run is weak evidence for this class.** An agent container //! has the hyperhive variables ambient-set, so a losing race still finds a //! plausible value; the nix sandbox strips them, so there the race can delete a //! variable out from under another thread. Reproduce the sandbox shape with //! `env -u HYPERHIVE_OTEL_ENDPOINT cargo test -p hive-c0re`. use std::sync::{Mutex, MutexGuard, PoisonError}; static ENV_LOCK: Mutex<()> = Mutex::new(()); /// Serialise this test against every other environment-mutating test in the /// crate. Hold the returned guard for as long as the variables are perturbed — /// bind it (`let _env = lock();`), never discard it with `let _ = lock();`, /// which drops the guard immediately and serialises nothing. /// /// Recovers from poisoning: a test that panicked mid-mutation has already /// failed and reported, and refusing to run every later test on top of that /// turns one failure into a cascade that hides which test actually broke. pub fn lock() -> MutexGuard<'static, ()> { ENV_LOCK.lock().unwrap_or_else(PoisonError::into_inner) }