mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-22 09:32:33 +00:00
239 lines
5.5 KiB
Python
239 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
from nanobot.agent.tools.apply_patch import ApplyPatchTool
|
|
|
|
|
|
def test_apply_patch_adds_file(tmp_path):
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
result = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Add File: hello.txt
|
|
+Hello
|
|
+world
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "Patch applied" in result
|
|
assert (tmp_path / "hello.txt").read_text() == "Hello\nworld\n"
|
|
|
|
|
|
def test_apply_patch_updates_multiple_hunks(tmp_path):
|
|
target = tmp_path / "multi.txt"
|
|
target.write_text("line1\nline2\nline3\nline4\n")
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
result = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Update File: multi.txt
|
|
@@
|
|
-line2
|
|
+changed2
|
|
@@
|
|
-line4
|
|
+changed4
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "update multi.txt" in result
|
|
assert target.read_text() == "line1\nchanged2\nline3\nchanged4\n"
|
|
|
|
|
|
def test_apply_patch_ignores_standard_no_newline_marker(tmp_path):
|
|
target = tmp_path / "plain.txt"
|
|
target.write_text("before")
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
result = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Update File: plain.txt
|
|
@@ -1,1 +1,1 @@
|
|
-before
|
|
\\ No newline at end of file
|
|
+after
|
|
\\ No newline at end of file
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "update plain.txt" in result
|
|
assert target.read_text() == "after\n"
|
|
|
|
|
|
def test_apply_patch_rejects_empty_hunk(tmp_path):
|
|
target = tmp_path / "plain.txt"
|
|
target.write_text("before\n")
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
result = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Update File: plain.txt
|
|
@@
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "hunk is empty" in result
|
|
assert target.read_text() == "before\n"
|
|
|
|
|
|
def test_apply_patch_uses_unified_diff_line_hint(tmp_path):
|
|
target = tmp_path / "repeated.txt"
|
|
target.write_text("target\nmiddle\ntarget\n")
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
result = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Update File: repeated.txt
|
|
@@ -3,1 +3,1 @@
|
|
-target
|
|
+changed
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "update repeated.txt" in result
|
|
assert target.read_text() == "target\nmiddle\nchanged\n"
|
|
|
|
|
|
def test_apply_patch_line_hint_does_not_fallback_to_earlier_match(tmp_path):
|
|
target = tmp_path / "repeated.txt"
|
|
target.write_text("target\nmiddle\nother\n")
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
result = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Update File: repeated.txt
|
|
@@ -3,1 +3,1 @@
|
|
-target
|
|
+changed
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "hunk does not match repeated.txt" in result
|
|
assert target.read_text() == "target\nmiddle\nother\n"
|
|
|
|
|
|
def test_apply_patch_mismatch_reports_best_match(tmp_path):
|
|
target = tmp_path / "near.txt"
|
|
target.write_text("alpha\nbeta\ngamma\n")
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
result = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Update File: near.txt
|
|
@@ -2,1 +2,1 @@
|
|
-betx
|
|
+delta
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "hunk does not match near.txt" in result
|
|
assert "Best match" in result
|
|
assert "line 2" in result
|
|
assert target.read_text() == "alpha\nbeta\ngamma\n"
|
|
|
|
|
|
def test_apply_patch_moves_and_updates_file(tmp_path):
|
|
source = tmp_path / "old" / "name.txt"
|
|
source.parent.mkdir()
|
|
source.write_text("old content\n")
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
result = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Update File: old/name.txt
|
|
*** Move to: renamed/dir/name.txt
|
|
@@
|
|
-old content
|
|
+new content
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "move old/name.txt -> renamed/dir/name.txt" in result
|
|
assert not source.exists()
|
|
assert (tmp_path / "renamed" / "dir" / "name.txt").read_text() == "new content\n"
|
|
|
|
|
|
def test_apply_patch_deletes_file(tmp_path):
|
|
target = tmp_path / "obsolete.txt"
|
|
target.write_text("remove me\n")
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
result = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Delete File: obsolete.txt
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "delete obsolete.txt" in result
|
|
assert not target.exists()
|
|
|
|
|
|
def test_apply_patch_rejects_absolute_and_parent_paths(tmp_path):
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
absolute = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Add File: /tmp/owned.txt
|
|
+nope
|
|
*** End Patch
|
|
"""
|
|
))
|
|
parent = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Add File: ../owned.txt
|
|
+nope
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "must be relative" in absolute
|
|
assert "must not contain '..'" in parent
|
|
assert not (tmp_path.parent / "owned.txt").exists()
|
|
|
|
|
|
def test_apply_patch_does_not_overwrite_existing_file_with_add(tmp_path):
|
|
target = tmp_path / "existing.txt"
|
|
target.write_text("keep me\n")
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
result = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Add File: existing.txt
|
|
+replace me
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "file to add already exists" in result
|
|
assert target.read_text() == "keep me\n"
|
|
|
|
|
|
def test_apply_patch_rolls_back_when_late_operation_fails(tmp_path):
|
|
first = tmp_path / "first.txt"
|
|
first.write_text("before\n")
|
|
tool = ApplyPatchTool(workspace=tmp_path)
|
|
|
|
result = asyncio.run(tool.execute(
|
|
patch="""*** Begin Patch
|
|
*** Update File: first.txt
|
|
@@
|
|
-before
|
|
+after
|
|
*** Delete File: missing.txt
|
|
*** End Patch
|
|
"""
|
|
))
|
|
|
|
assert "file to delete does not exist" in result
|
|
assert first.read_text() == "before\n"
|