ai-workbench-lib 0.4.0

AI Workbench library for file processing, splitting, and model interactions
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
use anyhow::{Result};
use aws_sdk_s3::Client as S3Client;
use std::collections::HashMap;
use std::path::Path;
use super::file_splitter::types::FileType;
use tracing::{info, error, warn};

/// Handles discovery and processing of multiple files and folders
pub struct FileDiscovery {
    s3_client: S3Client,
    workspace_bucket: String,
}

impl FileDiscovery {
    pub fn new(s3_client: S3Client, workspace_bucket: String) -> Self {
        Self {
            s3_client,
            workspace_bucket,
        }
    }
    
    /// Discover all files to process from the input specification
    pub async fn discover_files(&self, input_spec: &str) -> Result<Vec<FileInfo>> {
        info!("=== FileDiscovery::discover_files DEBUG START ===");
        info!("Input spec: '{}'", input_spec);
        info!("Workspace bucket: '{}'", self.workspace_bucket);
        
        let result = if input_spec.ends_with('/') {
            info!("Detected folder input (ends with '/') - processing as folder");
            // Folder specified
            self.process_folder(input_spec).await
        } else {
            info!("Detected single file input (no trailing '/') - processing as single file");
            // Single file
            self.process_single_file(input_spec).await
        };
        
        match &result {
            Ok(files) => {
                info!("discover_files completed successfully with {} files", files.len());
            },
            Err(e) => {
                error!("discover_files failed: {}", e);
                error!("Error details: {:?}", e);
            }
        }
        
        info!("=== FileDiscovery::discover_files DEBUG END ===");
        result
    }
    
