use base64::Engine;
use phi_core::tools::edit::EditFileTool;
use phi_core::tools::list::ListFilesTool;
use phi_core::tools::*;
use phi_core::types::*;
use tokio_util::sync::CancellationToken;
fn ctx(name: &str) -> ToolContext {
ToolContext {
tool_call_id: "t1".into(),
tool_name: name.into(),
cancel: CancellationToken::new(),
on_update: None,
on_progress: None,
}
}
fn ctx_with_cancel(name: &str, cancel: CancellationToken) -> ToolContext {
ToolContext {
tool_call_id: "t1".into(),
tool_name: name.into(),
cancel,
on_update: None,
on_progress: None,
}
}
#[tokio::test]
async fn test_bash_echo() {
let tool = BashTool::new();
let result = tool
.execute(serde_json::json!({"command": "echo hello"}), ctx("bash"))
.await
.unwrap();
let text = match &result.content[0] {
Content::Text { text } => text,
_ => panic!("expected text"),
};
assert!(text.contains("hello"));
assert!(text.contains("Exit code: 0"));
}
#[tokio::test]
async fn test_bash_failure() {
let tool = BashTool::new();
let result = tool
.execute(serde_json::json!({"command": "false"}), ctx("bash"))
.await
.unwrap();
let text = match &result.content[0] {
Content::Text { text } => text,
_ => panic!("expected text"),
};
assert!(text.contains("Exit code: 1"));
}
#[tokio::test]
async fn test_bash_deny_pattern() {
let tool = BashTool::new();
let result = tool
.execute(serde_json::json!({"command": "rm -rf /"}), ctx("bash"))
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("blocked"));
}
#[tokio::test]
async fn test_bash_timeout() {
let tool = BashTool::new().with_timeout(std::time::Duration::from_millis(100));
let result = tool
.execute(serde_json::json!({"command": "sleep 10"}), ctx("bash"))
.await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("timed out"));
}
#[tokio::test]
async fn test_bash_cancel() {
let tool = BashTool::new();
let cancel = CancellationToken::new();
cancel.cancel();
let result = tool
.execute(
serde_json::json!({"command": "echo should not run"}),
ctx_with_cancel("bash", cancel),
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_read_write_file() {
let tmp = std::env::temp_dir().join("phi-core-test-rw.txt");
let path = tmp.to_str().unwrap();
let write_tool = WriteFileTool::new();
let result = write_tool
.execute(
serde_json::json!({"path": path, "content": "hello from phi-core"}),
ctx("write_file"),
)
.await
.unwrap();
let text = match &result.content[0] {
Content::Text { text } => text,
_ => panic!("expected text"),
};
assert!(text.contains("Wrote"));
let read_tool = ReadFileTool::new();
let result = read_tool
.execute(serde_json::json!({"path": path}), ctx("read_file"))
.await
.unwrap();
let text = match &result.content[0] {
Content::Text { text } => text,
_ => panic!("expected text"),
};
assert!(text.contains("hello from phi-core"));
let _ = std::fs::remove_file(tmp);
}
#[tokio::test]
async fn test_read_file_with_offset_limit() {
let tmp = std::env::temp_dir().join("phi-core-test-lines.txt");
let path = tmp.to_str().unwrap();
let content = (1..=20)
.map(|i| format!("line {}", i))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(&tmp, &content).unwrap();
let tool = ReadFileTool::new();
let result = tool
.execute(
serde_json::json!({"path": path, "offset": 5, "limit": 3}),
ctx("read_file"),
)
.await
.unwrap();
let text = match &result.content[0] {
Content::Text { text } => text,
_ => panic!("expected text"),
};
assert!(text.contains("line 5"));
assert!(text.contains("line 7"));
assert!(!text.contains("line 8"));
let _ = std::fs::remove_file(tmp);
}
#[tokio::test]
async fn test_read_file_not_found() {
let tool = ReadFileTool::new();
let result = tool
.execute(
serde_json::json!({"path": "/nonexistent/file.txt"}),
ctx("read_file"),
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_write_creates_directories() {
let tmp = std::env::temp_dir().join("phi-core-test-nested/deep/dir/file.txt");
let path = tmp.to_str().unwrap();
let tool = WriteFileTool::new();
let result = tool
.execute(
serde_json::json!({"path": path, "content": "nested!"}),
ctx("write_file"),
)
.await;
assert!(result.is_ok());
assert!(tmp.exists());
let _ = std::fs::remove_dir_all(std::env::temp_dir().join("phi-core-test-nested"));
}
#[tokio::test]
async fn test_search_pattern() {
let tmp_dir = std::env::temp_dir().join("phi-core-test-search");
let _ = std::fs::create_dir_all(&tmp_dir);
std::fs::write(tmp_dir.join("a.txt"), "hello world\nfoo bar\nhello again").unwrap();
std::fs::write(tmp_dir.join("b.txt"), "no match here\nhello there").unwrap();
let tool = SearchTool::new().with_root(tmp_dir.to_str().unwrap());
let result = tool
.execute(serde_json::json!({"pattern": "hello"}), ctx("search"))
.await
.unwrap();
let text = match &result.content[0] {
Content::Text { text } => text,
_ => panic!("expected text"),
};
assert!(text.contains("hello"));
assert!(text.contains("3 matches") || text.contains("matches"));
let _ = std::fs::remove_dir_all(tmp_dir);
}
#[tokio::test]
async fn test_search_no_matches() {
let tmp_dir = std::env::temp_dir().join("phi-core-test-search-empty");
let _ = std::fs::create_dir_all(&tmp_dir);
std::fs::write(tmp_dir.join("a.txt"), "nothing interesting").unwrap();
let tool = SearchTool::new().with_root(tmp_dir.to_str().unwrap());
let result = tool
.execute(
serde_json::json!({"pattern": "zzzznotfound"}),
ctx("search"),
)
.await
.unwrap();
let text = match &result.content[0] {
Content::Text { text } => text,
_ => panic!("expected text"),
};
assert!(text.contains("No matches"));
let _ = std::fs::remove_dir_all(tmp_dir);
}
#[tokio::test]
async fn test_edit_file() {
let tmp = std::env::temp_dir().join("phi-core-test-edit.txt");
let path = tmp.to_str().unwrap();
std::fs::write(&tmp, "fn main() {\n println!(\"hello\");\n}\n").unwrap();
let tool = EditFileTool::new();
let result = tool
.execute(
serde_json::json!({
"path": path,
"old_text": "println!(\"hello\")",
"new_text": "println!(\"goodbye\")"
}),
ctx("edit_file"),
)
.await
.unwrap();
let text = match &result.content[0] {
Content::Text { text } => text,
_ => panic!("expected text"),
};
assert!(text.contains("Replaced"));
let content = std::fs::read_to_string(&tmp).unwrap();
assert!(content.contains("goodbye"));
let _ = std::fs::remove_file(tmp);
}
#[tokio::test]
async fn test_edit_file_no_match() {
let tmp = std::env::temp_dir().join("phi-core-test-edit-nomatch.txt");
let path = tmp.to_str().unwrap();
std::fs::write(&tmp, "hello world\n").unwrap();
let tool = EditFileTool::new();
let result = tool
.execute(
serde_json::json!({"path": path, "old_text": "nonexistent", "new_text": "bar"}),
ctx("edit_file"),
)
.await;
assert!(result.is_err());
let _ = std::fs::remove_file(tmp);
}
#[tokio::test]
async fn test_list_files_tool() {
let tmp_dir = std::env::temp_dir().join("phi-core-test-list2");
let _ = std::fs::create_dir_all(tmp_dir.join("sub"));
std::fs::write(tmp_dir.join("a.rs"), "").unwrap();
std::fs::write(tmp_dir.join("sub/c.rs"), "").unwrap();
let tool = ListFilesTool::new();
let result = tool
.execute(
serde_json::json!({"path": tmp_dir.to_str().unwrap()}),
ctx("list_files"),
)
.await
.unwrap();
let text = match &result.content[0] {
Content::Text { text } => text,
_ => panic!("expected text"),
};
assert!(text.contains("a.rs"));
let _ = std::fs::remove_dir_all(tmp_dir);
}
#[tokio::test]
async fn test_read_file_line_numbers() {
let tmp = std::env::temp_dir().join("phi-core-test-lineno2.txt");
let path = tmp.to_str().unwrap();
std::fs::write(&tmp, "first\nsecond\nthird\n").unwrap();
let tool = ReadFileTool::new();
let result = tool
.execute(serde_json::json!({"path": path}), ctx("read_file"))
.await
.unwrap();
let text = match &result.content[0] {
Content::Text { text } => text,
_ => panic!("expected text"),
};
assert!(text.contains(" 1 | first"));
assert!(text.contains(" 2 | second"));
let _ = std::fs::remove_file(tmp);
}
#[tokio::test]
async fn test_bash_blocked_command() {
let tool = BashTool::new();
let result = tool
.execute(serde_json::json!({"command": "rm -rf /"}), ctx("bash"))
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("blocked"));
}
#[tokio::test]
async fn test_default_tools_complete() {
let tools = phi_core::tools::default_tools();
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert_eq!(names.len(), 6);
assert!(names.contains(&"bash"));
assert!(names.contains(&"edit_file"));
assert!(names.contains(&"list_files"));
}
#[tokio::test]
async fn test_read_image_file() {
let png_bytes: Vec<u8> = vec![
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE2, 0x21, 0xBC,
0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
];
let tmp = std::env::temp_dir().join("phi-core-test-image.png");
std::fs::write(&tmp, &png_bytes).unwrap();
let tool = ReadFileTool::new();
let result = tool
.execute(
serde_json::json!({"path": tmp.to_str().unwrap()}),
ctx("read_file"),
)
.await
.unwrap();
match &result.content[0] {
Content::Image { data, mime_type } => {
assert_eq!(mime_type, "image/png");
assert!(!data.is_empty());
let decoded = base64::engine::general_purpose::STANDARD
.decode(data)
.unwrap();
assert_eq!(decoded, png_bytes);
}
_ => panic!("expected Content::Image"),
}
let _ = std::fs::remove_file(tmp);
}
#[tokio::test]
async fn test_read_jpeg_file() {
let tmp = std::env::temp_dir().join("phi-core-test-image.jpg");
std::fs::write(&tmp, b"fake-jpeg-data").unwrap();
let tool = ReadFileTool::new();
let result = tool
.execute(
serde_json::json!({"path": tmp.to_str().unwrap()}),
ctx("read_file"),
)
.await
.unwrap();
match &result.content[0] {
Content::Image { mime_type, .. } => {
assert_eq!(mime_type, "image/jpeg");
}
_ => panic!("expected Content::Image for .jpg"),
}
let _ = std::fs::remove_file(tmp);
}
#[tokio::test]
async fn test_read_text_file_unchanged() {
let tmp = std::env::temp_dir().join("phi-core-test-notimage.txt");
std::fs::write(&tmp, "just text").unwrap();
let tool = ReadFileTool::new();
let result = tool
.execute(
serde_json::json!({"path": tmp.to_str().unwrap()}),
ctx("read_file"),
)
.await
.unwrap();
match &result.content[0] {
Content::Text { text } => {
assert!(text.contains("just text"));
}
_ => panic!("expected Content::Text for .txt file"),
}
let _ = std::fs::remove_file(tmp);
}