@@ -205,7 +205,7 @@ async fn starts_session_and_reports_ready() {
205205 evs
206206 } ) ;
207207
208- let session = provider . start_session ( start_input ( "t-ready" ) ) . await . unwrap ( ) ;
208+ let session = start_session_resilient ( & provider , start_input ( "t-ready" ) ) . await . unwrap ( ) ;
209209 assert_eq ! ( session. thread_id. 0 , "t-ready" ) ;
210210 assert ! ( matches!( session. status, SessionStatus :: Ready ) ) ;
211211
@@ -233,7 +233,7 @@ async fn send_turn_emits_turn_started_then_delta_then_completed() {
233233 ( "FAKE_CODEX_SCRIPT" , & script. to_string_lossy ( ) ) ,
234234 ] ) ;
235235 let provider = provider_with_fixture_and_binary ( wrapper. to_path_buf ( ) ) ;
236- provider . start_session ( start_input ( "t-seq" ) ) . await . unwrap ( ) ;
236+ start_session_resilient ( & provider , start_input ( "t-seq" ) ) . await . unwrap ( ) ;
237237 let mut stream = provider. event_stream ( ) ;
238238
239239 provider
@@ -292,7 +292,7 @@ async fn thread_resume_with_recoverable_error_falls_back_to_start() {
292292
293293 let mut input = start_input ( "t-resume" ) ;
294294 input. resume_cursor = Some ( json ! ( { "threadId" : "old-thread" } ) ) ;
295- let session = provider . start_session ( input) . await . unwrap ( ) ;
295+ let session = start_session_resilient ( & provider , input) . await . unwrap ( ) ;
296296 assert ! ( matches!( session. status, SessionStatus :: Ready ) ) ;
297297
298298 // Look for the RuntimeWarning on the stream.
@@ -326,7 +326,7 @@ async fn unknown_notification_surfaces_as_runtime_warning() {
326326 ( "FAKE_CODEX_SCRIPT" , & script. to_string_lossy ( ) ) ,
327327 ] ) ;
328328 let provider = provider_with_fixture_and_binary ( wrapper. to_path_buf ( ) ) ;
329- provider . start_session ( start_input ( "t-unk" ) ) . await . unwrap ( ) ;
329+ start_session_resilient ( & provider , start_input ( "t-unk" ) ) . await . unwrap ( ) ;
330330 let mut stream = provider. event_stream ( ) ;
331331 provider
332332 . send_turn ( SendTurnInput {
@@ -372,7 +372,7 @@ async fn command_approval_request_roundtrip() {
372372 ( "FAKE_CODEX_SCRIPT" , & script. to_string_lossy ( ) ) ,
373373 ] ) ;
374374 let provider = provider_with_fixture_and_binary ( wrapper. to_path_buf ( ) ) ;
375- provider . start_session ( start_input ( "t-ap" ) ) . await . unwrap ( ) ;
375+ start_session_resilient ( & provider , start_input ( "t-ap" ) ) . await . unwrap ( ) ;
376376 let mut stream = provider. event_stream ( ) ;
377377 provider
378378 . send_turn ( SendTurnInput {
@@ -428,7 +428,7 @@ async fn command_approval_deny_roundtrip() {
428428 ( "FAKE_CODEX_SCRIPT" , & script. to_string_lossy ( ) ) ,
429429 ] ) ;
430430 let provider = provider_with_fixture_and_binary ( wrapper. to_path_buf ( ) ) ;
431- provider . start_session ( start_input ( "t-deny" ) ) . await . unwrap ( ) ;
431+ start_session_resilient ( & provider , start_input ( "t-deny" ) ) . await . unwrap ( ) ;
432432 let mut stream = provider. event_stream ( ) ;
433433 provider
434434 . send_turn ( SendTurnInput {
@@ -476,7 +476,7 @@ async fn interrupt_turn_sends_turn_interrupt() {
476476 ( "FAKE_CODEX_SCRIPT" , & script. to_string_lossy ( ) ) ,
477477 ] ) ;
478478 let provider = provider_with_fixture_and_binary ( wrapper. to_path_buf ( ) ) ;
479- provider . start_session ( start_input ( "t-int" ) ) . await . unwrap ( ) ;
479+ start_session_resilient ( & provider , start_input ( "t-int" ) ) . await . unwrap ( ) ;
480480 let res = provider
481481 . send_turn ( SendTurnInput {
482482 thread_id : ThreadId ( "t-int" . into ( ) ) ,
@@ -510,7 +510,7 @@ async fn interrupt_turn_with_wrong_turn_id_fails_validation() {
510510 ( "FAKE_CODEX_SCRIPT" , & script. to_string_lossy ( ) ) ,
511511 ] ) ;
512512 let provider = provider_with_fixture_and_binary ( wrapper. to_path_buf ( ) ) ;
513- provider . start_session ( start_input ( "t-mm" ) ) . await . unwrap ( ) ;
513+ start_session_resilient ( & provider , start_input ( "t-mm" ) ) . await . unwrap ( ) ;
514514 provider
515515 . send_turn ( SendTurnInput {
516516 thread_id : ThreadId ( "t-mm" . into ( ) ) ,
@@ -535,7 +535,7 @@ async fn interrupt_turn_with_wrong_turn_id_fails_validation() {
535535#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
536536async fn set_permission_mode_returns_validation_error ( ) {
537537 let provider = provider_with_fixture ( ) ;
538- provider . start_session ( start_input ( "t-perm" ) ) . await . unwrap ( ) ;
538+ start_session_resilient ( & provider , start_input ( "t-perm" ) ) . await . unwrap ( ) ;
539539 let err = provider
540540 . set_permission_mode ( ThreadId ( "t-perm" . into ( ) ) , "acceptEdits" . into ( ) )
541541 . await
@@ -547,7 +547,7 @@ async fn set_permission_mode_returns_validation_error() {
547547#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
548548async fn set_model_updates_session_state ( ) {
549549 let provider = provider_with_fixture ( ) ;
550- provider . start_session ( start_input ( "t-model" ) ) . await . unwrap ( ) ;
550+ start_session_resilient ( & provider , start_input ( "t-model" ) ) . await . unwrap ( ) ;
551551 provider
552552 . set_model ( ThreadId ( "t-model" . into ( ) ) , "opus-4-7" . into ( ) )
553553 . await
@@ -571,7 +571,7 @@ async fn set_model_updates_session_state() {
571571#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
572572async fn stop_session_removes_from_list_and_closes_child ( ) {
573573 let provider = provider_with_fixture ( ) ;
574- provider . start_session ( start_input ( "t-close" ) ) . await . unwrap ( ) ;
574+ start_session_resilient ( & provider , start_input ( "t-close" ) ) . await . unwrap ( ) ;
575575 assert_eq ! ( provider. list_sessions( ) . await . unwrap( ) . len( ) , 1 ) ;
576576 provider
577577 . stop_session ( ThreadId ( "t-close" . into ( ) ) )
@@ -583,7 +583,7 @@ async fn stop_session_removes_from_list_and_closes_child() {
583583#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
584584async fn stop_session_is_idempotent ( ) {
585585 let provider = provider_with_fixture ( ) ;
586- provider . start_session ( start_input ( "t-idem" ) ) . await . unwrap ( ) ;
586+ start_session_resilient ( & provider , start_input ( "t-idem" ) ) . await . unwrap ( ) ;
587587 provider. stop_session ( ThreadId ( "t-idem" . into ( ) ) ) . await . unwrap ( ) ;
588588 let err = provider
589589 . stop_session ( ThreadId ( "t-idem" . into ( ) ) )
@@ -612,8 +612,8 @@ async fn send_turn_on_nonexistent_thread_returns_session_not_found() {
612612#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
613613async fn duplicate_start_session_returns_validation_error ( ) {
614614 let provider = provider_with_fixture ( ) ;
615- provider . start_session ( start_input ( "t-dup" ) ) . await . unwrap ( ) ;
616- let err = provider . start_session ( start_input ( "t-dup" ) ) . await . unwrap_err ( ) ;
615+ start_session_resilient ( & provider , start_input ( "t-dup" ) ) . await . unwrap ( ) ;
616+ let err = start_session_resilient ( & provider , start_input ( "t-dup" ) ) . await . unwrap_err ( ) ;
617617 assert ! ( matches!( err, ProviderError :: ValidationError { .. } ) ) ;
618618 provider. stop_session ( ThreadId ( "t-dup" . into ( ) ) ) . await . ok ( ) ;
619619}
@@ -624,7 +624,7 @@ async fn child_process_crash_emits_error_state() {
624624 // acknowledged.
625625 let wrapper = wrapper_with_env ( & [ ( "FAKE_CODEX_EXIT_AFTER" , "turn/start" ) ] ) ;
626626 let provider = provider_with_fixture_and_binary ( wrapper. to_path_buf ( ) ) ;
627- provider . start_session ( start_input ( "t-crash" ) ) . await . unwrap ( ) ;
627+ start_session_resilient ( & provider , start_input ( "t-crash" ) ) . await . unwrap ( ) ;
628628 let mut stream = provider. event_stream ( ) ;
629629 // turn/start will succeed then the fixture exits.
630630 let _ = provider
@@ -666,7 +666,7 @@ async fn concurrent_send_turn_returns_validation_error() {
666666 ( "FAKE_CODEX_SCRIPT" , & script. to_string_lossy ( ) ) ,
667667 ] ) ;
668668 let provider = provider_with_fixture_and_binary ( wrapper. to_path_buf ( ) ) ;
669- provider . start_session ( start_input ( "t-busy" ) ) . await . unwrap ( ) ;
669+ start_session_resilient ( & provider , start_input ( "t-busy" ) ) . await . unwrap ( ) ;
670670 provider
671671 . send_turn ( SendTurnInput {
672672 thread_id : ThreadId ( "t-busy" . into ( ) ) ,
@@ -701,7 +701,7 @@ async fn multiple_concurrent_sessions_are_isolated() {
701701 for i in 0 ..3 {
702702 let p = Arc :: clone ( & provider) ;
703703 handles. push ( tokio:: spawn ( async move {
704- p . start_session ( start_input ( & format ! ( "t-iso-{i}" ) ) ) . await
704+ start_session_resilient ( & p , start_input ( & format ! ( "t-iso-{i}" ) ) ) . await
705705 } ) ) ;
706706 }
707707 for h in handles {
@@ -723,7 +723,7 @@ async fn event_stream_subscribers_each_receive_events() {
723723 let provider = provider_with_fixture ( ) ;
724724 let mut a = provider. event_stream ( ) ;
725725 let mut b = provider. event_stream ( ) ;
726- provider . start_session ( start_input ( "t-sub" ) ) . await . unwrap ( ) ;
726+ start_session_resilient ( & provider , start_input ( "t-sub" ) ) . await . unwrap ( ) ;
727727 let got_a = timeout ( Duration :: from_secs ( 2 ) , a. next ( ) ) . await . unwrap ( ) ;
728728 let got_b = timeout ( Duration :: from_secs ( 2 ) , b. next ( ) ) . await . unwrap ( ) ;
729729 assert ! ( got_a. is_some( ) ) ;
@@ -734,7 +734,7 @@ async fn event_stream_subscribers_each_receive_events() {
734734#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
735735async fn late_subscriber_does_not_get_old_events ( ) {
736736 let provider = provider_with_fixture ( ) ;
737- provider . start_session ( start_input ( "t-late" ) ) . await . unwrap ( ) ;
737+ start_session_resilient ( & provider , start_input ( "t-late" ) ) . await . unwrap ( ) ;
738738 // Wait for the first burst of events to pass.
739739 tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
740740
@@ -755,7 +755,7 @@ async fn translate_turn_completed_error_emits_both_events_end_to_end() {
755755 ( "FAKE_CODEX_SCRIPT" , & script. to_string_lossy ( ) ) ,
756756 ] ) ;
757757 let provider = provider_with_fixture_and_binary ( wrapper. to_path_buf ( ) ) ;
758- provider . start_session ( start_input ( "t-fail" ) ) . await . unwrap ( ) ;
758+ start_session_resilient ( & provider , start_input ( "t-fail" ) ) . await . unwrap ( ) ;
759759 let mut stream = provider. event_stream ( ) ;
760760 provider
761761 . send_turn ( SendTurnInput {
@@ -807,6 +807,37 @@ fn write_bash_script(_prefix: &str, body: &str) -> ScriptFile {
807807 ScriptFile { _dir : dir, path }
808808}
809809
810+ /// Start a session, retrying transient ETXTBSY ("Text file busy")
811+ /// failures from the kernel.
812+ ///
813+ /// Why this exists: cargo runs tests in parallel inside one binary,
814+ /// and tokio's `Command::spawn` falls back to fork+exec on Linux when
815+ /// it can't use `posix_spawn`. If another test's `File::create` for
816+ /// its own wrapper script is in-flight at the moment we fork, the
817+ /// child inherits that write fd briefly; then our `execve()` of OUR
818+ /// wrapper sees a non-zero `i_writecount` on the inode (the kernel
819+ /// doesn't distinguish "this exec target" from "any open writer
820+ /// across all fds the child inherited") and rejects with ETXTBSY.
821+ /// The race window is microseconds, but it's real and shows up under
822+ /// CI load. A small retry loop sidesteps it without changing
823+ /// production code.
824+ async fn start_session_resilient (
825+ provider : & CodexAgentProvider ,
826+ input : StartSessionInput ,
827+ ) -> Result < codemux_lib:: agent_provider:: ProviderSession , ProviderError > {
828+ for _ in 0 ..10 {
829+ match provider. start_session ( input. clone ( ) ) . await {
830+ Ok ( s) => return Ok ( s) ,
831+ Err ( e) if format ! ( "{e:?}" ) . contains ( "Text file busy" ) => {
832+ tokio:: time:: sleep ( Duration :: from_millis ( 50 ) ) . await ;
833+ continue ;
834+ }
835+ Err ( e) => return Err ( e) ,
836+ }
837+ }
838+ provider. start_session ( input) . await
839+ }
840+
810841#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
811842async fn auth_probe_installed_returns_version_when_codex_works ( ) {
812843 let wrapper = write_bash_script (
@@ -838,7 +869,9 @@ async fn bogus_response_to_unknown_jsonrpc_id_does_not_crash_adapter() {
838869 ) ;
839870 let helper = write_bash_script ( "codex-bogus-" , & body) ;
840871 let provider = provider_with_fixture_and_binary ( helper. to_path_buf ( ) ) ;
841- provider. start_session ( start_input ( "t-bogus" ) ) . await . unwrap ( ) ;
872+ start_session_resilient ( & provider, start_input ( "t-bogus" ) )
873+ . await
874+ . unwrap ( ) ;
842875 // Adapter should still work after the spurious id.
843876 provider
844877 . send_turn ( SendTurnInput {
@@ -857,7 +890,7 @@ async fn bogus_response_to_unknown_jsonrpc_id_does_not_crash_adapter() {
857890#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
858891async fn dropping_provider_shuts_down_sessions ( ) {
859892 let provider = provider_with_fixture ( ) ;
860- provider . start_session ( start_input ( "t-drop" ) ) . await . unwrap ( ) ;
893+ start_session_resilient ( & provider , start_input ( "t-drop" ) ) . await . unwrap ( ) ;
861894 let sessions = provider. list_sessions ( ) . await . unwrap ( ) ;
862895 assert_eq ! ( sessions. len( ) , 1 ) ;
863896 drop ( provider) ;
@@ -899,7 +932,7 @@ async fn shutdown_during_event_streaming_does_not_panic() {
899932 ( "FAKE_CODEX_SCRIPT" , & script. to_string_lossy ( ) ) ,
900933 ] ) ;
901934 let provider = provider_with_fixture_and_binary ( wrapper. to_path_buf ( ) ) ;
902- provider . start_session ( start_input ( "t-race" ) ) . await . unwrap ( ) ;
935+ start_session_resilient ( & provider , start_input ( "t-race" ) ) . await . unwrap ( ) ;
903936 // Start a turn that will emit events mid-flight, then stop quickly.
904937 provider
905938 . send_turn ( SendTurnInput {
0 commit comments