    /// Process a single file
    async fn process_single_file(&self, file_key: &str) -> Result<Vec<FileInfo>> {
        info!("=== process_single_file DEBUG START ===");
        info!("Processing single file: '{}'", file_key);
        info!("Full S3 path: s3://{}/{}", self.workspace_bucket, file_key);
        
        info!("Attempting to get file metadata...");
        let metadata_result = self.get_file_metadata(file_key).await;
        
        let metadata = match metadata_result {
            Ok(meta) => {
                info!("Metadata retrieved successfully:");
                info!("  - Size: {} bytes", meta.size_bytes);
                info!("  - File type: {:?}", meta.file_type);
                info!("  - Is processable: {}", meta.is_processable);
                
                if !meta.is_processable {
                    error!("File '{}' is not processable! Reasons:", file_key);
                    error!("  - File type: {:?}", meta.file_type);
                    error!("  - Size: {} bytes", meta.size_bytes);
                    if meta.size_bytes == 0 {
                        error!("  - File is empty (0 bytes)");
                    }
                    if matches!(meta.file_type, super::file_splitter::types::FileType::Binary) {
                        error!("  - File is binary type");
                        if meta.size_bytes > 100 * 1024 * 1024 {
                            error!("  - File is too large (> 100MB)");
                        }
                    }
                    return Err(anyhow::anyhow!("File '{}' is not processable", file_key));
                }
                
                meta
            },
            Err(e) => {
                error!("Failed to get metadata for file '{}': {}", file_key, e);
                error!("This usually means:");
                error!("  1. File does not exist at s3://{}/{}", self.workspace_bucket, file_key);
                error!("  2. No permissions to access the file");
                error!("  3. S3 service is unavailable");
                error!("  4. Incorrect bucket name or file path");
                return Err(e.context(format!("Failed to get metadata for single file: {}", file_key)));
            }
        };
        
        let file_info = FileInfo {
            s3_key: file_key.to_string(),
            relative_path: file_key.to_string(),
            size_bytes: metadata.size_bytes,
            file_type: metadata.file_type,
        };
        
        info!("Single file processed successfully:");
        info!("  - S3 key: '{}'", file_info.s3_key);
        info!("  - Relative path: '{}'", file_info.relative_path);
        info!("  - Size: {} bytes", file_info.size_bytes);
        info!("  - Type: {:?}", file_info.file_type);
        info!("=== process_single_file DEBUG END (SUCCESS) ===");
        
        Ok(vec![file_info])
    }
    
    
    /// Process a folder (recursively discover all files)
    async fn process_folder(&self, folder_prefix: &str) -> Result<Vec<FileInfo>> {
        info!("=== process_folder DEBUG START ===");
        info!("Processing folder: '{}'", folder_prefix);
        info!("Full S3 path: s3://{}/{}", self.workspace_bucket, folder_prefix);
        
        let mut files = Vec::new();
        let mut continuation_token: Option<String> = None;
        let mut request_count = 0;
        
        loop {
            request_count += 1;
            info!("Making S3 ListObjects request #{}", request_count);
            
            let mut request = self.s3_client
                .list_objects_v2()
                .bucket(&self.workspace_bucket)
                .prefix(folder_prefix)
                .max_keys(1000);
                
            if let Some(token) = &continuation_token {
                info!("Using continuation token: {}", token);
                request = request.continuation_token(token);
            }
            
            info!("Sending ListObjectsV2 request to S3...");
            let response_result = request.send().await;
            
            let response = match response_result {
                Ok(resp) => {
                    info!("S3 ListObjectsV2 request #{} successful!", request_count);
                    resp
                },
                Err(e) => {
                    error!("S3 ListObjectsV2 request failed: {}", e);
                    error!("Error details: {:?}", e);
                    error!("Possible causes:");
                    error!("  1. Folder/prefix '{}' does not exist in bucket '{}'", folder_prefix, self.workspace_bucket);
                    error!("  2. No list permissions for this bucket/prefix");
                    error!("  3. AWS credentials are invalid or expired");
                    error!("  4. Bucket '{}' does not exist", self.workspace_bucket);
                    error!("  5. Network connectivity issues");
                    return Err(anyhow::anyhow!("S3 ListObjectsV2 failed for folder '{}': {}", folder_prefix, e)
                        .context("Failed to list objects in S3"));
                }
            };
            
            let objects = response.contents();
            info!("Found {} objects in this batch", objects.len());
            
            if objects.is_empty() {
                warn!("No objects found in folder '{}' - this might be expected if folder is empty", folder_prefix);
            }
            
            for (i, object) in objects.iter().enumerate() {
                let key = object.key().unwrap_or_default();
                info!("Processing object {}: '{}'", i + 1, key);
                
                // Skip directories (keys ending with /)
                if key.ends_with('/') {
                    info!("  Skipping directory: '{}'", key);
                    continue;
                }
                
                info!("  Creating metadata for file: '{}'", key);
                let metadata = FileMetadata::from_s3_object(object, key);
                info!("  File metadata: size={} bytes, type={:?}, processable={}", 
                     metadata.size_bytes, metadata.file_type, metadata.is_processable);
                
                if metadata.is_processable {
                    let relative_path = key.strip_prefix(folder_prefix).unwrap_or(key).to_string();
                    let file_info = FileInfo {
                        s3_key: key.to_string(),
                        relative_path: relative_path.clone(),
                        size_bytes: metadata.size_bytes,
                        file_type: metadata.file_type,
                    };
                    
                    info!("  Adding processable file: '{}' (relative: '{}')", key, relative_path);
                    files.push(file_info);
                } else {
                    info!("  Skipping non-processable file: '{}' (size={}, type={:?})", 
                         key, metadata.size_bytes, metadata.file_type);
                }
            }
            
            continuation_token = response.next_continuation_token().map(|s| s.to_string());
            if let Some(ref token) = continuation_token {
                info!("More objects available, continuing with token: {}", token);
            } else {
                info!("No more objects to fetch, finishing folder processing");
                break;
            }
        }
        
        info!("Folder processing completed: {} processable files found", files.len());
        if files.is_empty() {
            warn!("No processable files found in folder '{}'", folder_prefix);
            warn!("This could mean:");
            warn!("  1. Folder is empty");
            warn!("  2. All files are binary/non-processable");
            warn!("  3. All files are empty (0 bytes)");
            warn!("  4. All files are too large (> 100MB for binary files)");
        }
        
        info!("=== process_folder DEBUG END ===");
        Ok(files)
    }
    
