test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
"""Tests for the fuzzy matching module."""
|
|
|
|
|
|
|
|
|
|
from tools.fuzzy_match import fuzzy_find_and_replace
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestExactMatch:
|
|
|
|
|
def test_single_replacement(self):
|
|
|
|
|
content = "hello world"
|
fix(patch): harden V4A patch parser and fuzzy match — 9 correctness bugs
- Bug 1: replace read_file(limit=10000) with read_file_raw in _apply_update,
preventing silent truncation of files >2000 lines and corruption of lines
>2000 chars; add read_file_raw to FileOperations abstract interface and
ShellFileOperations
- Bug 2: split apply_v4a_operations into validate-then-apply phases; if any
hunk fails validation, zero writes occur (was: continue after failure,
leaving filesystem partially modified)
- Bug 3: parse_v4a_patch now returns an error for begin-marker-with-no-ops,
empty file paths, and moves missing a destination (was: always returned
error=None)
- Bug 4: raise strategy 7 (block anchor) single-candidate similarity threshold
from 0.10 to 0.50, eliminating false-positive matches in repetitive code
- Bug 5: add _strategy_unicode_normalized (new strategy 7) with position
mapping via _build_orig_to_norm_map; smart quotes and em-dashes in
LLM-generated patches now match via strategies 1-6 before falling through
to fuzzy strategies
- Bug 6: extend fuzzy_find_and_replace to return 4-tuple (content, count,
error, strategy); update all 5 call sites across patch_parser.py,
file_operations.py, and skill_manager_tool.py
- Bug 7: guard in _apply_update returns error when addition-only context hint
is ambiguous (>1 occurrences); validation phase errors on both 0 and >1
- Bug 8: _apply_delete returns error (not silent success) on missing file
- Bug 9: _validate_operations checks source existence and destination absence
for MOVE operations before any write occurs
2026-04-10 00:11:07 +02:00
|
|
|
new, count, _, err = fuzzy_find_and_replace(content, "hello", "hi")
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
assert err is None
|
|
|
|
|
assert count == 1
|
|
|
|
|
assert new == "hi world"
|
|
|
|
|
|
|
|
|
|
def test_no_match(self):
|
|
|
|
|
content = "hello world"
|
fix(patch): harden V4A patch parser and fuzzy match — 9 correctness bugs
- Bug 1: replace read_file(limit=10000) with read_file_raw in _apply_update,
preventing silent truncation of files >2000 lines and corruption of lines
>2000 chars; add read_file_raw to FileOperations abstract interface and
ShellFileOperations
- Bug 2: split apply_v4a_operations into validate-then-apply phases; if any
hunk fails validation, zero writes occur (was: continue after failure,
leaving filesystem partially modified)
- Bug 3: parse_v4a_patch now returns an error for begin-marker-with-no-ops,
empty file paths, and moves missing a destination (was: always returned
error=None)
- Bug 4: raise strategy 7 (block anchor) single-candidate similarity threshold
from 0.10 to 0.50, eliminating false-positive matches in repetitive code
- Bug 5: add _strategy_unicode_normalized (new strategy 7) with position
mapping via _build_orig_to_norm_map; smart quotes and em-dashes in
LLM-generated patches now match via strategies 1-6 before falling through
to fuzzy strategies
- Bug 6: extend fuzzy_find_and_replace to return 4-tuple (content, count,
error, strategy); update all 5 call sites across patch_parser.py,
file_operations.py, and skill_manager_tool.py
- Bug 7: guard in _apply_update returns error when addition-only context hint
is ambiguous (>1 occurrences); validation phase errors on both 0 and >1
- Bug 8: _apply_delete returns error (not silent success) on missing file
- Bug 9: _validate_operations checks source existence and destination absence
for MOVE operations before any write occurs
2026-04-10 00:11:07 +02:00
|
|
|
new, count, _, err = fuzzy_find_and_replace(content, "xyz", "abc")
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
assert count == 0
|
|
|
|
|
assert err is not None
|
|
|
|
|
assert new == content
|
|
|
|
|
|
|
|
|
|
def test_empty_old_string(self):
|
fix(patch): harden V4A patch parser and fuzzy match — 9 correctness bugs
- Bug 1: replace read_file(limit=10000) with read_file_raw in _apply_update,
preventing silent truncation of files >2000 lines and corruption of lines
>2000 chars; add read_file_raw to FileOperations abstract interface and
ShellFileOperations
- Bug 2: split apply_v4a_operations into validate-then-apply phases; if any
hunk fails validation, zero writes occur (was: continue after failure,
leaving filesystem partially modified)
- Bug 3: parse_v4a_patch now returns an error for begin-marker-with-no-ops,
empty file paths, and moves missing a destination (was: always returned
error=None)
- Bug 4: raise strategy 7 (block anchor) single-candidate similarity threshold
from 0.10 to 0.50, eliminating false-positive matches in repetitive code
- Bug 5: add _strategy_unicode_normalized (new strategy 7) with position
mapping via _build_orig_to_norm_map; smart quotes and em-dashes in
LLM-generated patches now match via strategies 1-6 before falling through
to fuzzy strategies
- Bug 6: extend fuzzy_find_and_replace to return 4-tuple (content, count,
error, strategy); update all 5 call sites across patch_parser.py,
file_operations.py, and skill_manager_tool.py
- Bug 7: guard in _apply_update returns error when addition-only context hint
is ambiguous (>1 occurrences); validation phase errors on both 0 and >1
- Bug 8: _apply_delete returns error (not silent success) on missing file
- Bug 9: _validate_operations checks source existence and destination absence
for MOVE operations before any write occurs
2026-04-10 00:11:07 +02:00
|
|
|
new, count, _, err = fuzzy_find_and_replace("abc", "", "x")
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
assert count == 0
|
|
|
|
|
assert err is not None
|
|
|
|
|
|
|
|
|
|
def test_identical_strings(self):
|
fix(patch): harden V4A patch parser and fuzzy match — 9 correctness bugs
- Bug 1: replace read_file(limit=10000) with read_file_raw in _apply_update,
preventing silent truncation of files >2000 lines and corruption of lines
>2000 chars; add read_file_raw to FileOperations abstract interface and
ShellFileOperations
- Bug 2: split apply_v4a_operations into validate-then-apply phases; if any
hunk fails validation, zero writes occur (was: continue after failure,
leaving filesystem partially modified)
- Bug 3: parse_v4a_patch now returns an error for begin-marker-with-no-ops,
empty file paths, and moves missing a destination (was: always returned
error=None)
- Bug 4: raise strategy 7 (block anchor) single-candidate similarity threshold
from 0.10 to 0.50, eliminating false-positive matches in repetitive code
- Bug 5: add _strategy_unicode_normalized (new strategy 7) with position
mapping via _build_orig_to_norm_map; smart quotes and em-dashes in
LLM-generated patches now match via strategies 1-6 before falling through
to fuzzy strategies
- Bug 6: extend fuzzy_find_and_replace to return 4-tuple (content, count,
error, strategy); update all 5 call sites across patch_parser.py,
file_operations.py, and skill_manager_tool.py
- Bug 7: guard in _apply_update returns error when addition-only context hint
is ambiguous (>1 occurrences); validation phase errors on both 0 and >1
- Bug 8: _apply_delete returns error (not silent success) on missing file
- Bug 9: _validate_operations checks source existence and destination absence
for MOVE operations before any write occurs
2026-04-10 00:11:07 +02:00
|
|
|
new, count, _, err = fuzzy_find_and_replace("abc", "abc", "abc")
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
assert count == 0
|
|
|
|
|
assert "identical" in err
|
|
|
|
|
|
|
|
|
|
def test_multiline_exact(self):
|
|
|
|
|
content = "line1\nline2\nline3"
|
fix(patch): harden V4A patch parser and fuzzy match — 9 correctness bugs
- Bug 1: replace read_file(limit=10000) with read_file_raw in _apply_update,
preventing silent truncation of files >2000 lines and corruption of lines
>2000 chars; add read_file_raw to FileOperations abstract interface and
ShellFileOperations
- Bug 2: split apply_v4a_operations into validate-then-apply phases; if any
hunk fails validation, zero writes occur (was: continue after failure,
leaving filesystem partially modified)
- Bug 3: parse_v4a_patch now returns an error for begin-marker-with-no-ops,
empty file paths, and moves missing a destination (was: always returned
error=None)
- Bug 4: raise strategy 7 (block anchor) single-candidate similarity threshold
from 0.10 to 0.50, eliminating false-positive matches in repetitive code
- Bug 5: add _strategy_unicode_normalized (new strategy 7) with position
mapping via _build_orig_to_norm_map; smart quotes and em-dashes in
LLM-generated patches now match via strategies 1-6 before falling through
to fuzzy strategies
- Bug 6: extend fuzzy_find_and_replace to return 4-tuple (content, count,
error, strategy); update all 5 call sites across patch_parser.py,
file_operations.py, and skill_manager_tool.py
- Bug 7: guard in _apply_update returns error when addition-only context hint
is ambiguous (>1 occurrences); validation phase errors on both 0 and >1
- Bug 8: _apply_delete returns error (not silent success) on missing file
- Bug 9: _validate_operations checks source existence and destination absence
for MOVE operations before any write occurs
2026-04-10 00:11:07 +02:00
|
|
|
new, count, _, err = fuzzy_find_and_replace(content, "line1\nline2", "replaced")
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
assert err is None
|
|
|
|
|
assert count == 1
|
|
|
|
|
assert new == "replaced\nline3"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestWhitespaceDifference:
|
|
|
|
|
def test_extra_spaces_match(self):
|
|
|
|
|
content = "def foo( x, y ):"
|
fix(patch): harden V4A patch parser and fuzzy match — 9 correctness bugs
- Bug 1: replace read_file(limit=10000) with read_file_raw in _apply_update,
preventing silent truncation of files >2000 lines and corruption of lines
>2000 chars; add read_file_raw to FileOperations abstract interface and
ShellFileOperations
- Bug 2: split apply_v4a_operations into validate-then-apply phases; if any
hunk fails validation, zero writes occur (was: continue after failure,
leaving filesystem partially modified)
- Bug 3: parse_v4a_patch now returns an error for begin-marker-with-no-ops,
empty file paths, and moves missing a destination (was: always returned
error=None)
- Bug 4: raise strategy 7 (block anchor) single-candidate similarity threshold
from 0.10 to 0.50, eliminating false-positive matches in repetitive code
- Bug 5: add _strategy_unicode_normalized (new strategy 7) with position
mapping via _build_orig_to_norm_map; smart quotes and em-dashes in
LLM-generated patches now match via strategies 1-6 before falling through
to fuzzy strategies
- Bug 6: extend fuzzy_find_and_replace to return 4-tuple (content, count,
error, strategy); update all 5 call sites across patch_parser.py,
file_operations.py, and skill_manager_tool.py
- Bug 7: guard in _apply_update returns error when addition-only context hint
is ambiguous (>1 occurrences); validation phase errors on both 0 and >1
- Bug 8: _apply_delete returns error (not silent success) on missing file
- Bug 9: _validate_operations checks source existence and destination absence
for MOVE operations before any write occurs
2026-04-10 00:11:07 +02:00
|
|
|
new, count, _, err = fuzzy_find_and_replace(content, "def foo( x, y ):", "def bar(x, y):")
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
assert count == 1
|
|
|
|
|
assert "bar" in new
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestIndentDifference:
|
|
|
|
|
def test_different_indentation(self):
|
|
|
|
|
content = " def foo():\n pass"
|
fix(patch): harden V4A patch parser and fuzzy match — 9 correctness bugs
- Bug 1: replace read_file(limit=10000) with read_file_raw in _apply_update,
preventing silent truncation of files >2000 lines and corruption of lines
>2000 chars; add read_file_raw to FileOperations abstract interface and
ShellFileOperations
- Bug 2: split apply_v4a_operations into validate-then-apply phases; if any
hunk fails validation, zero writes occur (was: continue after failure,
leaving filesystem partially modified)
- Bug 3: parse_v4a_patch now returns an error for begin-marker-with-no-ops,
empty file paths, and moves missing a destination (was: always returned
error=None)
- Bug 4: raise strategy 7 (block anchor) single-candidate similarity threshold
from 0.10 to 0.50, eliminating false-positive matches in repetitive code
- Bug 5: add _strategy_unicode_normalized (new strategy 7) with position
mapping via _build_orig_to_norm_map; smart quotes and em-dashes in
LLM-generated patches now match via strategies 1-6 before falling through
to fuzzy strategies
- Bug 6: extend fuzzy_find_and_replace to return 4-tuple (content, count,
error, strategy); update all 5 call sites across patch_parser.py,
file_operations.py, and skill_manager_tool.py
- Bug 7: guard in _apply_update returns error when addition-only context hint
is ambiguous (>1 occurrences); validation phase errors on both 0 and >1
- Bug 8: _apply_delete returns error (not silent success) on missing file
- Bug 9: _validate_operations checks source existence and destination absence
for MOVE operations before any write occurs
2026-04-10 00:11:07 +02:00
|
|
|
new, count, _, err = fuzzy_find_and_replace(content, "def foo():\n pass", "def bar():\n return 1")
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
assert count == 1
|
|
|
|
|
assert "bar" in new
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestReplaceAll:
|
|
|
|
|
def test_multiple_matches_without_flag_errors(self):
|
|
|
|
|
content = "aaa bbb aaa"
|
fix(patch): harden V4A patch parser and fuzzy match — 9 correctness bugs
- Bug 1: replace read_file(limit=10000) with read_file_raw in _apply_update,
preventing silent truncation of files >2000 lines and corruption of lines
>2000 chars; add read_file_raw to FileOperations abstract interface and
ShellFileOperations
- Bug 2: split apply_v4a_operations into validate-then-apply phases; if any
hunk fails validation, zero writes occur (was: continue after failure,
leaving filesystem partially modified)
- Bug 3: parse_v4a_patch now returns an error for begin-marker-with-no-ops,
empty file paths, and moves missing a destination (was: always returned
error=None)
- Bug 4: raise strategy 7 (block anchor) single-candidate similarity threshold
from 0.10 to 0.50, eliminating false-positive matches in repetitive code
- Bug 5: add _strategy_unicode_normalized (new strategy 7) with position
mapping via _build_orig_to_norm_map; smart quotes and em-dashes in
LLM-generated patches now match via strategies 1-6 before falling through
to fuzzy strategies
- Bug 6: extend fuzzy_find_and_replace to return 4-tuple (content, count,
error, strategy); update all 5 call sites across patch_parser.py,
file_operations.py, and skill_manager_tool.py
- Bug 7: guard in _apply_update returns error when addition-only context hint
is ambiguous (>1 occurrences); validation phase errors on both 0 and >1
- Bug 8: _apply_delete returns error (not silent success) on missing file
- Bug 9: _validate_operations checks source existence and destination absence
for MOVE operations before any write occurs
2026-04-10 00:11:07 +02:00
|
|
|
new, count, _, err = fuzzy_find_and_replace(content, "aaa", "ccc", replace_all=False)
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
assert count == 0
|
|
|
|
|
assert "Found 2 matches" in err
|
|
|
|
|
|
|
|
|
|
def test_multiple_matches_with_flag(self):
|
|
|
|
|
content = "aaa bbb aaa"
|
fix(patch): harden V4A patch parser and fuzzy match — 9 correctness bugs
- Bug 1: replace read_file(limit=10000) with read_file_raw in _apply_update,
preventing silent truncation of files >2000 lines and corruption of lines
>2000 chars; add read_file_raw to FileOperations abstract interface and
ShellFileOperations
- Bug 2: split apply_v4a_operations into validate-then-apply phases; if any
hunk fails validation, zero writes occur (was: continue after failure,
leaving filesystem partially modified)
- Bug 3: parse_v4a_patch now returns an error for begin-marker-with-no-ops,
empty file paths, and moves missing a destination (was: always returned
error=None)
- Bug 4: raise strategy 7 (block anchor) single-candidate similarity threshold
from 0.10 to 0.50, eliminating false-positive matches in repetitive code
- Bug 5: add _strategy_unicode_normalized (new strategy 7) with position
mapping via _build_orig_to_norm_map; smart quotes and em-dashes in
LLM-generated patches now match via strategies 1-6 before falling through
to fuzzy strategies
- Bug 6: extend fuzzy_find_and_replace to return 4-tuple (content, count,
error, strategy); update all 5 call sites across patch_parser.py,
file_operations.py, and skill_manager_tool.py
- Bug 7: guard in _apply_update returns error when addition-only context hint
is ambiguous (>1 occurrences); validation phase errors on both 0 and >1
- Bug 8: _apply_delete returns error (not silent success) on missing file
- Bug 9: _validate_operations checks source existence and destination absence
for MOVE operations before any write occurs
2026-04-10 00:11:07 +02:00
|
|
|
new, count, _, err = fuzzy_find_and_replace(content, "aaa", "ccc", replace_all=True)
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
assert err is None
|
|
|
|
|
assert count == 2
|
|
|
|
|
assert new == "ccc bbb ccc"
|
fix(patch): harden V4A patch parser and fuzzy match — 9 correctness bugs
- Bug 1: replace read_file(limit=10000) with read_file_raw in _apply_update,
preventing silent truncation of files >2000 lines and corruption of lines
>2000 chars; add read_file_raw to FileOperations abstract interface and
ShellFileOperations
- Bug 2: split apply_v4a_operations into validate-then-apply phases; if any
hunk fails validation, zero writes occur (was: continue after failure,
leaving filesystem partially modified)
- Bug 3: parse_v4a_patch now returns an error for begin-marker-with-no-ops,
empty file paths, and moves missing a destination (was: always returned
error=None)
- Bug 4: raise strategy 7 (block anchor) single-candidate similarity threshold
from 0.10 to 0.50, eliminating false-positive matches in repetitive code
- Bug 5: add _strategy_unicode_normalized (new strategy 7) with position
mapping via _build_orig_to_norm_map; smart quotes and em-dashes in
LLM-generated patches now match via strategies 1-6 before falling through
to fuzzy strategies
- Bug 6: extend fuzzy_find_and_replace to return 4-tuple (content, count,
error, strategy); update all 5 call sites across patch_parser.py,
file_operations.py, and skill_manager_tool.py
- Bug 7: guard in _apply_update returns error when addition-only context hint
is ambiguous (>1 occurrences); validation phase errors on both 0 and >1
- Bug 8: _apply_delete returns error (not silent success) on missing file
- Bug 9: _validate_operations checks source existence and destination absence
for MOVE operations before any write occurs
2026-04-10 00:11:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestUnicodeNormalized:
|
|
|
|
|
"""Tests for the unicode_normalized strategy (Bug 5)."""
|
|
|
|
|
|
|
|
|
|
def test_em_dash_matched(self):
|
|
|
|
|
"""Em-dash in content should match ASCII '--' in pattern."""
|
|
|
|
|
content = "return value\u2014fallback"
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace(
|
|
|
|
|
content, "return value--fallback", "return value or fallback"
|
|
|
|
|
)
|
|
|
|
|
assert count == 1, f"Expected match via unicode_normalized, got err={err}"
|
|
|
|
|
assert strategy == "unicode_normalized"
|
|
|
|
|
assert "return value or fallback" in new
|
|
|
|
|
|
|
|
|
|
def test_smart_quotes_matched(self):
|
|
|
|
|
"""Smart double quotes in content should match straight quotes in pattern."""
|
|
|
|
|
content = 'print(\u201chello\u201d)'
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace(
|
|
|
|
|
content, 'print("hello")', 'print("world")'
|
|
|
|
|
)
|
|
|
|
|
assert count == 1, f"Expected match via unicode_normalized, got err={err}"
|
|
|
|
|
assert "world" in new
|
|
|
|
|
|
|
|
|
|
def test_no_unicode_skips_strategy(self):
|
|
|
|
|
"""When content and pattern have no Unicode variants, strategy is skipped."""
|
|
|
|
|
content = "hello world"
|
|
|
|
|
# Should match via exact, not unicode_normalized
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace(content, "hello", "hi")
|
|
|
|
|
assert count == 1
|
|
|
|
|
assert strategy == "exact"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBlockAnchorThreshold:
|
|
|
|
|
"""Tests for the raised block_anchor threshold (Bug 4)."""
|
|
|
|
|
|
|
|
|
|
def test_high_similarity_matches(self):
|
|
|
|
|
"""A block with >50% middle similarity should match."""
|
|
|
|
|
content = "def foo():\n x = 1\n y = 2\n return x + y\n"
|
|
|
|
|
pattern = "def foo():\n x = 1\n y = 9\n return x + y"
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace(content, pattern, "def foo():\n return 0\n")
|
|
|
|
|
# Should match via block_anchor or earlier strategy
|
|
|
|
|
assert count == 1
|
|
|
|
|
|
|
|
|
|
def test_completely_different_middle_does_not_match(self):
|
|
|
|
|
"""A block where only first+last lines match but middle is completely different
|
|
|
|
|
should NOT match under the raised 0.50 threshold."""
|
|
|
|
|
content = (
|
|
|
|
|
"class Foo:\n"
|
|
|
|
|
" completely = 'unrelated'\n"
|
|
|
|
|
" content = 'here'\n"
|
|
|
|
|
" nothing = 'in common'\n"
|
|
|
|
|
" pass\n"
|
|
|
|
|
)
|
|
|
|
|
# Pattern has same first/last lines but completely different middle
|
|
|
|
|
pattern = (
|
|
|
|
|
"class Foo:\n"
|
|
|
|
|
" x = 1\n"
|
|
|
|
|
" y = 2\n"
|
|
|
|
|
" z = 3\n"
|
|
|
|
|
" pass"
|
|
|
|
|
)
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace(content, pattern, "replaced")
|
|
|
|
|
# With threshold=0.50, this near-zero-similarity middle should not match
|
|
|
|
|
assert count == 0, (
|
|
|
|
|
f"Block with unrelated middle should not match under threshold=0.50, "
|
|
|
|
|
f"but matched via strategy={strategy}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestStrategyNameSurfaced:
|
|
|
|
|
"""Tests for the strategy name in the 4-tuple return (Bug 6)."""
|
|
|
|
|
|
|
|
|
|
def test_exact_strategy_name(self):
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace("hello", "hello", "world")
|
|
|
|
|
assert strategy == "exact"
|
|
|
|
|
assert count == 1
|
|
|
|
|
|
|
|
|
|
def test_failed_match_returns_none_strategy(self):
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace("hello", "xyz", "world")
|
|
|
|
|
assert count == 0
|
|
|
|
|
assert strategy is None
|
fix(patch): catch silent persistence failures and escape-drift in tool-call transport (#12669)
Two hardening layers in the patch tool, triggered by a real silent failure
in the previous session:
(1) Post-write verification in patch_replace — after write_file succeeds,
re-read the file and confirm the bytes on disk match the intended write.
If not, return an error instead of the current success-with-diff. Catches
silent persistence failures from any cause (backend FS oddities, stdin
pipe truncation, concurrent task races, mount drift).
(2) Escape-drift guard in fuzzy_find_and_replace — when a non-exact
strategy matches and both old_string and new_string contain literal
\' or \" sequences but the matched file region does not, reject the
patch with a clear error pointing at the likely cause (tool-call
serialization adding a spurious backslash around apostrophes/quotes).
Exact matches bypass the guard, and legitimate edits that add or
preserve escape sequences in files that already have them still work.
Why: in a prior tool call, old_string was sent with \' where the file
has ' (tool-call transport drift). The fuzzy matcher's block_anchor
strategy matched anyway and produced a diff the tool reported as
successful — but the file was never modified on disk. The agent moved
on believing the edit landed when it hadn't.
Tests: added TestPatchReplacePostWriteVerification (3 cases) and
TestEscapeDriftGuard (6 cases). All pass, existing fuzzy match and
file_operations tests unaffected.
2026-04-19 12:27:34 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestEscapeDriftGuard:
|
|
|
|
|
"""Tests for the escape-drift guard that catches bash/JSON serialization
|
|
|
|
|
artifacts where an apostrophe gets prefixed with a spurious backslash
|
|
|
|
|
in tool-call transport.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_drift_blocked_apostrophe(self):
|
|
|
|
|
"""File has ', old_string and new_string both have \\' — classic
|
|
|
|
|
tool-call drift. Guard must block with a helpful error instead of
|
|
|
|
|
writing \\' literals into source code."""
|
|
|
|
|
content = "x = \"hello there\"\n"
|
|
|
|
|
# Simulate transport-corrupted old_string and new_string where an
|
|
|
|
|
# apostrophe-like context got prefixed with a backslash. The content
|
|
|
|
|
# itself has no apostrophe, but both strings do — matching via
|
|
|
|
|
# whitespace/anchor strategies would otherwise succeed.
|
|
|
|
|
old_string = "x = \"hello there\" # don\\'t edit\n"
|
|
|
|
|
new_string = "x = \"hi there\" # don\\'t edit\n"
|
|
|
|
|
# This particular pair won't match anything, so it exits via
|
|
|
|
|
# no-match path. Build a case where a non-exact strategy DOES match.
|
|
|
|
|
content = "line\n x = 1\nline"
|
|
|
|
|
old_string = "line\n x = \\'a\\'\nline"
|
|
|
|
|
new_string = "line\n x = \\'b\\'\nline"
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace(content, old_string, new_string)
|
|
|
|
|
assert count == 0
|
|
|
|
|
assert err is not None and "Escape-drift" in err
|
|
|
|
|
assert "backslash" in err.lower()
|
|
|
|
|
assert new == content # file untouched
|
|
|
|
|
|
|
|
|
|
def test_drift_blocked_double_quote(self):
|
|
|
|
|
"""Same idea but with \\" drift instead of \\'."""
|
|
|
|
|
content = 'line\n x = 1\nline'
|
|
|
|
|
old_string = 'line\n x = \\"a\\"\nline'
|
|
|
|
|
new_string = 'line\n x = \\"b\\"\nline'
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace(content, old_string, new_string)
|
|
|
|
|
assert count == 0
|
|
|
|
|
assert err is not None and "Escape-drift" in err
|
|
|
|
|
|
|
|
|
|
def test_drift_allowed_when_file_genuinely_has_backslash_escapes(self):
|
|
|
|
|
"""If the file already contains \\' (e.g. inside an existing escaped
|
|
|
|
|
string), the model is legitimately preserving it. Guard must NOT
|
|
|
|
|
fire."""
|
|
|
|
|
content = "line\n x = \\'a\\'\nline"
|
|
|
|
|
old_string = "line\n x = \\'a\\'\nline"
|
|
|
|
|
new_string = "line\n x = \\'b\\'\nline"
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace(content, old_string, new_string)
|
|
|
|
|
assert err is None
|
|
|
|
|
assert count == 1
|
|
|
|
|
assert "\\'b\\'" in new
|
|
|
|
|
|
|
|
|
|
def test_drift_allowed_on_exact_match(self):
|
|
|
|
|
"""Exact matches bypass the drift guard entirely — if the file
|
|
|
|
|
really contains the exact bytes old_string specified, it's not
|
|
|
|
|
drift."""
|
|
|
|
|
content = "hello \\'world\\'"
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace(
|
|
|
|
|
content, "hello \\'world\\'", "hello \\'there\\'"
|
|
|
|
|
)
|
|
|
|
|
assert err is None
|
|
|
|
|
assert count == 1
|
|
|
|
|
assert strategy == "exact"
|
|
|
|
|
|
|
|
|
|
def test_drift_allowed_when_adding_escaped_strings(self):
|
|
|
|
|
"""Model is adding new content with \\' that wasn't in the original.
|
|
|
|
|
old_string has no \\', so guard doesn't fire."""
|
|
|
|
|
content = "line1\nline2\nline3"
|
|
|
|
|
old_string = "line1\nline2\nline3"
|
|
|
|
|
new_string = "line1\nprint(\\'added\\')\nline2\nline3"
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace(content, old_string, new_string)
|
|
|
|
|
assert err is None
|
|
|
|
|
assert count == 1
|
|
|
|
|
assert "\\'added\\'" in new
|
|
|
|
|
|
|
|
|
|
def test_no_drift_check_when_new_string_lacks_suspect_chars(self):
|
|
|
|
|
"""Fast-path: if new_string has no \\' or \\", guard must not
|
|
|
|
|
fire even on fuzzy match."""
|
|
|
|
|
content = "def foo():\n pass" # extra space ignored by line_trimmed
|
|
|
|
|
old_string = "def foo():\n pass"
|
|
|
|
|
new_string = "def bar():\n return 1"
|
|
|
|
|
new, count, strategy, err = fuzzy_find_and_replace(content, old_string, new_string)
|
|
|
|
|
assert err is None
|
|
|
|
|
assert count == 1
|