1use async_trait::async_trait;
34use std::path::{Path, PathBuf};
35use std::sync::Arc;
36
37use super::{
38 BackendError, BackendResult, KernelBackend, LocalBackend, PatchOp, ReadRange,
39 ToolInfo, ToolResult, WriteMode,
40};
41use crate::tools::{ToolArgs, ToolCtx};
42use crate::vfs::{DirEntry, Filesystem, MountInfo, VfsRouter};
43use kaish_types::PathAccess;
44
45fn dir_basename(path: &Path) -> String {
48 path.file_name()
49 .map(|n| n.to_string_lossy().into_owned())
50 .unwrap_or_else(|| "/".to_string())
51}
52
53fn synth_dir_note(path: &Path) -> String {
57 format!("{} is a synthesized directory that only holds kaish mounts", path.display())
58}
59
60pub struct VirtualOverlayBackend {
65 inner: Arc<dyn KernelBackend>,
67 vfs: Arc<VfsRouter>,
69}
70
71impl VirtualOverlayBackend {
72 pub fn new(inner: Arc<dyn KernelBackend>, vfs: Arc<VfsRouter>) -> Self {
85 Self { inner, vfs }
86 }
87
88 fn is_virtual_path(&self, path: &Path) -> bool {
99 self.vfs.has_mount(path)
100 }
101
102 fn is_shared_ancestor(&self, path: &Path) -> bool {
110 !self.is_virtual_path(path) && self.vfs.has_mount_under(path)
111 }
112
113 pub fn inner(&self) -> &Arc<dyn KernelBackend> {
115 &self.inner
116 }
117
118 pub fn vfs(&self) -> &Arc<VfsRouter> {
120 &self.vfs
121 }
122}
123
124impl std::fmt::Debug for VirtualOverlayBackend {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 f.debug_struct("VirtualOverlayBackend")
127 .field("inner_type", &self.inner.backend_type())
128 .field("vfs", &self.vfs)
129 .finish()
130 }
131}
132
133#[async_trait]
134impl KernelBackend for VirtualOverlayBackend {
135 async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>> {
140 if self.is_virtual_path(path) {
141 Ok(self.vfs.read_range(path, range).await?)
142 } else if self.is_shared_ancestor(path) {
143 Err(BackendError::IsDirectory(synth_dir_note(path)))
144 } else {
145 self.inner.read(path, range).await
146 }
147 }
148
149 async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()> {
150 if self.is_virtual_path(path) {
151 match mode {
152 WriteMode::CreateNew => {
153 if self.vfs.exists(path).await {
154 return Err(BackendError::AlreadyExists(path.display().to_string()));
155 }
156 self.vfs.write(path, content).await?;
157 }
158 WriteMode::Overwrite | WriteMode::Truncate => {
159 self.vfs.write(path, content).await?;
160 }
161 WriteMode::UpdateOnly => {
162 if !self.vfs.exists(path).await {
163 return Err(BackendError::NotFound(path.display().to_string()));
164 }
165 self.vfs.write(path, content).await?;
166 }
167 _ => {
169 self.vfs.write(path, content).await?;
170 }
171 }
172 Ok(())
173 } else if self.is_shared_ancestor(path) {
174 Err(BackendError::IsDirectory(synth_dir_note(path)))
175 } else {
176 self.inner.write(path, content, mode).await
177 }
178 }
179
180 async fn set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> BackendResult<()> {
181 if self.is_virtual_path(path) {
182 self.vfs.set_mtime(path, mtime).await?;
183 Ok(())
184 } else if self.is_shared_ancestor(path) {
185 Err(BackendError::InvalidOperation(format!("cannot set mtime: {}", synth_dir_note(path))))
186 } else {
187 self.inner.set_mtime(path, mtime).await
188 }
189 }
190
191 async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()> {
192 if self.is_virtual_path(path) {
193 self.vfs.append(path, content).await?;
194 Ok(())
195 } else if self.is_shared_ancestor(path) {
196 Err(BackendError::IsDirectory(synth_dir_note(path)))
197 } else {
198 self.inner.append(path, content).await
199 }
200 }
201
202 async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()> {
203 if self.is_virtual_path(path) {
204 let data = self.vfs.read(path).await?;
206 let mut content = String::from_utf8(data)
207 .map_err(|e| BackendError::InvalidOperation(format!("file is not valid UTF-8: {}", e)))?;
208
209 for op in ops {
211 LocalBackend::apply_patch_op(&mut content, op)?;
212 }
213
214 self.vfs.write(path, content.as_bytes()).await?;
216 Ok(())
217 } else if self.is_shared_ancestor(path) {
218 Err(BackendError::IsDirectory(synth_dir_note(path)))
219 } else {
220 self.inner.patch(path, ops).await
221 }
222 }
223
224 async fn list(&self, path: &Path) -> BackendResult<Vec<DirEntry>> {
229 if self.is_virtual_path(path) {
230 Ok(self.vfs.list(path).await?)
231 } else if self.is_shared_ancestor(path) {
232 let mut by_name: std::collections::HashMap<String, DirEntry> =
244 std::collections::HashMap::new();
245 if let Ok(inner_entries) = self.inner.list(path).await {
246 for entry in inner_entries {
247 by_name.insert(entry.name.clone(), entry);
248 }
249 }
250 for entry in self.vfs.list(path).await? {
251 if self.vfs.has_mount(&path.join(&entry.name)) {
252 by_name.insert(entry.name.clone(), entry);
253 } else {
254 by_name.entry(entry.name.clone()).or_insert(entry);
255 }
256 }
257 let mut entries: Vec<DirEntry> = by_name.into_values().collect();
258 entries.sort_by(|a, b| a.name.cmp(&b.name));
259 Ok(entries)
260 } else {
261 self.inner.list(path).await
262 }
263 }
264
265 async fn stat(&self, path: &Path) -> BackendResult<DirEntry> {
266 if self.is_virtual_path(path) {
267 Ok(self.vfs.stat(path).await?)
268 } else if self.is_shared_ancestor(path) {
269 match self.inner.stat(path).await {
273 Ok(entry) if entry.is_dir() => Ok(entry),
274 _ => Ok(DirEntry::directory(dir_basename(path))),
275 }
276 } else {
277 self.inner.stat(path).await
278 }
279 }
280
281 async fn mkdir(&self, path: &Path) -> BackendResult<()> {
282 if self.is_virtual_path(path) {
283 self.vfs.mkdir(path).await?;
284 Ok(())
285 } else if self.is_shared_ancestor(path) {
286 Err(BackendError::AlreadyExists(synth_dir_note(path)))
287 } else {
288 self.inner.mkdir(path).await
289 }
290 }
291
292 async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()> {
293 if self.is_virtual_path(path) {
294 if recursive
295 && let Ok(entry) = self.vfs.lstat(path).await
296 && entry.is_dir()
297 && let Ok(entries) = self.vfs.list(path).await
298 {
299 for entry in entries {
300 let child_path = path.join(&entry.name);
301 Box::pin(self.remove(&child_path, true)).await?;
302 }
303 }
304 self.vfs.remove(path).await?;
305 Ok(())
306 } else if self.is_shared_ancestor(path) {
307 Err(BackendError::InvalidOperation(format!("cannot remove: {}", synth_dir_note(path))))
310 } else {
311 self.inner.remove(path, recursive).await
312 }
313 }
314
315 async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()> {
316 let from_virtual = self.is_virtual_path(from);
317 let to_virtual = self.is_virtual_path(to);
318
319 if from_virtual != to_virtual {
320 return Err(BackendError::InvalidOperation(
321 "cannot rename between virtual and non-virtual paths".into(),
322 ));
323 }
324
325 if self.is_shared_ancestor(from) || self.is_shared_ancestor(to) {
326 return Err(BackendError::InvalidOperation(format!(
327 "cannot rename: {} is a synthesized directory",
328 if self.is_shared_ancestor(from) { from.display() } else { to.display() }
329 )));
330 }
331
332 if from_virtual {
333 self.vfs.rename(from, to).await?;
334 Ok(())
335 } else {
336 self.inner.rename(from, to).await
337 }
338 }
339
340 async fn exists(&self, path: &Path) -> bool {
341 if self.is_virtual_path(path) {
342 self.vfs.exists(path).await
343 } else {
344 self.is_shared_ancestor(path) || self.inner.exists(path).await
347 }
348 }
349
350 async fn lstat(&self, path: &Path) -> BackendResult<DirEntry> {
355 if self.is_virtual_path(path) {
356 Ok(self.vfs.lstat(path).await?)
357 } else if self.is_shared_ancestor(path) {
358 match self.inner.lstat(path).await {
359 Ok(entry) if entry.is_dir() => Ok(entry),
360 _ => Ok(DirEntry::directory(dir_basename(path))),
361 }
362 } else {
363 self.inner.lstat(path).await
364 }
365 }
366
367 async fn read_link(&self, path: &Path) -> BackendResult<PathBuf> {
368 if self.is_virtual_path(path) {
369 Ok(self.vfs.read_link(path).await?)
370 } else if self.is_shared_ancestor(path) {
371 Err(BackendError::InvalidOperation(format!(
372 "{} is a directory, not a symlink",
373 path.display()
374 )))
375 } else {
376 self.inner.read_link(path).await
377 }
378 }
379
380 async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()> {
381 if self.is_virtual_path(link) {
382 self.vfs.symlink(target, link).await?;
383 Ok(())
384 } else if self.is_shared_ancestor(link) {
385 Err(BackendError::AlreadyExists(synth_dir_note(link)))
386 } else {
387 self.inner.symlink(target, link).await
388 }
389 }
390
391 async fn canonicalize(&self, path: &Path, allow_missing_final: bool) -> BackendResult<PathBuf> {
406 if self.is_virtual_path(path) {
407 Ok(self.vfs.canonicalize(path, allow_missing_final).await?)
408 } else if self.is_shared_ancestor(path) {
409 Ok(path.to_path_buf())
410 } else {
411 self.inner.canonicalize(path, allow_missing_final).await
412 }
413 }
414
415 async fn call_tool(
420 &self,
421 name: &str,
422 args: ToolArgs,
423 ctx: &mut dyn ToolCtx,
424 ) -> BackendResult<ToolResult> {
425 self.inner.call_tool(name, args, ctx).await
427 }
428
429 async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>> {
430 self.inner.list_tools().await
431 }
432
433 async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>> {
434 self.inner.get_tool(name).await
435 }
436
437 fn read_only(&self) -> bool {
442 self.inner.read_only() && self.vfs.read_only()
444 }
445
446 async fn path_access(&self, path: &Path) -> BackendResult<PathAccess> {
450 if self.is_virtual_path(path) {
451 Ok(self.vfs.path_access(path).await?)
452 } else if self.is_shared_ancestor(path) {
453 Ok(PathAccess::resolve(Some(0o555), true))
456 } else {
457 self.inner.path_access(path).await
458 }
459 }
460
461 fn backend_type(&self) -> &str {
462 "virtual-overlay"
463 }
464
465 fn mounts(&self) -> Vec<MountInfo> {
466 let mut mounts = self.inner.mounts();
467 mounts.extend(self.vfs.list_mounts());
468 mounts
469 }
470
471 fn resolve_real_path(&self, path: &Path) -> Option<PathBuf> {
472 if self.is_virtual_path(path) {
473 None
475 } else {
476 self.inner.resolve_real_path(path)
477 }
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use crate::backend::testing::MockBackend;
485 use crate::vfs::MemoryFs;
486
487 async fn make_overlay() -> VirtualOverlayBackend {
488 let (mock, _) = MockBackend::new();
490 let inner: Arc<dyn KernelBackend> = Arc::new(mock);
491
492 let mut vfs = VfsRouter::new();
494 let blobs = MemoryFs::new();
495 blobs.write(Path::new("test.bin"), b"blob data").await.unwrap();
496 vfs.mount("/v/blobs", blobs);
497 vfs.mount("/v/jobs", MemoryFs::new());
498
499 VirtualOverlayBackend::new(inner, Arc::new(vfs))
500 }
501
502 #[tokio::test]
503 async fn test_virtual_path_detection() {
504 let overlay = make_overlay().await;
505 assert!(overlay.is_virtual_path(Path::new("/v/jobs")));
507 assert!(overlay.is_virtual_path(Path::new("/v/blobs")));
508 assert!(overlay.is_virtual_path(Path::new("/v/blobs/test.bin")));
509
510 assert!(!overlay.is_virtual_path(Path::new("/v")));
514 assert!(!overlay.is_virtual_path(Path::new("/v/")));
515 assert!(!overlay.is_virtual_path(Path::new("/v/unclaimed")));
516
517 assert!(!overlay.is_virtual_path(Path::new("/docs")));
518 assert!(!overlay.is_virtual_path(Path::new("/g/repo")));
519 assert!(!overlay.is_virtual_path(Path::new("/")));
520 assert!(!overlay.is_virtual_path(Path::new("/var")));
521 }
522
523 #[tokio::test]
524 async fn test_non_v_mount_is_virtual_path() {
525 let (mock, _) = MockBackend::new();
530 let inner: Arc<dyn KernelBackend> = Arc::new(mock);
531 let mut vfs = VfsRouter::new();
532 vfs.mount("/dev", MemoryFs::new());
533 let overlay = VirtualOverlayBackend::new(inner, Arc::new(vfs));
534
535 assert!(overlay.is_virtual_path(Path::new("/dev/null")));
536 assert!(!overlay.is_virtual_path(Path::new("/docs")));
537 }
538
539 #[tokio::test]
540 async fn test_read_virtual_path() {
541 let overlay = make_overlay().await;
542 let content = overlay.read(Path::new("/v/blobs/test.bin"), None).await.unwrap();
543 assert_eq!(content, b"blob data");
544 }
545
546 #[tokio::test]
547 async fn test_write_virtual_path() {
548 let overlay = make_overlay().await;
549 overlay
550 .write(Path::new("/v/blobs/new.bin"), b"new data", WriteMode::Overwrite)
551 .await
552 .unwrap();
553 let content = overlay.read(Path::new("/v/blobs/new.bin"), None).await.unwrap();
554 assert_eq!(content, b"new data");
555 }
556
557 #[tokio::test]
558 async fn test_list_virtual_path() {
559 let overlay = make_overlay().await;
560 let entries = overlay.list(Path::new("/v")).await.unwrap();
561 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
562 assert!(names.contains(&"blobs"));
563 assert!(names.contains(&"jobs"));
564 }
565
566 #[tokio::test]
567 async fn test_root_listing_includes_v() {
568 let overlay = make_overlay().await;
569 let entries = overlay.list(Path::new("/")).await.unwrap();
570 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
571 assert!(names.contains(&"v"), "Root listing should include 'v' directory");
572 }
573
574 #[tokio::test]
575 async fn test_stat_virtual_path() {
576 let overlay = make_overlay().await;
577 let info = overlay.stat(Path::new("/v/blobs/test.bin")).await.unwrap();
578 assert!(info.is_file());
579 assert_eq!(info.size, 9); }
581
582 #[tokio::test]
583 async fn test_exists_virtual_path() {
584 let overlay = make_overlay().await;
585 assert!(overlay.exists(Path::new("/v/blobs/test.bin")).await);
586 assert!(!overlay.exists(Path::new("/v/blobs/nonexistent")).await);
587 }
588
589 #[tokio::test]
590 async fn test_mkdir_virtual_path() {
591 let overlay = make_overlay().await;
592 overlay.mkdir(Path::new("/v/blobs/newdir")).await.unwrap();
594 assert!(overlay.exists(Path::new("/v/blobs/newdir")).await);
595 }
596
597 #[tokio::test]
598 async fn test_remove_virtual_path() {
599 let overlay = make_overlay().await;
600 overlay.remove(Path::new("/v/blobs/test.bin"), false).await.unwrap();
601 assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
602 }
603
604 #[tokio::test]
605 async fn test_rename_within_virtual() {
606 let overlay = make_overlay().await;
607 overlay
608 .rename(Path::new("/v/blobs/test.bin"), Path::new("/v/blobs/renamed.bin"))
609 .await
610 .unwrap();
611 assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
612 assert!(overlay.exists(Path::new("/v/blobs/renamed.bin")).await);
613 }
614
615 #[tokio::test]
616 async fn test_rename_across_boundary_fails() {
617 let overlay = make_overlay().await;
618 let result = overlay
619 .rename(Path::new("/v/blobs/test.bin"), Path::new("/docs/test.bin"))
620 .await;
621 assert!(matches!(result, Err(BackendError::InvalidOperation(_))));
622 }
623
624 #[tokio::test]
625 async fn test_backend_type() {
626 let overlay = make_overlay().await;
627 assert_eq!(overlay.backend_type(), "virtual-overlay");
628 }
629
630 #[tokio::test]
631 async fn test_resolve_real_path_virtual() {
632 let overlay = make_overlay().await;
633 assert!(overlay.resolve_real_path(Path::new("/v/blobs/test.bin")).is_none());
635 }
636
637 async fn overlay_over_inner(cas: bool) -> VirtualOverlayBackend {
642 let mut inner_router = VfsRouter::new();
643 let inner_mem = MemoryFs::new();
644 if cas {
645 inner_mem
646 .write(Path::new("v/cas/blob.bin"), b"cas data")
647 .await
648 .unwrap();
649 }
650 inner_router.mount("/", inner_mem);
651 let inner: Arc<dyn KernelBackend> = Arc::new(LocalBackend::new(Arc::new(inner_router)));
652
653 let mut vfs = VfsRouter::new();
654 vfs.mount("/v/jobs", MemoryFs::new());
655 vfs.mount("/dev", MemoryFs::new());
656 VirtualOverlayBackend::new(inner, Arc::new(vfs))
657 }
658
659 #[tokio::test]
660 async fn test_unclaimed_v_reaches_inner_backend() {
661 let overlay = overlay_over_inner(true).await;
664 let data = overlay.read(Path::new("/v/cas/blob.bin"), None).await.unwrap();
665 assert_eq!(data, b"cas data");
666 assert!(overlay.exists(Path::new("/v/cas/blob.bin")).await);
667 }
668
669 #[tokio::test]
670 async fn test_v_listing_unions_kaish_and_inner() {
671 let overlay = overlay_over_inner(true).await;
672 let names: Vec<String> = overlay
673 .list(Path::new("/v"))
674 .await
675 .unwrap()
676 .into_iter()
677 .map(|e| e.name)
678 .collect();
679 assert!(names.iter().any(|n| n == "jobs"), "kaish mount missing: {names:?}");
680 assert!(names.iter().any(|n| n == "cas"), "embedder mount missing: {names:?}");
681 }
682
683 #[tokio::test]
684 async fn test_v_synthesized_when_inner_lacks_it() {
685 let overlay = overlay_over_inner(false).await;
689 assert!(overlay.stat(Path::new("/v")).await.unwrap().is_dir());
690 assert!(overlay.lstat(Path::new("/v")).await.unwrap().is_dir());
691 assert!(overlay.exists(Path::new("/v")).await);
692 let names: Vec<String> = overlay
693 .list(Path::new("/v"))
694 .await
695 .unwrap()
696 .into_iter()
697 .map(|e| e.name)
698 .collect();
699 assert_eq!(names, vec!["jobs".to_string()]);
700 }
701
702 #[cfg(feature = "localfs")]
703 #[tokio::test]
704 async fn test_unclaimed_v_resolves_to_inner_real_path() {
705 use crate::vfs::LocalFs;
706 let dir = tempfile::tempdir().unwrap();
708 std::fs::create_dir_all(dir.path().join("v/cas")).unwrap();
709 std::fs::write(dir.path().join("v/cas/blob.bin"), b"x").unwrap();
710
711 let mut inner_router = VfsRouter::new();
712 inner_router.mount("/", LocalFs::read_only(dir.path().to_path_buf()));
713 let inner: Arc<dyn KernelBackend> = Arc::new(LocalBackend::new(Arc::new(inner_router)));
714 let mut vfs = VfsRouter::new();
715 vfs.mount("/v/jobs", MemoryFs::new());
716 let overlay = VirtualOverlayBackend::new(inner, Arc::new(vfs));
717
718 let real = overlay.resolve_real_path(Path::new("/v/cas/blob.bin"));
724 assert!(real.is_some(), "unclaimed /v/* must resolve to the embedder real path");
725 assert!(real.unwrap().ends_with("v/cas/blob.bin"));
726 assert!(overlay.resolve_real_path(Path::new("/v/jobs/1")).is_none());
728 }
729
730 #[tokio::test]
731 async fn test_root_lists_both_v_and_dev() {
732 let overlay = overlay_over_inner(false).await;
735 let names: Vec<String> = overlay
736 .list(Path::new("/"))
737 .await
738 .unwrap()
739 .into_iter()
740 .map(|e| e.name)
741 .collect();
742 assert!(names.iter().any(|n| n == "v"), "{names:?}");
743 assert!(names.iter().any(|n| n == "dev"), "{names:?}");
744 }
745
746 #[tokio::test]
747 async fn test_shared_ancestor_is_a_directory_even_over_an_inner_file() {
748 let inner_mem = MemoryFs::new();
754 inner_mem.write(Path::new("v"), b"i am a file").await.unwrap();
755 let mut inner_router = VfsRouter::new();
756 inner_router.mount("/", inner_mem);
757 let inner: Arc<dyn KernelBackend> = Arc::new(LocalBackend::new(Arc::new(inner_router)));
758 let mut vfs = VfsRouter::new();
759 vfs.mount("/v/jobs", MemoryFs::new());
760 let overlay = VirtualOverlayBackend::new(inner, Arc::new(vfs));
761
762 assert!(overlay.stat(Path::new("/v")).await.unwrap().is_dir(), "kaish dir wins over inner file");
763 assert!(overlay.lstat(Path::new("/v")).await.unwrap().is_dir());
764 assert!(overlay.exists(Path::new("/v")).await);
765 let names: Vec<String> = overlay
766 .list(Path::new("/v"))
767 .await
768 .unwrap()
769 .into_iter()
770 .map(|e| e.name)
771 .collect();
772 assert_eq!(names, vec!["jobs".to_string()], "lists kaish mount; no NotADirectory error");
773 }
774
775 #[cfg(feature = "localfs")]
776 #[tokio::test]
777 async fn test_listing_keeps_inner_real_metadata_for_intermediate_child() {
778 use crate::vfs::LocalFs;
779 let dir = tempfile::tempdir().unwrap();
781 std::fs::create_dir_all(dir.path().join("v")).unwrap();
782
783 let mut inner_router = VfsRouter::new();
784 inner_router.mount("/", LocalFs::read_only(dir.path().to_path_buf()));
785 let inner: Arc<dyn KernelBackend> = Arc::new(LocalBackend::new(Arc::new(inner_router)));
786 let mut vfs = VfsRouter::new();
787 vfs.mount("/v/jobs", MemoryFs::new());
788 vfs.mount("/dev", MemoryFs::new());
789 let overlay = VirtualOverlayBackend::new(inner, Arc::new(vfs));
790
791 let entries = overlay.list(Path::new("/")).await.unwrap();
792 let v = entries.iter().find(|e| e.name == "v").expect("v listed");
793 let dev = entries.iter().find(|e| e.name == "dev").expect("dev listed");
794 assert!(v.is_dir());
798 assert!(v.modified.is_some(), "intermediate child keeps inner real metadata");
799 assert!(dev.is_dir());
801 assert!(dev.modified.is_none(), "real kaish mount shadows inner");
802 }
803
804 #[tokio::test]
805 async fn test_mutations_on_shared_ancestor_are_rejected_clearly() {
806 let overlay = overlay_over_inner(false).await;
810
811 assert!(
812 matches!(overlay.mkdir(Path::new("/v")).await, Err(BackendError::AlreadyExists(_))),
813 "mkdir on an existing synthesized dir → AlreadyExists"
814 );
815 assert!(
816 matches!(overlay.remove(Path::new("/v"), true).await, Err(BackendError::InvalidOperation(_))),
817 "remove of a synthesized dir that holds kaish mounts → InvalidOperation"
818 );
819 assert!(
820 matches!(
821 overlay.set_mtime(Path::new("/v"), std::time::SystemTime::now()).await,
822 Err(BackendError::InvalidOperation(_))
823 ),
824 "set_mtime (touch) on a synthesized dir → InvalidOperation"
825 );
826 assert!(
827 matches!(
828 overlay.write(Path::new("/v"), b"x", WriteMode::Overwrite).await,
829 Err(BackendError::IsDirectory(_))
830 ),
831 "write to a synthesized dir → IsDirectory"
832 );
833 assert!(
834 matches!(overlay.read(Path::new("/v"), None).await, Err(BackendError::IsDirectory(_))),
835 "read of a synthesized dir → IsDirectory"
836 );
837 }
838}