mcp_execution_files/
builder.rs

1//! Builder pattern for constructing virtual filesystems.
2//!
3//! Provides a fluent API for building VFS instances from generated code
4//! or by adding files programmatically.
5//!
6//! # Examples
7//!
8//! ```
9//! use mcp_execution_files::FilesBuilder;
10//!
11//! let vfs = FilesBuilder::new()
12//!     .add_file("/mcp-tools/manifest.json", "{}")
13//!     .add_file("/mcp-tools/types.ts", "export type Params = {};")
14//!     .build()
15//!     .unwrap();
16//!
17//! assert_eq!(vfs.file_count(), 2);
18//! ```
19
20use crate::filesystem::FileSystem;
21use crate::types::{FilesError, Result};
22use mcp_execution_codegen::GeneratedCode;
23use std::fs;
24use std::path::{Path, PathBuf};
25
26/// Builder for constructing a virtual filesystem.
27///
28/// `FilesBuilder` provides a fluent API for creating VFS instances,
29/// with support for adding files individually or bulk-loading from
30/// generated code.
31///
32/// # Examples
33///
34/// ## Building from scratch
35///
36/// ```
37/// use mcp_execution_files::FilesBuilder;
38///
39/// let vfs = FilesBuilder::new()
40///     .add_file("/test.ts", "console.log('test');")
41///     .build()
42///     .unwrap();
43///
44/// assert!(vfs.exists("/test.ts"));
45/// # Ok::<(), mcp_execution_files::FilesError>(())
46/// ```
47///
48/// ## Building from generated code
49///
50/// ```
51/// use mcp_execution_files::FilesBuilder;
52/// use mcp_execution_codegen::{GeneratedCode, GeneratedFile};
53///
54/// let mut code = GeneratedCode::new();
55/// code.add_file(GeneratedFile {
56///     path: "manifest.json".to_string(),
57///     content: "{}".to_string(),
58/// });
59///
60/// let vfs = FilesBuilder::from_generated_code(code, "/mcp-tools/servers/test")
61///     .build()
62///     .unwrap();
63///
64/// assert!(vfs.exists("/mcp-tools/servers/test/manifest.json"));
65/// # Ok::<(), mcp_execution_files::FilesError>(())
66/// ```
67#[derive(Debug, Default)]
68pub struct FilesBuilder {
69    vfs: FileSystem,
70    errors: Vec<FilesError>,
71}
72
73impl FilesBuilder {
74    /// Creates a new empty VFS builder.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use mcp_execution_files::FilesBuilder;
80    ///
81    /// let builder = FilesBuilder::new();
82    /// let vfs = builder.build().unwrap();
83    /// assert_eq!(vfs.file_count(), 0);
84    /// ```
85    #[must_use]
86    pub fn new() -> Self {
87        Self {
88            vfs: FileSystem::new(),
89            errors: Vec::new(),
90        }
91    }
92
93    /// Creates a VFS builder from generated code.
94    ///
95    /// All files from the generated code will be placed under the specified
96    /// base path. The base path should be an absolute VFS path like
97    /// `/mcp-tools/servers/<server-id>`.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use mcp_execution_files::FilesBuilder;
103    /// use mcp_execution_codegen::{GeneratedCode, GeneratedFile};
104    ///
105    /// let mut code = GeneratedCode::new();
106    /// code.add_file(GeneratedFile {
107    ///     path: "types.ts".to_string(),
108    ///     content: "export type Params = {};".to_string(),
109    /// });
110    ///
111    /// let vfs = FilesBuilder::from_generated_code(code, "/mcp-tools/servers/test")
112    ///     .build()
113    ///     .unwrap();
114    ///
115    /// assert!(vfs.exists("/mcp-tools/servers/test/types.ts"));
116    /// # Ok::<(), mcp_execution_files::FilesError>(())
117    /// ```
118    #[must_use]
119    pub fn from_generated_code(code: GeneratedCode, base_path: impl AsRef<Path>) -> Self {
120        let mut builder = Self::new();
121        let base = base_path.as_ref().to_string_lossy();
122
123        // Ensure base path ends with a trailing slash for proper joining
124        let base_normalized = if base.ends_with('/') {
125            base.into_owned()
126        } else {
127            format!("{base}/")
128        };
129
130        for file in code.files {
131            // Use string concatenation to maintain Unix-style paths on all platforms
132            // This ensures VFS paths are always forward-slash separated, even on Windows
133            let full_path = format!("{}{}", base_normalized, file.path);
134            builder = builder.add_file(full_path.as_str(), file.content);
135        }
136
137        builder
138    }
139
140    /// Adds a file to the VFS being built.
141    ///
142    /// If the path is invalid, the error will be collected and returned
143    /// when `build()` is called.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use mcp_execution_files::FilesBuilder;
149    ///
150    /// let vfs = FilesBuilder::new()
151    ///     .add_file("/test.ts", "export const x = 1;")
152    ///     .build()
153    ///     .unwrap();
154    ///
155    /// assert_eq!(vfs.read_file("/test.ts").unwrap(), "export const x = 1;");
156    /// # Ok::<(), mcp_execution_files::FilesError>(())
157    /// ```
158    #[must_use]
159    pub fn add_file(mut self, path: impl AsRef<Path>, content: impl Into<String>) -> Self {
160        if let Err(e) = self.vfs.add_file(path, content) {
161            self.errors.push(e);
162        }
163        self
164    }
165
166    /// Adds multiple files to the VFS being built.
167    ///
168    /// This is a convenience method for adding many files at once.
169    ///
170    /// # Examples
171    ///
172    /// ```
173    /// use mcp_execution_files::FilesBuilder;
174    ///
175    /// let files = vec![
176    ///     ("/file1.ts", "content1"),
177    ///     ("/file2.ts", "content2"),
178    /// ];
179    ///
180    /// let vfs = FilesBuilder::new()
181    ///     .add_files(files)
182    ///     .build()
183    ///     .unwrap();
184    ///
185    /// assert_eq!(vfs.file_count(), 2);
186    /// # Ok::<(), mcp_execution_files::FilesError>(())
187    /// ```
188    #[must_use]
189    pub fn add_files<P, C>(mut self, files: impl IntoIterator<Item = (P, C)>) -> Self
190    where
191        P: AsRef<Path>,
192        C: Into<String>,
193    {
194        for (path, content) in files {
195            if let Err(e) = self.vfs.add_file(path, content) {
196                self.errors.push(e);
197            }
198        }
199        self
200    }
201
202    /// Builds the VFS and exports all files to the real filesystem.
203    ///
204    /// Files are written to disk at the specified base path with atomic
205    /// operations (write to temp file, then rename). Parent directories
206    /// are created automatically. The tilde (`~`) is expanded to the
207    /// user's home directory.
208    ///
209    /// # Arguments
210    ///
211    /// * `base_path` - Root directory for export (e.g., `~/.claude/servers/`)
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if:
216    /// - Any file path is invalid
217    /// - Home directory cannot be determined (when using `~`)
218    /// - I/O operations fail (permissions, disk space, etc.)
219    ///
220    /// # Examples
221    ///
222    /// ```no_run
223    /// use mcp_execution_files::FilesBuilder;
224    ///
225    /// let vfs = FilesBuilder::new()
226    ///     .add_file("/github/createIssue.ts", "export function createIssue() {}")
227    ///     .build_and_export("~/.claude/servers/")?;
228    ///
229    /// // Files are now at: ~/.claude/servers/github/createIssue.ts
230    /// # Ok::<(), mcp_execution_files::FilesError>(())
231    /// ```
232    pub fn build_and_export(self, base_path: impl AsRef<Path>) -> Result<FileSystem> {
233        // First, build the VFS to check for errors
234        let vfs = self.build()?;
235
236        // Expand tilde in path
237        let base = expand_tilde(base_path.as_ref())?;
238
239        // Export all files to disk
240        for path in vfs.all_paths() {
241            let content = vfs.read_file(path)?;
242            write_file_atomic(&base, path.as_str(), content)?;
243        }
244
245        Ok(vfs)
246    }
247
248    /// Consumes the builder and returns the constructed VFS.
249    ///
250    /// # Errors
251    ///
252    /// Returns the first error encountered during file addition, if any.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// use mcp_execution_files::FilesBuilder;
258    ///
259    /// let vfs = FilesBuilder::new()
260    ///     .add_file("/test.ts", "content")
261    ///     .build()
262    ///     .unwrap();
263    ///
264    /// assert_eq!(vfs.file_count(), 1);
265    /// ```
266    ///
267    /// ```
268    /// use mcp_execution_files::FilesBuilder;
269    ///
270    /// let result = FilesBuilder::new()
271    ///     .add_file("invalid/relative/path", "content")
272    ///     .build();
273    ///
274    /// assert!(result.is_err());
275    /// ```
276    pub fn build(self) -> Result<FileSystem> {
277        if let Some(error) = self.errors.into_iter().next() {
278            return Err(error);
279        }
280        Ok(self.vfs)
281    }
282
283    /// Returns the number of files currently in the builder.
284    ///
285    /// This can be used to check progress during construction.
286    ///
287    /// # Examples
288    ///
289    /// ```
290    /// use mcp_execution_files::FilesBuilder;
291    ///
292    /// let mut builder = FilesBuilder::new();
293    /// assert_eq!(builder.file_count(), 0);
294    ///
295    /// builder = builder.add_file("/test.ts", "");
296    /// assert_eq!(builder.file_count(), 1);
297    /// ```
298    #[must_use]
299    pub fn file_count(&self) -> usize {
300        self.vfs.file_count()
301    }
302}
303
304/// Expands tilde (~) in path to user's home directory.
305///
306/// # Errors
307///
308/// Returns an error if the home directory cannot be determined.
309fn expand_tilde(path: &Path) -> Result<PathBuf> {
310    let path_str = path.to_str().ok_or_else(|| FilesError::InvalidPath {
311        path: path.display().to_string(),
312    })?;
313
314    if path_str.starts_with("~/") || path_str == "~" {
315        let home = dirs::home_dir().ok_or_else(|| FilesError::IoError {
316            path: path_str.to_string(),
317            source: std::io::Error::new(
318                std::io::ErrorKind::NotFound,
319                "Cannot determine home directory",
320            ),
321        })?;
322
323        if path_str == "~" {
324            Ok(home)
325        } else {
326            Ok(home.join(&path_str[2..]))
327        }
328    } else {
329        Ok(path.to_path_buf())
330    }
331}
332
333/// Writes file content to disk atomically using temp file + rename.
334///
335/// Creates parent directories automatically. Uses atomic write pattern:
336/// 1. Write to temporary file
337/// 2. Rename temp file to final path
338///
339/// This ensures no partial files are visible if write fails.
340///
341/// # Security
342///
343/// - Validates path to prevent directory traversal
344/// - Creates parent directories with mode 0755
345/// - Writes files with default permissions (typically 0644)
346///
347/// # Errors
348///
349/// Returns an error if I/O operations fail.
350fn write_file_atomic(base_path: &Path, vfs_path: &str, content: &str) -> Result<()> {
351    // Remove leading slash and validate
352    let relative_path = vfs_path.strip_prefix('/').unwrap_or(vfs_path);
353
354    // Security: Check for directory traversal
355    if relative_path.contains("..") {
356        return Err(FilesError::InvalidPathComponent {
357            path: vfs_path.to_string(),
358        });
359    }
360
361    // Construct full disk path
362    let disk_path = base_path.join(relative_path);
363
364    // Create parent directories
365    if let Some(parent) = disk_path.parent() {
366        fs::create_dir_all(parent).map_err(|e| FilesError::IoError {
367            path: parent.display().to_string(),
368            source: e,
369        })?;
370    }
371
372    // Atomic write: write to temp file, then rename
373    let temp_path = disk_path.with_extension("tmp");
374
375    fs::write(&temp_path, content).map_err(|e| FilesError::IoError {
376        path: temp_path.display().to_string(),
377        source: e,
378    })?;
379
380    fs::rename(&temp_path, &disk_path).map_err(|e| FilesError::IoError {
381        path: disk_path.display().to_string(),
382        source: e,
383    })?;
384
385    Ok(())
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use mcp_execution_codegen::GeneratedFile;
392    use std::fs;
393    use tempfile::TempDir;
394
395    #[test]
396    fn test_builder_new() {
397        let builder = FilesBuilder::new();
398        let vfs = builder.build().unwrap();
399        assert_eq!(vfs.file_count(), 0);
400    }
401
402    #[test]
403    fn test_builder_default() {
404        let builder = FilesBuilder::default();
405        let vfs = builder.build().unwrap();
406        assert_eq!(vfs.file_count(), 0);
407    }
408
409    #[test]
410    fn test_add_file() {
411        let vfs = FilesBuilder::new()
412            .add_file("/test.ts", "content")
413            .build()
414            .unwrap();
415
416        assert_eq!(vfs.file_count(), 1);
417        assert_eq!(vfs.read_file("/test.ts").unwrap(), "content");
418    }
419
420    #[test]
421    fn test_add_file_invalid_path() {
422        let result = FilesBuilder::new()
423            .add_file("relative/path", "content")
424            .build();
425
426        assert!(result.is_err());
427        assert!(result.unwrap_err().is_invalid_path());
428    }
429
430    #[test]
431    fn test_add_files() {
432        let files = vec![("/file1.ts", "content1"), ("/file2.ts", "content2")];
433
434        let vfs = FilesBuilder::new().add_files(files).build().unwrap();
435
436        assert_eq!(vfs.file_count(), 2);
437        assert_eq!(vfs.read_file("/file1.ts").unwrap(), "content1");
438        assert_eq!(vfs.read_file("/file2.ts").unwrap(), "content2");
439    }
440
441    #[test]
442    fn test_from_generated_code() {
443        let mut code = GeneratedCode::new();
444        code.add_file(GeneratedFile {
445            path: "manifest.json".to_string(),
446            content: "{}".to_string(),
447        });
448        code.add_file(GeneratedFile {
449            path: "types.ts".to_string(),
450            content: "export {};".to_string(),
451        });
452
453        let vfs = FilesBuilder::from_generated_code(code, "/mcp-tools/servers/test")
454            .build()
455            .unwrap();
456
457        assert_eq!(vfs.file_count(), 2);
458        assert!(vfs.exists("/mcp-tools/servers/test/manifest.json"));
459        assert!(vfs.exists("/mcp-tools/servers/test/types.ts"));
460    }
461
462    #[test]
463    fn test_from_generated_code_nested_paths() {
464        let mut code = GeneratedCode::new();
465        code.add_file(GeneratedFile {
466            path: "tools/sendMessage.ts".to_string(),
467            content: "export function sendMessage() {}".to_string(),
468        });
469
470        let vfs = FilesBuilder::from_generated_code(code, "/mcp-tools/servers/test")
471            .build()
472            .unwrap();
473
474        assert!(vfs.exists("/mcp-tools/servers/test/tools/sendMessage.ts"));
475    }
476
477    #[test]
478    fn test_file_count() {
479        let mut builder = FilesBuilder::new();
480        assert_eq!(builder.file_count(), 0);
481
482        builder = builder.add_file("/test1.ts", "");
483        assert_eq!(builder.file_count(), 1);
484
485        builder = builder.add_file("/test2.ts", "");
486        assert_eq!(builder.file_count(), 2);
487    }
488
489    #[test]
490    fn test_chaining() {
491        let vfs = FilesBuilder::new()
492            .add_file("/file1.ts", "content1")
493            .add_file("/file2.ts", "content2")
494            .add_file("/file3.ts", "content3")
495            .build()
496            .unwrap();
497
498        assert_eq!(vfs.file_count(), 3);
499    }
500
501    #[test]
502    fn test_error_collection() {
503        let result = FilesBuilder::new()
504            .add_file("/valid.ts", "content")
505            .add_file("invalid", "content") // Invalid path
506            .add_file("/another-valid.ts", "content")
507            .build();
508
509        // Should fail due to invalid path
510        assert!(result.is_err());
511    }
512
513    #[test]
514    fn test_from_generated_code_with_additional_files() {
515        let mut code = GeneratedCode::new();
516        code.add_file(GeneratedFile {
517            path: "generated.ts".to_string(),
518            content: "// generated".to_string(),
519        });
520
521        let vfs = FilesBuilder::from_generated_code(code, "/mcp-tools/servers/test")
522            .add_file("/mcp-tools/servers/test/manual.ts", "// manual")
523            .build()
524            .unwrap();
525
526        assert_eq!(vfs.file_count(), 2);
527        assert!(vfs.exists("/mcp-tools/servers/test/generated.ts"));
528        assert!(vfs.exists("/mcp-tools/servers/test/manual.ts"));
529    }
530
531    // Tests for build_and_export
532
533    #[test]
534    fn test_build_and_export_creates_files() {
535        let temp_dir = TempDir::new().unwrap();
536
537        let vfs = FilesBuilder::new()
538            .add_file("/test.ts", "export const VERSION = '1.0';")
539            .build_and_export(temp_dir.path())
540            .unwrap();
541
542        // Verify file was created on disk
543        let file_path = temp_dir.path().join("test.ts");
544        assert!(file_path.exists(), "File should exist on disk");
545
546        // Verify content matches
547        let content = fs::read_to_string(&file_path).unwrap();
548        assert_eq!(content, "export const VERSION = '1.0';");
549
550        // Verify VFS was also returned correctly
551        assert_eq!(vfs.file_count(), 1);
552        assert_eq!(
553            vfs.read_file("/test.ts").unwrap(),
554            "export const VERSION = '1.0';"
555        );
556    }
557
558    #[test]
559    fn test_build_and_export_preserves_structure() {
560        let temp_dir = TempDir::new().unwrap();
561
562        let vfs = FilesBuilder::new()
563            .add_file("/index.ts", "export {};")
564            .add_file("/tools/create.ts", "export function create() {}")
565            .add_file("/tools/update.ts", "export function update() {}")
566            .add_file("/types/models.ts", "export type Model = {};")
567            .build_and_export(temp_dir.path())
568            .unwrap();
569
570        // Verify directory hierarchy
571        assert!(temp_dir.path().join("index.ts").exists());
572        assert!(temp_dir.path().join("tools").is_dir());
573        assert!(temp_dir.path().join("tools/create.ts").exists());
574        assert!(temp_dir.path().join("tools/update.ts").exists());
575        assert!(temp_dir.path().join("types").is_dir());
576        assert!(temp_dir.path().join("types/models.ts").exists());
577
578        // Verify VFS
579        assert_eq!(vfs.file_count(), 4);
580    }
581
582    #[test]
583    fn test_build_and_export_creates_parent_dirs() {
584        let temp_dir = TempDir::new().unwrap();
585
586        let vfs = FilesBuilder::new()
587            .add_file("/deeply/nested/path/to/file.ts", "content")
588            .build_and_export(temp_dir.path())
589            .unwrap();
590
591        let file_path = temp_dir.path().join("deeply/nested/path/to/file.ts");
592        assert!(file_path.exists());
593        assert_eq!(fs::read_to_string(file_path).unwrap(), "content");
594        assert_eq!(vfs.file_count(), 1);
595    }
596
597    #[test]
598    fn test_build_and_export_overwrites_existing() {
599        let temp_dir = TempDir::new().unwrap();
600
601        // First export
602        let vfs1 = FilesBuilder::new()
603            .add_file("/test.ts", "original content")
604            .build_and_export(temp_dir.path())
605            .unwrap();
606
607        assert_eq!(vfs1.file_count(), 1);
608        let file_path = temp_dir.path().join("test.ts");
609        assert_eq!(fs::read_to_string(&file_path).unwrap(), "original content");
610
611        // Second export with updated content
612        let vfs2 = FilesBuilder::new()
613            .add_file("/test.ts", "updated content")
614            .build_and_export(temp_dir.path())
615            .unwrap();
616
617        assert_eq!(vfs2.file_count(), 1);
618        assert_eq!(fs::read_to_string(&file_path).unwrap(), "updated content");
619    }
620
621    #[test]
622    fn test_build_and_export_returns_vfs() {
623        let temp_dir = TempDir::new().unwrap();
624
625        let vfs = FilesBuilder::new()
626            .add_file("/file1.ts", "content1")
627            .add_file("/file2.ts", "content2")
628            .build_and_export(temp_dir.path())
629            .unwrap();
630
631        // VFS should be fully functional
632        assert_eq!(vfs.file_count(), 2);
633        assert!(vfs.exists("/file1.ts"));
634        assert!(vfs.exists("/file2.ts"));
635        assert_eq!(vfs.read_file("/file1.ts").unwrap(), "content1");
636        assert_eq!(vfs.read_file("/file2.ts").unwrap(), "content2");
637    }
638
639    #[test]
640    fn test_build_and_export_with_invalid_path_in_vfs() {
641        let temp_dir = TempDir::new().unwrap();
642
643        let result = FilesBuilder::new()
644            .add_file("/valid.ts", "content")
645            .add_file("invalid/relative", "content")
646            .build_and_export(temp_dir.path());
647
648        assert!(result.is_err());
649        let err = result.unwrap_err();
650        assert!(err.is_invalid_path());
651    }
652
653    #[test]
654    fn test_build_and_export_multiple_files() {
655        let temp_dir = TempDir::new().unwrap();
656
657        let files = vec![
658            ("/index.ts", "export {};"),
659            ("/tool1.ts", "export function tool1() {}"),
660            ("/tool2.ts", "export function tool2() {}"),
661            ("/manifest.json", r#"{"version": "1.0.0"}"#),
662        ];
663
664        let vfs = FilesBuilder::new()
665            .add_files(files)
666            .build_and_export(temp_dir.path())
667            .unwrap();
668
669        assert_eq!(vfs.file_count(), 4);
670        assert!(temp_dir.path().join("index.ts").exists());
671        assert!(temp_dir.path().join("tool1.ts").exists());
672        assert!(temp_dir.path().join("tool2.ts").exists());
673        assert!(temp_dir.path().join("manifest.json").exists());
674    }
675
676    #[test]
677    fn test_build_and_export_empty_vfs() {
678        let temp_dir = TempDir::new().unwrap();
679
680        let vfs = FilesBuilder::new()
681            .build_and_export(temp_dir.path())
682            .unwrap();
683
684        assert_eq!(vfs.file_count(), 0);
685        // Directory should be created even if empty
686        assert!(temp_dir.path().exists());
687    }
688
689    #[test]
690    fn test_expand_tilde_expands_home() {
691        let path = Path::new("~/test/path");
692        let expanded = expand_tilde(path).unwrap();
693
694        // Should not contain tilde anymore
695        assert!(!expanded.to_string_lossy().contains('~'));
696
697        // Should be absolute
698        assert!(expanded.is_absolute());
699    }
700
701    #[test]
702    fn test_expand_tilde_preserves_absolute() {
703        let path = Path::new("/absolute/path");
704        let expanded = expand_tilde(path).unwrap();
705
706        assert_eq!(expanded, Path::new("/absolute/path"));
707    }
708
709    #[test]
710    fn test_expand_tilde_just_tilde() {
711        let path = Path::new("~");
712        let expanded = expand_tilde(path).unwrap();
713
714        // Should expand to home directory
715        assert!(expanded.is_absolute());
716        assert!(!expanded.to_string_lossy().contains('~'));
717    }
718
719    #[test]
720    fn test_write_file_atomic_directory_traversal() {
721        let temp_dir = TempDir::new().unwrap();
722
723        let result = write_file_atomic(temp_dir.path(), "/../etc/passwd", "malicious");
724
725        assert!(result.is_err());
726        assert!(result.unwrap_err().is_invalid_path());
727    }
728
729    #[test]
730    fn test_write_file_atomic_creates_parents() {
731        let temp_dir = TempDir::new().unwrap();
732
733        write_file_atomic(
734            temp_dir.path(),
735            "/deep/nested/structure/file.txt",
736            "content",
737        )
738        .unwrap();
739
740        let file_path = temp_dir.path().join("deep/nested/structure/file.txt");
741        assert!(file_path.exists());
742        assert_eq!(fs::read_to_string(file_path).unwrap(), "content");
743    }
744
745    #[test]
746    fn test_build_and_export_from_generated_code() {
747        let temp_dir = TempDir::new().unwrap();
748
749        let mut code = GeneratedCode::new();
750        code.add_file(GeneratedFile {
751            path: "index.ts".to_string(),
752            content: "export {};".to_string(),
753        });
754        code.add_file(GeneratedFile {
755            path: "tools/create.ts".to_string(),
756            content: "export function create() {}".to_string(),
757        });
758
759        let vfs = FilesBuilder::from_generated_code(code, "/github")
760            .build_and_export(temp_dir.path())
761            .unwrap();
762
763        assert_eq!(vfs.file_count(), 2);
764        assert!(temp_dir.path().join("github/index.ts").exists());
765        assert!(temp_dir.path().join("github/tools/create.ts").exists());
766    }
767
768    #[test]
769    fn test_build_and_export_unicode_content() {
770        let temp_dir = TempDir::new().unwrap();
771
772        let vfs = FilesBuilder::new()
773            .add_file("/unicode.ts", "export const emoji = '🚀';")
774            .build_and_export(temp_dir.path())
775            .unwrap();
776
777        let content = fs::read_to_string(temp_dir.path().join("unicode.ts")).unwrap();
778        assert_eq!(content, "export const emoji = '🚀';");
779        assert_eq!(vfs.file_count(), 1);
780    }
781
782    #[test]
783    fn test_build_and_export_large_content() {
784        let temp_dir = TempDir::new().unwrap();
785
786        // Create a large file (100KB)
787        let large_content = "x".repeat(100_000);
788
789        let vfs = FilesBuilder::new()
790            .add_file("/large.ts", &large_content)
791            .build_and_export(temp_dir.path())
792            .unwrap();
793
794        let content = fs::read_to_string(temp_dir.path().join("large.ts")).unwrap();
795        assert_eq!(content.len(), 100_000);
796        assert_eq!(vfs.file_count(), 1);
797    }
798}