    /// Get metadata for a single file
    async fn get_file_metadata(&self, file_key: &str) -> Result<FileMetadata> {
        info!("=== get_file_metadata DEBUG START ===");
        info!("Getting metadata for file: '{}'", file_key);
        info!("Bucket: '{}'", self.workspace_bucket);
        info!("Full S3 path: s3://{}/{}", self.workspace_bucket, file_key);
        
        info!("Sending HEAD request to S3...");
        let response_result = self.s3_client
            .head_object()
            .bucket(&self.workspace_bucket)
            .key(file_key)
            .send()
            .await;
        
        let response = match response_result {
            Ok(resp) => {
                info!("S3 HEAD request successful!");
                info!("Response metadata:");
                if let Some(content_length) = resp.content_length() {
                    info!("  - Content length: {} bytes", content_length);
                } else {
                    warn!("  - No content length in response");
                }
                if let Some(content_type) = resp.content_type() {
                    info!("  - Content type: {}", content_type);
                } else {
                    info!("  - No content type specified");
                }
                if let Some(last_modified) = resp.last_modified() {
                    info!("  - Last modified: {:?}", last_modified);
                }
                resp
            },
            Err(e) => {
                error!("S3 HEAD request failed for '{}': {}", file_key, e);
                error!("Error details: {:?}", e);
                error!("Possible causes:");
                error!("  1. File '{}' does not exist in bucket '{}'", file_key, self.workspace_bucket);
                error!("  2. No read permissions for this file/bucket");
                error!("  3. AWS credentials are invalid or expired");
                error!("  4. Network connectivity issues");
                error!("  5. S3 service is temporarily unavailable");
                return Err(anyhow::anyhow!("S3 HEAD request failed for '{}': {}", file_key, e)
                    .context(format!("Failed to get metadata for {}", file_key)));
            }
        };
        
        let size_bytes = response.content_length().unwrap_or(0) as usize;
        let path = Path::new(file_key);
        let file_type = super::file_splitter::types::FileType::from_extension(path);
        let is_processable = Self::is_file_processable(&file_type, size_bytes);
        
        info!("File metadata analysis:");
        info!("  - Raw size from S3: {} bytes", size_bytes);
        info!("  - File path for type detection: {:?}", path);
        info!("  - Detected file type: {:?}", file_type);
        info!("  - Is processable: {}", is_processable);
        
        if !is_processable {
            info!("File is not processable because:");
            if size_bytes == 0 {
                info!("  - File is empty (0 bytes)");
            }
            if matches!(file_type, super::file_splitter::types::FileType::Binary) {
                info!("  - File type is Binary (not text-based)");
                if size_bytes > 100 * 1024 * 1024 {
                    info!("  - Binary file is too large (> 100MB): {} bytes", size_bytes);
                }
            }
        }
        
        let metadata = FileMetadata {
            size_bytes,
            file_type,
            is_processable,
        };
        
        info!("=== get_file_metadata DEBUG END (SUCCESS) ===");
        Ok(metadata)
    }
    
    /// Check if a file should be processed based on type and size
    fn is_file_processable(file_type: &FileType, size_bytes: usize) -> bool {
        
        // Skip binary files that are too large (> 100MB)
        if matches!(file_type, FileType::Binary) && size_bytes > 100 * 1024 * 1024 {
            return false;
        }
        
        // Skip empty files
        if size_bytes == 0 {
            return false;
        }
        
        // Process most text-based files
        !matches!(file_type, FileType::Binary)
    }
    
