1use async_trait::async_trait;
7use std::path::Path;
8use std::sync::Arc;
9use std::time::UNIX_EPOCH;
10
11use super::{
12 BackendError, BackendResult, ConflictError, EntryInfo, KernelBackend, PatchOp, ReadRange,
13 ToolInfo, ToolResult, WriteMode,
14};
15use crate::tools::{ExecContext, ToolArgs, ToolRegistry};
16use crate::vfs::{EntryType, Filesystem, MountInfo, VfsRouter};
17
18pub struct LocalBackend {
24 vfs: Arc<VfsRouter>,
26 tools: Option<Arc<ToolRegistry>>,
28}
29
30impl LocalBackend {
31 pub fn new(vfs: Arc<VfsRouter>) -> Self {
33 Self { vfs, tools: None }
34 }
35
36 pub fn with_tools(vfs: Arc<VfsRouter>, tools: Arc<ToolRegistry>) -> Self {
38 Self {
39 vfs,
40 tools: Some(tools),
41 }
42 }
43
44 pub fn vfs(&self) -> &Arc<VfsRouter> {
46 &self.vfs
47 }
48
49 pub fn tools(&self) -> Option<&Arc<ToolRegistry>> {
51 self.tools.as_ref()
52 }
53
54 pub fn apply_patch_op(content: &mut String, op: &PatchOp) -> BackendResult<()> {
58 match op {
59 PatchOp::Insert { offset, content: insert_content } => {
60 if *offset > content.len() {
61 return Err(BackendError::InvalidOperation(format!(
62 "insert offset {} exceeds content length {}",
63 offset,
64 content.len()
65 )));
66 }
67 content.insert_str(*offset, insert_content);
68 }
69
70 PatchOp::Delete { offset, len, expected } => {
71 let end = offset.saturating_add(*len);
72 if end > content.len() {
73 return Err(BackendError::InvalidOperation(format!(
74 "delete range {}..{} exceeds content length {}",
75 offset, end, content.len()
76 )));
77 }
78 if let Some(expected_content) = expected {
80 let actual = &content[*offset..end];
81 if actual != expected_content {
82 return Err(BackendError::Conflict(ConflictError {
83 location: format!("offset {}", offset),
84 expected: expected_content.clone(),
85 actual: actual.to_string(),
86 }));
87 }
88 }
89 content.drain(*offset..end);
90 }
91
92 PatchOp::Replace {
93 offset,
94 len,
95 content: replace_content,
96 expected,
97 } => {
98 let end = offset.saturating_add(*len);
99 if end > content.len() {
100 return Err(BackendError::InvalidOperation(format!(
101 "replace range {}..{} exceeds content length {}",
102 offset, end, content.len()
103 )));
104 }
105 if let Some(expected_content) = expected {
107 let actual = &content[*offset..end];
108 if actual != expected_content {
109 return Err(BackendError::Conflict(ConflictError {
110 location: format!("offset {}", offset),
111 expected: expected_content.clone(),
112 actual: actual.to_string(),
113 }));
114 }
115 }
116 content.replace_range(*offset..end, replace_content);
117 }
118
119 PatchOp::InsertLine { line, content: insert_content } => {
120 let lines: Vec<&str> = content.lines().collect();
121 let line_idx = line.saturating_sub(1); if line_idx > lines.len() {
123 return Err(BackendError::InvalidOperation(format!(
124 "line {} exceeds line count {}",
125 line,
126 lines.len()
127 )));
128 }
129 let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
130 new_lines.insert(line_idx, insert_content.clone());
131 *content = new_lines.join("\n");
132 if !content.is_empty() && !content.ends_with('\n') {
134 content.push('\n');
135 }
136 }
137
138 PatchOp::DeleteLine { line, expected } => {
139 let lines: Vec<&str> = content.lines().collect();
140 let line_idx = line.saturating_sub(1); if line_idx >= lines.len() {
142 return Err(BackendError::InvalidOperation(format!(
143 "line {} exceeds line count {}",
144 line,
145 lines.len()
146 )));
147 }
148 if let Some(expected_content) = expected {
150 let actual = lines[line_idx];
151 if actual != expected_content {
152 return Err(BackendError::Conflict(ConflictError {
153 location: format!("line {}", line),
154 expected: expected_content.clone(),
155 actual: actual.to_string(),
156 }));
157 }
158 }
159 let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
160 new_lines.remove(line_idx);
161 *content = new_lines.join("\n");
162 if !content.is_empty() && !content.ends_with('\n') {
163 content.push('\n');
164 }
165 }
166
167 PatchOp::ReplaceLine {
168 line,
169 content: replace_content,
170 expected,
171 } => {
172 let lines: Vec<&str> = content.lines().collect();
173 let line_idx = line.saturating_sub(1); if line_idx >= lines.len() {
175 return Err(BackendError::InvalidOperation(format!(
176 "line {} exceeds line count {}",
177 line,
178 lines.len()
179 )));
180 }
181 if let Some(expected_content) = expected {
183 let actual = lines[line_idx];
184 if actual != expected_content {
185 return Err(BackendError::Conflict(ConflictError {
186 location: format!("line {}", line),
187 expected: expected_content.clone(),
188 actual: actual.to_string(),
189 }));
190 }
191 }
192 let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
193 new_lines[line_idx] = replace_content.clone();
194 *content = new_lines.join("\n");
195 if !content.is_empty() && !content.ends_with('\n') {
196 content.push('\n');
197 }
198 }
199
200 PatchOp::Append { content: append_content } => {
201 content.push_str(append_content);
202 }
203 }
204 Ok(())
205 }
206
207 pub fn apply_read_range(content: &[u8], range: &ReadRange) -> Vec<u8> {
211 if range.offset.is_some() || range.limit.is_some() {
213 let offset = range.offset.unwrap_or(0) as usize;
214 let limit = range.limit.map(|l| l as usize).unwrap_or(content.len());
215 let end = (offset + limit).min(content.len());
216 return content.get(offset..end).unwrap_or(&[]).to_vec();
217 }
218
219 if range.start_line.is_some() || range.end_line.is_some() {
221 let content_str = match std::str::from_utf8(content) {
222 Ok(s) => s,
223 Err(_) => return content.to_vec(), };
225 let lines: Vec<&str> = content_str.lines().collect();
226 let start = range.start_line.unwrap_or(1).saturating_sub(1);
227 let end = range.end_line.unwrap_or(lines.len()).min(lines.len());
228 let selected: Vec<&str> = lines.get(start..end).unwrap_or(&[]).to_vec();
229 let mut result = selected.join("\n");
230 if range.end_line.is_none() && content_str.ends_with('\n') && !result.is_empty() {
233 result.push('\n');
234 }
235 return result.into_bytes();
236 }
237
238 content.to_vec()
239 }
240}
241
242#[async_trait]
243impl KernelBackend for LocalBackend {
244 async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>> {
249 let content = self.vfs.read(path).await?;
250 match range {
251 Some(r) => Ok(Self::apply_read_range(&content, &r)),
252 None => Ok(content),
253 }
254 }
255
256 async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()> {
257 match mode {
258 WriteMode::CreateNew => {
259 if self.vfs.exists(path).await {
261 return Err(BackendError::AlreadyExists(path.display().to_string()));
262 }
263 self.vfs.write(path, content).await?;
264 }
265 WriteMode::Overwrite | WriteMode::Truncate => {
266 self.vfs.write(path, content).await?;
267 }
268 WriteMode::UpdateOnly => {
269 if !self.vfs.exists(path).await {
270 return Err(BackendError::NotFound(path.display().to_string()));
271 }
272 self.vfs.write(path, content).await?;
273 }
274 }
275 Ok(())
276 }
277
278 async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()> {
279 let mut existing = match self.vfs.read(path).await {
281 Ok(data) => data,
282 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
283 Err(e) => return Err(e.into()),
284 };
285 existing.extend_from_slice(content);
286 self.vfs.write(path, &existing).await?;
287 Ok(())
288 }
289
290 async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()> {
291 let data = self.vfs.read(path).await?;
293 let mut content = String::from_utf8(data)
294 .map_err(|e| BackendError::InvalidOperation(format!("file is not valid UTF-8: {}", e)))?;
295
296 for op in ops {
298 Self::apply_patch_op(&mut content, op)?;
299 }
300
301 self.vfs.write(path, content.as_bytes()).await?;
303 Ok(())
304 }
305
306 async fn list(&self, path: &Path) -> BackendResult<Vec<EntryInfo>> {
311 let entries = self.vfs.list(path).await?;
312 Ok(entries
313 .into_iter()
314 .map(|e| {
315 let (is_dir, is_file, is_symlink) = match e.entry_type {
316 EntryType::Directory => (true, false, false),
317 EntryType::File => (false, true, false),
318 EntryType::Symlink => (false, false, true),
319 };
320 EntryInfo {
321 name: e.name,
322 is_dir,
323 is_file,
324 is_symlink,
325 size: e.size,
326 modified: None, permissions: None,
328 symlink_target: e.symlink_target,
329 }
330 })
331 .collect())
332 }
333
334 async fn stat(&self, path: &Path) -> BackendResult<EntryInfo> {
335 let meta = self.vfs.stat(path).await?;
336 let modified = meta.modified.and_then(|t| {
337 t.duration_since(UNIX_EPOCH)
338 .ok()
339 .map(|d| d.as_secs())
340 });
341 Ok(EntryInfo {
342 name: path
343 .file_name()
344 .map(|s| s.to_string_lossy().to_string())
345 .unwrap_or_else(|| "/".to_string()),
346 is_dir: meta.is_dir,
347 is_file: meta.is_file,
348 is_symlink: meta.is_symlink,
349 size: meta.size,
350 modified,
351 permissions: None,
352 symlink_target: None, })
354 }
355
356 async fn mkdir(&self, path: &Path) -> BackendResult<()> {
357 self.vfs.mkdir(path).await?;
358 Ok(())
359 }
360
361 async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()> {
362 if recursive {
363 if let Ok(meta) = self.vfs.stat(path).await
366 && meta.is_dir
367 {
368 if let Ok(entries) = self.vfs.list(path).await {
370 for entry in entries {
371 let child_path = path.join(&entry.name);
372 Box::pin(self.remove(&child_path, true)).await?;
374 }
375 }
376 }
377 }
378 self.vfs.remove(path).await?;
379 Ok(())
380 }
381
382 async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()> {
383 self.vfs.rename(from, to).await?;
384 Ok(())
385 }
386
387 async fn exists(&self, path: &Path) -> bool {
388 self.vfs.exists(path).await
389 }
390
391 async fn read_link(&self, path: &Path) -> BackendResult<std::path::PathBuf> {
396 Ok(self.vfs.read_link(path).await?)
397 }
398
399 async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()> {
400 self.vfs.symlink(target, link).await?;
401 Ok(())
402 }
403
404 async fn call_tool(
409 &self,
410 name: &str,
411 args: ToolArgs,
412 ctx: &mut ExecContext,
413 ) -> BackendResult<ToolResult> {
414 let registry = self.tools.as_ref().ok_or_else(|| {
415 BackendError::ToolNotFound(format!("no tool registry configured for: {}", name))
416 })?;
417
418 let tool = registry.get(name).ok_or_else(|| {
419 BackendError::ToolNotFound(format!("{}: command not found", name))
420 })?;
421
422 let exec_result = tool.execute(args, ctx).await;
424 Ok(exec_result.into())
425 }
426
427 async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>> {
428 match &self.tools {
429 Some(registry) => {
430 let schemas = registry.schemas();
431 Ok(schemas
432 .into_iter()
433 .map(|schema| ToolInfo {
434 name: schema.name.clone(),
435 description: schema.description.clone(),
436 schema,
437 })
438 .collect())
439 }
440 None => Ok(Vec::new()),
441 }
442 }
443
444 async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>> {
445 match &self.tools {
446 Some(registry) => match registry.get(name) {
447 Some(tool) => {
448 let schema = tool.schema();
449 Ok(Some(ToolInfo {
450 name: schema.name.clone(),
451 description: schema.description.clone(),
452 schema,
453 }))
454 }
455 None => Ok(None),
456 },
457 None => Ok(None),
458 }
459 }
460
461 fn read_only(&self) -> bool {
466 self.vfs.read_only()
467 }
468
469 fn backend_type(&self) -> &str {
470 "local"
471 }
472
473 fn mounts(&self) -> Vec<MountInfo> {
474 self.vfs.list_mounts()
475 }
476
477 fn resolve_real_path(&self, path: &Path) -> Option<std::path::PathBuf> {
478 self.vfs.resolve_real_path(path)
479 }
480}
481
482impl std::fmt::Debug for LocalBackend {
483 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
484 f.debug_struct("LocalBackend")
485 .field("vfs", &self.vfs)
486 .field("has_tools", &self.tools.is_some())
487 .finish()
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::vfs::MemoryFs;
495 use std::path::PathBuf;
496
497 async fn make_backend() -> LocalBackend {
498 let mut vfs = VfsRouter::new();
499 let mem = MemoryFs::new();
500 mem.write(Path::new("test.txt"), b"hello world")
501 .await
502 .unwrap();
503 mem.write(Path::new("lines.txt"), b"line1\nline2\nline3\n")
504 .await
505 .unwrap();
506 mem.mkdir(Path::new("dir")).await.unwrap();
507 mem.write(Path::new("dir/nested.txt"), b"nested content")
508 .await
509 .unwrap();
510 vfs.mount("/", mem);
511 LocalBackend::new(Arc::new(vfs))
512 }
513
514 #[tokio::test]
515 async fn test_read_full() {
516 let backend = make_backend().await;
517 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
518 assert_eq!(content, b"hello world");
519 }
520
521 #[tokio::test]
522 async fn test_read_with_byte_range() {
523 let backend = make_backend().await;
524 let range = ReadRange::bytes(0, 5);
525 let content = backend.read(Path::new("/test.txt"), Some(range)).await.unwrap();
526 assert_eq!(content, b"hello");
527 }
528
529 #[tokio::test]
530 async fn test_read_with_line_range() {
531 let backend = make_backend().await;
532 let range = ReadRange::lines(2, 3);
533 let content = backend.read(Path::new("/lines.txt"), Some(range)).await.unwrap();
534 assert_eq!(std::str::from_utf8(&content).unwrap(), "line2\nline3");
535 }
536
537 #[tokio::test]
538 async fn test_write_overwrite() {
539 let backend = make_backend().await;
540 backend
541 .write(Path::new("/test.txt"), b"new content", WriteMode::Overwrite)
542 .await
543 .unwrap();
544 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
545 assert_eq!(content, b"new content");
546 }
547
548 #[tokio::test]
549 async fn test_write_create_new() {
550 let backend = make_backend().await;
551 backend
552 .write(Path::new("/new.txt"), b"created", WriteMode::CreateNew)
553 .await
554 .unwrap();
555 let content = backend.read(Path::new("/new.txt"), None).await.unwrap();
556 assert_eq!(content, b"created");
557 }
558
559 #[tokio::test]
560 async fn test_write_create_new_fails_if_exists() {
561 let backend = make_backend().await;
562 let result = backend
563 .write(Path::new("/test.txt"), b"fail", WriteMode::CreateNew)
564 .await;
565 assert!(matches!(result, Err(BackendError::AlreadyExists(_))));
566 }
567
568 #[tokio::test]
569 async fn test_write_update_only() {
570 let backend = make_backend().await;
571 backend
572 .write(Path::new("/test.txt"), b"updated", WriteMode::UpdateOnly)
573 .await
574 .unwrap();
575 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
576 assert_eq!(content, b"updated");
577 }
578
579 #[tokio::test]
580 async fn test_write_update_only_fails_if_not_exists() {
581 let backend = make_backend().await;
582 let result = backend
583 .write(Path::new("/nonexistent.txt"), b"fail", WriteMode::UpdateOnly)
584 .await;
585 assert!(matches!(result, Err(BackendError::NotFound(_))));
586 }
587
588 #[tokio::test]
589 async fn test_append() {
590 let backend = make_backend().await;
591 backend.append(Path::new("/test.txt"), b" appended").await.unwrap();
592 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
593 assert_eq!(content, b"hello world appended");
594 }
595
596 #[tokio::test]
597 async fn test_patch_insert() {
598 let backend = make_backend().await;
599 let ops = vec![PatchOp::Insert {
600 offset: 5,
601 content: " there".to_string(),
602 }];
603 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
604 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
605 assert_eq!(std::str::from_utf8(&content).unwrap(), "hello there world");
606 }
607
608 #[tokio::test]
609 async fn test_patch_delete() {
610 let backend = make_backend().await;
611 let ops = vec![PatchOp::Delete {
612 offset: 5,
613 len: 6,
614 expected: None,
615 }];
616 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
617 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
618 assert_eq!(std::str::from_utf8(&content).unwrap(), "hello");
619 }
620
621 #[tokio::test]
622 async fn test_patch_delete_with_cas() {
623 let backend = make_backend().await;
624 let ops = vec![PatchOp::Delete {
625 offset: 0,
626 len: 5,
627 expected: Some("hello".to_string()),
628 }];
629 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
630 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
631 assert_eq!(std::str::from_utf8(&content).unwrap(), " world");
632 }
633
634 #[tokio::test]
635 async fn test_patch_delete_cas_conflict() {
636 let backend = make_backend().await;
637 let ops = vec![PatchOp::Delete {
638 offset: 0,
639 len: 5,
640 expected: Some("wrong".to_string()),
641 }];
642 let result = backend.patch(Path::new("/test.txt"), &ops).await;
643 assert!(matches!(result, Err(BackendError::Conflict(_))));
644 }
645
646 #[tokio::test]
647 async fn test_patch_replace() {
648 let backend = make_backend().await;
649 let ops = vec![PatchOp::Replace {
650 offset: 0,
651 len: 5,
652 content: "hi".to_string(),
653 expected: None,
654 }];
655 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
656 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
657 assert_eq!(std::str::from_utf8(&content).unwrap(), "hi world");
658 }
659
660 #[tokio::test]
661 async fn test_patch_replace_line() {
662 let backend = make_backend().await;
663 let ops = vec![PatchOp::ReplaceLine {
664 line: 2,
665 content: "replaced".to_string(),
666 expected: None,
667 }];
668 backend.patch(Path::new("/lines.txt"), &ops).await.unwrap();
669 let content = backend.read(Path::new("/lines.txt"), None).await.unwrap();
670 let text = std::str::from_utf8(&content).unwrap();
671 assert!(text.contains("line1"));
672 assert!(text.contains("replaced"));
673 assert!(text.contains("line3"));
674 assert!(!text.contains("line2"));
675 }
676
677 #[tokio::test]
678 async fn test_patch_delete_line() {
679 let backend = make_backend().await;
680 let ops = vec![PatchOp::DeleteLine {
681 line: 2,
682 expected: None,
683 }];
684 backend.patch(Path::new("/lines.txt"), &ops).await.unwrap();
685 let content = backend.read(Path::new("/lines.txt"), None).await.unwrap();
686 let text = std::str::from_utf8(&content).unwrap();
687 assert!(text.contains("line1"));
688 assert!(!text.contains("line2"));
689 assert!(text.contains("line3"));
690 }
691
692 #[tokio::test]
693 async fn test_patch_insert_line() {
694 let backend = make_backend().await;
695 let ops = vec![PatchOp::InsertLine {
696 line: 2,
697 content: "inserted".to_string(),
698 }];
699 backend.patch(Path::new("/lines.txt"), &ops).await.unwrap();
700 let content = backend.read(Path::new("/lines.txt"), None).await.unwrap();
701 let text = std::str::from_utf8(&content).unwrap();
702 let lines: Vec<&str> = text.lines().collect();
703 assert_eq!(lines[0], "line1");
704 assert_eq!(lines[1], "inserted");
705 assert_eq!(lines[2], "line2");
706 }
707
708 #[tokio::test]
709 async fn test_patch_append() {
710 let backend = make_backend().await;
711 let ops = vec![PatchOp::Append {
712 content: "!".to_string(),
713 }];
714 backend.patch(Path::new("/test.txt"), &ops).await.unwrap();
715 let content = backend.read(Path::new("/test.txt"), None).await.unwrap();
716 assert_eq!(std::str::from_utf8(&content).unwrap(), "hello world!");
717 }
718
719 #[tokio::test]
720 async fn test_list() {
721 let backend = make_backend().await;
722 let entries = backend.list(Path::new("/")).await.unwrap();
723 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
724 assert!(names.contains(&"test.txt"));
725 assert!(names.contains(&"lines.txt"));
726 assert!(names.contains(&"dir"));
727 }
728
729 #[tokio::test]
730 async fn test_stat() {
731 let backend = make_backend().await;
732 let info = backend.stat(Path::new("/test.txt")).await.unwrap();
733 assert!(info.is_file);
734 assert!(!info.is_dir);
735 assert_eq!(info.size, 11); let info = backend.stat(Path::new("/dir")).await.unwrap();
738 assert!(info.is_dir);
739 assert!(!info.is_file);
740 }
741
742 #[tokio::test]
743 async fn test_mkdir() {
744 let backend = make_backend().await;
745 backend.mkdir(Path::new("/newdir")).await.unwrap();
746 assert!(backend.exists(Path::new("/newdir")).await);
747 let info = backend.stat(Path::new("/newdir")).await.unwrap();
748 assert!(info.is_dir);
749 }
750
751 #[tokio::test]
752 async fn test_remove() {
753 let backend = make_backend().await;
754 assert!(backend.exists(Path::new("/test.txt")).await);
755 backend.remove(Path::new("/test.txt"), false).await.unwrap();
756 assert!(!backend.exists(Path::new("/test.txt")).await);
757 }
758
759 #[tokio::test]
760 async fn test_remove_recursive() {
761 let backend = make_backend().await;
762 assert!(backend.exists(Path::new("/dir/nested.txt")).await);
763 backend.remove(Path::new("/dir"), true).await.unwrap();
764 assert!(!backend.exists(Path::new("/dir")).await);
765 assert!(!backend.exists(Path::new("/dir/nested.txt")).await);
766 }
767
768 #[tokio::test]
769 async fn test_exists() {
770 let backend = make_backend().await;
771 assert!(backend.exists(Path::new("/test.txt")).await);
772 assert!(!backend.exists(Path::new("/nonexistent.txt")).await);
773 }
774
775 #[tokio::test]
776 async fn test_backend_info() {
777 let backend = make_backend().await;
778 assert_eq!(backend.backend_type(), "local");
779 assert!(!backend.read_only());
780 let mounts = backend.mounts();
781 assert!(!mounts.is_empty());
782 }
783
784 #[tokio::test]
785 async fn test_list_includes_symlinks() {
786 use crate::vfs::Filesystem;
787
788 let mut vfs = VfsRouter::new();
789 let mem = MemoryFs::new();
790 mem.write(Path::new("target.txt"), b"content").await.unwrap();
791 mem.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
792 vfs.mount("/", mem);
793 let backend = LocalBackend::new(Arc::new(vfs));
794
795 let entries = backend.list(Path::new("/")).await.unwrap();
796 println!("Entries: {:?}", entries);
797
798 let link_entry = entries.iter().find(|e| e.name == "link.txt").unwrap();
799 assert!(link_entry.is_symlink, "link.txt should be a symlink");
800 assert_eq!(link_entry.symlink_target, Some(PathBuf::from("target.txt")));
801 }
802}