mod common;
use common::call_tool_raw;
#[tokio::test]
async fn edit_overwrite_working_dir_writes_inside_working_dir() {
let cwd = std::env::current_dir().expect("should get cwd");
let temp_dir = tempfile::TempDir::new_in(&cwd).expect("should create temp dir in cwd");
let working_dir = temp_dir
.path()
.to_str()
.expect("temp dir path is valid UTF-8");
let file_name = "output.txt";
let expected_path = temp_dir.path().join(file_name);
let resp = call_tool_raw(
"edit_overwrite",
serde_json::json!({
"path": file_name,
"content": "hello from working_dir",
"working_dir": working_dir
}),
)
.await;
assert!(
!resp["result"]["isError"].as_bool().unwrap_or(false),
"expected success but got error: {resp}"
);
assert!(
expected_path.exists(),
"file should exist inside working_dir at {:?}",
expected_path
);
let written = std::fs::read_to_string(&expected_path).expect("should read written file");
assert_eq!(written, "hello from working_dir");
}
#[tokio::test]
async fn edit_overwrite_new_file_no_working_dir() {
let cwd = std::env::current_dir().expect("should get cwd");
let temp_dir = tempfile::TempDir::new_in(&cwd).expect("should create temp dir in cwd");
let file_name = "new_output.txt";
let expected_path = temp_dir.path().join(file_name);
let temp_stem = temp_dir
.path()
.file_name()
.expect("temp dir has file name")
.to_str()
.expect("temp dir name is valid UTF-8");
let relative_path = format!("{temp_stem}/{file_name}");
let resp = call_tool_raw(
"edit_overwrite",
serde_json::json!({
"path": relative_path,
"content": "hello no working_dir"
}),
)
.await;
assert!(
!resp["result"]["isError"].as_bool().unwrap_or(false),
"expected success but got error: {resp}"
);
assert!(
expected_path.exists(),
"file should exist at {:?}",
expected_path
);
let written = std::fs::read_to_string(&expected_path).expect("should read written file");
assert_eq!(written, "hello no working_dir");
}
#[tokio::test]
async fn edit_replace_working_dir_modifies_inside_working_dir() {
let cwd = std::env::current_dir().expect("should get cwd");
let temp_dir = tempfile::TempDir::new_in(&cwd).expect("should create temp dir in cwd");
let working_dir = temp_dir
.path()
.to_str()
.expect("temp dir path is valid UTF-8");
let file_name = "source.txt";
let file_path = temp_dir.path().join(file_name);
std::fs::write(&file_path, "old text here").expect("should write initial file");
let resp = call_tool_raw(
"edit_replace",
serde_json::json!({
"path": file_name,
"old_text": "old text here",
"new_text": "new text here",
"working_dir": working_dir
}),
)
.await;
assert!(
!resp["result"]["isError"].as_bool().unwrap_or(false),
"expected success but got error: {resp}"
);
let updated = std::fs::read_to_string(&file_path).expect("should read updated file");
assert_eq!(updated, "new text here");
}
#[cfg(unix)]
#[tokio::test]
async fn edit_overwrite_io_error_no_path_leak() {
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
let cwd = std::env::current_dir().expect("should get cwd");
let temp_dir = tempfile::TempDir::new_in(&cwd).expect("should create temp dir in cwd");
let file_name = "readonly.txt";
let file_path = temp_dir.path().join(file_name);
std::fs::write(&file_path, "original content").expect("should write file");
let working_dir = temp_dir
.path()
.to_str()
.expect("temp dir path is valid UTF-8");
std::fs::set_permissions(temp_dir.path(), Permissions::from_mode(0o555))
.expect("should set directory permissions");
let probe_path = temp_dir.path().join("probe");
if std::fs::write(&probe_path, "probe").is_ok() {
std::fs::set_permissions(temp_dir.path(), Permissions::from_mode(0o755)).ok();
return;
}
let resp = call_tool_raw(
"edit_overwrite",
serde_json::json!({
"path": file_name,
"content": "new content",
"working_dir": working_dir
}),
)
.await;
std::fs::set_permissions(temp_dir.path(), Permissions::from_mode(0o755)).ok();
assert!(
resp["result"]["isError"].as_bool().unwrap_or(false),
"expected error but got success: {resp}"
);
let msg = resp["result"]["content"][0]["text"]
.as_str()
.expect("should have error text");
assert!(
!msg.contains(file_name),
"error message must not contain file name: {msg}"
);
assert!(
!msg.contains(working_dir),
"error message must not contain working_dir path: {msg}"
);
}
#[tokio::test]
async fn edit_overwrite_invalid_working_dir_no_path_leak() {
let bad_wd = "/nonexistent-working-dir-for-edit-overwrite-test";
let resp = call_tool_raw(
"edit_overwrite",
serde_json::json!({
"path": "test.txt",
"content": "hello",
"working_dir": bad_wd
}),
)
.await;
assert!(
resp["result"]["isError"].as_bool().unwrap_or(false),
"expected error but got success: {resp}"
);
let msg = resp["result"]["content"][0]["text"]
.as_str()
.expect("should have error text");
assert!(
!msg.contains(bad_wd),
"error message must not contain working_dir path: {msg}"
);
}
#[tokio::test]
async fn edit_replace_invalid_working_dir_no_path_leak() {
let bad_wd = "/nonexistent-working-dir-for-edit-replace-test";
let resp = call_tool_raw(
"edit_replace",
serde_json::json!({
"path": "test.txt",
"old_text": "old",
"new_text": "new",
"working_dir": bad_wd
}),
)
.await;
assert!(
resp["result"]["isError"].as_bool().unwrap_or(false),
"expected error but got success: {resp}"
);
let msg = resp["result"]["content"][0]["text"]
.as_str()
.expect("should have error text");
assert!(
!msg.contains(bad_wd),
"error message must not contain working_dir path: {msg}"
);
}
#[tokio::test]
async fn test_edit_replace_empty_new_text_deletes_block() {
let cwd = std::env::current_dir().expect("should get cwd");
let temp_dir = tempfile::TempDir::new_in(&cwd).expect("should create temp dir in cwd");
let working_dir = temp_dir
.path()
.to_str()
.expect("temp dir path is valid UTF-8");
let file_name = "delete_block.txt";
let file_path = temp_dir.path().join(file_name);
std::fs::write(&file_path, "line one\nDELETE ME\nline three\n").expect("should write file");
let resp = call_tool_raw(
"edit_replace",
serde_json::json!({
"path": file_name,
"old_text": "DELETE ME\n",
"new_text": "",
"working_dir": working_dir
}),
)
.await;
assert!(
!resp["result"]["isError"].as_bool().unwrap_or(false),
"expected success, got: {resp}"
);
let content = std::fs::read_to_string(&file_path).expect("should read updated file");
assert_eq!(content, "line one\nline three\n");
}
#[tokio::test]
async fn test_edit_replace_empty_new_text_entire_file() {
let cwd = std::env::current_dir().expect("should get cwd");
let temp_dir = tempfile::TempDir::new_in(&cwd).expect("should create temp dir in cwd");
let working_dir = temp_dir
.path()
.to_str()
.expect("temp dir path is valid UTF-8");
let file_name = "whole_file.txt";
let file_path = temp_dir.path().join(file_name);
std::fs::write(&file_path, "entire content").expect("should write file");
let resp = call_tool_raw(
"edit_replace",
serde_json::json!({
"path": file_name,
"old_text": "entire content",
"new_text": "",
"working_dir": working_dir
}),
)
.await;
assert!(
!resp["result"]["isError"].as_bool().unwrap_or(false),
"expected success, got: {resp}"
);
let content = std::fs::read_to_string(&file_path).expect("should read updated file");
assert_eq!(
content, "",
"file should be empty after full-content deletion"
);
}
#[tokio::test]
async fn test_edit_overwrite_new_file_missing_parent_dir() {
let cwd = std::env::current_dir().expect("should get cwd");
let temp_dir = tempfile::TempDir::new_in(&cwd).expect("should create temp dir in cwd");
let working_dir = temp_dir
.path()
.to_str()
.expect("temp dir path is valid UTF-8");
let file_name = "nonexistent/new_file.txt";
let resp = call_tool_raw(
"edit_overwrite",
serde_json::json!({
"path": file_name,
"content": "should not be written",
"working_dir": working_dir
}),
)
.await;
assert!(
resp["result"]["isError"].as_bool().unwrap_or(false),
"expected error but got success: {resp}"
);
}
#[tokio::test]
async fn test_edit_overwrite_new_file_traversal_path() {
let cwd = std::env::current_dir().expect("should get cwd");
let temp_dir = tempfile::TempDir::new_in(&cwd).expect("should create temp dir in cwd");
let working_dir = temp_dir
.path()
.to_str()
.expect("temp dir path is valid UTF-8");
let file_name = "../escaped_file.txt";
let resp = call_tool_raw(
"edit_overwrite",
serde_json::json!({
"path": file_name,
"content": "should not be written",
"working_dir": working_dir
}),
)
.await;
assert!(
resp["result"]["isError"].as_bool().unwrap_or(false),
"expected error but got success: {resp}"
);
}
#[tokio::test]
async fn test_edit_overwrite_new_file_deeply_nested() {
let cwd = std::env::current_dir().expect("should get cwd");
let temp_dir = tempfile::TempDir::new_in(&cwd).expect("should create temp dir in cwd");
let working_dir = temp_dir
.path()
.to_str()
.expect("temp dir path is valid UTF-8");
std::fs::create_dir(temp_dir.path().join("a")).expect("should create dir a");
let file_name = "a/b/c/new.txt";
let resp = call_tool_raw(
"edit_overwrite",
serde_json::json!({
"path": file_name,
"content": "should not be written",
"working_dir": working_dir
}),
)
.await;
assert!(
resp["result"]["isError"].as_bool().unwrap_or(false),
"expected error but got success: {resp}"
);
}
#[tokio::test]
async fn test_edit_overwrite_new_file_symlink_parent() {
let cwd = std::env::current_dir().expect("should get cwd");
let temp_dir = tempfile::TempDir::new_in(&cwd).expect("should create temp dir in cwd");
let working_dir = temp_dir.path();
let real_sub = working_dir.join("real_dir");
std::fs::create_dir(&real_sub).expect("should create real_dir");
let symlink_path = working_dir.join("link_to_real");
std::os::unix::fs::symlink(&real_sub, &symlink_path)
.expect("should create symlink to real_dir");
let working_dir_str = working_dir.to_str().expect("temp dir path is valid UTF-8");
let file_name = "link_to_real/through_symlink.txt";
let resp = call_tool_raw(
"edit_overwrite",
serde_json::json!({
"path": file_name,
"content": "written via symlink parent",
"working_dir": working_dir_str
}),
)
.await;
assert!(
!resp["result"]["isError"].as_bool().unwrap_or(false),
"expected success but got error: {resp}"
);
let expected_path = real_sub.join("through_symlink.txt");
assert!(
expected_path.exists(),
"file should exist inside real_dir at {:?}",
expected_path
);
let written =
std::fs::read_to_string(&expected_path).expect("should read written file via symlink");
assert_eq!(written, "written via symlink parent");
}
#[tokio::test]
async fn test_edit_overwrite_parent_is_file() {
let cwd = std::env::current_dir().expect("should get cwd");
let temp_dir = tempfile::TempDir::new_in(&cwd).expect("should create temp dir in cwd");
let working_dir = temp_dir
.path()
.to_str()
.expect("temp dir path is valid UTF-8");
let file_path = temp_dir.path().join("not_a_dir.txt");
std::fs::write(&file_path, "i am a file, not a directory").expect("should write test file");
let file_name = "not_a_dir.txt/child.txt";
let resp = call_tool_raw(
"edit_overwrite",
serde_json::json!({
"path": file_name,
"content": "should not be written",
"working_dir": working_dir
}),
)
.await;
assert!(
resp["result"]["isError"].as_bool().unwrap_or(false),
"expected error but got success: {resp}"
);
}