@@ -190,6 +190,39 @@ async fn adapter_denies_every_preexisting_workspace_hardlink_alias() {
190190 }
191191}
192192
193+ #[ test]
194+ fn workspace_scan_treats_a_concurrently_removed_entry_as_absent ( ) {
195+ let workspace = tempfile:: tempdir ( ) . unwrap ( ) ;
196+ let transient = workspace. path ( ) . join ( "transient" ) ;
197+ std:: fs:: write ( & transient, "temporary" ) . unwrap ( ) ;
198+ std:: fs:: remove_file ( & transient) . unwrap ( ) ;
199+
200+ let metadata = workspace_scan_result ( std:: fs:: symlink_metadata ( & transient) , || {
201+ format ! ( "failed to inspect {}" , transient. display( ) )
202+ } )
203+ . unwrap ( ) ;
204+
205+ assert ! ( metadata. is_none( ) ) ;
206+ }
207+
208+ #[ test]
209+ fn workspace_scan_keeps_non_missing_io_failures_fatal ( ) {
210+ let error = workspace_scan_result :: < ( ) > (
211+ Err ( std:: io:: Error :: from ( std:: io:: ErrorKind :: PermissionDenied ) ) ,
212+ || "failed to inspect protected entry" . to_string ( ) ,
213+ )
214+ . unwrap_err ( ) ;
215+
216+ assert_eq ! ( error. to_string( ) , "failed to inspect protected entry" ) ;
217+ assert_eq ! (
218+ error
219+ . source( )
220+ . and_then( |source| source. downcast_ref:: <std:: io:: Error >( ) )
221+ . map( std:: io:: Error :: kind) ,
222+ Some ( std:: io:: ErrorKind :: PermissionDenied )
223+ ) ;
224+ }
225+
193226#[ cfg( unix) ]
194227#[ tokio:: test]
195228async fn adapter_preserves_stdout_and_stderr_as_separate_streams ( ) {
@@ -420,6 +453,70 @@ async fn real_srt_allows_workspace_writes_and_blocks_outside_writes() {
420453 }
421454}
422455
456+ /// Run explicitly with `A3S_TEST_SRT_BIN=/absolute/path/to/cli.js` and
457+ /// `A3S_TEST_SRT_NODE=/absolute/path/to/node`.
458+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
459+ #[ ignore = "requires an installed srt runtime and Node.js" ]
460+ async fn real_srt_probe_survives_concurrent_workspace_churn ( ) {
461+ use std:: sync:: {
462+ atomic:: { AtomicBool , Ordering } ,
463+ Arc ,
464+ } ;
465+
466+ let binary = std:: env:: var_os ( "A3S_TEST_SRT_BIN" )
467+ . map ( PathBuf :: from)
468+ . expect ( "set A3S_TEST_SRT_BIN" ) ;
469+ let node = std:: env:: var_os ( "A3S_TEST_SRT_NODE" )
470+ . map ( PathBuf :: from)
471+ . expect ( "set A3S_TEST_SRT_NODE" ) ;
472+ let workspace = tempfile:: tempdir ( ) . unwrap ( ) ;
473+ let churn_root = workspace. path ( ) . join ( "concurrent-writer" ) ;
474+ std:: fs:: create_dir_all ( & churn_root) . unwrap ( ) ;
475+
476+ let running = Arc :: new ( AtomicBool :: new ( true ) ) ;
477+ let writer_running = Arc :: clone ( & running) ;
478+ let writer = std:: thread:: spawn ( move || {
479+ let mut generation = 0usize ;
480+ while writer_running. load ( Ordering :: Acquire ) {
481+ let batch = churn_root. join ( format ! ( "batch-{}" , generation % 4 ) ) ;
482+ let nested = batch. join ( "nested" ) ;
483+ let _ = std:: fs:: remove_dir_all ( & batch) ;
484+ if std:: fs:: create_dir_all ( & nested) . is_ok ( ) {
485+ for index in 0 ..4 {
486+ let _ =
487+ std:: fs:: write ( nested. join ( format ! ( ".env-{index}" ) ) , b"TRANSIENT=removed" ) ;
488+ }
489+ }
490+ let _ = std:: fs:: remove_dir_all ( & batch) ;
491+ generation = generation. wrapping_add ( 1 ) ;
492+ }
493+ } ) ;
494+
495+ let result = async {
496+ for _ in 0 ..50 {
497+ let sandbox =
498+ SrtBashSandbox :: from_verified_npm_with_node ( & binary, & node, workspace. path ( ) ) ?;
499+ let output = sandbox
500+ . exec_command ( "printf a3s-managed-srt-ready" , "/workspace" )
501+ . await ?;
502+ if output. exit_code != 0 || output. stdout != "a3s-managed-srt-ready" {
503+ bail ! (
504+ "SRT capability probe failed with code {}: {}{}" ,
505+ output. exit_code,
506+ output. stdout,
507+ output. stderr
508+ ) ;
509+ }
510+ }
511+ Ok :: < ( ) , anyhow:: Error > ( ( ) )
512+ }
513+ . await ;
514+
515+ running. store ( false , Ordering :: Release ) ;
516+ writer. join ( ) . unwrap ( ) ;
517+ result. unwrap ( ) ;
518+ }
519+
423520/// Run explicitly with `A3S_TEST_SRT_BIN=/absolute/path/to/srt`.
424521#[ tokio:: test]
425522#[ ignore = "requires an installed srt runtime" ]
0 commit comments