nika_engine/runtime/builtin/
file_adapter.rs1use super::BuiltinTool;
7use crate::error::NikaError;
8use crate::tools::{EditTool, FileTool, GlobTool, GrepTool, ReadTool, ToolContext, WriteTool};
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12
13pub struct FileToolAdapter<T: FileTool + Send + Sync + 'static> {
18 tool: Arc<T>,
19}
20
21impl<T: FileTool + Send + Sync + 'static> FileToolAdapter<T> {
22 pub fn new(tool: T) -> Self {
24 Self {
25 tool: Arc::new(tool),
26 }
27 }
28
29 pub fn from_arc(tool: Arc<T>) -> Self {
31 Self { tool }
32 }
33}
34
35impl<T: FileTool + Send + Sync + 'static> BuiltinTool for FileToolAdapter<T> {
36 fn name(&self) -> &'static str {
37 self.tool.name()
38 }
39
40 fn description(&self) -> &'static str {
41 self.tool.description()
42 }
43
44 fn parameters_schema(&self) -> serde_json::Value {
45 self.tool.parameters_schema()
46 }
47
48 fn call<'a>(
49 &'a self,
50 args: String,
51 ) -> Pin<Box<dyn Future<Output = Result<String, NikaError>> + Send + 'a>> {
52 let tool = Arc::clone(&self.tool);
53
54 Box::pin(async move {
55 let params: serde_json::Value =
57 serde_json::from_str(&args).map_err(|e| NikaError::BuiltinToolError {
58 tool: tool.name().into(),
59 reason: format!("Invalid JSON parameters: {e}"),
60 })?;
61
62 let output = tool.call(params).await?;
64
65 if output.is_error {
67 Err(NikaError::BuiltinToolError {
68 tool: tool.name().into(),
69 reason: output.content,
70 })
71 } else {
72 if let Some(data) = output.data {
74 serde_json::to_string(&serde_json::json!({
75 "content": output.content,
76 "data": data
77 }))
78 .map_err(|e| NikaError::BuiltinToolError {
79 tool: tool.name().into(),
80 reason: format!("Failed to serialize result: {e}"),
81 })
82 } else {
83 Ok(output.content)
84 }
85 }
86 })
87 }
88}
89
90pub fn create_file_tool_adapters(ctx: Arc<ToolContext>) -> Vec<Box<dyn BuiltinTool>> {
98 vec![
99 Box::new(FileToolAdapter::new(ReadTool::new(Arc::clone(&ctx)))),
100 Box::new(FileToolAdapter::new(WriteTool::new(Arc::clone(&ctx)))),
101 Box::new(FileToolAdapter::new(EditTool::new(Arc::clone(&ctx)))),
102 Box::new(FileToolAdapter::new(GlobTool::new(Arc::clone(&ctx)))),
103 Box::new(FileToolAdapter::new(GrepTool::new(ctx))),
104 ]
105}
106
107#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::tools::PermissionMode;
115 use tempfile::TempDir;
116
117 async fn setup_test() -> (TempDir, Arc<ToolContext>) {
118 let temp_dir = TempDir::new().unwrap();
119 let ctx = Arc::new(ToolContext::new(
120 temp_dir.path().to_path_buf(),
121 PermissionMode::YoloMode,
122 ));
123 (temp_dir, ctx)
124 }
125
126 #[tokio::test]
127 async fn test_read_adapter_name() {
128 let (_temp, ctx) = setup_test().await;
129 let adapter = FileToolAdapter::new(ReadTool::new(ctx));
130 assert_eq!(adapter.name(), "read");
131 }
132
133 #[tokio::test]
134 async fn test_write_adapter_name() {
135 let (_temp, ctx) = setup_test().await;
136 let adapter = FileToolAdapter::new(WriteTool::new(ctx));
137 assert_eq!(adapter.name(), "write");
138 }
139
140 #[tokio::test]
141 async fn test_edit_adapter_name() {
142 let (_temp, ctx) = setup_test().await;
143 let adapter = FileToolAdapter::new(EditTool::new(ctx));
144 assert_eq!(adapter.name(), "edit");
145 }
146
147 #[tokio::test]
148 async fn test_glob_adapter_name() {
149 let (_temp, ctx) = setup_test().await;
150 let adapter = FileToolAdapter::new(GlobTool::new(ctx));
151 assert_eq!(adapter.name(), "glob");
152 }
153
154 #[tokio::test]
155 async fn test_grep_adapter_name() {
156 let (_temp, ctx) = setup_test().await;
157 let adapter = FileToolAdapter::new(GrepTool::new(ctx));
158 assert_eq!(adapter.name(), "grep");
159 }
160
161 #[tokio::test]
162 async fn test_create_file_tool_adapters() {
163 let (_temp, ctx) = setup_test().await;
164 let adapters = create_file_tool_adapters(ctx);
165
166 assert_eq!(adapters.len(), 5);
167
168 let names: Vec<&str> = adapters.iter().map(|a| a.name()).collect();
169 assert!(names.contains(&"read"));
170 assert!(names.contains(&"write"));
171 assert!(names.contains(&"edit"));
172 assert!(names.contains(&"glob"));
173 assert!(names.contains(&"grep"));
174 }
175
176 #[tokio::test]
177 async fn test_write_then_read() {
178 let (temp_dir, ctx) = setup_test().await;
179 let file_path = temp_dir.path().join("test.txt");
180
181 let write_adapter = FileToolAdapter::new(WriteTool::new(Arc::clone(&ctx)));
183 let write_args = serde_json::json!({
184 "file_path": file_path.to_string_lossy(),
185 "content": "Hello, Nika!"
186 })
187 .to_string();
188
189 let result = write_adapter.call(write_args).await;
190 assert!(result.is_ok(), "Write failed: {:?}", result);
191
192 let read_adapter = FileToolAdapter::new(ReadTool::new(ctx));
194 let read_args = serde_json::json!({
195 "file_path": file_path.to_string_lossy()
196 })
197 .to_string();
198
199 let result = read_adapter.call(read_args).await;
200 assert!(result.is_ok());
201 let content = result.unwrap();
202 assert!(content.contains("Hello, Nika!"));
203 }
204
205 #[tokio::test]
206 async fn test_edit_file() {
207 let (temp_dir, ctx) = setup_test().await;
208 let file_path = temp_dir.path().join("edit-test.txt");
209
210 std::fs::write(&file_path, "Hello World").unwrap();
212
213 let read_adapter = FileToolAdapter::new(ReadTool::new(Arc::clone(&ctx)));
215 let read_args = serde_json::json!({
216 "file_path": file_path.to_string_lossy()
217 })
218 .to_string();
219 read_adapter.call(read_args).await.unwrap();
220
221 let edit_adapter = FileToolAdapter::new(EditTool::new(ctx));
223 let edit_args = serde_json::json!({
224 "file_path": file_path.to_string_lossy(),
225 "old_string": "World",
226 "new_string": "Nika"
227 })
228 .to_string();
229
230 let result = edit_adapter.call(edit_args).await;
231 assert!(result.is_ok(), "Edit failed: {:?}", result);
232
233 let content = std::fs::read_to_string(&file_path).unwrap();
235 assert_eq!(content, "Hello Nika");
236 }
237
238 #[tokio::test]
239 async fn test_glob_find_files() {
240 let (temp_dir, ctx) = setup_test().await;
241
242 std::fs::write(temp_dir.path().join("file1.txt"), "content1").unwrap();
244 std::fs::write(temp_dir.path().join("file2.txt"), "content2").unwrap();
245 std::fs::write(temp_dir.path().join("other.md"), "markdown").unwrap();
246
247 let glob_adapter = FileToolAdapter::new(GlobTool::new(ctx));
248 let glob_args = serde_json::json!({
249 "pattern": "*.txt",
250 "path": temp_dir.path().to_string_lossy()
251 })
252 .to_string();
253
254 let result = glob_adapter.call(glob_args).await;
255 assert!(result.is_ok());
256 let output = result.unwrap();
257 assert!(output.contains("file1.txt"));
258 assert!(output.contains("file2.txt"));
259 assert!(!output.contains("other.md"));
260 }
261
262 #[tokio::test]
263 async fn test_grep_search_content() {
264 let (temp_dir, ctx) = setup_test().await;
265
266 std::fs::write(
268 temp_dir.path().join("search.txt"),
269 "Line 1: Hello\nLine 2: World\nLine 3: Hello World",
270 )
271 .unwrap();
272
273 let grep_adapter = FileToolAdapter::new(GrepTool::new(ctx));
274 let grep_args = serde_json::json!({
275 "pattern": "Hello",
276 "path": temp_dir.path().to_string_lossy()
277 })
278 .to_string();
279
280 let result = grep_adapter.call(grep_args).await;
281 assert!(result.is_ok());
282 let output = result.unwrap();
283 assert!(output.contains("search.txt"));
285 }
286
287 #[tokio::test]
288 async fn test_invalid_json_params() {
289 let (_temp, ctx) = setup_test().await;
290 let adapter = FileToolAdapter::new(ReadTool::new(ctx));
291
292 let result = adapter.call("not valid json".to_string()).await;
293 assert!(result.is_err());
294
295 let err = result.unwrap_err();
296 assert!(err.to_string().contains("Invalid JSON parameters"));
297 }
298
299 #[tokio::test]
300 async fn test_path_outside_boundary() {
301 let (_temp, ctx) = setup_test().await;
302 let adapter = FileToolAdapter::new(ReadTool::new(ctx));
303
304 let args = serde_json::json!({
306 "file_path": "/etc/passwd"
307 })
308 .to_string();
309
310 let result = adapter.call(args).await;
311 assert!(result.is_err());
312 }
313
314 #[tokio::test]
319 async fn test_read_nonexistent_file() {
320 let (temp_dir, ctx) = setup_test().await;
321 let adapter = FileToolAdapter::new(ReadTool::new(ctx));
322
323 let args = serde_json::json!({
324 "file_path": temp_dir.path().join("does_not_exist.txt").to_string_lossy()
325 })
326 .to_string();
327
328 let result = adapter.call(args).await;
329 assert!(result.is_err());
330 let err = result.unwrap_err();
331 assert!(err.to_string().contains("read") || err.to_string().contains("File"));
332 }
333
334 #[tokio::test]
335 async fn test_edit_nonexistent_file() {
336 let (temp_dir, ctx) = setup_test().await;
337 let adapter = FileToolAdapter::new(EditTool::new(ctx));
338
339 let args = serde_json::json!({
340 "file_path": temp_dir.path().join("nonexistent.txt").to_string_lossy(),
341 "old_string": "foo",
342 "new_string": "bar"
343 })
344 .to_string();
345
346 let result = adapter.call(args).await;
347 assert!(result.is_err());
349 }
350
351 #[tokio::test]
352 async fn test_edit_old_string_not_found() {
353 let (temp_dir, ctx) = setup_test().await;
354 let file_path = temp_dir.path().join("edit-miss.txt");
355
356 std::fs::write(&file_path, "Hello World").unwrap();
358
359 let read_adapter = FileToolAdapter::new(ReadTool::new(Arc::clone(&ctx)));
361 let read_args = serde_json::json!({
362 "file_path": file_path.to_string_lossy()
363 })
364 .to_string();
365 read_adapter.call(read_args).await.unwrap();
366
367 let edit_adapter = FileToolAdapter::new(EditTool::new(ctx));
369 let edit_args = serde_json::json!({
370 "file_path": file_path.to_string_lossy(),
371 "old_string": "NONEXISTENT_STRING",
372 "new_string": "replaced"
373 })
374 .to_string();
375
376 let result = edit_adapter.call(edit_args).await;
377 assert!(result.is_err());
379 let err = result.unwrap_err();
380 assert!(
381 err.to_string().contains("not found")
382 || err.to_string().contains("does not exist")
383 || err.to_string().contains("old_string")
384 );
385 }
386
387 #[tokio::test]
388 async fn test_write_missing_content_param() {
389 let (temp_dir, ctx) = setup_test().await;
390 let adapter = FileToolAdapter::new(WriteTool::new(ctx));
391
392 let args = serde_json::json!({
394 "file_path": temp_dir.path().join("test.txt").to_string_lossy()
395 })
396 .to_string();
397
398 let result = adapter.call(args).await;
399 assert!(result.is_err());
400 }
401
402 #[tokio::test]
403 async fn test_glob_no_matches() {
404 let (temp_dir, ctx) = setup_test().await;
405
406 std::fs::write(temp_dir.path().join("file.txt"), "content").unwrap();
408
409 let adapter = FileToolAdapter::new(GlobTool::new(ctx));
410 let args = serde_json::json!({
411 "pattern": "*.rs", "path": temp_dir.path().to_string_lossy()
413 })
414 .to_string();
415
416 let result = adapter.call(args).await;
417 assert!(result.is_ok());
419 let output = result.unwrap();
420 assert!(
422 output.is_empty()
423 || output.contains("[]")
424 || output.contains("No files")
425 || !output.contains(".rs")
426 );
427 }
428
429 #[tokio::test]
430 async fn test_grep_no_matches() {
431 let (temp_dir, ctx) = setup_test().await;
432
433 std::fs::write(temp_dir.path().join("search.txt"), "Hello World").unwrap();
435
436 let adapter = FileToolAdapter::new(GrepTool::new(ctx));
437 let args = serde_json::json!({
438 "pattern": "NONEXISTENT_PATTERN_12345",
439 "path": temp_dir.path().to_string_lossy()
440 })
441 .to_string();
442
443 let result = adapter.call(args).await;
444 assert!(result.is_ok());
446 let output = result.unwrap();
447 assert!(!output.contains("Hello"));
448 }
449
450 #[tokio::test]
451 async fn test_read_missing_file_path_param() {
452 let (_temp, ctx) = setup_test().await;
453 let adapter = FileToolAdapter::new(ReadTool::new(ctx));
454
455 let args = serde_json::json!({}).to_string();
457
458 let result = adapter.call(args).await;
459 assert!(result.is_err());
460 }
461}