From dfeedf613dcd2ca97d0903ad7fcacad118e39bca Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:42:18 -0700 Subject: [PATCH] fix(patch): ignore inert context-only hunks (#63678) --- tests/tools/test_patch_parser.py | 68 ++++++++++++++++++++++++++++++++ tools/patch_parser.py | 19 ++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/tests/tools/test_patch_parser.py b/tests/tools/test_patch_parser.py index 79077a84a165..52cca09fa371 100644 --- a/tests/tools/test_patch_parser.py +++ b/tests/tools/test_patch_parser.py @@ -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.""" diff --git a/tools/patch_parser.py b/tools/patch_parser.py index e16cb446ee03..c02037f77cae 100644 --- a/tools/patch_parser.py +++ b/tools/patch_parser.py @@ -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)