"""Phase 2 — OR-Set CRDT semantics + symbol-level independence-aware merge. Tier 1 — OR-Set for imports and variables ------------------------------------------ Import and variable additions from concurrent branches are always independent: add-wins, never conflict. This is the OR-Set guarantee: union of additions, with tombstones only when *both* sides agree on a deletion. Tier 2 — Symbol-level independence for functions and classes ------------------------------------------------------------- Two branches that add or modify *different* named symbols in the same file should produce a clean merged file containing all changes. Currently they produce a spurious file-level conflict because the raw blob IDs diverge. The fix: when merge_ops() finds that all child ops across PatchOps for the same file commute (no symbol-level conflict), it reconstructs the merged blob via three_way_merge_lines() and writes it to the object store. A clean text merge removes the file from the conflict list and updates the manifest. Test categories --------------- TestORSetImports — concurrent import adds never conflict (Tier 1) TestORSetVariables — concurrent variable adds never conflict (Tier 1) TestSymbolIndependence — concurrent adds/edits of different symbols are clean (Tier 2) TestSymbolConflictPreserved — genuine same-symbol conflicts still surface (Tier 2) TestMergedBlobCorrectness — merged file content is complete and well-formed """ from __future__ import annotations from collections.abc import Mapping from typing import TYPE_CHECKING import pathlib import textwrap import pytest from muse.plugins.code.plugin import CodePlugin from muse.core.types import blob_id if TYPE_CHECKING: from muse.domain import MergeResult # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _oid(content: bytes) -> str: return blob_id(content) def _write_blob(root: pathlib.Path, content: bytes) -> str: from muse.core.object_store import write_object oid = _oid(content) write_object(root, oid, content) return oid def _snap(root: pathlib.Path, files: Mapping[str, bytes]) -> Mapping[str, object]: return { "files": {path: _write_blob(root, content) for path, content in files.items()}, "domain": "code", "directories": [], } def _read_merged_blob(root: pathlib.Path, result: "MergeResult", path: str) -> str: from muse.core.object_store import read_object oid = result.merged["files"][path] raw = read_object(root, oid) assert raw is not None, f"merged blob for {path} not in object store" return raw.decode("utf-8") # --------------------------------------------------------------------------- # Tier 1 — OR-Set for imports # --------------------------------------------------------------------------- class TestORSetImports: """Concurrent import additions from two branches must never conflict.""" def test_concurrent_import_adds_no_conflict(self, tmp_path: pathlib.Path) -> None: """Branch A adds 'import os', branch B adds 'import sys' → clean merge.""" plugin = CodePlugin() base_src = b"# utils.py\n\ndef process(): pass\n" ours_src = b"# utils.py\nimport os\n\ndef process(): pass\n" theirs_src = b"# utils.py\nimport sys\n\ndef process(): pass\n" base = _snap(tmp_path, {"src/utils.py": base_src}) ours = _snap(tmp_path, {"src/utils.py": ours_src}) theirs = _snap(tmp_path, {"src/utils.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) assert "src/utils.py" not in result.conflicts, ( "Concurrent import additions must not conflict — OR-Set semantics" ) def test_concurrent_import_adds_both_present_in_merged(self, tmp_path: pathlib.Path) -> None: """The merged file must contain both imports.""" plugin = CodePlugin() base_src = b"# utils.py\n\ndef process(): pass\n" ours_src = b"# utils.py\nimport os\n\ndef process(): pass\n" theirs_src = b"# utils.py\nimport sys\n\ndef process(): pass\n" base = _snap(tmp_path, {"src/utils.py": base_src}) ours = _snap(tmp_path, {"src/utils.py": ours_src}) theirs = _snap(tmp_path, {"src/utils.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) merged_text = _read_merged_blob(tmp_path, result, "src/utils.py") assert "import os" in merged_text, "ours import must survive in merged blob" assert "import sys" in merged_text, "theirs import must survive in merged blob" def test_three_concurrent_import_adds_all_survive(self, tmp_path: pathlib.Path) -> None: """Even with multiple imports added per side, all survive.""" plugin = CodePlugin() base_src = b"def fn(): pass\n" ours_src = b"import os\nimport pathlib\n\ndef fn(): pass\n" theirs_src = b"import sys\nimport json\n\ndef fn(): pass\n" base = _snap(tmp_path, {"lib.py": base_src}) ours = _snap(tmp_path, {"lib.py": ours_src}) theirs = _snap(tmp_path, {"lib.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) assert "lib.py" not in result.conflicts merged_text = _read_merged_blob(tmp_path, result, "lib.py") for imp in ("import os", "import pathlib", "import sys", "import json"): assert imp in merged_text, f"{imp} missing from merged blob" def test_same_import_added_on_both_sides_deduplicates(self, tmp_path: pathlib.Path) -> None: """Both branches adding the same import → one copy in merged file, no conflict.""" plugin = CodePlugin() base_src = b"def fn(): pass\n" both_src = b"import os\n\ndef fn(): pass\n" base = _snap(tmp_path, {"lib.py": base_src}) ours = _snap(tmp_path, {"lib.py": both_src}) theirs = _snap(tmp_path, {"lib.py": both_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) assert "lib.py" not in result.conflicts merged_text = _read_merged_blob(tmp_path, result, "lib.py") assert merged_text.count("import os") == 1, "duplicate import must be deduplicated" # --------------------------------------------------------------------------- # Tier 1 — OR-Set for variables # --------------------------------------------------------------------------- class TestORSetVariables: """Concurrent top-level variable additions must never conflict.""" def test_concurrent_variable_adds_no_conflict(self, tmp_path: pathlib.Path) -> None: """Branch A adds MAX=100, branch B adds MIN=0 → clean merge.""" plugin = CodePlugin() base_src = b"def fn(): pass\n" ours_src = b"MAX = 100\n\ndef fn(): pass\n" theirs_src = b"MIN = 0\n\ndef fn(): pass\n" base = _snap(tmp_path, {"config.py": base_src}) ours = _snap(tmp_path, {"config.py": ours_src}) theirs = _snap(tmp_path, {"config.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) assert "config.py" not in result.conflicts, ( "Concurrent variable additions must not conflict" ) def test_concurrent_variable_adds_both_present(self, tmp_path: pathlib.Path) -> None: """Both variables appear in the merged file.""" plugin = CodePlugin() base_src = b"def fn(): pass\n" ours_src = b"MAX = 100\n\ndef fn(): pass\n" theirs_src = b"MIN = 0\n\ndef fn(): pass\n" base = _snap(tmp_path, {"config.py": base_src}) ours = _snap(tmp_path, {"config.py": ours_src}) theirs = _snap(tmp_path, {"config.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) merged_text = _read_merged_blob(tmp_path, result, "config.py") assert "MAX = 100" in merged_text assert "MIN = 0" in merged_text # --------------------------------------------------------------------------- # Tier 2 — Symbol independence for functions and classes # --------------------------------------------------------------------------- class TestSymbolIndependence: """Non-overlapping symbol changes in the same file must produce a clean merge.""" def test_concurrent_function_adds_no_conflict(self, tmp_path: pathlib.Path) -> None: """Branch A adds def foo(), branch B adds def bar() → clean merge.""" plugin = CodePlugin() base_src = textwrap.dedent("""\ def existing(): pass """).encode() ours_src = textwrap.dedent("""\ def existing(): pass def foo(): return 1 """).encode() theirs_src = textwrap.dedent("""\ def existing(): pass def bar(): return 2 """).encode() base = _snap(tmp_path, {"module.py": base_src}) ours = _snap(tmp_path, {"module.py": ours_src}) theirs = _snap(tmp_path, {"module.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) assert "module.py" not in result.conflicts, ( "Concurrent additions of different functions must not conflict" ) def test_concurrent_function_adds_both_present(self, tmp_path: pathlib.Path) -> None: """Both added functions appear in the merged file.""" plugin = CodePlugin() base_src = b"def existing(): pass\n" ours_src = b"def existing(): pass\n\ndef foo():\n return 1\n" theirs_src = b"def existing(): pass\n\ndef bar():\n return 2\n" base = _snap(tmp_path, {"module.py": base_src}) ours = _snap(tmp_path, {"module.py": ours_src}) theirs = _snap(tmp_path, {"module.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) merged_text = _read_merged_blob(tmp_path, result, "module.py") assert "def foo" in merged_text, "ours function must appear in merged file" assert "def bar" in merged_text, "theirs function must appear in merged file" assert "def existing" in merged_text, "base function must be preserved" def test_different_functions_modified_no_conflict(self, tmp_path: pathlib.Path) -> None: """Branch A modifies foo(), branch B modifies bar() → clean merge.""" plugin = CodePlugin() base_src = textwrap.dedent("""\ def foo(): return 0 def bar(): return 0 """).encode() ours_src = textwrap.dedent("""\ def foo(): return 1 def bar(): return 0 """).encode() theirs_src = textwrap.dedent("""\ def foo(): return 0 def bar(): return 2 """).encode() base = _snap(tmp_path, {"module.py": base_src}) ours = _snap(tmp_path, {"module.py": ours_src}) theirs = _snap(tmp_path, {"module.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) assert "module.py" not in result.conflicts, ( "Modifications to different functions must not conflict" ) def test_different_functions_modified_both_present(self, tmp_path: pathlib.Path) -> None: """The merged file has ours' version of foo() and theirs' version of bar().""" plugin = CodePlugin() base_src = b"def foo():\n return 0\n\ndef bar():\n return 0\n" ours_src = b"def foo():\n return 1\n\ndef bar():\n return 0\n" theirs_src = b"def foo():\n return 0\n\ndef bar():\n return 2\n" base = _snap(tmp_path, {"module.py": base_src}) ours = _snap(tmp_path, {"module.py": ours_src}) theirs = _snap(tmp_path, {"module.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) merged_text = _read_merged_blob(tmp_path, result, "module.py") assert "return 1" in merged_text, "ours change to foo() must be in merged file" assert "return 2" in merged_text, "theirs change to bar() must be in merged file" def test_concurrent_class_adds_no_conflict(self, tmp_path: pathlib.Path) -> None: """Branch A adds class Foo, branch B adds class Bar → clean merge.""" plugin = CodePlugin() base_src = b"# module\n" ours_src = b"# module\n\nclass Foo:\n pass\n" theirs_src = b"# module\n\nclass Bar:\n pass\n" base = _snap(tmp_path, {"module.py": base_src}) ours = _snap(tmp_path, {"module.py": ours_src}) theirs = _snap(tmp_path, {"module.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) assert "module.py" not in result.conflicts def test_independent_changes_in_multiple_files_all_clean(self, tmp_path: pathlib.Path) -> None: """Multiple files with independent changes all merge cleanly.""" plugin = CodePlugin() base = _snap(tmp_path, { "a.py": b"def fa(): pass\n", "b.py": b"def fb(): pass\n", }) ours = _snap(tmp_path, { "a.py": b"def fa(): pass\n\ndef fa2(): pass\n", "b.py": b"def fb(): pass\n", }) theirs = _snap(tmp_path, { "a.py": b"def fa(): pass\n", "b.py": b"def fb(): pass\n\ndef fb2(): pass\n", }) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) assert result.conflicts == [], f"Expected no conflicts, got: {result.conflicts}" # --------------------------------------------------------------------------- # Tier 2 — Genuine conflicts still surface # --------------------------------------------------------------------------- class TestSymbolConflictPreserved: """Genuine same-symbol conflicts must still be detected and reported.""" def test_same_function_modified_both_sides_conflicts(self, tmp_path: pathlib.Path) -> None: """Both branches modified the same function body → real conflict.""" plugin = CodePlugin() base_src = b"def compute():\n return 0\n" ours_src = b"def compute():\n return 1\n" theirs_src = b"def compute():\n return 2\n" base = _snap(tmp_path, {"ops.py": base_src}) ours = _snap(tmp_path, {"ops.py": ours_src}) theirs = _snap(tmp_path, {"ops.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) assert any("ops.py" in c for c in result.conflicts), ( "Same-function conflict must still be detected" ) def test_mixed_file_some_symbols_conflict_some_independent( self, tmp_path: pathlib.Path ) -> None: """When one symbol conflicts, the file is in conflicts — independent ones don't suppress it.""" plugin = CodePlugin() base_src = textwrap.dedent("""\ def shared(): return 0 def independent_a(): pass """).encode() ours_src = textwrap.dedent("""\ def shared(): return 1 def independent_a(): pass def only_on_ours(): pass """).encode() theirs_src = textwrap.dedent("""\ def shared(): return 2 def independent_a(): pass def only_on_theirs(): pass """).encode() base = _snap(tmp_path, {"mixed.py": base_src}) ours = _snap(tmp_path, {"mixed.py": ours_src}) theirs = _snap(tmp_path, {"mixed.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) # The shared() conflict must surface — even though independent symbols exist assert any("mixed.py" in c for c in result.conflicts), ( "File with a genuine symbol conflict must still appear in conflicts" ) # --------------------------------------------------------------------------- # Merged blob correctness # --------------------------------------------------------------------------- class TestMergedBlobCorrectness: """Reconstructed blobs must be syntactically valid and not contain conflict markers.""" def test_merged_blob_has_no_conflict_markers(self, tmp_path: pathlib.Path) -> None: """Blobs auto-resolved via independence must not contain <<<<<<< markers.""" plugin = CodePlugin() base_src = b"def existing(): pass\n" ours_src = b"def existing(): pass\n\ndef foo(): return 1\n" theirs_src = b"def existing(): pass\n\ndef bar(): return 2\n" base = _snap(tmp_path, {"m.py": base_src}) ours = _snap(tmp_path, {"m.py": ours_src}) theirs = _snap(tmp_path, {"m.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) merged_text = _read_merged_blob(tmp_path, result, "m.py") assert "<<<<<<<" not in merged_text, "auto-resolved blob must not contain conflict markers" assert "=======" not in merged_text assert ">>>>>>>" not in merged_text def test_merged_blob_is_valid_python(self, tmp_path: pathlib.Path) -> None: """Reconstructed blob must parse without SyntaxError.""" import ast plugin = CodePlugin() base_src = b"def existing(): pass\n" ours_src = b"def existing(): pass\n\ndef foo():\n return 1\n" theirs_src = b"def existing(): pass\n\ndef bar():\n return 2\n" base = _snap(tmp_path, {"m.py": base_src}) ours = _snap(tmp_path, {"m.py": ours_src}) theirs = _snap(tmp_path, {"m.py": theirs_src}) result = plugin.merge_ops(base, ours, theirs, [], [], repo_root=tmp_path) merged_text = _read_merged_blob(tmp_path, result, "m.py") try: ast.parse(merged_text) except SyntaxError as exc: pytest.fail(f"Merged blob is not valid Python: {exc}\n\n{merged_text}")