    /// Generate processing summary
    pub fn generate_processing_summary(&self, files: &[FileInfo]) -> ProcessingSummary {
        let mut summary = ProcessingSummary {
            total_files: files.len(),
            total_size_bytes: files.iter().map(|f| f.size_bytes).sum(),
            files_by_type: HashMap::new(),
            files_by_category: HashMap::new(),
        };
        
        for file in files {
            // Count by file type
            *summary.files_by_type.entry(file.file_type).or_insert(0) += 1;
            
            // Count by category
            let category = file.file_type.language_category();
            *summary.files_by_category.entry(category.to_string()).or_insert(0) += 1;
        }
        
        summary
    }
}

/// Information about a discovered file
#[derive(Debug, Clone,)]
pub struct FileInfo {
    pub s3_key: String,
    pub relative_path: String,
    pub size_bytes: usize,
    pub file_type: FileType,
}

/// File metadata
#[derive(Debug, Clone)]
struct FileMetadata {
    size_bytes: usize,
    pub file_type: FileType,
    is_processable: bool,
}

impl FileMetadata {
    fn from_s3_object(object: &aws_sdk_s3::types::Object, key: &str) -> Self {
        let size_bytes = object.size().unwrap_or(0) as usize;
        let path = Path::new(key);
        let file_type = FileType::from_extension(path);
        let is_processable = FileDiscovery::is_file_processable(&file_type, size_bytes);
        
        Self {
            size_bytes,
            file_type,
            is_processable,
        }
    }
}

/// Summary of files to be processed
#[derive(Debug, Clone)]
pub struct ProcessingSummary {
    pub total_files: usize,
    pub total_size_bytes: usize,
    pub files_by_type: HashMap<FileType, usize>,
    pub files_by_category: HashMap<String, usize>,
}

impl ProcessingSummary {
    pub fn format_summary(&self) -> String {
        let mut summary = format!(
            "Processing Summary:\n- Total files: {}\n- Total size: {:.2} MB\n",
            self.total_files,
            self.total_size_bytes as f64 / (1024.0 * 1024.0)
        );
        
        summary.push_str("\nFiles by category:\n");
        for (category, count) in &self.files_by_category {
            summary.push_str(&format!("- {}: {} files\n", category, count));
        }
        
        summary.push_str("\nFiles by type:\n");
        for (file_type, count) in &self.files_by_type {
            summary.push_str(&format!("- {:?}: {} files\n", file_type, count));
        }
        
        summary
    }
}


#[cfg(test)]
mod file_discovery_tests {
    use crate::modules::file_discovery::{FileDiscovery, FileInfo};
    use crate::modules::file_splitter::types::FileType;
    use aws_config::BehaviorVersion;
    use aws_sdk_s3::Client as S3Client;
    use std::env;

    /// Helper to create S3 client for testing
    async fn create_test_s3_client() -> S3Client {
        let config = aws_config::defaults(BehaviorVersion::latest())
            .region("eu-west-2")
            .load()
            .await;
        S3Client::new(&config)
    }

    /// Helper to get test workspace bucket from environment
    fn get_test_bucket() -> String {
        env::var("TEST_WORKSPACE_BUCKET")
            .unwrap_or_else(|_| "ai-workbench-6c9c43db-7fe6-42f1-8b11-8f82323f83f0-eu-west-2".to_string())
    }

    #[tokio::test]
    async fn test_single_file_discovery() {
        let s3_client = create_test_s3_client().await;
        let bucket = get_test_bucket();
        let discovery = FileDiscovery::new(s3_client, bucket);

        // Test with a known file key (you'll provide this)
        let test_file_key = "test-data/sample.txt";
        
        let result = discovery.discover_files(test_file_key).await;
        
        match result {
            Ok(files) => {
                assert_eq!(files.len(), 1);
                let file = &files[0];
                assert_eq!(file.s3_key, test_file_key);
                assert_eq!(file.relative_path, test_file_key);
                assert!(file.size_bytes > 0);
                println!("Discovered file: {} ({} bytes, type: {:?})", 
                         file.s3_key, file.size_bytes, file.file_type);
            }
            Err(e) => {
                eprintln!("Test failed - file not found or error: {}", e);
                // For now, we'll make this non-failing until you provide real keys
                println!("Skipping test - no test file available");
            }
        }
    }

