fix(patch): ignore inert context-only hunks (#63678)

This commit is contained in:
Teknium 2026-07-13 02:42:18 -07:00 committed by GitHub
parent b03c94dbed
commit dfeedf613d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 85 additions and 2 deletions

View file

@ -401,6 +401,74 @@ class TestValidationPhase:
assert result.success is True
assert set(written.keys()) == {"a.py", "b.py"}
def test_context_only_hunk_does_not_reject_later_real_hunk(self):
patch = """\
*** Begin Patch
*** Update File: a.py
@@ anchor @@
anchor
@@ value @@
-value = 1
+value = 2
*** End Patch"""
ops, err = parse_v4a_patch(patch)
assert err is None
class FakeFileOps:
written = None
def read_file_raw(self, path):
return SimpleNamespace(content="anchor\nvalue = 1\n", error=None)
def write_file(self, path, content):
self.written = content
return SimpleNamespace(error=None)
file_ops = FakeFileOps()
result = apply_v4a_operations(ops, file_ops)
assert result.success is True
assert file_ops.written == "anchor\nvalue = 2\n"
def test_patch_with_only_context_hunks_reports_no_changes(self):
patch = """\
*** Begin Patch
*** Update File: a.py
@@ anchor @@
anchor
*** End Patch"""
ops, err = parse_v4a_patch(patch)
assert err is None
class FakeFileOps:
def read_file_raw(self, path):
return SimpleNamespace(content="anchor\n", error=None)
def write_file(self, path, content):
raise AssertionError("no-op patch must not write")
result = apply_v4a_operations(ops, FakeFileOps())
assert result.success is False
assert "no changes" in result.error.lower()
def test_validation_error_identifies_hunk_number(self):
patch = """\
*** Begin Patch
*** Update File: a.py
@@ first @@
-first = 1
+first = 2
@@ missing @@
-does_not_exist = 1
+does_not_exist = 2
*** End Patch"""
ops, err = parse_v4a_patch(patch)
assert err is None
class FakeFileOps:
def read_file_raw(self, path):
return SimpleNamespace(content="first = 1\n", error=None)
result = apply_v4a_operations(ops, FakeFileOps())
assert result.success is False
assert "hunk 2" in result.error.lower()
class TestApplyDelete:
"""Tests for _apply_delete producing a real unified diff."""

View file

@ -253,8 +253,11 @@ def _validate_operations(
from tools.fuzzy_match import fuzzy_find_and_replace
errors: List[str] = []
real_change_count = 0
for op in operations:
if op.operation != OperationType.UPDATE:
real_change_count += 1
if op.operation == OperationType.UPDATE:
read_result = file_ops.read_file_raw(op.file_path)
if read_result.error:
@ -262,8 +265,15 @@ def _validate_operations(
continue
simulated = read_result.content
for hunk in op.hunks:
for hunk_index, hunk in enumerate(op.hunks, start=1):
search_lines = [l.content for l in hunk.lines if l.prefix in {' ', '-'}]
removed_lines = [l.content for l in hunk.lines if l.prefix == '-']
added_lines = [l.content for l in hunk.lines if l.prefix == '+']
if not removed_lines and not added_lines:
# Models occasionally emit inert anchor hunks between real
# changes. Ignore them without poisoning the atomic patch.
continue
real_change_count += 1
if not search_lines:
# Addition-only hunk: validate context hint uniqueness
if hunk.context_hint:
@ -291,7 +301,7 @@ def _validate_operations(
if count == 0:
label = f"'{hunk.context_hint}'" if hunk.context_hint else "(no hint)"
msg = (
f"{op.file_path}: hunk {label} not found"
f"{op.file_path}: hunk {hunk_index} {label} not found"
+ (f"{match_error}" if match_error else "")
)
try:
@ -325,6 +335,9 @@ def _validate_operations(
# ADD: parent directory creation handled by write_file; no pre-check needed.
if not errors and real_change_count == 0:
errors.append("Patch contains no changes (only context lines were provided)")
return errors
@ -545,6 +558,8 @@ def _apply_update(op: PatchOperation, file_ops: Any) -> Tuple[bool, str, Optiona
elif line.prefix == '+':
replace_lines.append(line.content)
if search_lines and search_lines == replace_lines:
continue
if search_lines:
search_pattern = '\n'.join(search_lines)
replacement = '\n'.join(replace_lines)