@@ -16,6 +16,20 @@ use tracedecay::storage::{
1616 default_profile_project_id, write_enrollment_marker, EnrollmentMarker , StorageMode ,
1717} ;
1818
19+ /// Bound for waits that depend on spawning and running the real `tracedecay`
20+ /// CLI as a child process: connecting to the fake daemon socket and forwarding
21+ /// the observed request back to the test thread. Under nextest's
22+ /// process-per-test parallelism the fork/exec + init of that child can be
23+ /// scheduled slowly on a loaded runner, so a 2s bound false-fires. This is a
24+ /// generous ceiling that still fails fast on a genuine hang (the CLI normally
25+ /// connects in well under a second).
26+ const CLI_ROUNDTRIP_TIMEOUT : Duration = Duration :: from_secs ( 20 ) ;
27+
28+ /// Bound for local, in-process readiness signals (a spawned thread binding a
29+ /// socket and sending on an mpsc channel). These do not spawn external
30+ /// processes, but the thread can still be scheduled slowly under load.
31+ const LOCAL_READY_TIMEOUT : Duration = Duration :: from_secs ( 10 ) ;
32+
1933fn init_project_with_cli ( home : & Path , project : & Path ) {
2034 std:: fs:: create_dir_all ( project. join ( "src" ) ) . unwrap ( ) ;
2135 std:: fs:: write (
@@ -41,11 +55,29 @@ fn init_project_with_cli(home: &Path, project: &Path) {
4155}
4256
4357fn git ( project : & Path , args : & [ & str ] ) {
44- let output = std:: process:: Command :: new ( "git" )
45- . args ( args)
46- . current_dir ( project)
47- . output ( )
48- . expect ( "git should run" ) ;
58+ let git = crate :: common:: git_program ( ) ;
59+ // Retry a transient spawn ENOENT under heavy parallel load.
60+ let mut last_err: Option < std:: io:: Error > = None ;
61+ let mut output = None ;
62+ for attempt in 0 ..5 {
63+ match std:: process:: Command :: new ( & git)
64+ . args ( args)
65+ . current_dir ( project)
66+ . output ( )
67+ {
68+ Ok ( out) => {
69+ output = Some ( out) ;
70+ break ;
71+ }
72+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound && attempt < 4 => {
73+ last_err = Some ( e) ;
74+ std:: thread:: sleep ( Duration :: from_millis ( 20 * ( attempt + 1 ) ) ) ;
75+ }
76+ Err ( e) => panic ! ( "git {args:?} should run (program {git:?}): {e}" ) ,
77+ }
78+ }
79+ let output =
80+ output. unwrap_or_else ( || panic ! ( "git {args:?} should run after retries: {last_err:?}" ) ) ;
4981 assert ! (
5082 output. status. success( ) ,
5183 "git {:?} failed\n stdout:\n {}\n stderr:\n {}" ,
@@ -154,7 +186,7 @@ fn spawn_sentinel_daemon_with_notification(
154186 . expect ( "set listener nonblocking" ) ;
155187 ready_tx. send ( ( ) ) . expect ( "notify fake daemon readiness" ) ;
156188
157- let deadline = Instant :: now ( ) + Duration :: from_secs ( 2 ) ;
189+ let deadline = Instant :: now ( ) + CLI_ROUNDTRIP_TIMEOUT ;
158190 let ( stream, _) = loop {
159191 match listener. accept ( ) {
160192 Ok ( accepted) => break accepted,
@@ -171,7 +203,7 @@ fn spawn_sentinel_daemon_with_notification(
171203 . set_nonblocking ( false )
172204 . expect ( "set accepted stream blocking" ) ;
173205 stream
174- . set_write_timeout ( Some ( Duration :: from_secs ( 2 ) ) )
206+ . set_write_timeout ( Some ( CLI_ROUNDTRIP_TIMEOUT ) )
175207 . expect ( "write timeout" ) ;
176208
177209 let mut reader = BufReader :: new ( stream. try_clone ( ) . expect ( "clone fake daemon stream" ) ) ;
@@ -228,7 +260,7 @@ fn spawn_sentinel_daemon_with_notification(
228260 } ) ;
229261
230262 ready_rx
231- . recv_timeout ( Duration :: from_secs ( 2 ) )
263+ . recv_timeout ( LOCAL_READY_TIMEOUT )
232264 . expect ( "fake daemon should become ready" ) ;
233265 request_rx
234266}
@@ -245,7 +277,7 @@ fn spawn_hook_event_daemon(socket_path: PathBuf) -> mpsc::Receiver<Value> {
245277 . expect ( "set listener nonblocking" ) ;
246278 ready_tx. send ( ( ) ) . expect ( "notify fake daemon readiness" ) ;
247279
248- let deadline = Instant :: now ( ) + Duration :: from_secs ( 2 ) ;
280+ let deadline = Instant :: now ( ) + CLI_ROUNDTRIP_TIMEOUT ;
249281 let ( stream, _) = loop {
250282 match listener. accept ( ) {
251283 Ok ( accepted) => break accepted,
@@ -292,7 +324,7 @@ fn spawn_hook_event_daemon(socket_path: PathBuf) -> mpsc::Receiver<Value> {
292324 } ) ;
293325
294326 ready_rx
295- . recv_timeout ( Duration :: from_secs ( 2 ) )
327+ . recv_timeout ( LOCAL_READY_TIMEOUT )
296328 . expect ( "fake daemon should become ready" ) ;
297329 request_rx
298330}
@@ -342,7 +374,7 @@ fn assert_hook_notification(
342374 ) ;
343375
344376 let request = observed_request
345- . recv_timeout ( Duration :: from_secs ( 2 ) )
377+ . recv_timeout ( CLI_ROUNDTRIP_TIMEOUT )
346378 . expect ( "fake daemon should receive hook event" ) ;
347379 assert_eq ! ( request[ "params" ] [ "agent" ] , expected_agent) ;
348380 assert_eq ! ( request[ "params" ] [ "event" ] , expected_event) ;
@@ -592,7 +624,7 @@ fn tool_cli_invokes_mcp_tool_through_daemon_socket() {
592624 "tool CLI should print daemon response, got:\n {stdout}"
593625 ) ;
594626 observed_request
595- . recv_timeout ( Duration :: from_secs ( 2 ) )
627+ . recv_timeout ( CLI_ROUNDTRIP_TIMEOUT )
596628 . expect ( "fake daemon should receive tools/call request" ) ;
597629}
598630
@@ -635,7 +667,7 @@ fn tool_cli_skips_daemon_notifications_until_matching_response() {
635667 "tool CLI should print daemon response after notification, got:\n {stdout}"
636668 ) ;
637669 observed_request
638- . recv_timeout ( Duration :: from_secs ( 2 ) )
670+ . recv_timeout ( CLI_ROUNDTRIP_TIMEOUT )
639671 . expect ( "fake daemon should receive tools/call request" ) ;
640672}
641673
@@ -690,7 +722,7 @@ fn profile_scoped_tool_cli_invokes_daemon_without_project_handshake() {
690722 "tool CLI should print daemon response, got:\n {stdout}"
691723 ) ;
692724 let request = observed_request
693- . recv_timeout ( Duration :: from_secs ( 2 ) )
725+ . recv_timeout ( CLI_ROUNDTRIP_TIMEOUT )
694726 . expect ( "fake daemon should receive profile-scoped tools/call request" ) ;
695727 assert_eq ! (
696728 request[ "params" ] [ "arguments" ] [ "storage_scope" ] ,
@@ -786,7 +818,7 @@ fn first_touch_store_tool_cli_invokes_daemon_with_init_permission() {
786818 "tool CLI should print daemon response, got:\n {stdout}"
787819 ) ;
788820 let request = observed_request
789- . recv_timeout ( Duration :: from_secs ( 2 ) )
821+ . recv_timeout ( CLI_ROUNDTRIP_TIMEOUT )
790822 . expect ( "fake daemon should receive first-touch tools/call request" ) ;
791823 assert_eq ! ( request[ "params" ] [ "arguments" ] [ "action" ] , "add" ) ;
792824}
0 commit comments