use super::*;
#[test]
fn write_still_creates_files() {
let (temp, runtime) = runtime();
let write = runtime.dispatch(
"write",
json!({"path":"dir/file.txt","content":"alpha beta"}),
);
assert!(write.success, "{}", write.content);
assert_eq!(
fs::read_to_string(temp.path().join("dir/file.txt")).unwrap(),
"alpha beta"
);
}
#[cfg(unix)]
#[test]
fn write_rejects_final_symlink_and_symlink_parent_escape() {
use std::os::unix::fs::symlink;
let (temp, runtime) = runtime();
let outside = TempDir::new().unwrap();
fs::write(outside.path().join("target.txt"), "outside").unwrap();
fs::write(temp.path().join("inside.txt"), "inside").unwrap();
symlink(
temp.path().join("inside.txt"),
temp.path().join("inside-link.txt"),
)
.unwrap();
symlink(
outside.path().join("target.txt"),
temp.path().join("link.txt"),
)
.unwrap();
let inside_final_symlink = runtime.dispatch(
"write",
json!({"path":"inside-link.txt","content":"changed"}),
);
assert!(!inside_final_symlink.success);
assert_eq!(
fs::read_to_string(temp.path().join("inside.txt")).unwrap(),
"inside"
);
let final_symlink = runtime.dispatch("write", json!({"path":"link.txt","content":"escaped"}));
assert!(!final_symlink.success);
assert_eq!(
fs::read_to_string(outside.path().join("target.txt")).unwrap(),
"outside"
);
symlink(outside.path(), temp.path().join("outside-dir")).unwrap();
let parent_symlink = runtime.dispatch(
"write",
json!({"path":"outside-dir/new.txt","content":"escaped"}),
);
assert!(!parent_symlink.success);
assert!(!outside.path().join("new.txt").exists());
}
#[test]
fn write_revalidates_created_parent_inside_cwd() {
let (temp, runtime) = runtime();
let result = runtime.dispatch(
"write",
json!({"path":"new/nested/file.txt","content":"allowed"}),
);
assert!(result.success, "{}", result.content);
assert_eq!(
fs::canonicalize(temp.path().join("new/nested")).unwrap(),
temp.path().join("new/nested").canonicalize().unwrap()
);
assert_eq!(
fs::read_to_string(temp.path().join("new/nested/file.txt")).unwrap(),
"allowed"
);
}
#[test]
fn mutation_lock_poisoning_returns_tool_errors() {
let (temp, runtime) = runtime();
let registry = Arc::clone(&runtime.mutation_locks);
let _ = std::thread::spawn(move || {
let _guard = registry.lock().unwrap();
panic!("poison mutation lock registry");
})
.join();
let registry_error = runtime.dispatch("write", json!({"path":"a.txt","content":"x"}));
assert!(!registry_error.success);
assert!(
registry_error
.content
.contains("mutation lock registry mutex poisoned"),
"{}",
registry_error.content
);
let file_runtime = ToolRuntime::new(temp.path()).unwrap();
let target = file_runtime.resolve_for_write("b.txt").unwrap();
let lock = file_runtime.file_lock(&target).unwrap();
let poisoned = Arc::clone(&lock);
let _ = std::thread::spawn(move || {
let _guard = poisoned.lock().unwrap();
panic!("poison file mutation lock");
})
.join();
let file_error = file_runtime.dispatch("write", json!({"path":"b.txt","content":"x"}));
assert!(!file_error.success);
assert!(
file_error.content.contains("file mutation lock poisoned"),
"{}",
file_error.content
);
}
#[test]
fn write_enforces_content_bounds() {
let (_temp, runtime) = runtime();
let too_large_write = runtime.dispatch(
"write",
json!({"path":"too-large.txt", "content":"x".repeat(FILE_WRITE_MAX_BYTES + 1)}),
);
assert!(!too_large_write.success);
assert!(
too_large_write.content.contains("write content"),
"{}",
too_large_write.content
);
}
#[test]
fn cloned_runtimes_serialize_mutations_to_same_path() {
let (temp, runtime) = runtime();
let left = runtime.clone();
let right = runtime.clone();
let a = std::thread::spawn(move || {
left.dispatch("write", json!({"path":"same.txt","content":"a"}))
});
let b = std::thread::spawn(move || {
right.dispatch("write", json!({"path":"same.txt","content":"b"}))
});
assert!(a.join().unwrap().success);
assert!(b.join().unwrap().success);
let final_text = fs::read_to_string(temp.path().join("same.txt")).unwrap();
assert!(final_text == "a" || final_text == "b");
}