Skip to content

Commit 3f76dc5

Browse files
committed
[worker] fix flaky allowed-roots env race in serve tests (test-linux CI)
allowed_roots_tests mutated CODESEARCH_ALLOWED_ROOTS under a private Mutex that cannot serialize against tests outside the mod; the parallel add_repo handler test read the stale multi-root value through the real handler and got 403 instead of 202 (observed on the Linux runner). Fix per the AGENTS.md env-test rule: allowed_roots_tests move to #[serial] + EnvRestore (panic-safe restore), and the handler tests that read the var (add_repo, 2x reindex) join the serial group with an explicit env reset so they control their own state. cargo test --lib: 694 passed / 0 failed.
1 parent d802aab commit 3f76dc5

1 file changed

Lines changed: 49 additions & 42 deletions

File tree

src/serve/tests.rs

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::*;
2+
use serial_test::serial;
23
use std::io::Write;
34

45
#[test]
@@ -796,8 +797,15 @@ async fn try_open_stores_creates_db_for_brand_new_repo() {
796797
/// handler's synchronous pre-spawn state — no embedding model required, no
797798
/// race. `persist_config` honors the temp config override, so the real
798799
/// `~/.codesearch/repos.json` is never touched.
800+
///
801+
/// `#[serial]` + env reset: the handler reads `CODESEARCH_ALLOWED_ROOTS` via
802+
/// `validate_path_within_allowed_roots`, so this test must not run while the
803+
/// `allowed_roots_tests` below are mutating it (and must not inherit a stale
804+
/// value from ambient state).
805+
#[serial]
799806
#[tokio::test]
800807
async fn add_repo_handler_registers_brand_new_repo_without_rollback() {
808+
let _env = crate::testing::EnvRestore::remove(&[ALLOWED_ROOTS_ENV]);
801809
let tmp = tempfile::tempdir().unwrap();
802810
let repo_path = tmp.path().join("brandnew");
803811
std::fs::create_dir(&repo_path).unwrap();
@@ -984,8 +992,13 @@ fn config_reload_no_spurious_reload() {
984992

985993
/// Verify that the /repos/:alias/reindex route is registered and reachable.
986994
/// This test starts a real axum server on a random port and sends a POST request.
995+
///
996+
/// `#[serial]` + env reset — same allowed-roots race guard as the add_repo
997+
/// handler test above.
998+
#[serial]
987999
#[tokio::test]
9881000
async fn reindex_route_is_registered() {
1001+
let _env = crate::testing::EnvRestore::remove(&[ALLOWED_ROOTS_ENV]);
9891002
let tmp = tempfile::tempdir().unwrap();
9901003
let repo_path = tmp.path().join("myrepo");
9911004
std::fs::create_dir(&repo_path).unwrap();
@@ -1073,8 +1086,10 @@ async fn reindex_route_is_registered() {
10731086
/// corpus index it only holds read-only. The handler returns 409 CONFLICT with
10741087
/// `status: "read_only"` (see the read-only guard in `reindex_handler`,
10751088
/// src/serve/mod.rs).
1089+
#[serial]
10761090
#[tokio::test]
10771091
async fn reindex_refused_for_read_only_repo_even_with_force() {
1092+
let _env = crate::testing::EnvRestore::remove(&[ALLOWED_ROOTS_ENV]);
10781093
let (_tmp, _repo_path, state) = state_with_repo("readonlyrepo");
10791094
// Mark the repo read-only in the live config (how a snapshot-restore sets it).
10801095
state
@@ -1547,20 +1562,17 @@ async fn concurrent_reindex_returns_conflict() {
15471562

15481563
/// Unit tests for `validate_path_within_allowed_roots`.
15491564
///
1550-
/// These tests temporarily set/remove the `CODESEARCH_ALLOWED_ROOTS` env var.
1551-
/// A static Mutex serializes env mutation to prevent races under parallel test execution.
1565+
/// These tests mutate the `CODESEARCH_ALLOWED_ROOTS` env var. Per the
1566+
/// AGENTS.md rule they are `#[serial]` and restore the var via `EnvRestore`:
1567+
/// a private Mutex cannot protect against non-serial tests elsewhere in the
1568+
/// process that READ the var through the real handlers (the add_repo
1569+
/// handler test below), which is exactly the 403 flake this closed.
15521570
#[cfg(test)]
15531571
mod allowed_roots_tests {
15541572
use super::*;
1573+
use crate::testing::EnvRestore;
1574+
use serial_test::serial;
15551575
use std::path::PathBuf;
1556-
use std::sync::Mutex;
1557-
1558-
/// Global lock to serialize env var mutations across parallel test threads.
1559-
static ENV_LOCK: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
1560-
1561-
fn lock() -> std::sync::MutexGuard<'static, ()> {
1562-
ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
1563-
}
15641576

15651577
/// Helper: create a unique temp dir per test, return its canonical path.
15661578
fn temp_root(suffix: &str) -> PathBuf {
@@ -1569,57 +1581,49 @@ mod allowed_roots_tests {
15691581
safe_canonicalize(&dir).unwrap()
15701582
}
15711583

1572-
fn clear_env() {
1573-
std::env::remove_var(ALLOWED_ROOTS_ENV);
1574-
}
1575-
1576-
fn set_env(val: &str) {
1577-
std::env::set_var(ALLOWED_ROOTS_ENV, val);
1578-
}
1579-
1584+
#[serial]
15801585
#[test]
15811586
fn env_unset_allows_all() {
1582-
let _guard = lock();
1583-
clear_env();
1587+
let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]);
15841588
let path = PathBuf::from("/some/random/path");
15851589
assert!(validate_path_within_allowed_roots(&path).is_ok());
15861590
}
15871591

1592+
#[serial]
15881593
#[test]
15891594
fn env_empty_allows_all() {
1590-
let _guard = lock();
1591-
set_env("");
1595+
let _env = EnvRestore::set(&[(ALLOWED_ROOTS_ENV, "")]);
15921596
let path = PathBuf::from("/some/random/path");
15931597
assert!(validate_path_within_allowed_roots(&path).is_ok());
1594-
clear_env();
15951598
}
15961599

1600+
#[serial]
15971601
#[test]
15981602
fn path_within_root_is_allowed() {
1599-
let _guard = lock();
1603+
let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]);
16001604
let root = temp_root("within");
1601-
set_env(&root.display().to_string());
1605+
std::env::set_var(ALLOWED_ROOTS_ENV, root.display().to_string());
16021606
let child = root.join("my-project");
16031607
let _ = std::fs::create_dir_all(&child);
16041608
let canonical_child = safe_canonicalize(&child).unwrap();
16051609
assert!(validate_path_within_allowed_roots(&canonical_child).is_ok());
1606-
clear_env();
16071610
}
16081611

1612+
#[serial]
16091613
#[test]
16101614
fn exact_root_match_is_allowed() {
1611-
let _guard = lock();
1615+
let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]);
16121616
let root = temp_root("exact");
1613-
set_env(&root.display().to_string());
1617+
std::env::set_var(ALLOWED_ROOTS_ENV, root.display().to_string());
16141618
assert!(validate_path_within_allowed_roots(&root).is_ok());
1615-
clear_env();
16161619
}
16171620

1621+
#[serial]
16181622
#[test]
16191623
fn path_outside_root_is_rejected() {
1620-
let _guard = lock();
1624+
let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]);
16211625
let root = temp_root("outside");
1622-
set_env(&root.display().to_string());
1626+
std::env::set_var(ALLOWED_ROOTS_ENV, root.display().to_string());
16231627
// Construct a path guaranteed outside the temp root
16241628
let outside = if cfg!(windows) {
16251629
PathBuf::from("C:\\Windows\\System32")
@@ -1635,40 +1639,45 @@ mod allowed_roots_tests {
16351639
let result = validate_path_within_allowed_roots(&outside);
16361640
assert!(result.is_err(), "Expected rejection for path outside root");
16371641
assert!(result.unwrap_err().contains("outside allowed roots"));
1638-
clear_env();
16391642
}
16401643

1644+
#[serial]
16411645
#[test]
16421646
fn all_nonexistent_roots_rejects() {
1643-
let _guard = lock();
1644-
set_env("/nonexistent/path/abc;/also/nonexistent/xyz");
1647+
let _env = EnvRestore::set(&[(
1648+
ALLOWED_ROOTS_ENV,
1649+
"/nonexistent/path/abc;/also/nonexistent/xyz",
1650+
)]);
16451651
let some_path = std::env::temp_dir();
16461652
let canonical = safe_canonicalize(&some_path).unwrap();
16471653
let result = validate_path_within_allowed_roots(&canonical);
16481654
assert!(result.is_err());
16491655
assert!(result.unwrap_err().contains("No valid roots found"));
1650-
clear_env();
16511656
}
16521657

1658+
#[serial]
16531659
#[test]
16541660
fn semicolons_with_empty_segments_works() {
1655-
let _guard = lock();
1661+
let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]);
16561662
let root = temp_root("semicolons");
1657-
set_env(&format!(";{};;", root.display()));
1663+
std::env::set_var(ALLOWED_ROOTS_ENV, format!(";{};;", root.display()));
16581664
let child = root.join("project");
16591665
let _ = std::fs::create_dir_all(&child);
16601666
let canonical_child = safe_canonicalize(&child).unwrap();
16611667
assert!(validate_path_within_allowed_roots(&canonical_child).is_ok());
1662-
clear_env();
16631668
}
16641669

1670+
#[serial]
16651671
#[test]
16661672
fn multiple_roots_any_match() {
1667-
let _guard = lock();
1673+
let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]);
16681674
let root1 = temp_root("multi1");
16691675
let root2 = temp_root("multi2");
16701676

1671-
set_env(&format!("{};{}", root1.display(), root2.display()));
1677+
std::env::set_var(
1678+
ALLOWED_ROOTS_ENV,
1679+
format!("{};{}", root1.display(), root2.display()),
1680+
);
16721681

16731682
// Path under root1
16741683
let child1 = root1.join("project");
@@ -1681,8 +1690,6 @@ mod allowed_roots_tests {
16811690
let _ = std::fs::create_dir_all(&child2);
16821691
let canonical2 = safe_canonicalize(&child2).unwrap();
16831692
assert!(validate_path_within_allowed_roots(&canonical2).is_ok());
1684-
1685-
clear_env();
16861693
}
16871694
}
16881695

0 commit comments

Comments
 (0)