mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(patch): ignore inert context-only hunks (#63678)
This commit is contained in:
parent
b03c94dbed
commit
dfeedf613d
2 changed files with 85 additions and 2 deletions
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue