diff --git a/hive-agent/src/paths.rs b/hive-agent/src/paths.rs index fdc4e968..dc4ceddc 100644 --- a/hive-agent/src/paths.rs +++ b/hive-agent/src/paths.rs @@ -131,3 +131,38 @@ pub fn claude_dir() -> PathBuf { } PathBuf::from("/root/.claude") } + +/// Run `f` with `HYPERHIVE_HARNESS_DIR` pointing at a fresh temp dir, then +/// restore the previous value. Tests that build a `Bus` or read the harness +/// state files go through this: the variable is process-wide, so the calls +/// are serialised on a lock. +#[cfg(test)] +pub fn with_harness_dir(f: impl FnOnce() -> T) -> T { + use std::ffi::OsString; + use std::sync::Mutex; + + static LOCK: Mutex<()> = Mutex::new(()); + + struct Restore(Option); + impl Drop for Restore { + fn drop(&mut self) { + // SAFETY: still holding `LOCK`, the only writer of this variable. + unsafe { + match self.0.take() { + Some(v) => std::env::set_var("HYPERHIVE_HARNESS_DIR", v), + None => std::env::remove_var("HYPERHIVE_HARNESS_DIR"), + } + } + } + } + + let _lock = LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let dir = tempfile::tempdir().unwrap(); + // Declared after `_lock` so the variable is restored before the lock drops. + let _restore = Restore(std::env::var_os("HYPERHIVE_HARNESS_DIR")); + // SAFETY: serialised by `LOCK`. + unsafe { std::env::set_var("HYPERHIVE_HARNESS_DIR", dir.path()) }; + f() +} diff --git a/hive-agent/src/swarm_inference.rs b/hive-agent/src/swarm_inference.rs index 3be47c2d..eebb3d50 100644 --- a/hive-agent/src/swarm_inference.rs +++ b/hive-agent/src/swarm_inference.rs @@ -320,34 +320,29 @@ mod tests { /// persisted files, so clearing it, or a restart, finds the local value. #[test] fn the_swarm_value_is_not_persisted_over_the_local_one() { - let dir = tempfile::tempdir().unwrap(); - let prev = std::env::var_os("HYPERHIVE_HARNESS_DIR"); - // SAFETY: no other test in this crate reads or sets this variable. - unsafe { std::env::set_var("HYPERHIVE_HARNESS_DIR", dir.path()) }; - let bus = Bus::new(); - bus.set_model("sonnet"); - bus.set_effort("low"); - let target = Harness { - bus: bus.clone(), - nix_model: None, - nix_effort: None, - }; - let mut o = Overridden::default(); - o.apply(&set(Some("opus"), Some(Effort::Max)), &target); - let swarm = (bus.model(), bus.effort()); - let swarm_sources = (bus.model_source(), bus.effort_source()); - let files = ( - crate::harness_state::load_model(), - crate::harness_state::load_effort(), - ); - o.apply(&AgentInference::default(), &target); - let cleared = (bus.model(), bus.effort()); - let cleared_sources = (bus.model_source(), bus.effort_source()); - match prev { - // SAFETY: as above. - Some(v) => unsafe { std::env::set_var("HYPERHIVE_HARNESS_DIR", v) }, - None => unsafe { std::env::remove_var("HYPERHIVE_HARNESS_DIR") }, - } + let (swarm, swarm_sources, files, cleared, cleared_sources) = + crate::paths::with_harness_dir(|| { + let bus = Bus::new(); + bus.set_model("sonnet"); + bus.set_effort("low"); + let target = Harness { + bus: bus.clone(), + nix_model: None, + nix_effort: None, + }; + let mut o = Overridden::default(); + o.apply(&set(Some("opus"), Some(Effort::Max)), &target); + let swarm = (bus.model(), bus.effort()); + let swarm_sources = (bus.model_source(), bus.effort_source()); + let files = ( + crate::harness_state::load_model(), + crate::harness_state::load_effort(), + ); + o.apply(&AgentInference::default(), &target); + let cleared = (bus.model(), bus.effort()); + let cleared_sources = (bus.model_source(), bus.effort_source()); + (swarm, swarm_sources, files, cleared, cleared_sources) + }); assert_eq!(swarm, pair("opus", "max")); assert_eq!(files, (Some("sonnet".to_owned()), Some("low".to_owned()))); assert_eq!(cleared, pair("sonnet", "low")); diff --git a/hive-agent/src/turn.rs b/hive-agent/src/turn.rs index 4828d753..156e83ef 100644 --- a/hive-agent/src/turn.rs +++ b/hive-agent/src/turn.rs @@ -983,16 +983,18 @@ mod tests { } fn compact_report(result: Result<(), &str>) -> (bool, Vec) { - let bus = Bus::new(); - let mut rx = bus.subscribe(); - let ok = report_compact(&bus, result); - let mut notes = Vec::new(); - while let Ok(ev) = rx.try_recv() { - if let LiveEvent::Note { text } = ev.event { - notes.push(text); + crate::paths::with_harness_dir(|| { + let bus = Bus::new(); + let mut rx = bus.subscribe(); + let ok = report_compact(&bus, result); + let mut notes = Vec::new(); + while let Ok(ev) = rx.try_recv() { + if let LiveEvent::Note { text } = ev.event { + notes.push(text); + } } - } - (ok, notes) + (ok, notes) + }) } #[test]