@@ -54,16 +54,37 @@ fn test_config() -> (TempDir, Config) {
5454
5555// ── RAII env guard shared by all tests in this file ──────────────────────────
5656
57+ /// Process-wide mutex that serialises every test in this binary that
58+ /// mutates `OPENHUMAN_WORKSPACE`. Cargo runs integration-test binaries
59+ /// multi-threaded by default (`test-threads = num_cpus`), so without
60+ /// this serialisation two tests would race on the env var: test A sets
61+ /// it to `/tmp/aaa`, test B overwrites it with `/tmp/bbb`, then when
62+ /// B's `TempDir` drops it unlinks `/tmp/bbb` while A is still reading
63+ /// from it. That race surfaced in CI as `SQLITE_IOERR_FSTAT` (error
64+ /// code 1802) during a later `with_connection` call on the now-deleted
65+ /// path, and earlier as `fetch_leaves` returning 0 hits when the
66+ /// resolved workspace temporarily pointed at the wrong sibling test's
67+ /// (otherwise empty) tempdir.
68+ ///
69+ /// `unwrap_or_else(|p| p.into_inner())` keeps the lock usable after a
70+ /// poisoning panic so one failing test never cascades.
71+ static ENV_LOCK : std:: sync:: Mutex < ( ) > = std:: sync:: Mutex :: new ( ( ) ) ;
72+
5773struct EnvGuard {
5874 key : & ' static str ,
5975 prev : Option < std:: ffi:: OsString > ,
76+ /// Last field — dropped after `Drop::drop` has already restored
77+ /// the env var, so the next test acquires the lock against a
78+ /// clean `OPENHUMAN_WORKSPACE` value.
79+ _lock : std:: sync:: MutexGuard < ' static , ( ) > ,
6080}
6181
6282impl Drop for EnvGuard {
6383 fn drop ( & mut self ) {
6484 // SAFETY: cargo test runs each integration test binary in its own
65- // process; nothing else in this bin mutates these env vars, and the
66- // guard restores the previous value on drop.
85+ // process; the `ENV_LOCK` mutex held in `_lock` serialises all
86+ // mutations within this binary, and the guard restores the
87+ // previous value before the lock is released.
6788 unsafe {
6889 match self . prev . take ( ) {
6990 Some ( v) => std:: env:: set_var ( self . key , v) ,
@@ -77,13 +98,19 @@ impl Drop for EnvGuard {
7798/// restores the previous value on drop. This makes the tool wrappers (which
7899/// call `load_config_with_timeout` internally) resolve to the same workspace
79100/// that was used for ingest.
101+ ///
102+ /// The returned guard also holds [`ENV_LOCK`] for its lifetime, so concurrent
103+ /// tests in the same binary cannot stomp on each other's
104+ /// `OPENHUMAN_WORKSPACE` setting.
80105fn set_workspace_env ( tmp : & TempDir ) -> EnvGuard {
106+ let lock = ENV_LOCK . lock ( ) . unwrap_or_else ( |p| p. into_inner ( ) ) ;
81107 let prev = std:: env:: var_os ( "OPENHUMAN_WORKSPACE" ) ;
82108 // SAFETY: see EnvGuard::Drop above.
83109 unsafe { std:: env:: set_var ( "OPENHUMAN_WORKSPACE" , tmp. path ( ) ) } ;
84110 EnvGuard {
85111 key : "OPENHUMAN_WORKSPACE" ,
86112 prev,
113+ _lock : lock,
87114 }
88115}
89116
0 commit comments