11use super :: * ;
2+ use serial_test:: serial;
23use 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]
800807async 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]
9881000async 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]
10771091async 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) ]
15531571mod 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