Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def apply_edits(repo: Path, payload: dict[str, Any]) -> tuple[bool, str]:
edits = payload.get("edits")
if not isinstance(edits, list):
return False, "Model response missing edits array"
staged: list[tuple[Path, str]] = []
staged: dict[Path, str] = {}
for i, edit in enumerate(edits):
if not isinstance(edit, dict):
return False, f"Edit {i+1} is not an object"
Expand All @@ -234,17 +234,20 @@ def apply_edits(repo: Path, payload: dict[str, Any]) -> tuple[bool, str]:
return False, f"Edit {i+1} requires string old/new values"
if not path.exists() or not path.is_file():
return False, f"File not found: {rel}"
try:
current = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return False, f"Binary/non-text file is not editable: {rel}"
if path in staged:
current = staged[path]
else:
try:
current = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return False, f"Binary/non-text file is not editable: {rel}"
if current.count(old) != 1:
return False, f"Expected exactly one match for old text in {rel}, found {current.count(old)}"
staged.append((path, current.replace(old, new, 1)))
staged[path] = current.replace(old, new, 1)

for path, content in staged:
for path, content in staged.items():
path.write_text(content, encoding="utf-8")
return True, f"Applied {len(staged)} edit(s)"
return True, f"Applied {len(edits)} edit(s)"


def build_patch(repo: Path) -> str:
Expand Down
20 changes: 20 additions & 0 deletions backend/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ def test_apply_edits_updates_exact_match(self) -> None:
self.assertTrue(ok, message)
self.assertEqual(target.read_text(encoding="utf-8"), "after\n")

def test_apply_edits_preserves_multiple_edits_to_same_file(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
repo = Path(temp_dir)
target = repo / "example.txt"
target.write_text("alpha beta gamma", encoding="utf-8")

ok, message = apply_edits(
repo,
{
"edits": [
{"path": "example.txt", "old": "alpha", "new": "one"},
{"path": "example.txt", "old": "gamma", "new": "three"},
]
},
)

self.assertTrue(ok, message)
self.assertEqual(target.read_text(encoding="utf-8"), "one beta three")
self.assertEqual(message, "Applied 2 edit(s)")

def test_apply_edits_rejects_path_escape(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
repo = Path(temp_dir)
Expand Down
Loading