Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:06:19.268Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

fs_tests.rs

1123 lines · 35.9 KB · rust
1mod fake_git_repo_tests;
2
3use std::{
4    collections::BTreeSet,
5    io::Write,
6    path::{Path, PathBuf},
7    pin::Pin,
8    sync::Arc,
9    time::Duration,
10};
11
12use futures::{FutureExt, StreamExt};
13
14use fs::*;
15use gpui::{BackgroundExecutor, TestAppContext};
16use serde_json::json;
17use tempfile::TempDir;
18use util::path;
19
20#[gpui::test]
21async fn test_fake_fs(executor: BackgroundExecutor) {
22    let fs = FakeFs::new(executor.clone());
23    fs.insert_tree(
24        path!("/root"),
25        json!({
26            "dir1": {
27                "a": "A",
28                "b": "B"
29            },
30            "dir2": {
31                "c": "C",
32                "dir3": {
33                    "d": "D"
34                }
35            }
36        }),
37    )
38    .await;
39
40    assert_eq!(
41        fs.files(),
42        vec![
43            PathBuf::from(path!("/root/dir1/a")),
44            PathBuf::from(path!("/root/dir1/b")),
45            PathBuf::from(path!("/root/dir2/c")),
46            PathBuf::from(path!("/root/dir2/dir3/d")),
47        ]
48    );
49
50    fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into())
51        .await
52        .unwrap();
53
54    assert_eq!(
55        fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref())
56            .await
57            .unwrap(),
58        PathBuf::from(path!("/root/dir2/dir3")),
59    );
60    assert_eq!(
61        fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref())
62            .await
63            .unwrap(),
64        PathBuf::from(path!("/root/dir2/dir3/d")),
65    );
66    assert_eq!(
67        fs.load(path!("/root/dir2/link-to-dir3/d").as_ref())
68            .await
69            .unwrap(),
70        "D",
71    );
72}
73
74#[gpui::test]
75async fn test_copy_recursive_with_single_file(executor: BackgroundExecutor) {
76    let fs = FakeFs::new(executor.clone());
77    fs.insert_tree(
78        path!("/outer"),
79        json!({
80            "a": "A",
81            "b": "B",
82            "inner": {}
83        }),
84    )
85    .await;
86
87    assert_eq!(
88        fs.files(),
89        vec![
90            PathBuf::from(path!("/outer/a")),
91            PathBuf::from(path!("/outer/b")),
92        ]
93    );
94
95    let source = Path::new(path!("/outer/a"));
96    let target = Path::new(path!("/outer/a copy"));
97    copy_recursive(fs.as_ref(), source, target, Default::default())
98        .await
99        .unwrap();
100
101    assert_eq!(
102        fs.files(),
103        vec![
104            PathBuf::from(path!("/outer/a")),
105            PathBuf::from(path!("/outer/a copy")),
106            PathBuf::from(path!("/outer/b")),
107        ]
108    );
109
110    let source = Path::new(path!("/outer/a"));
111    let target = Path::new(path!("/outer/inner/a copy"));
112    copy_recursive(fs.as_ref(), source, target, Default::default())
113        .await
114        .unwrap();
115
116    assert_eq!(
117        fs.files(),
118        vec![
119            PathBuf::from(path!("/outer/a")),
120            PathBuf::from(path!("/outer/a copy")),
121            PathBuf::from(path!("/outer/b")),
122            PathBuf::from(path!("/outer/inner/a copy")),
123        ]
124    );
125}
126
127#[gpui::test]
128async fn test_copy_recursive_with_single_dir(executor: BackgroundExecutor) {
129    let fs = FakeFs::new(executor.clone());
130    fs.insert_tree(
131        path!("/outer"),
132        json!({
133            "a": "A",
134            "empty": {},
135            "non-empty": {
136                "b": "B",
137            }
138        }),
139    )
140    .await;
141
142    assert_eq!(
143        fs.files(),
144        vec![
145            PathBuf::from(path!("/outer/a")),
146            PathBuf::from(path!("/outer/non-empty/b")),
147        ]
148    );
149    assert_eq!(
150        fs.directories(false),
151        vec![
152            PathBuf::from(path!("/")),
153            PathBuf::from(path!("/outer")),
154            PathBuf::from(path!("/outer/empty")),
155            PathBuf::from(path!("/outer/non-empty")),
156        ]
157    );
158
159    let source = Path::new(path!("/outer/empty"));
160    let target = Path::new(path!("/outer/empty copy"));
161    copy_recursive(fs.as_ref(), source, target, Default::default())
162        .await
163        .unwrap();
164
165    assert_eq!(
166        fs.files(),
167        vec![
168            PathBuf::from(path!("/outer/a")),
169            PathBuf::from(path!("/outer/non-empty/b")),
170        ]
171    );
172    assert_eq!(
173        fs.directories(false),
174        vec![
175            PathBuf::from(path!("/")),
176            PathBuf::from(path!("/outer")),
177            PathBuf::from(path!("/outer/empty")),
178            PathBuf::from(path!("/outer/empty copy")),
179            PathBuf::from(path!("/outer/non-empty")),
180        ]
181    );
182
183    let source = Path::new(path!("/outer/non-empty"));
184    let target = Path::new(path!("/outer/non-empty copy"));
185    copy_recursive(fs.as_ref(), source, target, Default::default())
186        .await
187        .unwrap();
188
189    assert_eq!(
190        fs.files(),
191        vec![
192            PathBuf::from(path!("/outer/a")),
193            PathBuf::from(path!("/outer/non-empty/b")),
194            PathBuf::from(path!("/outer/non-empty copy/b")),
195        ]
196    );
197    assert_eq!(
198        fs.directories(false),
199        vec![
200            PathBuf::from(path!("/")),
201            PathBuf::from(path!("/outer")),
202            PathBuf::from(path!("/outer/empty")),
203            PathBuf::from(path!("/outer/empty copy")),
204            PathBuf::from(path!("/outer/non-empty")),
205            PathBuf::from(path!("/outer/non-empty copy")),
206        ]
207    );
208}
209
210#[gpui::test]
211async fn test_copy_recursive(executor: BackgroundExecutor) {
212    let fs = FakeFs::new(executor.clone());
213    fs.insert_tree(
214        path!("/outer"),
215        json!({
216            "inner1": {
217                "a": "A",
218                "b": "B",
219                "inner3": {
220                    "d": "D",
221                },
222                "inner4": {}
223            },
224            "inner2": {
225                "c": "C",
226            }
227        }),
228    )
229    .await;
230
231    assert_eq!(
232        fs.files(),
233        vec![
234            PathBuf::from(path!("/outer/inner1/a")),
235            PathBuf::from(path!("/outer/inner1/b")),
236            PathBuf::from(path!("/outer/inner2/c")),
237            PathBuf::from(path!("/outer/inner1/inner3/d")),
238        ]
239    );
240    assert_eq!(
241        fs.directories(false),
242        vec![
243            PathBuf::from(path!("/")),
244            PathBuf::from(path!("/outer")),
245            PathBuf::from(path!("/outer/inner1")),
246            PathBuf::from(path!("/outer/inner2")),
247            PathBuf::from(path!("/outer/inner1/inner3")),
248            PathBuf::from(path!("/outer/inner1/inner4")),
249        ]
250    );
251
252    let source = Path::new(path!("/outer"));
253    let target = Path::new(path!("/outer/inner1/outer"));
254    copy_recursive(fs.as_ref(), source, target, Default::default())
255        .await
256        .unwrap();
257
258    assert_eq!(
259        fs.files(),
260        vec![
261            PathBuf::from(path!("/outer/inner1/a")),
262            PathBuf::from(path!("/outer/inner1/b")),
263            PathBuf::from(path!("/outer/inner2/c")),
264            PathBuf::from(path!("/outer/inner1/inner3/d")),
265            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
266            PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
267            PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
268            PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")),
269        ]
270    );
271    assert_eq!(
272        fs.directories(false),
273        vec![
274            PathBuf::from(path!("/")),
275            PathBuf::from(path!("/outer")),
276            PathBuf::from(path!("/outer/inner1")),
277            PathBuf::from(path!("/outer/inner2")),
278            PathBuf::from(path!("/outer/inner1/inner3")),
279            PathBuf::from(path!("/outer/inner1/inner4")),
280            PathBuf::from(path!("/outer/inner1/outer")),
281            PathBuf::from(path!("/outer/inner1/outer/inner1")),
282            PathBuf::from(path!("/outer/inner1/outer/inner2")),
283            PathBuf::from(path!("/outer/inner1/outer/inner1/inner3")),
284            PathBuf::from(path!("/outer/inner1/outer/inner1/inner4")),
285        ]
286    );
287}
288
289#[gpui::test]
290async fn test_copy_recursive_with_overwriting(executor: BackgroundExecutor) {
291    let fs = FakeFs::new(executor.clone());
292    fs.insert_tree(
293        path!("/outer"),
294        json!({
295            "inner1": {
296                "a": "A",
297                "b": "B",
298                "outer": {
299                    "inner1": {
300                        "a": "B"
301                    }
302                }
303            },
304            "inner2": {
305                "c": "C",
306            }
307        }),
308    )
309    .await;
310
311    assert_eq!(
312        fs.files(),
313        vec![
314            PathBuf::from(path!("/outer/inner1/a")),
315            PathBuf::from(path!("/outer/inner1/b")),
316            PathBuf::from(path!("/outer/inner2/c")),
317            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
318        ]
319    );
320    assert_eq!(
321        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
322            .await
323            .unwrap(),
324        "B",
325    );
326
327    let source = Path::new(path!("/outer"));
328    let target = Path::new(path!("/outer/inner1/outer"));
329    copy_recursive(
330        fs.as_ref(),
331        source,
332        target,
333        CopyOptions {
334            overwrite: true,
335            ..Default::default()
336        },
337    )
338    .await
339    .unwrap();
340
341    assert_eq!(
342        fs.files(),
343        vec![
344            PathBuf::from(path!("/outer/inner1/a")),
345            PathBuf::from(path!("/outer/inner1/b")),
346            PathBuf::from(path!("/outer/inner2/c")),
347            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
348            PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
349            PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
350            PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
351        ]
352    );
353    assert_eq!(
354        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
355            .await
356            .unwrap(),
357        "A"
358    );
359}
360
361#[gpui::test]
362async fn test_copy_recursive_with_ignoring(executor: BackgroundExecutor) {
363    let fs = FakeFs::new(executor.clone());
364    fs.insert_tree(
365        path!("/outer"),
366        json!({
367            "inner1": {
368                "a": "A",
369                "b": "B",
370                "outer": {
371                    "inner1": {
372                        "a": "B"
373                    }
374                }
375            },
376            "inner2": {
377                "c": "C",
378            }
379        }),
380    )
381    .await;
382
383    assert_eq!(
384        fs.files(),
385        vec![
386            PathBuf::from(path!("/outer/inner1/a")),
387            PathBuf::from(path!("/outer/inner1/b")),
388            PathBuf::from(path!("/outer/inner2/c")),
389            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
390        ]
391    );
392    assert_eq!(
393        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
394            .await
395            .unwrap(),
396        "B",
397    );
398
399    let source = Path::new(path!("/outer"));
400    let target = Path::new(path!("/outer/inner1/outer"));
401    copy_recursive(
402        fs.as_ref(),
403        source,
404        target,
405        CopyOptions {
406            ignore_if_exists: true,
407            ..Default::default()
408        },
409    )
410    .await
411    .unwrap();
412
413    assert_eq!(
414        fs.files(),
415        vec![
416            PathBuf::from(path!("/outer/inner1/a")),
417            PathBuf::from(path!("/outer/inner1/b")),
418            PathBuf::from(path!("/outer/inner2/c")),
419            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
420            PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
421            PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
422            PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
423        ]
424    );
425    assert_eq!(
426        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
427            .await
428            .unwrap(),
429        "B"
430    );
431}
432
433#[gpui::test]
434async fn test_realfs_atomic_write(executor: BackgroundExecutor) {
435    // With the file handle still open, the file should be replaced
436    // https://github.com/zed-industries/zed/issues/30054
437    let fs = RealFs::new(None, executor);
438    let temp_dir = TempDir::new().unwrap();
439    let file_to_be_replaced = temp_dir.path().join("file.txt");
440    let mut file = std::fs::File::create_new(&file_to_be_replaced).unwrap();
441    file.write_all(b"Hello").unwrap();
442    // drop(file);  // We still hold the file handle here
443    let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
444    assert_eq!(content, "Hello");
445    gpui::block_on(fs.atomic_write(file_to_be_replaced.clone(), "World".into())).unwrap();
446    let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
447    assert_eq!(content, "World");
448}
449
450#[gpui::test]
451async fn test_realfs_atomic_write_non_existing_file(executor: BackgroundExecutor) {
452    let fs = RealFs::new(None, executor);
453    let temp_dir = TempDir::new().unwrap();
454    let file_to_be_replaced = temp_dir.path().join("file.txt");
455    gpui::block_on(fs.atomic_write(file_to_be_replaced.clone(), "Hello".into())).unwrap();
456    let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
457    assert_eq!(content, "Hello");
458}
459
460#[gpui::test]
461#[cfg(target_os = "windows")]
462async fn test_realfs_canonicalize(executor: BackgroundExecutor) {
463    use util::paths::SanitizedPath;
464
465    let fs = RealFs::new(None, executor);
466    let temp_dir = TempDir::new().unwrap();
467    let file = temp_dir.path().join("test (1).txt");
468    let file = SanitizedPath::new(&file);
469    std::fs::write(&file, "test").unwrap();
470
471    let canonicalized = fs.canonicalize(file.as_path()).await;
472    assert!(canonicalized.is_ok());
473}
474
475#[gpui::test]
476async fn test_rename(executor: BackgroundExecutor) {
477    let fs = FakeFs::new(executor.clone());
478    fs.insert_tree(
479        path!("/root"),
480        json!({
481            "src": {
482                "file_a.txt": "content a",
483                "file_b.txt": "content b"
484            }
485        }),
486    )
487    .await;
488
489    fs.rename(
490        Path::new(path!("/root/src/file_a.txt")),
491        Path::new(path!("/root/src/new/renamed_a.txt")),
492        RenameOptions {
493            create_parents: true,
494            ..Default::default()
495        },
496    )
497    .await
498    .unwrap();
499
500    // Assert that the `file_a.txt` file was being renamed and moved to a
501    // different directory that did not exist before.
502    assert_eq!(
503        fs.files(),
504        vec![
505            PathBuf::from(path!("/root/src/file_b.txt")),
506            PathBuf::from(path!("/root/src/new/renamed_a.txt")),
507        ]
508    );
509
510    let result = fs
511        .rename(
512            Path::new(path!("/root/src/file_b.txt")),
513            Path::new(path!("/root/src/old/renamed_b.txt")),
514            RenameOptions {
515                create_parents: false,
516                ..Default::default()
517            },
518        )
519        .await;
520
521    // Assert that the `file_b.txt` file was not renamed nor moved, as
522    // `create_parents` was set to `false`.
523    // different directory that did not exist before.
524    assert!(result.is_err());
525    assert_eq!(
526        fs.files(),
527        vec![
528            PathBuf::from(path!("/root/src/file_b.txt")),
529            PathBuf::from(path!("/root/src/new/renamed_a.txt")),
530        ]
531    );
532}
533
534#[gpui::test]
535#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
536async fn test_realfs_parallel_rename_without_overwrite_preserves_losing_source(
537    executor: BackgroundExecutor,
538) {
539    let temp_dir = TempDir::new().unwrap();
540    let root = temp_dir.path();
541    let source_a = root.join("dir_a/shared.txt");
542    let source_b = root.join("dir_b/shared.txt");
543    let target = root.join("shared.txt");
544
545    std::fs::create_dir_all(source_a.parent().unwrap()).unwrap();
546    std::fs::create_dir_all(source_b.parent().unwrap()).unwrap();
547    std::fs::write(&source_a, "from a").unwrap();
548    std::fs::write(&source_b, "from b").unwrap();
549
550    let fs = RealFs::new(None, executor);
551    let (first_result, second_result) = futures::future::join(
552        fs.rename(&source_a, &target, RenameOptions::default()),
553        fs.rename(&source_b, &target, RenameOptions::default()),
554    )
555    .await;
556
557    assert_ne!(first_result.is_ok(), second_result.is_ok());
558    assert!(target.exists());
559    assert_eq!(source_a.exists() as u8 + source_b.exists() as u8, 1);
560}
561
562#[gpui::test]
563#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
564async fn test_realfs_rename_ignore_if_exists_leaves_source_and_target_unchanged(
565    executor: BackgroundExecutor,
566) {
567    let temp_dir = TempDir::new().unwrap();
568    let root = temp_dir.path();
569    let source = root.join("source.txt");
570    let target = root.join("target.txt");
571
572    std::fs::write(&source, "from source").unwrap();
573    std::fs::write(&target, "from target").unwrap();
574
575    let fs = RealFs::new(None, executor);
576    let result = fs
577        .rename(
578            &source,
579            &target,
580            RenameOptions {
581                ignore_if_exists: true,
582                ..Default::default()
583            },
584        )
585        .await;
586
587    assert!(result.is_ok());
588
589    assert_eq!(std::fs::read_to_string(&source).unwrap(), "from source");
590    assert_eq!(std::fs::read_to_string(&target).unwrap(), "from target");
591}
592
593#[gpui::test]
594#[cfg(unix)]
595async fn test_realfs_broken_symlink_metadata(executor: BackgroundExecutor) {
596    let tempdir = TempDir::new().unwrap();
597    let path = tempdir.path();
598    let fs = RealFs::new(None, executor);
599    let symlink_path = path.join("symlink");
600    gpui::block_on(fs.create_symlink(&symlink_path, PathBuf::from("file_a.txt"))).unwrap();
601    let metadata = fs
602        .metadata(&symlink_path)
603        .await
604        .expect("metadata call succeeds")
605        .expect("metadata returned");
606    assert!(metadata.is_symlink);
607    assert!(!metadata.is_dir);
608    assert!(!metadata.is_fifo);
609    assert!(!metadata.is_executable);
610    // don't care about len or mtime on symlinks?
611}
612
613#[gpui::test]
614#[cfg(unix)]
615async fn test_realfs_symlink_loop_metadata(executor: BackgroundExecutor) {
616    let tempdir = TempDir::new().unwrap();
617    let path = tempdir.path();
618    let fs = RealFs::new(None, executor);
619    let symlink_path = path.join("symlink");
620    gpui::block_on(fs.create_symlink(&symlink_path, PathBuf::from("symlink"))).unwrap();
621    let metadata = fs
622        .metadata(&symlink_path)
623        .await
624        .expect("metadata call succeeds")
625        .expect("metadata returned");
626    assert!(metadata.is_symlink);
627    assert!(!metadata.is_dir);
628    assert!(!metadata.is_fifo);
629    assert!(!metadata.is_executable);
630    // don't care about len or mtime on symlinks?
631}
632
633#[gpui::test]
634async fn test_fake_fs_trash(executor: BackgroundExecutor) {
635    let fs = FakeFs::new(executor.clone());
636    fs.insert_tree(
637        path!("/root"),
638        json!({
639            "src": {
640                "file_c.txt": "File C",
641                "file_d.txt": "File D"
642            },
643            "file_a.txt": "File A",
644            "file_b.txt": "File B",
645        }),
646    )
647    .await;
648
649    // Trashing a file.
650    let path = path!("/root/file_a.txt").as_ref();
651    fs.trash(path, Default::default())
652        .await
653        .expect("should be able to trash {path:?}");
654
655    assert_eq!(
656        fs.files(),
657        vec![
658            PathBuf::from(path!("/root/file_b.txt")),
659            PathBuf::from(path!("/root/src/file_c.txt")),
660            PathBuf::from(path!("/root/src/file_d.txt"))
661        ]
662    );
663
664    // Trashing a directory.
665    let path = path!("/root/src").as_ref();
666    fs.trash(
667        path,
668        RemoveOptions {
669            recursive: true,
670            ..Default::default()
671        },
672    )
673    .await
674    .expect("should be able to trash {path:?}");
675
676    assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_b.txt"))]);
677}
678
679#[gpui::test]
680async fn test_fake_fs_restore(executor: BackgroundExecutor) {
681    let fs = FakeFs::new(executor.clone());
682    fs.insert_tree(
683        path!("/root"),
684        json!({
685            "src": {
686                "file_a.txt": "File A",
687                "file_b.txt": "File B",
688            },
689            "file_c.txt": "File C",
690        }),
691    )
692    .await;
693
694    // Attempt deleting a file, asserting that the filesystem no longer reports
695    // it as part of its list of files, restore it and verify that the list of
696    // files and trash has been updated accordingly.
697    let path = path!("/root/src/file_a.txt").as_ref();
698    let trashed_entry = fs.trash(path, Default::default()).await.unwrap();
699
700    fs.restore(trashed_entry).await.unwrap();
701
702    assert_eq!(
703        fs.files(),
704        vec![
705            PathBuf::from(path!("/root/file_c.txt")),
706            PathBuf::from(path!("/root/src/file_a.txt")),
707            PathBuf::from(path!("/root/src/file_b.txt"))
708        ]
709    );
710
711    // Deleting and restoring a directory should also remove all of its files
712    // but create a single trashed entry, which should be removed after
713    // restoration.
714    let options = RemoveOptions {
715        recursive: true,
716        ..Default::default()
717    };
718    let path = path!("/root/src/").as_ref();
719    let trashed_entry = fs.trash(path, options).await.unwrap();
720
721    assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
722
723    fs.restore(trashed_entry).await.unwrap();
724
725    assert_eq!(
726        fs.files(),
727        vec![
728            PathBuf::from(path!("/root/file_c.txt")),
729            PathBuf::from(path!("/root/src/file_a.txt")),
730            PathBuf::from(path!("/root/src/file_b.txt"))
731        ]
732    );
733
734    // A collision error should be returned in case a file is being restored to
735    // a path where a file already exists.
736    let path = path!("/root/src/file_a.txt").as_ref();
737    let trashed_entry = fs.trash(path, Default::default()).await.unwrap();
738
739    assert_eq!(
740        fs.files(),
741        vec![
742            PathBuf::from(path!("/root/file_c.txt")),
743            PathBuf::from(path!("/root/src/file_b.txt"))
744        ]
745    );
746
747    fs.write(path, "New File A".as_bytes()).await.unwrap();
748
749    assert_eq!(
750        fs.files(),
751        vec![
752            PathBuf::from(path!("/root/file_c.txt")),
753            PathBuf::from(path!("/root/src/file_a.txt")),
754            PathBuf::from(path!("/root/src/file_b.txt"))
755        ]
756    );
757
758    let file_contents = fs.files_with_contents(path);
759    assert!(fs.restore(trashed_entry).await.is_err());
760    assert_eq!(
761        file_contents,
762        vec![(PathBuf::from(path), b"New File A".to_vec())]
763    );
764
765    // A collision error should be returned in case a directory is being
766    // restored to a path where a directory already exists.
767    let options = RemoveOptions {
768        recursive: true,
769        ..Default::default()
770    };
771    let path = path!("/root/src/").as_ref();
772    let trashed_entry = fs.trash(path, options).await.unwrap();
773
774    assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
775
776    fs.create_dir(path).await.unwrap();
777
778    assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
779
780    let result = fs.restore(trashed_entry).await;
781    assert!(result.is_err());
782
783    assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
784}
785
786/// Create a directory symlink (`link` -> `target`) in a cross-platform way.
787///
788/// Returns `Err` when the platform cannot create symlinks (e.g. Windows without
789/// the create-symlink privilege), so callers can skip a scenario gracefully
790/// rather than failing the whole test.
791fn make_dir_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
792    #[cfg(unix)]
793    {
794        std::os::unix::fs::symlink(target, link)
795    }
796    #[cfg(windows)]
797    {
798        std::os::windows::fs::symlink_dir(target, link)
799    }
800    #[cfg(not(any(unix, windows)))]
801    {
802        let _ = (target, link);
803        Err(std::io::Error::new(
804            std::io::ErrorKind::Unsupported,
805            "symlinks are not supported on this platform",
806        ))
807    }
808}
809
810/// Waits up to `timeout` for `events` to deliver something that covers the
811/// written file: either a path event whose path satisfies `path_matches`, or a
812/// `Rescan` (which tells the consumer to re-scan this watcher's whole tree, so
813/// it would discover the file anyway). Returns `false` if nothing relevant
814/// arrives before the timeout.
815async fn watcher_delivered_event(
816    events: &mut (impl futures::Stream<Item = Vec<PathEvent>> + Unpin),
817    executor: &BackgroundExecutor,
818    timeout: Duration,
819    path_matches: &(dyn Fn(&Path) -> bool + Send + Sync),
820) -> bool {
821    let timeout = executor.timer(timeout).fuse();
822    futures::pin_mut!(timeout);
823    loop {
824        futures::select_biased! {
825            batch = events.next().fuse() => {
826                let Some(batch) = batch else { return false };
827                let covered = batch.iter().any(|event| {
828                    path_matches(&event.path) || event.kind == Some(PathEventKind::Rescan)
829                });
830                if covered {
831                    return true;
832                }
833            }
834            _ = timeout => return false,
835        }
836    }
837}
838
839/// Exercises a spread of real watchers whose registered watch path is spelled
840/// differently from the path the OS reports events under. Each scenario watches
841/// some directory and then mutates the on-disk file; a correct watcher must
842/// deliver an event (or a rescan) for every scenario.
843///
844/// This asserts the residual path-aliasing bugs that real-casing the watch root
845/// at add-time does NOT fix. The headline failure is `symlink_ancestor`:
846/// watching a path that traverses a symlinked ancestor. On macOS FSEvents
847/// reports events under the resolved real path, which no longer has the
848/// symlinked prefix the watch root was registered with, so the events are
849/// filtered out and never delivered.
850///
851/// Platform notes (the test runs everywhere but scenarios self-skip when they
852/// cannot apply):
853/// - Case scenarios require a case-insensitive filesystem (macOS/Windows
854///   default; case-sensitive Linux/APFS skip them).
855/// - Symlink scenarios require symlink creation (skipped on Windows without the
856///   privilege).
857/// - `symlink_ancestor` fails on macOS (FSEvents canonicalizes) but is expected
858///   to pass on Linux (notify reconstructs paths from the watch path you pass),
859///   which is itself a useful demonstration that this is an FSEvents-specific
860///   bug.
861#[gpui::test]
862async fn test_realfs_watch_aliased_watch_paths_deliver_events(
863    executor: BackgroundExecutor,
864    cx: &mut TestAppContext,
865) {
866    cx.executor().allow_parking();
867
868    let fs = RealFs::new(None, executor.clone());
869    let temp_dir = TempDir::new().expect("create temp dir");
870    let root = temp_dir.path().to_path_buf();
871    let latency = Duration::from_millis(10);
872
873    // Probe the real filesystem for case sensitivity rather than guessing from
874    // the platform.
875    std::fs::create_dir_all(root.join("CaseProbe")).expect("create case probe dir");
876    let case_insensitive = root.join("caseprobe").exists();
877    eprintln!("filesystem is case-insensitive: {case_insensitive}");
878
879    struct Scenario {
880        name: &'static str,
881        events: Pin<Box<dyn Send + futures::Stream<Item = Vec<PathEvent>>>>,
882        _watcher: Arc<dyn Watcher>,
883        path_matches: Box<dyn Fn(&Path) -> bool + Send + Sync>,
884        action: Option<Box<dyn FnOnce() + Send>>,
885    }
886
887    let mut scenarios: Vec<Scenario> = Vec::new();
888    let mut skipped: Vec<String> = Vec::new();
889
890    // --- Headline residual bug: watch path traverses a symlinked ancestor. ---
891    {
892        let real = root.join("ancestor_real");
893        let inner = real.join("inner");
894        std::fs::create_dir_all(&inner).expect("create symlinked-ancestor target");
895        let link = root.join("ancestor_link");
896        match make_dir_symlink(&real, &link) {
897            Ok(()) => {
898                let (events, watcher) = fs.watch(&link.join("inner"), latency).await;
899                let file = inner.join("symlink_ancestor.txt");
900                scenarios.push(Scenario {
901                    name: "symlink_ancestor",
902                    events,
903                    _watcher: watcher,
904                    path_matches: Box::new(|path| {
905                        path.ends_with(Path::new("inner/symlink_ancestor.txt"))
906                    }),
907                    action: Some(Box::new(move || {
908                        std::fs::write(&file, b"x").expect("write symlink-ancestor file");
909                    })),
910                });
911            }
912            Err(error) => skipped.push(format!("symlink_ancestor (cannot symlink: {error})")),
913        }
914    }
915
916    // --- Control: watching a symlinked root IS handled (RealFs::watch follows
917    //     the root symlink and also watches the target). ---
918    {
919        let real = root.join("root_real");
920        std::fs::create_dir_all(&real).expect("create symlinked-root target");
921        let link = root.join("root_link");
922        match make_dir_symlink(&real, &link) {
923            Ok(()) => {
924                let (events, watcher) = fs.watch(&link, latency).await;
925                let file = real.join("symlink_root.txt");
926                scenarios.push(Scenario {
927                    name: "symlink_root",
928                    events,
929                    _watcher: watcher,
930                    path_matches: Box::new(|path| path.ends_with(Path::new("symlink_root.txt"))),
931                    action: Some(Box::new(move || {
932                        std::fs::write(&file, b"x").expect("write symlink-root file");
933                    })),
934                });
935            }
936            Err(error) => skipped.push(format!("symlink_root (cannot symlink: {error})")),
937        }
938    }
939
940    // --- Control: wrong-case watch root (the originally-reported bug, which the
941    //     real-casing fix already addresses). ---
942    if case_insensitive {
943        let real = root.join("CaseAlpha");
944        std::fs::create_dir_all(&real).expect("create wrong-case root");
945        let lower = PathBuf::from(real.to_string_lossy().to_lowercase());
946        let (events, watcher) = fs.watch(&lower, latency).await;
947        let file = real.join("alpha.txt");
948        scenarios.push(Scenario {
949            name: "wrong_case_root",
950            events,
951            _watcher: watcher,
952            path_matches: Box::new(|path| path.ends_with(Path::new("alpha.txt"))),
953            action: Some(Box::new(move || {
954                std::fs::write(&file, b"x").expect("write wrong-case-root file");
955            })),
956        });
957    } else {
958        skipped.push("wrong_case_root (case-sensitive fs)".to_owned());
959    }
960
961    // --- Control: wrong-case nested watch path. ---
962    if case_insensitive {
963        let real = root.join("CaseBravo").join("Inner");
964        std::fs::create_dir_all(&real).expect("create wrong-case nested dir");
965        let lower = PathBuf::from(real.to_string_lossy().to_lowercase());
966        let (events, watcher) = fs.watch(&lower, latency).await;
967        let file = real.join("bravo.txt");
968        scenarios.push(Scenario {
969            name: "nested_wrong_case",
970            events,
971            _watcher: watcher,
972            path_matches: Box::new(|path| path.ends_with(Path::new("bravo.txt"))),
973            action: Some(Box::new(move || {
974                std::fs::write(&file, b"x").expect("write nested-wrong-case file");
975            })),
976        });
977    } else {
978        skipped.push("nested_wrong_case (case-sensitive fs)".to_owned());
979    }
980
981    // --- Residual bug: the watched root is renamed to a different casing after
982    //     the watch is established, so later events arrive under a spelling the
983    //     registered (old-case) root no longer matches. ---
984    if case_insensitive {
985        let real = root.join("CaseEcho");
986        std::fs::create_dir_all(&real).expect("create case-rename dir");
987        let (events, watcher) = fs.watch(&real, latency).await;
988        let renamed = root.join("CASEECHO");
989        let file = renamed.join("echo.txt");
990        scenarios.push(Scenario {
991            name: "case_rename_root",
992            events,
993            _watcher: watcher,
994            path_matches: Box::new(|path| path.ends_with(Path::new("echo.txt"))),
995            action: Some(Box::new(move || {
996                std::fs::rename(&real, &renamed).expect("case-only rename of watched root");
997                std::fs::write(&file, b"x").expect("write case-rename file");
998            })),
999        });
1000    } else {
1001        skipped.push("case_rename_root (case-sensitive fs)".to_owned());
1002    }
1003
1004    // Let every watch settle before mutating, then perform all mutations.
1005    executor.timer(Duration::from_millis(250)).await;
1006    for scenario in &mut scenarios {
1007        if let Some(action) = scenario.action.take() {
1008            action();
1009        }
1010    }
1011
1012    let mut failures = Vec::new();
1013    for scenario in &mut scenarios {
1014        let delivered = watcher_delivered_event(
1015            &mut scenario.events,
1016            &executor,
1017            Duration::from_secs(3),
1018            scenario.path_matches.as_ref(),
1019        )
1020        .await;
1021        eprintln!("scenario {}: delivered={delivered}", scenario.name);
1022        if !delivered {
1023            failures.push(scenario.name);
1024        }
1025    }
1026
1027    for name in &skipped {
1028        eprintln!("scenario skipped: {name}");
1029    }
1030
1031    assert!(
1032        failures.is_empty(),
1033        "watchers failed to deliver events for {failures:?} (skipped: {skipped:?})"
1034    );
1035}
1036
1037#[gpui::test]
1038#[ignore = "stress test; run explicitly when needed"]
1039async fn test_realfs_watch_stress_reports_missed_paths(
1040    executor: BackgroundExecutor,
1041    cx: &mut TestAppContext,
1042) {
1043    const FILE_COUNT: usize = 32000;
1044    cx.executor().allow_parking();
1045
1046    let fs = RealFs::new(None, executor.clone());
1047    let temp_dir = TempDir::new().expect("create temp dir");
1048    let root = temp_dir.path();
1049
1050    let mut file_paths = Vec::with_capacity(FILE_COUNT);
1051    let mut expected_paths = BTreeSet::new();
1052
1053    for index in 0..FILE_COUNT {
1054        let dir_path = root.join(format!("dir-{index:04}"));
1055        let file_path = dir_path.join("file.txt");
1056        fs.create_dir(&dir_path).await.expect("create watched dir");
1057        fs.write(&file_path, b"before")
1058            .await
1059            .expect("create initial file");
1060        expected_paths.insert(file_path.clone());
1061        file_paths.push(file_path);
1062    }
1063
1064    let (mut events, watcher) = fs.watch(root, Duration::from_millis(10)).await;
1065    let _watcher = watcher;
1066
1067    for file_path in &expected_paths {
1068        _watcher
1069            .add(file_path.parent().expect("file has parent"))
1070            .expect("add explicit directory watch");
1071    }
1072
1073    for (index, file_path) in file_paths.iter().enumerate() {
1074        let content = format!("after-{index}");
1075        fs.write(file_path, content.as_bytes())
1076            .await
1077            .expect("modify watched file");
1078    }
1079
1080    let mut changed_paths = BTreeSet::new();
1081    let mut rescan_count: u32 = 0;
1082    let timeout = executor.timer(Duration::from_secs(10)).fuse();
1083
1084    futures::pin_mut!(timeout);
1085
1086    let mut ticks = 0;
1087    while ticks < 1000 {
1088        if let Some(batch) = events.next().fuse().now_or_never().flatten() {
1089            for event in batch {
1090                if event.kind == Some(PathEventKind::Rescan) {
1091                    rescan_count += 1;
1092                }
1093                if expected_paths.contains(&event.path) {
1094                    changed_paths.insert(event.path);
1095                }
1096            }
1097            if changed_paths.len() == expected_paths.len() {
1098                break;
1099            }
1100            ticks = 0;
1101        } else {
1102            ticks += 1;
1103            executor.timer(Duration::from_millis(10)).await;
1104        }
1105    }
1106
1107    let missed_paths: BTreeSet<_> = expected_paths.difference(&changed_paths).cloned().collect();
1108
1109    eprintln!(
1110        "realfs watch stress: expected={}, observed={}, missed={}, rescan={}",
1111        expected_paths.len(),
1112        changed_paths.len(),
1113        missed_paths.len(),
1114        rescan_count
1115    );
1116
1117    assert!(
1118        missed_paths.is_empty() || rescan_count > 0,
1119        "missed {} paths without rescan being reported",
1120        missed_paths.len()
1121    );
1122}
1123
Served at tenant.openagents/omega Member data and write actions are omitted.