@@ -2939,4 +2939,200 @@ mod tests {
29392939
29402940 ensure_clean ( & vol, & dir) . await ;
29412941 }
2942+
2943+ /// Cross-task content integrity: 100 concurrent SMB → local copies, each file
2944+ /// with unique deterministic content. After the batch completes, every
2945+ /// destination's blake3 hash must match the hash of the source it claims to
2946+ /// come from — catches buffer reuse across tasks, wrong-buffer-to-wrong-path
2947+ /// routing, races in the `Arc<Mutex<Option<SmbClient>>>` +
2948+ /// `Arc<RwLock<Option<Arc<Tree>>>>` split-session (Fix 2), and
2949+ /// cross-MessageId wire demux mistakes on cloned `Connection`s.
2950+ ///
2951+ /// Identical-content tests can't see any of these — every file would hash
2952+ /// the same, so a "swapped slice mid-file" or "task B's buffer landed under
2953+ /// task A's path" bug would pass trivially. Unique per-file content makes
2954+ /// any cross-contamination flip at least one destination's hash.
2955+ ///
2956+ /// Runs the real copy pipeline (`copy_volumes_with_progress` — the same
2957+ /// function `copy_between_volumes` calls) so `FuturesUnordered` + Fix 2's
2958+ /// split session + Fix 3's compound fast-path + Fix 4's pipelined scan all
2959+ /// execute together, the way a user's "copy 100 files" action does.
2960+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 4 ) ]
2961+ #[ ignore = "Requires Docker SMB containers (./apps/desktop/test/smb-servers/start.sh)" ]
2962+ async fn smb_integration_copy_100_unique_files_no_cross_contamination ( ) {
2963+ use crate :: file_system:: write_operations:: {
2964+ CollectorEventSink , VolumeCopyConfig , WriteOperationState , copy_volumes_with_progress,
2965+ } ;
2966+ use std:: sync:: atomic:: AtomicU8 ;
2967+ use std:: time:: { Duration , Instant } ;
2968+
2969+ // Content scheme: `blake3(b"cmdr-fix8-" || index_le) .as_bytes() repeated 320 times`
2970+ // = 10_240 bytes per file, truly unique per index, every byte position varies
2971+ // between files. Any cross-task slice swap — even a 32-byte block in the
2972+ // middle of one file coming from a neighbor's buffer — flips blake3.
2973+ // 10 KB keeps fixture setup cheap and stays inside the SMB compound
2974+ // fast-path (Fix 3) so we're exercising it, not the streaming fallback.
2975+ fn expected_content ( index : usize ) -> Vec < u8 > {
2976+ let mut seed = Vec :: with_capacity ( 10 + 8 ) ;
2977+ seed. extend_from_slice ( b"cmdr-fix8-" ) ;
2978+ seed. extend_from_slice ( & ( index as u64 ) . to_le_bytes ( ) ) ;
2979+ let block = * blake3:: hash ( & seed) . as_bytes ( ) ; // 32 bytes
2980+ let mut out = Vec :: with_capacity ( 32 * 320 ) ;
2981+ for _ in 0 ..320 {
2982+ out. extend_from_slice ( & block) ;
2983+ }
2984+ out
2985+ }
2986+
2987+ const FILE_COUNT : usize = 100 ;
2988+
2989+ // Hold the concrete `SmbVolume` for `ensure_clean` (which takes
2990+ // `&SmbVolume`) and clone an `Arc<dyn Volume>` view of the same
2991+ // session for the copy pipeline.
2992+ let smb_vol = Arc :: new ( make_docker_volume ( ) . await ) ;
2993+ let src_dir = test_dir_name ( ) ;
2994+ ensure_clean ( & smb_vol, & src_dir) . await ;
2995+ smb_vol. create_directory ( Path :: new ( & src_dir) ) . await . unwrap ( ) ;
2996+ let vol: Arc < dyn Volume > = smb_vol. clone ( ) ;
2997+
2998+ // Fixture: create 100 files on the SMB source, serially. Parallel
2999+ // `create_file` on a single SMB session wouldn't speed this up —
3000+ // creates are 1 RTT each — and keeping setup simple keeps any bug
3001+ // the test catches unambiguously a read/copy-path bug, not a
3002+ // write-path races-with-itself bug.
3003+ let fixture_start = Instant :: now ( ) ;
3004+ let mut source_paths: Vec < PathBuf > = Vec :: with_capacity ( FILE_COUNT ) ;
3005+ for i in 0 ..FILE_COUNT {
3006+ let name = format ! ( "f_{:03}.bin" , i) ;
3007+ let smb_path = format ! ( "{}/{}" , src_dir, name) ;
3008+ vol. create_file ( Path :: new ( & smb_path) , & expected_content ( i) )
3009+ . await
3010+ . unwrap ( ) ;
3011+ source_paths. push ( PathBuf :: from ( smb_path) ) ;
3012+ }
3013+ log:: info!(
3014+ "smb_integration_copy_100_unique_files: fixture setup took {:?}" ,
3015+ fixture_start. elapsed( )
3016+ ) ;
3017+
3018+ // Destination: local TempDir wrapped in a LocalPosixVolume. We feed the
3019+ // copy pipeline the same way production does (SMB volume → Local
3020+ // volume → `copy_volumes_with_progress`). `dest_path` is "/" relative to
3021+ // the local volume root — i.e. the TempDir itself.
3022+ let local_dir = tempfile:: TempDir :: new ( ) . expect ( "create TempDir" ) ;
3023+ let dest_vol: Arc < dyn Volume > = Arc :: new ( crate :: file_system:: volume:: LocalPosixVolume :: new (
3024+ "dest" ,
3025+ local_dir. path ( ) . to_path_buf ( ) ,
3026+ ) ) ;
3027+
3028+ let state = Arc :: new ( WriteOperationState {
3029+ intent : Arc :: new ( AtomicU8 :: new ( 0 ) ) ,
3030+ progress_interval : Duration :: from_millis ( 200 ) ,
3031+ conflict_resolution_tx : std:: sync:: Mutex :: new ( None ) ,
3032+ } ) ;
3033+ let events = CollectorEventSink :: new ( ) ;
3034+ let config = VolumeCopyConfig :: default ( ) ;
3035+
3036+ let copy_start = Instant :: now ( ) ;
3037+ let result = copy_volumes_with_progress (
3038+ & events,
3039+ "test-op-100-unique" ,
3040+ & state,
3041+ Arc :: clone ( & vol) ,
3042+ & source_paths,
3043+ Arc :: clone ( & dest_vol) ,
3044+ Path :: new ( "/" ) ,
3045+ & config,
3046+ )
3047+ . await ;
3048+ log:: info!(
3049+ "smb_integration_copy_100_unique_files: copy pipeline took {:?}" ,
3050+ copy_start. elapsed( )
3051+ ) ;
3052+ assert ! ( result. is_ok( ) , "copy should succeed: {:?}" , result) ;
3053+
3054+ // Count landed files — cheap aggregate sanity check before per-index
3055+ // verification. A cross-contamination bug that swapped two destinations
3056+ // would still show 100 files here, so this is not the real check.
3057+ let entries = std:: fs:: read_dir ( local_dir. path ( ) )
3058+ . expect ( "read dest dir" )
3059+ . filter_map ( |e| e. ok ( ) )
3060+ . count ( ) ;
3061+ assert_eq ! ( entries, FILE_COUNT , "expected {} files at destination" , FILE_COUNT ) ;
3062+
3063+ // Per-index integrity: for each source index, read its destination file
3064+ // and compare blake3 against the expected hash derived from the same
3065+ // index. Assert each one individually so a swap of two destinations
3066+ // fails loudly with both offending indices, not a vague aggregate.
3067+ let mut mismatches: Vec < String > = Vec :: new ( ) ;
3068+ for i in 0 ..FILE_COUNT {
3069+ let name = format ! ( "f_{:03}.bin" , i) ;
3070+ let dest_path = local_dir. path ( ) . join ( & name) ;
3071+ let actual_bytes = match std:: fs:: read ( & dest_path) {
3072+ Ok ( b) => b,
3073+ Err ( e) => {
3074+ mismatches. push ( format ! ( "{}: couldn't read destination: {}" , name, e) ) ;
3075+ continue ;
3076+ }
3077+ } ;
3078+ let expected_bytes = expected_content ( i) ;
3079+ let expected_hash = hash_bytes ( & expected_bytes) ;
3080+ let actual_hash = hash_bytes ( & actual_bytes) ;
3081+ if actual_hash != expected_hash {
3082+ // Find the first diff position and a small slice of context —
3083+ // a 10 KB diff dump would drown the terminal on any failure.
3084+ let first_diff = expected_bytes. iter ( ) . zip ( actual_bytes. iter ( ) ) . position ( |( a, b) | a != b) ;
3085+ let diff_detail = match first_diff {
3086+ Some ( pos) => {
3087+ let end_exp = pos. saturating_add ( 16 ) . min ( expected_bytes. len ( ) ) ;
3088+ let end_act = pos. saturating_add ( 16 ) . min ( actual_bytes. len ( ) ) ;
3089+ format ! (
3090+ "first diff at byte {}: expected {:02x?}, got {:02x?}" ,
3091+ pos,
3092+ & expected_bytes[ pos..end_exp] ,
3093+ & actual_bytes[ pos..end_act]
3094+ )
3095+ }
3096+ None => {
3097+ // Same bytes but different length (hashes differ so
3098+ // there must be a difference somewhere).
3099+ format ! (
3100+ "byte-for-byte equal in overlap but lengths differ: expected {}, got {}" ,
3101+ expected_bytes. len( ) ,
3102+ actual_bytes. len( )
3103+ )
3104+ }
3105+ } ;
3106+ mismatches. push ( format ! (
3107+ "{}: expected blake3 {} ({} bytes), got blake3 {} ({} bytes); {}" ,
3108+ name,
3109+ hex_of( & expected_hash) ,
3110+ expected_bytes. len( ) ,
3111+ hex_of( & actual_hash) ,
3112+ actual_bytes. len( ) ,
3113+ diff_detail,
3114+ ) ) ;
3115+ }
3116+ }
3117+ assert ! (
3118+ mismatches. is_empty( ) ,
3119+ "{} of {} destinations failed content check:\n - {}" ,
3120+ mismatches. len( ) ,
3121+ FILE_COUNT ,
3122+ mismatches. join( "\n - " ) ,
3123+ ) ;
3124+
3125+ // Cleanup the SMB source. The TempDir cleans itself on drop.
3126+ ensure_clean ( & smb_vol, & src_dir) . await ;
3127+ }
3128+
3129+ /// Hex formatter for blake3 hashes in failure messages. Avoids a hex-crate
3130+ /// dep just for test diagnostics.
3131+ fn hex_of ( bytes : & [ u8 ; 32 ] ) -> String {
3132+ let mut s = String :: with_capacity ( 64 ) ;
3133+ for b in bytes {
3134+ s. push_str ( & format ! ( "{:02x}" , b) ) ;
3135+ }
3136+ s
3137+ }
29423138}
0 commit comments