Files
owlen/crates/tools/fs/tests/fs_tools.rs
vikingowl 6108b9e3d1 feat(tools): implement Edit and Write tools with deterministic patches (M3 complete)
This commit implements the complete M3 milestone (Edit & Write tools) including:

Write tool:
- Creates new files with parent directory creation
- Overwrites existing files safely
- Simple and straightforward implementation

Edit tool:
- Exact string replacement with uniqueness enforcement
- Detects ambiguous matches (multiple occurrences) and fails safely
- Detects no-match scenarios and fails with clear error
- Automatic backup before modification
- Rollback on write failure (restores from backup)
- Supports multiline string replacements

CLI integration:
- Added `write` subcommand: `owlen write <path> <content>`
- Added `edit` subcommand: `owlen edit <path> <old_string> <new_string>`
- Permission checks for both Write and Edit tools
- Clear error messages for permission denials

Permission enforcement:
- Plan mode (default): blocks Write and Edit (asks for approval)
- AcceptEdits mode: allows Write and Edit
- Code mode: allows all operations

Testing:
- 6 new tests in tools-fs for Write/Edit functionality
- 5 new tests in CLI for permission enforcement with Edit/Write
- Tests verify plan mode blocks, acceptEdits allows, code mode allows all
- All 32 workspace tests passing

Dependencies:
- Added `similar` crate for future diff/patch enhancements

M3 milestone complete! 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 19:19:49 +01:00

104 lines
3.5 KiB
Rust

use tools_fs::{read_file, glob_list, grep, write_file, edit_file};
use std::fs;
use tempfile::tempdir;
#[test]
fn read_and_glob_respect_gitignore() {
let dir = tempdir().unwrap();
let root = dir.path();
fs::write(root.join("a.txt"), "hello").unwrap();
fs::create_dir(root.join("secret")).unwrap();
fs::write(root.join("secret/secret.txt"), "token=123").unwrap();
fs::write(root.join(".gitignore"), "secret/\n").unwrap();
let pattern = format!("{}/**/*", root.display());
let files = glob_list(&pattern).unwrap();
assert!(files.iter().any(|p| p.ends_with("a.txt")));
assert!(!files.iter().any(|p| p.contains("secret.txt")));
assert_eq!(read_file(root.join("a.txt").to_str().unwrap()).unwrap(), "hello");
}
#[test]
fn grep_finds_lines() {
let dir = tempdir().unwrap();
let root = dir.path();
fs::write(root.join("a.rs"), "fn main() { println!(\"hello\"); }").unwrap();
let hits = grep(root.to_str().unwrap(), "hello").unwrap();
assert!(hits.iter().any(|(_p, _ln, text)| text.contains("hello")));
}
#[test]
fn write_file_creates_new_file() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("new.txt");
write_file(file_path.to_str().unwrap(), "new content").unwrap();
assert_eq!(read_file(file_path.to_str().unwrap()).unwrap(), "new content");
}
#[test]
fn write_file_overwrites_existing() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("existing.txt");
fs::write(&file_path, "old content").unwrap();
write_file(file_path.to_str().unwrap(), "new content").unwrap();
assert_eq!(read_file(file_path.to_str().unwrap()).unwrap(), "new content");
}
#[test]
fn edit_file_replaces_exact_match() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original = "line 1\nline 2\nline 3\n";
fs::write(&file_path, original).unwrap();
edit_file(file_path.to_str().unwrap(), "line 2", "modified line 2").unwrap();
let result = read_file(file_path.to_str().unwrap()).unwrap();
assert_eq!(result, "line 1\nmodified line 2\nline 3\n");
}
#[test]
fn edit_file_replaces_multiline() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original = "line 1\nline 2\nline 3\nline 4\n";
fs::write(&file_path, original).unwrap();
edit_file(file_path.to_str().unwrap(), "line 2\nline 3", "new content").unwrap();
let result = read_file(file_path.to_str().unwrap()).unwrap();
assert_eq!(result, "line 1\nnew content\nline 4\n");
}
#[test]
fn edit_file_fails_on_ambiguous_match() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original = "duplicate\nsome text\nduplicate\n";
fs::write(&file_path, original).unwrap();
let result = edit_file(file_path.to_str().unwrap(), "duplicate", "changed");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Ambiguous") || err_msg.contains("multiple") || err_msg.contains("occurrences"));
}
#[test]
fn edit_file_fails_on_no_match() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let original = "line 1\nline 2\n";
fs::write(&file_path, original).unwrap();
let result = edit_file(file_path.to_str().unwrap(), "nonexistent", "changed");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not found") || err_msg.contains("String to replace"));
}