1use crate::builtin_tools::BuiltinTool;
6use crate::types::{Layer3Result, ToolCategory};
7use async_trait::async_trait;
8
9pub struct ReadFileTool;
11
12#[async_trait]
13impl BuiltinTool for ReadFileTool {
14 fn name(&self) -> &str {
15 "read_file"
16 }
17
18 fn description(&self) -> &str {
19 "Read the contents of a file from the filesystem."
20 }
21
22 fn parameters_schema(&self) -> serde_json::Value {
23 serde_json::json!({
24 "type": "object",
25 "properties": {
26 "path": {
27 "type": "string",
28 "description": "The path to the file to read"
29 },
30 "offset": {
31 "type": "integer",
32 "description": "Optional: line number to start reading from"
33 },
34 "limit": {
35 "type": "integer",
36 "description": "Optional: number of lines to read"
37 }
38 },
39 "required": ["path"]
40 })
41 }
42
43 fn category(&self) -> ToolCategory {
44 ToolCategory::FileOps
45 }
46
47 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
48 let path = args["path"]
49 .as_str()
50 .ok_or_else(|| anyhow::anyhow!("Missing path parameter"))?;
51
52 let content = tokio::fs::read_to_string(path).await?;
53
54 let offset = args.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
56 let limit = args
57 .get("limit")
58 .and_then(|v| v.as_u64())
59 .map(|v| v as usize);
60
61 if offset == 0 && limit.is_none() {
62 return Ok(content);
64 }
65
66 let lines: Vec<&str> = content.lines().collect();
68 let total_lines = lines.len();
69
70 if offset > total_lines {
72 return Err(anyhow::anyhow!(
73 "Offset {} exceeds total lines {}",
74 offset,
75 total_lines
76 ));
77 }
78
79 let end = limit.map_or(total_lines, |l| (offset + l).min(total_lines));
81 let page_lines = &lines[offset..end];
82
83 let result = if page_lines.is_empty() {
85 format!(
86 "[Lines {}-{} of {} total lines]\n(No content in this range)",
87 offset, end, total_lines
88 )
89 } else {
90 format!(
91 "[Lines {}-{} of {} total lines]\n{}",
92 offset,
93 end,
94 total_lines,
95 page_lines.join("\n")
96 )
97 };
98
99 Ok(result)
100 }
101}
102
103pub struct WriteFileTool;
105
106#[async_trait]
107impl BuiltinTool for WriteFileTool {
108 fn name(&self) -> &str {
109 "write_file"
110 }
111
112 fn description(&self) -> &str {
113 "Write content to a file, creating it if it doesn't exist."
114 }
115
116 fn parameters_schema(&self) -> serde_json::Value {
117 serde_json::json!({
118 "type": "object",
119 "properties": {
120 "path": {
121 "type": "string",
122 "description": "The path to the file to write"
123 },
124 "content": {
125 "type": "string",
126 "description": "The content to write to the file"
127 }
128 },
129 "required": ["path", "content"]
130 })
131 }
132
133 fn category(&self) -> ToolCategory {
134 ToolCategory::FileOps
135 }
136
137 fn is_dangerous(&self) -> bool {
138 true
139 }
140
141 fn requires_confirmation(&self) -> bool {
142 true
143 }
144
145 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
146 let path = args["path"]
147 .as_str()
148 .ok_or_else(|| anyhow::anyhow!("Missing path parameter"))?;
149 let content = args["content"]
150 .as_str()
151 .ok_or_else(|| anyhow::anyhow!("Missing content parameter"))?;
152 tokio::fs::write(path, content).await?;
153 Ok(format!("Successfully wrote to {}", path))
154 }
155}
156
157pub struct EditFileTool;
159
160#[async_trait]
161impl BuiltinTool for EditFileTool {
162 fn name(&self) -> &str {
163 "edit_file"
164 }
165
166 fn description(&self) -> &str {
167 "Edit a file by replacing specific text with new text."
168 }
169
170 fn parameters_schema(&self) -> serde_json::Value {
171 serde_json::json!({
172 "type": "object",
173 "properties": {
174 "path": {
175 "type": "string",
176 "description": "The path to the file to edit"
177 },
178 "old_string": {
179 "type": "string",
180 "description": "The text to search for and replace"
181 },
182 "new_string": {
183 "type": "string",
184 "description": "The text to replace with"
185 }
186 },
187 "required": ["path", "old_string", "new_string"]
188 })
189 }
190
191 fn category(&self) -> ToolCategory {
192 ToolCategory::FileOps
193 }
194
195 fn is_dangerous(&self) -> bool {
196 true
197 }
198
199 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
200 let path = args["path"]
201 .as_str()
202 .ok_or_else(|| anyhow::anyhow!("Missing path parameter"))?;
203 let old_string = args["old_string"]
204 .as_str()
205 .ok_or_else(|| anyhow::anyhow!("Missing old_string parameter"))?;
206 let new_string = args["new_string"]
207 .as_str()
208 .ok_or_else(|| anyhow::anyhow!("Missing new_string parameter"))?;
209
210 let content = tokio::fs::read_to_string(path).await?;
211 if !content.contains(old_string) {
212 return Err(anyhow::anyhow!("old_string not found in file"));
213 }
214
215 let new_content = content.replace(old_string, new_string);
216 tokio::fs::write(path, new_content).await?;
217 Ok(format!("Successfully edited {}", path))
218 }
219}
220
221pub struct ListDirectoryTool;
223
224#[async_trait]
225impl BuiltinTool for ListDirectoryTool {
226 fn name(&self) -> &str {
227 "list_directory"
228 }
229
230 fn description(&self) -> &str {
231 "List files and directories in a given path."
232 }
233
234 fn parameters_schema(&self) -> serde_json::Value {
235 serde_json::json!({
236 "type": "object",
237 "properties": {
238 "path": {
239 "type": "string",
240 "description": "The directory path to list"
241 }
242 },
243 "required": ["path"]
244 })
245 }
246
247 fn category(&self) -> ToolCategory {
248 ToolCategory::FileOps
249 }
250
251 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
252 let path = args["path"]
253 .as_str()
254 .ok_or_else(|| anyhow::anyhow!("Missing path parameter"))?;
255 let mut entries = tokio::fs::read_dir(path).await?;
256 let mut result = Vec::new();
257
258 while let Some(entry) = entries.next_entry().await? {
259 let name = entry.file_name().to_string_lossy().to_string();
260 let file_type = if entry.file_type().await?.is_dir() {
261 "dir"
262 } else {
263 "file"
264 };
265 result.push(format!("{} [{}]", name, file_type));
266 }
267
268 Ok(result.join("\n"))
269 }
270}
271
272pub struct MoveFileTool;
276
277#[async_trait]
278impl BuiltinTool for MoveFileTool {
279 fn name(&self) -> &str {
280 "move_file"
281 }
282
283 fn description(&self) -> &str {
284 "Move or rename a file or directory."
285 }
286
287 fn parameters_schema(&self) -> serde_json::Value {
288 serde_json::json!({
289 "type": "object",
290 "properties": {
291 "source": {
292 "type": "string",
293 "description": "The source path to move from"
294 },
295 "destination": {
296 "type": "string",
297 "description": "The destination path to move to"
298 },
299 "overwrite": {
300 "type": "boolean",
301 "description": "Optional: overwrite destination if exists (default: false)"
302 }
303 },
304 "required": ["source", "destination"]
305 })
306 }
307
308 fn category(&self) -> ToolCategory {
309 ToolCategory::FileOps
310 }
311
312 fn is_dangerous(&self) -> bool {
313 true
314 }
315
316 fn requires_confirmation(&self) -> bool {
317 true
318 }
319
320 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
321 let source = args["source"]
322 .as_str()
323 .ok_or_else(|| anyhow::anyhow!("Missing source parameter"))?;
324 let destination = args["destination"]
325 .as_str()
326 .ok_or_else(|| anyhow::anyhow!("Missing destination parameter"))?;
327 let overwrite = args["overwrite"].as_bool().unwrap_or(false);
328
329 let source_meta = tokio::fs::metadata(source)
331 .await
332 .map_err(|e| anyhow::anyhow!("Source path not found: {} ({})", source, e))?;
333
334 let dest_exists = tokio::fs::try_exists(destination).await.unwrap_or(false);
336
337 if dest_exists && !overwrite {
338 return Err(anyhow::anyhow!(
339 "Destination already exists: {}. Use overwrite=true to replace.",
340 destination
341 ));
342 }
343
344 if dest_exists && overwrite {
346 if tokio::fs::metadata(destination).await?.is_dir() {
347 tokio::fs::remove_dir_all(destination).await?;
348 } else {
349 tokio::fs::remove_file(destination).await?;
350 }
351 }
352
353 if let Some(parent) = std::path::Path::new(destination).parent() {
355 if !parent.exists() {
356 tokio::fs::create_dir_all(parent).await?;
357 }
358 }
359
360 tokio::fs::rename(source, destination).await.map_err(|e| {
362 if e.raw_os_error() == Some(18) {
364 anyhow::anyhow!(
366 "Cross-device move not supported directly. Use copy + delete instead."
367 )
368 } else {
369 anyhow::anyhow!("Failed to move file: {}", e)
370 }
371 })?;
372
373 let file_type = if source_meta.is_dir() {
374 "directory"
375 } else {
376 "file"
377 };
378 Ok(format!(
379 "Successfully moved {} from {} to {}",
380 file_type, source, destination
381 ))
382 }
383}
384
385pub struct CopyFileTool;
389
390#[async_trait]
391impl BuiltinTool for CopyFileTool {
392 fn name(&self) -> &str {
393 "copy_file"
394 }
395
396 fn description(&self) -> &str {
397 "Copy a file or directory to a new location."
398 }
399
400 fn parameters_schema(&self) -> serde_json::Value {
401 serde_json::json!({
402 "type": "object",
403 "properties": {
404 "source": {
405 "type": "string",
406 "description": "The source path to copy from"
407 },
408 "destination": {
409 "type": "string",
410 "description": "The destination path to copy to"
411 },
412 "overwrite": {
413 "type": "boolean",
414 "description": "Optional: overwrite destination if exists (default: false)"
415 }
416 },
417 "required": ["source", "destination"]
418 })
419 }
420
421 fn category(&self) -> ToolCategory {
422 ToolCategory::FileOps
423 }
424
425 fn is_dangerous(&self) -> bool {
426 true
427 }
428
429 fn requires_confirmation(&self) -> bool {
430 true
431 }
432
433 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
434 let source = args["source"]
435 .as_str()
436 .ok_or_else(|| anyhow::anyhow!("Missing source parameter"))?;
437 let destination = args["destination"]
438 .as_str()
439 .ok_or_else(|| anyhow::anyhow!("Missing destination parameter"))?;
440 let overwrite = args["overwrite"].as_bool().unwrap_or(false);
441
442 let source_meta = tokio::fs::metadata(source)
444 .await
445 .map_err(|e| anyhow::anyhow!("Source path not found: {} ({})", source, e))?;
446
447 let dest_exists = tokio::fs::try_exists(destination).await.unwrap_or(false);
449
450 if dest_exists && !overwrite {
451 return Err(anyhow::anyhow!(
452 "Destination already exists: {}. Use overwrite=true to replace.",
453 destination
454 ));
455 }
456
457 if let Some(parent) = std::path::Path::new(destination).parent() {
459 if !parent.exists() {
460 tokio::fs::create_dir_all(parent).await?;
461 }
462 }
463
464 if source_meta.is_dir() {
466 copy_dir_all(source.to_string(), destination.to_string()).await?;
468 Ok(format!(
469 "Successfully copied directory from {} to {}",
470 source, destination
471 ))
472 } else {
473 tokio::fs::copy(source, destination)
475 .await
476 .map_err(|e| anyhow::anyhow!("Failed to copy file: {}", e))?;
477 Ok(format!(
478 "Successfully copied file from {} to {}",
479 source, destination
480 ))
481 }
482 }
483}
484
485fn copy_dir_all(
489 source: String,
490 destination: String,
491) -> std::pin::Pin<Box<dyn std::future::Future<Output = Layer3Result<()>> + Send>> {
492 Box::pin(async move {
493 tokio::fs::create_dir_all(&destination).await?;
494
495 let mut entries = tokio::fs::read_dir(&source).await?;
496 while let Some(entry) = entries.next_entry().await? {
497 let ty = entry.file_type().await?;
498 let src_path = entry.path();
499 let dest_path = std::path::Path::new(&destination).join(entry.file_name());
500
501 if ty.is_dir() {
502 copy_dir_all(
503 src_path.to_string_lossy().to_string(),
504 dest_path.to_string_lossy().to_string(),
505 )
506 .await?;
507 } else {
508 tokio::fs::copy(&src_path, &dest_path).await?;
509 }
510 }
511
512 Ok(())
513 })
514}
515
516pub struct CreateDirectoryTool;
520
521#[async_trait]
522impl BuiltinTool for CreateDirectoryTool {
523 fn name(&self) -> &str {
524 "create_directory"
525 }
526
527 fn description(&self) -> &str {
528 "Create a directory and all parent directories if they don't exist."
529 }
530
531 fn parameters_schema(&self) -> serde_json::Value {
532 serde_json::json!({
533 "type": "object",
534 "properties": {
535 "path": {
536 "type": "string",
537 "description": "The directory path to create"
538 }
539 },
540 "required": ["path"]
541 })
542 }
543
544 fn category(&self) -> ToolCategory {
545 ToolCategory::FileOps
546 }
547
548 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
549 let path = args["path"]
550 .as_str()
551 .ok_or_else(|| anyhow::anyhow!("Missing path parameter"))?;
552
553 tokio::fs::create_dir_all(path)
554 .await
555 .map_err(|e| anyhow::anyhow!("Failed to create directory: {}", e))?;
556
557 Ok(format!("Successfully created directory: {}", path))
558 }
559}
560
561pub struct DeleteFileTool;
565
566#[async_trait]
567impl BuiltinTool for DeleteFileTool {
568 fn name(&self) -> &str {
569 "delete_file"
570 }
571
572 fn description(&self) -> &str {
573 "Delete a file or directory from the filesystem."
574 }
575
576 fn parameters_schema(&self) -> serde_json::Value {
577 serde_json::json!({
578 "type": "object",
579 "properties": {
580 "path": {
581 "type": "string",
582 "description": "The path to the file or directory to delete"
583 },
584 "recursive": {
585 "type": "boolean",
586 "description": "Optional: for directories, delete recursively (default: false)"
587 }
588 },
589 "required": ["path"]
590 })
591 }
592
593 fn category(&self) -> ToolCategory {
594 ToolCategory::FileOps
595 }
596
597 fn is_dangerous(&self) -> bool {
598 true
599 }
600
601 fn requires_confirmation(&self) -> bool {
602 true
603 }
604
605 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
606 let path = args["path"]
607 .as_str()
608 .ok_or_else(|| anyhow::anyhow!("Missing path parameter"))?;
609 let recursive = args["recursive"].as_bool().unwrap_or(false);
610
611 let meta = tokio::fs::metadata(path)
613 .await
614 .map_err(|e| anyhow::anyhow!("Path not found: {} ({})", path, e))?;
615
616 if meta.is_dir() {
617 if recursive {
619 tokio::fs::remove_dir_all(path)
620 .await
621 .map_err(|e| anyhow::anyhow!("Failed to delete directory: {}", e))?;
622 Ok(format!("Successfully deleted directory: {}", path))
623 } else {
624 tokio::fs::remove_dir(path).await
626 .map_err(|e| anyhow::anyhow!(
627 "Failed to delete directory (may not be empty): {}. Use recursive=true for non-empty directories.",
628 e
629 ))?;
630 Ok(format!("Successfully deleted directory: {}", path))
631 }
632 } else {
633 tokio::fs::remove_file(path)
635 .await
636 .map_err(|e| anyhow::anyhow!("Failed to delete file: {}", e))?;
637 Ok(format!("Successfully deleted file: {}", path))
638 }
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 #[test]
647 fn test_read_file_tool_meta() {
648 let tool = ReadFileTool;
649 assert_eq!(tool.name(), "read_file");
650 assert_eq!(tool.category(), ToolCategory::FileOps);
651 }
652
653 #[test]
654 fn test_write_file_tool_dangerous() {
655 let tool = WriteFileTool;
656 assert!(tool.is_dangerous());
657 assert!(tool.requires_confirmation());
658 }
659
660 #[test]
661 fn test_move_file_tool_meta() {
662 let tool = MoveFileTool;
663 assert_eq!(tool.name(), "move_file");
664 assert_eq!(tool.category(), ToolCategory::FileOps);
665 assert!(tool.is_dangerous());
666 assert!(tool.requires_confirmation());
667 }
668
669 #[tokio::test]
670 async fn test_move_file_success() {
671 use tempfile::TempDir;
672
673 let temp_dir = TempDir::new().unwrap();
674 let source = temp_dir.path().join("source.txt");
675 let dest = temp_dir.path().join("dest.txt");
676
677 tokio::fs::write(&source, "test content").await.unwrap();
679
680 let tool = MoveFileTool;
681 let result = tool
682 .execute(serde_json::json!({
683 "source": source.to_str().unwrap(),
684 "destination": dest.to_str().unwrap()
685 }))
686 .await;
687
688 assert!(result.is_ok());
689 assert!(dest.exists());
690 assert!(!source.exists());
691 }
692
693 #[tokio::test]
694 async fn test_move_file_missing_source() {
695 let tool = MoveFileTool;
696 let result = tool
697 .execute(serde_json::json!({
698 "source": "/nonexistent/file.txt",
699 "destination": "/tmp/dest.txt"
700 }))
701 .await;
702
703 assert!(result.is_err());
704 assert!(result
705 .unwrap_err()
706 .to_string()
707 .contains("Source path not found"));
708 }
709
710 #[tokio::test]
711 async fn test_move_file_destination_exists_no_overwrite() {
712 use tempfile::TempDir;
713
714 let temp_dir = TempDir::new().unwrap();
715 let source = temp_dir.path().join("source.txt");
716 let dest = temp_dir.path().join("dest.txt");
717
718 tokio::fs::write(&source, "source content").await.unwrap();
719 tokio::fs::write(&dest, "dest content").await.unwrap();
720
721 let tool = MoveFileTool;
722 let result = tool
723 .execute(serde_json::json!({
724 "source": source.to_str().unwrap(),
725 "destination": dest.to_str().unwrap()
726 }))
727 .await;
728
729 assert!(result.is_err());
730 assert!(result.unwrap_err().to_string().contains("already exists"));
731 }
732
733 #[tokio::test]
734 async fn test_move_file_destination_exists_with_overwrite() {
735 use tempfile::TempDir;
736
737 let temp_dir = TempDir::new().unwrap();
738 let source = temp_dir.path().join("source.txt");
739 let dest = temp_dir.path().join("dest.txt");
740
741 tokio::fs::write(&source, "source content").await.unwrap();
742 tokio::fs::write(&dest, "dest content").await.unwrap();
743
744 let tool = MoveFileTool;
745 let result = tool
746 .execute(serde_json::json!({
747 "source": source.to_str().unwrap(),
748 "destination": dest.to_str().unwrap(),
749 "overwrite": true
750 }))
751 .await;
752
753 assert!(result.is_ok());
754 let content = tokio::fs::read_to_string(&dest).await.unwrap();
756 assert_eq!(content, "source content");
757 }
758
759 #[test]
760 fn test_copy_file_tool_meta() {
761 let tool = CopyFileTool;
762 assert_eq!(tool.name(), "copy_file");
763 assert_eq!(tool.category(), ToolCategory::FileOps);
764 assert!(tool.is_dangerous());
765 assert!(tool.requires_confirmation());
766 }
767
768 #[tokio::test]
769 async fn test_copy_file_success() {
770 use tempfile::TempDir;
771
772 let temp_dir = TempDir::new().unwrap();
773 let source = temp_dir.path().join("source.txt");
774 let dest = temp_dir.path().join("dest.txt");
775
776 tokio::fs::write(&source, "test content").await.unwrap();
778
779 let tool = CopyFileTool;
780 let result = tool
781 .execute(serde_json::json!({
782 "source": source.to_str().unwrap(),
783 "destination": dest.to_str().unwrap()
784 }))
785 .await;
786
787 assert!(result.is_ok());
788 assert!(dest.exists());
789 assert!(source.exists()); let content = tokio::fs::read_to_string(&dest).await.unwrap();
791 assert_eq!(content, "test content");
792 }
793
794 #[tokio::test]
795 async fn test_copy_file_missing_source() {
796 let tool = CopyFileTool;
797 let result = tool
798 .execute(serde_json::json!({
799 "source": "/nonexistent/file.txt",
800 "destination": "/tmp/dest.txt"
801 }))
802 .await;
803
804 assert!(result.is_err());
805 assert!(result
806 .unwrap_err()
807 .to_string()
808 .contains("Source path not found"));
809 }
810
811 #[tokio::test]
812 async fn test_copy_file_destination_exists_no_overwrite() {
813 use tempfile::TempDir;
814
815 let temp_dir = TempDir::new().unwrap();
816 let source = temp_dir.path().join("source.txt");
817 let dest = temp_dir.path().join("dest.txt");
818
819 tokio::fs::write(&source, "source content").await.unwrap();
820 tokio::fs::write(&dest, "dest content").await.unwrap();
821
822 let tool = CopyFileTool;
823 let result = tool
824 .execute(serde_json::json!({
825 "source": source.to_str().unwrap(),
826 "destination": dest.to_str().unwrap()
827 }))
828 .await;
829
830 assert!(result.is_err());
831 assert!(result.unwrap_err().to_string().contains("already exists"));
832 }
833
834 #[tokio::test]
835 async fn test_copy_directory() {
836 use tempfile::TempDir;
837
838 let temp_dir = TempDir::new().unwrap();
839 let source_dir = temp_dir.path().join("source_dir");
840 let dest_dir = temp_dir.path().join("dest_dir");
841
842 tokio::fs::create_dir_all(&source_dir).await.unwrap();
844 tokio::fs::write(source_dir.join("file1.txt"), "content1")
845 .await
846 .unwrap();
847 tokio::fs::write(source_dir.join("file2.txt"), "content2")
848 .await
849 .unwrap();
850
851 let tool = CopyFileTool;
852 let result = tool
853 .execute(serde_json::json!({
854 "source": source_dir.to_str().unwrap(),
855 "destination": dest_dir.to_str().unwrap()
856 }))
857 .await;
858
859 assert!(result.is_ok());
860 assert!(dest_dir.exists());
861 assert!(dest_dir.join("file1.txt").exists());
862 assert!(dest_dir.join("file2.txt").exists());
863 assert!(source_dir.exists()); }
865
866 #[test]
867 fn test_delete_file_tool_meta() {
868 let tool = DeleteFileTool;
869 assert_eq!(tool.name(), "delete_file");
870 assert_eq!(tool.category(), ToolCategory::FileOps);
871 assert!(tool.is_dangerous());
872 assert!(tool.requires_confirmation());
873 }
874
875 #[tokio::test]
876 async fn test_delete_file_success() {
877 use tempfile::TempDir;
878
879 let temp_dir = TempDir::new().unwrap();
880 let file = temp_dir.path().join("test.txt");
881
882 tokio::fs::write(&file, "test content").await.unwrap();
884
885 let tool = DeleteFileTool;
886 let result = tool
887 .execute(serde_json::json!({
888 "path": file.to_str().unwrap()
889 }))
890 .await;
891
892 assert!(result.is_ok());
893 assert!(!file.exists());
894 }
895
896 #[tokio::test]
897 async fn test_delete_file_missing_path() {
898 let tool = DeleteFileTool;
899 let result = tool
900 .execute(serde_json::json!({
901 "path": "/nonexistent/file.txt"
902 }))
903 .await;
904
905 assert!(result.is_err());
906 assert!(result.unwrap_err().to_string().contains("Path not found"));
907 }
908
909 #[tokio::test]
910 async fn test_delete_empty_directory() {
911 use tempfile::TempDir;
912
913 let temp_dir = TempDir::new().unwrap();
914 let dir = temp_dir.path().join("empty_dir");
915
916 tokio::fs::create_dir_all(&dir).await.unwrap();
918
919 let tool = DeleteFileTool;
920 let result = tool
921 .execute(serde_json::json!({
922 "path": dir.to_str().unwrap()
923 }))
924 .await;
925
926 assert!(result.is_ok());
927 assert!(!dir.exists());
928 }
929
930 #[tokio::test]
931 async fn test_delete_non_empty_directory_without_recursive() {
932 use tempfile::TempDir;
933
934 let temp_dir = TempDir::new().unwrap();
935 let dir = temp_dir.path().join("non_empty_dir");
936
937 tokio::fs::create_dir_all(&dir).await.unwrap();
939 tokio::fs::write(dir.join("file.txt"), "content")
940 .await
941 .unwrap();
942
943 let tool = DeleteFileTool;
944 let result = tool
945 .execute(serde_json::json!({
946 "path": dir.to_str().unwrap()
947 }))
948 .await;
949
950 assert!(result.is_err());
951 assert!(result.unwrap_err().to_string().contains("may not be empty"));
952 }
953
954 #[tokio::test]
955 async fn test_delete_non_empty_directory_with_recursive() {
956 use tempfile::TempDir;
957
958 let temp_dir = TempDir::new().unwrap();
959 let dir = temp_dir.path().join("non_empty_dir");
960
961 tokio::fs::create_dir_all(&dir).await.unwrap();
963 tokio::fs::write(dir.join("file.txt"), "content")
964 .await
965 .unwrap();
966 tokio::fs::create_dir_all(dir.join("subdir")).await.unwrap();
967 tokio::fs::write(dir.join("subdir/nested.txt"), "nested")
968 .await
969 .unwrap();
970
971 let tool = DeleteFileTool;
972 let result = tool
973 .execute(serde_json::json!({
974 "path": dir.to_str().unwrap(),
975 "recursive": true
976 }))
977 .await;
978
979 assert!(result.is_ok());
980 assert!(!dir.exists());
981 }
982
983 #[tokio::test]
986 async fn test_read_file_no_pagination() {
987 use tempfile::TempDir;
988
989 let temp_dir = TempDir::new().unwrap();
990 let file = temp_dir.path().join("test.txt");
991 let content = "line1\nline2\nline3\n";
992 tokio::fs::write(&file, content).await.unwrap();
993
994 let tool = ReadFileTool;
995 let result = tool
996 .execute(serde_json::json!({
997 "path": file.to_str().unwrap()
998 }))
999 .await
1000 .unwrap();
1001
1002 assert_eq!(result, "line1\nline2\nline3\n");
1003 }
1004
1005 #[tokio::test]
1006 async fn test_read_file_with_offset() {
1007 use tempfile::TempDir;
1008
1009 let temp_dir = TempDir::new().unwrap();
1010 let file = temp_dir.path().join("test.txt");
1011 let content = "line1\nline2\nline3\nline4\nline5\n";
1012 tokio::fs::write(&file, content).await.unwrap();
1013
1014 let tool = ReadFileTool;
1015 let result = tool
1016 .execute(serde_json::json!({
1017 "path": file.to_str().unwrap(),
1018 "offset": 2
1019 }))
1020 .await
1021 .unwrap();
1022
1023 assert!(result.contains("[Lines 2-5 of 5 total lines]"));
1024 assert!(result.contains("line3"));
1025 assert!(result.contains("line5"));
1026 assert!(!result.contains("line1"));
1027 }
1028
1029 #[tokio::test]
1030 async fn test_read_file_with_limit() {
1031 use tempfile::TempDir;
1032
1033 let temp_dir = TempDir::new().unwrap();
1034 let file = temp_dir.path().join("test.txt");
1035 let content = "line1\nline2\nline3\nline4\nline5\n";
1036 tokio::fs::write(&file, content).await.unwrap();
1037
1038 let tool = ReadFileTool;
1039 let result = tool
1040 .execute(serde_json::json!({
1041 "path": file.to_str().unwrap(),
1042 "limit": 2
1043 }))
1044 .await
1045 .unwrap();
1046
1047 assert!(result.contains("[Lines 0-2 of 5 total lines]"));
1048 assert!(result.contains("line1"));
1049 assert!(result.contains("line2"));
1050 assert!(!result.contains("line3"));
1051 }
1052
1053 #[tokio::test]
1054 async fn test_read_file_with_offset_and_limit() {
1055 use tempfile::TempDir;
1056
1057 let temp_dir = TempDir::new().unwrap();
1058 let file = temp_dir.path().join("test.txt");
1059 let content = "line1\nline2\nline3\nline4\nline5\n";
1060 tokio::fs::write(&file, content).await.unwrap();
1061
1062 let tool = ReadFileTool;
1063 let result = tool
1064 .execute(serde_json::json!({
1065 "path": file.to_str().unwrap(),
1066 "offset": 1,
1067 "limit": 2
1068 }))
1069 .await
1070 .unwrap();
1071
1072 assert!(result.contains("[Lines 1-3 of 5 total lines]"));
1073 assert!(result.contains("line2"));
1074 assert!(result.contains("line3"));
1075 assert!(!result.contains("line1"));
1076 assert!(!result.contains("line4"));
1077 }
1078
1079 #[tokio::test]
1080 async fn test_read_file_offset_exceeds_total() {
1081 use tempfile::TempDir;
1082
1083 let temp_dir = TempDir::new().unwrap();
1084 let file = temp_dir.path().join("test.txt");
1085 tokio::fs::write(&file, "line1\nline2\n").await.unwrap();
1086
1087 let tool = ReadFileTool;
1088 let result = tool
1089 .execute(serde_json::json!({
1090 "path": file.to_str().unwrap(),
1091 "offset": 10
1092 }))
1093 .await;
1094
1095 assert!(result.is_err());
1096 assert!(result
1097 .unwrap_err()
1098 .to_string()
1099 .contains("exceeds total lines"));
1100 }
1101
1102 #[tokio::test]
1103 async fn test_read_file_empty_range() {
1104 use tempfile::TempDir;
1105
1106 let temp_dir = TempDir::new().unwrap();
1107 let file = temp_dir.path().join("test.txt");
1108 tokio::fs::write(&file, "line1\nline2\nline3\n")
1109 .await
1110 .unwrap();
1111
1112 let tool = ReadFileTool;
1113 let result = tool
1114 .execute(serde_json::json!({
1115 "path": file.to_str().unwrap(),
1116 "offset": 3,
1117 "limit": 5
1118 }))
1119 .await
1120 .unwrap();
1121
1122 assert!(result.contains("No content in this range"));
1123 }
1124
1125 #[tokio::test]
1126 async fn test_read_file_single_line_file() {
1127 use tempfile::TempDir;
1128
1129 let temp_dir = TempDir::new().unwrap();
1130 let file = temp_dir.path().join("test.txt");
1131 tokio::fs::write(&file, "single line content")
1132 .await
1133 .unwrap();
1134
1135 let tool = ReadFileTool;
1136 let result = tool
1137 .execute(serde_json::json!({
1138 "path": file.to_str().unwrap(),
1139 "offset": 0,
1140 "limit": 10
1141 }))
1142 .await
1143 .unwrap();
1144
1145 assert!(result.contains("[Lines 0-1 of 1 total lines]"));
1146 assert!(result.contains("single line content"));
1147 }
1148}