Skip to main content

sh_layer3/builtin_tools/
file_ops.rs

1//! # File Operations Tools
2//!
3//! 文件操作工具集:读写、编辑、创建、删除等。
4
5use crate::builtin_tools::BuiltinTool;
6use crate::types::{Layer3Result, ToolCategory};
7use async_trait::async_trait;
8
9/// Read File Tool
10pub 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        // 处理分页参数
55        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            // 无分页,返回完整内容
63            return Ok(content);
64        }
65
66        // 按行分页
67        let lines: Vec<&str> = content.lines().collect();
68        let total_lines = lines.len();
69
70        // 验证 offset
71        if offset > total_lines {
72            return Err(anyhow::anyhow!(
73                "Offset {} exceeds total lines {}",
74                offset,
75                total_lines
76            ));
77        }
78
79        // 计算分页范围
80        let end = limit.map_or(total_lines, |l| (offset + l).min(total_lines));
81        let page_lines = &lines[offset..end];
82
83        // 构建返回内容,包含分页元信息
84        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
103/// Write File Tool
104pub 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
157/// Edit File Tool (search and replace)
158pub 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
221/// List Directory Tool
222pub 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
272/// Move File Tool
273///
274/// 移动或重命名文件/目录。
275pub 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        // 验证源文件/目录存在
330        let source_meta = tokio::fs::metadata(source)
331            .await
332            .map_err(|e| anyhow::anyhow!("Source path not found: {} ({})", source, e))?;
333
334        // 检查目标是否存在
335        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        // 如果目标存在且要覆盖,先删除目标
345        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        // 确保目标父目录存在
354        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        // 执行移动操作
361        tokio::fs::rename(source, destination).await.map_err(|e| {
362            // 如果跨文件系统移动失败,提示用户
363            if e.raw_os_error() == Some(18) {
364                // EXDEV: cross-device link
365                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
385/// Copy File Tool
386///
387/// 复制文件或目录。
388pub 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        // 验证源文件/目录存在
443        let source_meta = tokio::fs::metadata(source)
444            .await
445            .map_err(|e| anyhow::anyhow!("Source path not found: {} ({})", source, e))?;
446
447        // 检查目标是否存在
448        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        // 确保目标父目录存在
458        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        // 执行复制操作
465        if source_meta.is_dir() {
466            // 目录复制:使用 copy_dir_all
467            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            // 文件复制
474            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
485/// 递归复制目录
486///
487/// 使用 Box::pin 处理递归异步函数
488fn 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
516/// Create Directory Tool
517///
518/// 创建目录(包括父目录)。
519pub 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
561/// Delete File Tool
562///
563/// 删除文件或目录。
564pub 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        // 验证路径存在
612        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            // 目录删除
618            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                // 非递归删除,要求目录为空
625                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            // 文件删除
634            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        // 创建源文件
678        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        // 验证目标文件内容是源文件内容
755        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        // 创建源文件
777        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()); // 复制后源文件仍然存在
790        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        // 创建源目录和文件
843        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()); // 源目录仍然存在
864    }
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        // 创建文件
883        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        // 创建空目录
917        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        // 创建非空目录
938        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        // 创建非空目录
962        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    // ========== ReadFileTool 分页测试 ==========
984
985    #[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}