summaryrefslogtreecommitdiffhomepage
path: root/spec/dispatch/tool/files/edit_file_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/dispatch/tool/files/edit_file_spec.rb')
-rw-r--r--spec/dispatch/tool/files/edit_file_spec.rb142
1 files changed, 142 insertions, 0 deletions
diff --git a/spec/dispatch/tool/files/edit_file_spec.rb b/spec/dispatch/tool/files/edit_file_spec.rb
new file mode 100644
index 0000000..b0366dd
--- /dev/null
+++ b/spec/dispatch/tool/files/edit_file_spec.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+RSpec.describe "edit_file tool" do
+ let(:worktree_path) { Dir.mktmpdir("edit-file-test") }
+ let(:context) { { worktree_path: } }
+ let(:registry) { Dispatch::Tools::Registry.new }
+
+ before { Dispatch::Tool::Files.register(registry) }
+
+ after { FileUtils.remove_entry(worktree_path) }
+
+ subject(:tool) { registry.get("edit_file") }
+
+ describe "single edit" do
+ it "replaces old_text with new_text in the file" do
+ file_path = File.join(worktree_path, "code.rb")
+ File.write(file_path, "def hello\n puts 'hello'\nend\n")
+
+ result = tool.call({
+ "path" => "code.rb",
+ "edits" => [{ "old_text" => "puts 'hello'", "new_text" => "puts 'goodbye'" }]
+ }, context:)
+
+ expect(result.success?).to be true
+ expect(File.read(file_path)).to include("puts 'goodbye'")
+ expect(File.read(file_path)).not_to include("puts 'hello'")
+ end
+
+ it "returns a confirmation with the number of edits applied" do
+ file_path = File.join(worktree_path, "file.txt")
+ File.write(file_path, "foo bar baz")
+
+ result = tool.call({
+ "path" => "file.txt",
+ "edits" => [{ "old_text" => "bar", "new_text" => "qux" }]
+ }, context:)
+
+ expect(result.success?).to be true
+ expect(result.output).to match(/1/i)
+ end
+ end
+
+ describe "multiple edits" do
+ it "applies multiple edits sequentially" do
+ file_path = File.join(worktree_path, "multi.txt")
+ File.write(file_path, "aaa bbb ccc")
+
+ result = tool.call({
+ "path" => "multi.txt",
+ "edits" => [
+ { "old_text" => "aaa", "new_text" => "xxx" },
+ { "old_text" => "ccc", "new_text" => "zzz" }
+ ]
+ }, context:)
+
+ expect(result.success?).to be true
+ expect(File.read(file_path)).to eq("xxx bbb zzz")
+ end
+
+ it "applies edits sequentially so later edits see results of earlier ones" do
+ file_path = File.join(worktree_path, "sequential.txt")
+ File.write(file_path, "hello world")
+
+ result = tool.call({
+ "path" => "sequential.txt",
+ "edits" => [
+ { "old_text" => "hello", "new_text" => "hi" },
+ { "old_text" => "hi world", "new_text" => "hi there" }
+ ]
+ }, context:)
+
+ expect(result.success?).to be true
+ expect(File.read(file_path)).to eq("hi there")
+ end
+ end
+
+ describe "error cases" do
+ it "returns failure when old_text is not found in the file" do
+ file_path = File.join(worktree_path, "missing.txt")
+ File.write(file_path, "actual content")
+
+ result = tool.call({
+ "path" => "missing.txt",
+ "edits" => [{ "old_text" => "nonexistent text", "new_text" => "replacement" }]
+ }, context:)
+
+ expect(result.failure?).to be true
+ expect(result.error).to match(/not found/i)
+ end
+
+ it "returns failure when the file does not exist" do
+ result = tool.call({
+ "path" => "ghost.txt",
+ "edits" => [{ "old_text" => "a", "new_text" => "b" }]
+ }, context:)
+
+ expect(result.failure?).to be true
+ expect(result.error).to match(/not found|does not exist/i)
+ end
+
+ it "returns failure when the path escapes the sandbox" do
+ result = tool.call({
+ "path" => "../../../etc/passwd",
+ "edits" => [{ "old_text" => "root", "new_text" => "hacked" }]
+ }, context:)
+
+ expect(result.failure?).to be true
+ expect(result.error).to match(/sandbox|outside/i)
+ end
+
+ it "returns failure when old_text matches ambiguously (multiple occurrences)" do
+ file_path = File.join(worktree_path, "ambiguous.txt")
+ File.write(file_path, "foo bar foo baz foo")
+
+ result = tool.call({
+ "path" => "ambiguous.txt",
+ "edits" => [{ "old_text" => "foo", "new_text" => "qux" }]
+ }, context:)
+
+ expect(result.failure?).to be true
+ expect(result.error).to match(/ambiguous|multiple/i)
+ end
+ end
+
+ describe "parameter validation" do
+ it "requires the path parameter" do
+ result = tool.call({
+ "edits" => [{ "old_text" => "a", "new_text" => "b" }]
+ }, context:)
+
+ expect(result.failure?).to be true
+ end
+
+ it "requires the edits parameter" do
+ File.write(File.join(worktree_path, "file.txt"), "content")
+
+ result = tool.call({ "path" => "file.txt" }, context:)
+
+ expect(result.failure?).to be true
+ end
+ end
+end