    #[tokio::test]
    async fn test_folder_discovery() {
        let s3_client = create_test_s3_client().await;
        let bucket = get_test_bucket();
        let discovery = FileDiscovery::new(s3_client, bucket);

        // Test with a known folder prefix (you'll provide this)
        let test_folder = "text_files/";
        
        let result = discovery.discover_files(test_folder).await;
        
        match result {
            Ok(files) => {
                println!("Discovered {} files in folder '{}'", files.len(), test_folder);
                
                for file in &files {
                    assert!(file.s3_key.starts_with(test_folder));
                    assert!(!file.s3_key.ends_with('/'));  // Should not include directories
                    assert!(file.size_bytes > 0);
                    assert!(!file.relative_path.starts_with(test_folder));  // Should be relative
                    
                    println!("  - {} ({} bytes, type: {:?})", 
                             file.relative_path, file.size_bytes, file.file_type);
                }
                
                // Generate and display processing summary
                let summary = discovery.generate_processing_summary(&files);
                println!("\n{}", summary.format_summary());
            }
            Err(e) => {
                eprintln!("Test failed - folder not found or error: {}", e);
                println!("Skipping test - no test folder available");
            }
        }
    }

    #[tokio::test]
    async fn test_file_type_detection() {
        let s3_client = create_test_s3_client().await;
        let bucket = get_test_bucket();
        let discovery = FileDiscovery::new(s3_client, bucket);

        // Test files with different extensions (you'll provide these)
        let test_files = vec![
            "test-data/sample.txt",
            "test-data/data.csv", 
            "test-data/config.json",
            "test-data/document.md",
            "test-data/script.py",
            "test-data/code.rs",
            "test-data/styles.css",
            "test-data/markup.html",
        ];

        for file_key in test_files {
            let result = discovery.discover_files(file_key).await;
            
            match result {
                Ok(files) if !files.is_empty() => {
                    let file = &files[0];
                    println!("File: {} -> Type: {:?}, Category: {}", 
                             file_key, file.file_type, file.file_type.language_category());
                    
                    // Verify file type matches extension
                    match file_key.split('.').last() {
                        Some("txt") | Some("md") => assert_eq!(file.file_type, FileType::Text),
                        Some("csv") => assert_eq!(file.file_type, FileType::Csv),
                        Some("json") => assert_eq!(file.file_type, FileType::Json),
                        Some("py") => assert_eq!(file.file_type, FileType::Python),
                        Some("rs") => assert_eq!(file.file_type, FileType::Rust),
                        Some("css") => assert_eq!(file.file_type, FileType::Css),
                        Some("html") | Some("htm") => assert_eq!(file.file_type, FileType::Html),
                        _ => {}  // Other types
                    }
                }
                Ok(_) => println!("File {} not found, skipping", file_key),
                Err(e) => println!("Error checking {}: {}", file_key, e),
            }
        }
    }

    #[tokio::test] 
    async fn test_large_folder_discovery() {
        let s3_client = create_test_s3_client().await;
        let bucket = get_test_bucket();
        let discovery = FileDiscovery::new(s3_client, bucket);

        // Test with a folder that might have many files
        let large_folder = "large-dataset/";
        
        let result = discovery.discover_files(large_folder).await;
        
        match result {
            Ok(files) => {
                println!("Discovered {} files in large folder", files.len());
                
                if files.len() > 10 {
                    // Test pagination worked correctly
                    let total_size: usize = files.iter().map(|f| f.size_bytes).sum();
                    println!("Total size: {:.2} MB", total_size as f64 / (1024.0 * 1024.0));
                    
                    // Verify all files are unique
                    let mut keys: Vec<_> = files.iter().map(|f| &f.s3_key).collect();
                    keys.sort();
                    keys.dedup();
                    assert_eq!(keys.len(), files.len(), "Found duplicate files in discovery");
                }
                
                // Generate summary for large folder
                let summary = discovery.generate_processing_summary(&files);
                println!("\n{}", summary.format_summary());
            }
            Err(e) => {
                println!("Large folder test skipped: {}", e);
            }
        }
    }

