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>
151 lines
4.4 KiB
Rust
151 lines
4.4 KiB
Rust
use assert_cmd::Command;
|
|
use std::fs;
|
|
use tempfile::tempdir;
|
|
|
|
#[test]
|
|
fn plan_mode_allows_read_operations() {
|
|
// Create a temp file to read
|
|
let dir = tempdir().unwrap();
|
|
let file = dir.path().join("test.txt");
|
|
fs::write(&file, "hello world").unwrap();
|
|
|
|
// Read operation should work in plan mode (default)
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
|
|
cmd.arg("read").arg(file.to_str().unwrap());
|
|
cmd.assert().success().stdout("hello world\n");
|
|
}
|
|
|
|
#[test]
|
|
fn plan_mode_allows_glob_operations() {
|
|
let dir = tempdir().unwrap();
|
|
fs::write(dir.path().join("a.txt"), "test").unwrap();
|
|
fs::write(dir.path().join("b.txt"), "test").unwrap();
|
|
|
|
let pattern = format!("{}/*.txt", dir.path().display());
|
|
|
|
// Glob operation should work in plan mode (default)
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
|
|
cmd.arg("glob").arg(&pattern);
|
|
cmd.assert().success();
|
|
}
|
|
|
|
#[test]
|
|
fn plan_mode_allows_grep_operations() {
|
|
let dir = tempdir().unwrap();
|
|
fs::write(dir.path().join("test.txt"), "hello world\nfoo bar").unwrap();
|
|
|
|
// Grep operation should work in plan mode (default)
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
|
|
cmd.arg("grep").arg(dir.path().to_str().unwrap()).arg("hello");
|
|
cmd.assert().success();
|
|
}
|
|
|
|
#[test]
|
|
fn mode_override_via_cli_flag() {
|
|
let dir = tempdir().unwrap();
|
|
let file = dir.path().join("test.txt");
|
|
fs::write(&file, "content").unwrap();
|
|
|
|
// Test with --mode code (should also allow read)
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
|
|
cmd.arg("--mode")
|
|
.arg("code")
|
|
.arg("read")
|
|
.arg(file.to_str().unwrap());
|
|
cmd.assert().success().stdout("content\n");
|
|
}
|
|
|
|
#[test]
|
|
fn plan_mode_blocks_write_operations() {
|
|
let dir = tempdir().unwrap();
|
|
let file = dir.path().join("new.txt");
|
|
|
|
// Write operation should be blocked in plan mode (default)
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
|
|
cmd.arg("write").arg(file.to_str().unwrap()).arg("content");
|
|
cmd.assert().failure();
|
|
}
|
|
|
|
#[test]
|
|
fn plan_mode_blocks_edit_operations() {
|
|
let dir = tempdir().unwrap();
|
|
let file = dir.path().join("test.txt");
|
|
fs::write(&file, "old content").unwrap();
|
|
|
|
// Edit operation should be blocked in plan mode (default)
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
|
|
cmd.arg("edit")
|
|
.arg(file.to_str().unwrap())
|
|
.arg("old")
|
|
.arg("new");
|
|
cmd.assert().failure();
|
|
}
|
|
|
|
#[test]
|
|
fn accept_edits_mode_allows_write() {
|
|
let dir = tempdir().unwrap();
|
|
let file = dir.path().join("new.txt");
|
|
|
|
// Write operation should work in acceptEdits mode
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
|
|
cmd.arg("--mode")
|
|
.arg("acceptEdits")
|
|
.arg("write")
|
|
.arg(file.to_str().unwrap())
|
|
.arg("new content");
|
|
cmd.assert().success();
|
|
|
|
// Verify file was written
|
|
assert_eq!(fs::read_to_string(&file).unwrap(), "new content");
|
|
}
|
|
|
|
#[test]
|
|
fn accept_edits_mode_allows_edit() {
|
|
let dir = tempdir().unwrap();
|
|
let file = dir.path().join("test.txt");
|
|
fs::write(&file, "line 1\nline 2\nline 3").unwrap();
|
|
|
|
// Edit operation should work in acceptEdits mode
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
|
|
cmd.arg("--mode")
|
|
.arg("acceptEdits")
|
|
.arg("edit")
|
|
.arg(file.to_str().unwrap())
|
|
.arg("line 2")
|
|
.arg("modified line");
|
|
cmd.assert().success();
|
|
|
|
// Verify file was edited
|
|
assert_eq!(
|
|
fs::read_to_string(&file).unwrap(),
|
|
"line 1\nmodified line\nline 3"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_allows_all_operations() {
|
|
let dir = tempdir().unwrap();
|
|
let file = dir.path().join("test.txt");
|
|
|
|
// Write in code mode
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
|
|
cmd.arg("--mode")
|
|
.arg("code")
|
|
.arg("write")
|
|
.arg(file.to_str().unwrap())
|
|
.arg("initial content");
|
|
cmd.assert().success();
|
|
|
|
// Edit in code mode
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("owlen"));
|
|
cmd.arg("--mode")
|
|
.arg("code")
|
|
.arg("edit")
|
|
.arg(file.to_str().unwrap())
|
|
.arg("initial")
|
|
.arg("modified");
|
|
cmd.assert().success();
|
|
|
|
assert_eq!(fs::read_to_string(&file).unwrap(), "modified content");
|
|
}
|