Skip to main content

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    // TODO(critic): not directory-atomic — needs staging treatment before any
233    // production caller is wired up (currently unused outside this crate's
234    // own tests, so the interrupted-export bug fixed in
235    // `FileSystem::export_to_filesystem` does not apply here yet).
236    pub fn build_and_export(self, base_path: impl AsRef<Path>) -> Result<FileSystem> {
237        // First, build the VFS to check for errors
238        let vfs = self.build()?;
239
240        // Expand tilde in path
241        let base = expand_tilde(base_path.as_ref())?;
242
243        // Export all files to disk
244        for path in vfs.all_paths() {
245            let content = vfs.read_file(path)?;
246            write_file_atomic(&base, path.as_str(), content)?;
247        }
248
249        Ok(vfs)
250    }
251
252    /// Consumes the builder and returns the constructed VFS.
253    ///
254    /// # Errors
255    ///
256    /// Returns the first error encountered during file addition, if any.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// use mcp_execution_files::FilesBuilder;
262    ///
263    /// let vfs = FilesBuilder::new()
264    ///     .add_file("/test.ts", "content")
265    ///     .build()
266    ///     .unwrap();
267    ///
268    /// assert_eq!(vfs.file_count(), 1);
269    /// ```
270    ///
271    /// ```
272    /// use mcp_execution_files::FilesBuilder;
273    ///
274    /// let result = FilesBuilder::new()
275    ///     .add_file("invalid/relative/path", "content")
276    ///     .build();
277    ///
278    /// assert!(result.is_err());
279    /// ```
280    pub fn build(self) -> Result<FileSystem> {
281        if let Some(error) = self.errors.into_iter().next() {
282            return Err(error);
283        }
284        Ok(self.vfs)
285    }
286
287    /// Returns the number of files currently in the builder.
288    ///
289    /// This can be used to check progress during construction.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// use mcp_execution_files::FilesBuilder;
295    ///
296    /// let mut builder = FilesBuilder::new();
297    /// assert_eq!(builder.file_count(), 0);
298    ///
299    /// builder = builder.add_file("/test.ts", "");
300    /// assert_eq!(builder.file_count(), 1);
301    /// ```
302    #[must_use]
303    pub fn file_count(&self) -> usize {
304        self.vfs.file_count()
305    }
306}
307
308/// Expands tilde (~) in path to user's home directory.
309///
310/// # Errors
311///
312/// Returns an error if the home directory cannot be determined.
313fn expand_tilde(path: &Path) -> Result<PathBuf> {
314    let path_str = path.to_str().ok_or_else(|| FilesError::InvalidPath {
315        path: path.display().to_string(),
316    })?;
317
318    if path_str.starts_with("~/") || path_str == "~" {
319        let home = dirs::home_dir().ok_or_else(|| FilesError::IoError {
320            path: path_str.to_string(),
321            source: std::io::Error::new(
322                std::io::ErrorKind::NotFound,
323                "Cannot determine home directory",
324            ),
325        })?;
326
327        if path_str == "~" {
328            Ok(home)
329        } else {
330            Ok(home.join(&path_str[2..]))
331        }
332    } else {
333        Ok(path.to_path_buf())
334    }
335}
336
337/// Writes file content to disk atomically using temp file + rename.
338///
339/// Creates parent directories automatically. Uses atomic write pattern:
340/// 1. Write to temporary file
341/// 2. Rename temp file to final path
342///
343/// This ensures no partial files are visible if write fails.
344///
345/// # Security
346///
347/// - Validates path to prevent directory traversal
348/// - Creates parent directories with mode 0755
349/// - Writes files with default permissions (typically 0644)
350///
351/// # Errors
352///
353/// Returns an error if I/O operations fail.
354fn write_file_atomic(base_path: &Path, vfs_path: &str, content: &str) -> Result<()> {
355    // Remove leading slash and validate
356    let relative_path = vfs_path.strip_prefix('/').unwrap_or(vfs_path);
357
358    // Security: Check for directory traversal
359    if relative_path.contains("..") {
360        return Err(FilesError::InvalidPathComponent {
361            path: vfs_path.to_string(),
362        });
363    }
364
365    // Construct full disk path
366    let disk_path = base_path.join(relative_path);
367
368    // Create parent directories
369    if let Some(parent) = disk_path.parent() {
370        fs::create_dir_all(parent).map_err(|e| FilesError::IoError {
371            path: parent.display().to_string(),
372            source: e,
373        })?;
374    }
375
376    // Atomic write: write to temp file, then rename
377    let temp_path = disk_path.with_added_extension("tmp");
378
379    fs::write(&temp_path, content).map_err(|e| FilesError::IoError {
380        path: temp_path.display().to_string(),
381        source: e,
382    })?;
383
384    fs::rename(&temp_path, &disk_path).map_err(|e| FilesError::IoError {
385        path: disk_path.display().to_string(),
386        source: e,
387    })?;
388
389    Ok(())
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use mcp_execution_codegen::GeneratedFile;
396    use std::fs;
397    use tempfile::TempDir;
398
399    #[test]
400    fn test_builder_new() {
401        let builder = FilesBuilder::new();
402        let vfs = builder.build().unwrap();
403        assert_eq!(vfs.file_count(), 0);
404    }
405
406    #[test]
407    fn test_builder_default() {
408        let builder = FilesBuilder::default();
409        let vfs = builder.build().unwrap();
410        assert_eq!(vfs.file_count(), 0);
411    }
412
413    #[test]
414    fn test_add_file() {
415        let vfs = FilesBuilder::new()
416            .add_file("/test.ts", "content")
417            .build()
418            .unwrap();
419
420        assert_eq!(vfs.file_count(), 1);
421        assert_eq!(vfs.read_file("/test.ts").unwrap(), "content");
422    }
423
424    #[test]
425    fn test_add_file_invalid_path() {
426        let result = FilesBuilder::new()
427            .add_file("relative/path", "content")
428            .build();
429
430        assert!(result.is_err());
431        assert!(result.unwrap_err().is_invalid_path());
432    }
433
434    #[test]
435    fn test_add_files() {
436        let files = vec![("/file1.ts", "content1"), ("/file2.ts", "content2")];
437
438        let vfs = FilesBuilder::new().add_files(files).build().unwrap();
439
440        assert_eq!(vfs.file_count(), 2);
441        assert_eq!(vfs.read_file("/file1.ts").unwrap(), "content1");
442        assert_eq!(vfs.read_file("/file2.ts").unwrap(), "content2");
443    }
444
445    #[test]
446    fn test_from_generated_code() {
447        let mut code = GeneratedCode::new();
448        code.add_file(GeneratedFile {
449            path: "manifest.json".to_string(),
450            content: "{}".to_string(),
451        });
452        code.add_file(GeneratedFile {
453            path: "types.ts".to_string(),
454            content: "export {};".to_string(),
455        });
456
457        let vfs = FilesBuilder::from_generated_code(code, "/mcp-tools/servers/test")
458            .build()
459            .unwrap();
460
461        assert_eq!(vfs.file_count(), 2);
462        assert!(vfs.exists("/mcp-tools/servers/test/manifest.json"));
463        assert!(vfs.exists("/mcp-tools/servers/test/types.ts"));
464    }
465
466    #[test]
467    fn test_from_generated_code_nested_paths() {
468        let mut code = GeneratedCode::new();
469        code.add_file(GeneratedFile {
470            path: "tools/sendMessage.ts".to_string(),
471            content: "export function sendMessage() {}".to_string(),
472        });
473
474        let vfs = FilesBuilder::from_generated_code(code, "/mcp-tools/servers/test")
475            .build()
476            .unwrap();
477
478        assert!(vfs.exists("/mcp-tools/servers/test/tools/sendMessage.ts"));
479    }
480
481    #[test]
482    fn test_file_count() {
483        let mut builder = FilesBuilder::new();
484        assert_eq!(builder.file_count(), 0);
485
486        builder = builder.add_file("/test1.ts", "");
487        assert_eq!(builder.file_count(), 1);
488
489        builder = builder.add_file("/test2.ts", "");
490        assert_eq!(builder.file_count(), 2);
491    }
492
493    #[test]
494    fn test_chaining() {
495        let vfs = FilesBuilder::new()
496            .add_file("/file1.ts", "content1")
497            .add_file("/file2.ts", "content2")
498            .add_file("/file3.ts", "content3")
499            .build()
500            .unwrap();
501
502        assert_eq!(vfs.file_count(), 3);
503    }
504
505    #[test]
506    fn test_error_collection() {
507        let result = FilesBuilder::new()
508            .add_file("/valid.ts", "content")
509            .add_file("invalid", "content") // Invalid path
510            .add_file("/another-valid.ts", "content")
511            .build();
512
513        // Should fail due to invalid path
514        assert!(result.is_err());
515    }
516
517    #[test]
518    fn test_from_generated_code_with_additional_files() {
519        let mut code = GeneratedCode::new();
520        code.add_file(GeneratedFile {
521            path: "generated.ts".to_string(),
522            content: "// generated".to_string(),
523        });
524
525        let vfs = FilesBuilder::from_generated_code(code, "/mcp-tools/servers/test")
526            .add_file("/mcp-tools/servers/test/manual.ts", "// manual")
527            .build()
528            .unwrap();
529
530        assert_eq!(vfs.file_count(), 2);
531        assert!(vfs.exists("/mcp-tools/servers/test/generated.ts"));
532        assert!(vfs.exists("/mcp-tools/servers/test/manual.ts"));
533    }
534
535    // Tests for build_and_export
536
537    #[test]
538    fn test_build_and_export_creates_files() {
539        let temp_dir = TempDir::new().unwrap();
540
541        let vfs = FilesBuilder::new()
542            .add_file("/test.ts", "export const VERSION = '1.0';")
543            .build_and_export(temp_dir.path())
544            .unwrap();
545
546        // Verify file was created on disk
547        let file_path = temp_dir.path().join("test.ts");
548        assert!(file_path.exists(), "File should exist on disk");
549
550        // Verify content matches
551        let content = fs::read_to_string(&file_path).unwrap();
552        assert_eq!(content, "export const VERSION = '1.0';");
553
554        // Verify VFS was also returned correctly
555        assert_eq!(vfs.file_count(), 1);
556        assert_eq!(
557            vfs.read_file("/test.ts").unwrap(),
558            "export const VERSION = '1.0';"
559        );
560    }
561
562    #[test]
563    fn test_build_and_export_preserves_structure() {
564        let temp_dir = TempDir::new().unwrap();
565
566        let vfs = FilesBuilder::new()
567            .add_file("/index.ts", "export {};")
568            .add_file("/tools/create.ts", "export function create() {}")
569            .add_file("/tools/update.ts", "export function update() {}")
570            .add_file("/types/models.ts", "export type Model = {};")
571            .build_and_export(temp_dir.path())
572            .unwrap();
573
574        // Verify directory hierarchy
575        assert!(temp_dir.path().join("index.ts").exists());
576        assert!(temp_dir.path().join("tools").is_dir());
577        assert!(temp_dir.path().join("tools/create.ts").exists());
578        assert!(temp_dir.path().join("tools/update.ts").exists());
579        assert!(temp_dir.path().join("types").is_dir());
580        assert!(temp_dir.path().join("types/models.ts").exists());
581
582        // Verify VFS
583        assert_eq!(vfs.file_count(), 4);
584    }
585
586    #[test]
587    fn test_build_and_export_creates_parent_dirs() {
588        let temp_dir = TempDir::new().unwrap();
589
590        let vfs = FilesBuilder::new()
591            .add_file("/deeply/nested/path/to/file.ts", "content")
592            .build_and_export(temp_dir.path())
593            .unwrap();
594
595        let file_path = temp_dir.path().join("deeply/nested/path/to/file.ts");
596        assert!(file_path.exists());
597        assert_eq!(fs::read_to_string(file_path).unwrap(), "content");
598        assert_eq!(vfs.file_count(), 1);
599    }
600
601    #[test]
602    fn test_build_and_export_overwrites_existing() {
603        let temp_dir = TempDir::new().unwrap();
604
605        // First export
606        let vfs1 = FilesBuilder::new()
607            .add_file("/test.ts", "original content")
608            .build_and_export(temp_dir.path())
609            .unwrap();
610
611        assert_eq!(vfs1.file_count(), 1);
612        let file_path = temp_dir.path().join("test.ts");
613        assert_eq!(fs::read_to_string(&file_path).unwrap(), "original content");
614
615        // Second export with updated content
616        let vfs2 = FilesBuilder::new()
617            .add_file("/test.ts", "updated content")
618            .build_and_export(temp_dir.path())
619            .unwrap();
620
621        assert_eq!(vfs2.file_count(), 1);
622        assert_eq!(fs::read_to_string(&file_path).unwrap(), "updated content");
623    }
624
625    #[test]
626    fn test_build_and_export_returns_vfs() {
627        let temp_dir = TempDir::new().unwrap();
628
629        let vfs = FilesBuilder::new()
630            .add_file("/file1.ts", "content1")
631            .add_file("/file2.ts", "content2")
632            .build_and_export(temp_dir.path())
633            .unwrap();
634
635        // VFS should be fully functional
636        assert_eq!(vfs.file_count(), 2);
637        assert!(vfs.exists("/file1.ts"));
638        assert!(vfs.exists("/file2.ts"));
639        assert_eq!(vfs.read_file("/file1.ts").unwrap(), "content1");
640        assert_eq!(vfs.read_file("/file2.ts").unwrap(), "content2");
641    }
642
643    #[test]
644    fn test_build_and_export_with_invalid_path_in_vfs() {
645        let temp_dir = TempDir::new().unwrap();
646
647        let result = FilesBuilder::new()
648            .add_file("/valid.ts", "content")
649            .add_file("invalid/relative", "content")
650            .build_and_export(temp_dir.path());
651
652        assert!(result.is_err());
653        let err = result.unwrap_err();
654        assert!(err.is_invalid_path());
655    }
656
657    #[test]
658    fn test_build_and_export_multiple_files() {
659        let temp_dir = TempDir::new().unwrap();
660
661        let files = vec![
662            ("/index.ts", "export {};"),
663            ("/tool1.ts", "export function tool1() {}"),
664            ("/tool2.ts", "export function tool2() {}"),
665            ("/manifest.json", r#"{"version": "1.0.0"}"#),
666        ];
667
668        let vfs = FilesBuilder::new()
669            .add_files(files)
670            .build_and_export(temp_dir.path())
671            .unwrap();
672
673        assert_eq!(vfs.file_count(), 4);
674        assert!(temp_dir.path().join("index.ts").exists());
675        assert!(temp_dir.path().join("tool1.ts").exists());
676        assert!(temp_dir.path().join("tool2.ts").exists());
677        assert!(temp_dir.path().join("manifest.json").exists());
678    }
679
680    #[test]
681    fn test_build_and_export_empty_vfs() {
682        let temp_dir = TempDir::new().unwrap();
683
684        let vfs = FilesBuilder::new()
685            .build_and_export(temp_dir.path())
686            .unwrap();
687
688        assert_eq!(vfs.file_count(), 0);
689        // Directory should be created even if empty
690        assert!(temp_dir.path().exists());
691    }
692
693    #[test]
694    fn test_expand_tilde_expands_home() {
695        let path = Path::new("~/test/path");
696        let expanded = expand_tilde(path).unwrap();
697
698        // Should not contain tilde anymore
699        assert!(!expanded.to_string_lossy().contains('~'));
700
701        // Should be absolute
702        assert!(expanded.is_absolute());
703    }
704
705    #[test]
706    fn test_expand_tilde_preserves_absolute() {
707        let path = Path::new("/absolute/path");
708        let expanded = expand_tilde(path).unwrap();
709
710        assert_eq!(expanded, Path::new("/absolute/path"));
711    }
712
713    #[test]
714    fn test_expand_tilde_just_tilde() {
715        let path = Path::new("~");
716        let expanded = expand_tilde(path).unwrap();
717
718        // Should expand to home directory
719        assert!(expanded.is_absolute());
720        assert!(!expanded.to_string_lossy().contains('~'));
721    }
722
723    #[test]
724    fn test_write_file_atomic_directory_traversal() {
725        let temp_dir = TempDir::new().unwrap();
726
727        let result = write_file_atomic(temp_dir.path(), "/../etc/passwd", "malicious");
728
729        assert!(result.is_err());
730        assert!(result.unwrap_err().is_invalid_path());
731    }
732
733    #[test]
734    fn test_write_file_atomic_creates_parents() {
735        let temp_dir = TempDir::new().unwrap();
736
737        write_file_atomic(
738            temp_dir.path(),
739            "/deep/nested/structure/file.txt",
740            "content",
741        )
742        .unwrap();
743
744        let file_path = temp_dir.path().join("deep/nested/structure/file.txt");
745        assert!(file_path.exists());
746        assert_eq!(fs::read_to_string(file_path).unwrap(), "content");
747    }
748
749    #[test]
750    fn test_build_and_export_from_generated_code() {
751        let temp_dir = TempDir::new().unwrap();
752
753        let mut code = GeneratedCode::new();
754        code.add_file(GeneratedFile {
755            path: "index.ts".to_string(),
756            content: "export {};".to_string(),
757        });
758        code.add_file(GeneratedFile {
759            path: "tools/create.ts".to_string(),
760            content: "export function create() {}".to_string(),
761        });
762
763        let vfs = FilesBuilder::from_generated_code(code, "/github")
764            .build_and_export(temp_dir.path())
765            .unwrap();
766
767        assert_eq!(vfs.file_count(), 2);
768        assert!(temp_dir.path().join("github/index.ts").exists());
769        assert!(temp_dir.path().join("github/tools/create.ts").exists());
770    }
771
772    #[test]
773    fn test_build_and_export_unicode_content() {
774        let temp_dir = TempDir::new().unwrap();
775
776        let vfs = FilesBuilder::new()
777            .add_file("/unicode.ts", "export const emoji = '🚀';")
778            .build_and_export(temp_dir.path())
779            .unwrap();
780
781        let content = fs::read_to_string(temp_dir.path().join("unicode.ts")).unwrap();
782        assert_eq!(content, "export const emoji = '🚀';");
783        assert_eq!(vfs.file_count(), 1);
784    }
785
786    #[test]
787    fn test_build_and_export_large_content() {
788        let temp_dir = TempDir::new().unwrap();
789
790        // Create a large file (100KB)
791        let large_content = "x".repeat(100_000);
792
793        let vfs = FilesBuilder::new()
794            .add_file("/large.ts", &large_content)
795            .build_and_export(temp_dir.path())
796            .unwrap();
797
798        let content = fs::read_to_string(temp_dir.path().join("large.ts")).unwrap();
799        assert_eq!(content.len(), 100_000);
800        assert_eq!(vfs.file_count(), 1);
801    }
802}