    #[tokio::test]
    async fn test_file_filtering() {
        let s3_client = create_test_s3_client().await;
        let bucket = get_test_bucket();
        let discovery = FileDiscovery::new(s3_client, bucket);

        // Test folder that might contain various file types including binaries
        let mixed_folder = "mixed-files/";
        
        let result = discovery.discover_files(mixed_folder).await;
        
        match result {
            Ok(files) => {
                println!("Found {} processable files in mixed folder", files.len());
                
                // All returned files should be processable
                for file in &files {
                    assert!(file.size_bytes > 0, "File {} has zero size", file.s3_key);
                    assert!(!matches!(file.file_type, FileType::Binary), 
                           "Binary file {} should have been filtered out", file.s3_key);
                    
                    // Very large files should be filtered (except text files)
                    if file.size_bytes > 100 * 1024 * 1024 {  // 100MB
                        assert!(!matches!(file.file_type, FileType::Binary),
                               "Large binary file {} should have been filtered", file.s3_key);
                    }
                }
            }
            Err(e) => {
                println!("Mixed files test skipped: {}", e);
            }
        }
    }

    #[tokio::test]
    async fn test_processing_summary() {
        let s3_client = create_test_s3_client().await;
        let bucket = get_test_bucket();
        let discovery = FileDiscovery::new(s3_client, bucket);

        let test_folder = "test-data/";
        
        let result = discovery.discover_files(test_folder).await;
        
        match result {
            Ok(files) if !files.is_empty() => {
                let summary = discovery.generate_processing_summary(&files);
                
                // Verify summary accuracy
                assert_eq!(summary.total_files, files.len());
                let expected_total_size: usize = files.iter().map(|f| f.size_bytes).sum();
                assert_eq!(summary.total_size_bytes, expected_total_size);
                
                // Verify file type counts
                let mut manual_type_counts = std::collections::HashMap::new();
                for file in &files {
                    *manual_type_counts.entry(file.file_type).or_insert(0) += 1;
                }
                assert_eq!(summary.files_by_type, manual_type_counts);
                
                // Verify category counts
                let mut manual_category_counts = std::collections::HashMap::new();
                for file in &files {
                    let category = file.file_type.language_category();
                    *manual_category_counts.entry(category.to_string()).or_insert(0) += 1;
                }
                assert_eq!(summary.files_by_category, manual_category_counts);
                
                // Test summary formatting
                let formatted = summary.format_summary();
                assert!(formatted.contains("Processing Summary:"));
                assert!(formatted.contains(&format!("Total files: {}", summary.total_files)));
                assert!(formatted.contains("Files by category:"));
                assert!(formatted.contains("Files by type:"));
                
                println!("Processing Summary Test Results:\n{}", formatted);
            }
            Ok(_) => println!("No files found for summary test"),
            Err(e) => println!("Summary test skipped: {}", e),
        }
    }

    #[test]
    fn test_file_info_structure() {
        use crate::modules::file_splitter::types::FileType;
        
        // Test FileInfo structure
        let file_info = FileInfo {
            s3_key: "test/path/file.txt".to_string(),
            relative_path: "path/file.txt".to_string(),
            size_bytes: 1024,
            file_type: FileType::Text,
        };
        
        assert_eq!(file_info.s3_key, "test/path/file.txt");
        assert_eq!(file_info.relative_path, "path/file.txt");
        assert_eq!(file_info.size_bytes, 1024);
        assert_eq!(file_info.file_type, FileType::Text);
        
        // Test that FileInfo can be cloned and debugged
        let cloned = file_info.clone();
        assert_eq!(file_info.s3_key, cloned.s3_key);
        
        let debug_str = format!("{:?}", file_info);
        assert!(debug_str.contains("FileInfo"));
    }
}