diff --git a/tests/tools/test_patch_already_applied.py b/tests/tools/test_patch_already_applied.py index 16588d0f2d..298c568b30 100644 --- a/tests/tools/test_patch_already_applied.py +++ b/tests/tools/test_patch_already_applied.py @@ -140,3 +140,27 @@ class TestV4AAlreadyApplied: r = _patch_tool(mode="patch", patch=patch_content, task_id="t-v4a") assert r["success"] is True, r assert f.read_text() == "STATUS = 'migrated_to_v2_schema'\n" + + def test_degenerate_identical_hunk_skipped_in_validation(self, workdir): + """A hunk whose -/+ lines are identical is a no-op: the apply phase + skips it, so validation must not fail the patch (previously it + reached fuzzy_find_and_replace, whose identical-strings error names + old_string/new_string — parameters that don't exist in patch mode). + The short text also dodges is_already_applied's >=8-char rescue.""" + f = workdir / "degen.py" + f.write_text("A = 1\nB = 2\n") + patch_content = ( + "*** Begin Patch\n" + f"*** Update File: {f}\n" + "-A = 1\n" + "+A = 1\n" + "@@ B @@\n" + "-B = 2\n" + "+B = 3\n" + "*** End Patch\n" + ) + r = _patch_tool(mode="patch", patch=patch_content, task_id="t-v4a") + assert r["success"] is True, r + text = f.read_text() + assert "A = 1" in text # degenerate hunk left intact + assert "B = 3" in text # live hunk applied diff --git a/tools/patch_parser.py b/tools/patch_parser.py index 37412336ee..a95859d5e4 100644 --- a/tools/patch_parser.py +++ b/tools/patch_parser.py @@ -323,6 +323,14 @@ def _validate_operations( replace_lines = [l.content for l in hunk.lines if l.prefix in {' ', '+'}] replacement = '\n'.join(replace_lines) + if search_lines == replace_lines: + # Degenerate hunk whose -/+ lines are identical: the apply + # phase skips it as a no-op, so validation must not fail it + # — fuzzy_find_and_replace would reject the identical + # search/replacement with old_string/new_string guidance + # that has no meaning in V4A patch mode. + continue + new_simulated, count, _strategy, match_error = fuzzy_find_and_replace( simulated, search_pattern, replacement, replace_all=False )