"""Tests for the V4A patch format parser.""" from types import SimpleNamespace from tools.patch_parser import ( OperationType, apply_v4a_operations, parse_v4a_patch, ) class TestParseUpdateFile: def test_basic_update(self): patch = """\ *** Begin Patch *** Update File: src/main.py @@ def greet @@ def greet(): - print("hello") + print("hi") *** End Patch""" ops, err = parse_v4a_patch(patch) assert err is None assert len(ops) == 1 op = ops[0] assert op.operation == OperationType.UPDATE assert op.file_path == "src/main.py" assert len(op.hunks) == 1 hunk = op.hunks[0] assert hunk.context_hint == "def greet" prefixes = [l.prefix for l in hunk.lines] assert " " in prefixes assert "-" in prefixes assert "+" in prefixes def test_multiple_hunks(self): patch = """\ *** Begin Patch *** Update File: f.py @@ first @@ a -b +c @@ second @@ x -y +z *** End Patch""" ops, err = parse_v4a_patch(patch) assert err is None assert len(ops) == 1 assert len(ops[0].hunks) == 2 assert ops[0].hunks[0].context_hint == "first" assert ops[0].hunks[1].context_hint == "second" class TestParseAddFile: def test_add_file(self): patch = """\ *** Begin Patch *** Add File: new/module.py +import os + +print("hello") *** End Patch""" ops, err = parse_v4a_patch(patch) assert err is None assert len(ops) == 1 op = ops[0] assert op.operation == OperationType.ADD assert op.file_path == "new/module.py" assert len(op.hunks) == 1 contents = [l.content for l in op.hunks[0].lines if l.prefix == "+"] assert contents[0] == "import os" assert contents[2] == 'print("hello")' class TestParseDeleteFile: def test_delete_file(self): patch = """\ *** Begin Patch *** Delete File: old/stuff.py *** End Patch""" ops, err = parse_v4a_patch(patch) assert err is None assert len(ops) == 1 assert ops[0].operation == OperationType.DELETE assert ops[0].file_path == "old/stuff.py" class TestParseMoveFile: def test_move_file(self): patch = """\ *** Begin Patch *** Move File: old/path.py -> new/path.py *** End Patch""" ops, err = parse_v4a_patch(patch) assert err is None assert len(ops) == 1 assert ops[0].operation == OperationType.MOVE assert ops[0].file_path == "old/path.py" assert ops[0].new_path == "new/path.py" class TestParseInvalidPatch: def test_empty_patch_returns_empty_ops(self): ops, err = parse_v4a_patch("") assert err is None assert ops == [] def test_no_begin_marker_still_parses(self): patch = """\ *** Update File: f.py line1 -old +new *** End Patch""" ops, err = parse_v4a_patch(patch) assert err is None assert len(ops) == 1 def test_multiple_operations(self): patch = """\ *** Begin Patch *** Add File: a.py +content_a *** Delete File: b.py *** Update File: c.py keep -remove +add *** End Patch""" ops, err = parse_v4a_patch(patch) assert err is None assert len(ops) == 3 assert ops[0].operation == OperationType.ADD assert ops[1].operation == OperationType.DELETE assert ops[2].operation == OperationType.UPDATE class TestApplyUpdate: def test_preserves_non_prefix_pipe_characters_in_unmodified_lines(self): patch = """\ *** Begin Patch *** Update File: sample.py @@ result @@ result = 1 - return result + return result + 1 *** End Patch""" operations, err = parse_v4a_patch(patch) assert err is None class FakeFileOps: def __init__(self): self.written = None def read_file(self, path, offset=1, limit=500): return SimpleNamespace( content=( 'def run():\n' ' cmd = "echo a | sed s/a/b/"\n' ' result = 1\n' ' return result' ), error=None, ) def write_file(self, path, content): self.written = content return SimpleNamespace(error=None) file_ops = FakeFileOps() result = apply_v4a_operations(operations, file_ops) assert result.success is True assert file_ops.written == ( 'def run():\n' ' cmd = "echo a | sed s/a/b/"\n' ' result = 1\n' ' return result + 1' )