"""Phase 6 — Unified MergeEngine code path (issue #86). Verifies that run_merge() produces correct results across all four diff_units and all three resolution policies, both with and without a domain plugin, and that the musehub execute_merge_strategy() canonical router delegates to run_merge() correctly. """ from __future__ import annotations import sys import types import pytest from muse.core.merge_engine import MergeEngine, STRATEGY_MAP, run_merge # ───────────────────────────────────────────────────────────────────────────── # Fixtures / helpers # ───────────────────────────────────────────────────────────────────────────── def _oid(tag: str) -> str: return f"sha256:{'a' * 60}{tag.ljust(4, '0')[:4]}" BASE = { "a.py": _oid("b001"), "b.py": _oid("b002"), "c.py": _oid("b003"), } OURS = { "a.py": _oid("o001"), # modified "b.py": _oid("b002"), # unchanged "c.py": _oid("b003"), # unchanged "d.py": _oid("o004"), # ours-only addition } THEIRS = { "a.py": _oid("t001"), # modified (conflict with ours) "b.py": _oid("t002"), # modified (ours unchanged → clean take) "c.py": _oid("b003"), # unchanged "e.py": _oid("t005"), # theirs-only addition } # ───────────────────────────────────────────────────────────────────────────── # ME_01 — three_way / escalate: conflicts surfaced, clean changes applied # ───────────────────────────────────────────────────────────────────────────── def test_ME_01_three_way_escalate_conflicts(): engine = MergeEngine(diff_unit="three_way", resolution="escalate") result = run_merge(BASE, OURS, THEIRS, engine) # a.py diverged on both sides → conflict assert "a.py" in result.conflicts # b.py only theirs changed → clean assert "b.py" not in result.conflicts assert result.merged["files"]["b.py"] == THEIRS["b.py"] # ours-only addition present assert result.merged["files"]["d.py"] == OURS["d.py"] # theirs-only addition present assert result.merged["files"]["e.py"] == THEIRS["e.py"] # ───────────────────────────────────────────────────────────────────────────── # ME_02 — three_way / prefer_ours: no conflicts, ours wins # ───────────────────────────────────────────────────────────────────────────── def test_ME_02_three_way_prefer_ours(): engine = MergeEngine(diff_unit="three_way", resolution="prefer_ours") result = run_merge(BASE, OURS, THEIRS, engine) assert result.conflicts == [] assert result.merged["files"]["a.py"] == OURS["a.py"] # conflict → ours wins assert result.merged["files"]["b.py"] == THEIRS["b.py"] # clean theirs still applied # ───────────────────────────────────────────────────────────────────────────── # ME_03 — three_way / prefer_theirs: no conflicts, theirs wins # ───────────────────────────────────────────────────────────────────────────── def test_ME_03_three_way_prefer_theirs(): engine = MergeEngine(diff_unit="three_way", resolution="prefer_theirs") result = run_merge(BASE, OURS, THEIRS, engine) assert result.conflicts == [] assert result.merged["files"]["a.py"] == THEIRS["a.py"] # ───────────────────────────────────────────────────────────────────────────── # ME_04 — snapshot / prefer_theirs (overlay): theirs always wins # ───────────────────────────────────────────────────────────────────────────── def test_ME_04_snapshot_prefer_theirs(): engine = MergeEngine(diff_unit="snapshot", resolution="prefer_theirs") result = run_merge(BASE, OURS, THEIRS, engine) assert result.conflicts == [] # theirs wins on a.py (conflict in snapshot means same in snapshot = both have it) assert result.merged["files"]["a.py"] == THEIRS["a.py"] # ours-only d.py present (not in theirs, keep from ours via apply_merge) assert "d.py" in result.merged["files"] # theirs-only e.py present assert "e.py" in result.merged["files"] # ───────────────────────────────────────────────────────────────────────────── # ME_05 — snapshot / escalate: conflicts surfaced at snapshot level (no base) # ───────────────────────────────────────────────────────────────────────────── def test_ME_05_snapshot_escalate(): engine = MergeEngine(diff_unit="snapshot", resolution="escalate") result = run_merge(BASE, OURS, THEIRS, engine) # a.py differs in ours vs theirs with empty base → every differing path conflicts assert "a.py" in result.conflicts # ───────────────────────────────────────────────────────────────────────────── # ME_06 — replay_ours: ours delta applied onto theirs state # ───────────────────────────────────────────────────────────────────────────── def test_ME_06_replay_ours(): engine = MergeEngine(diff_unit="replay_ours", resolution="escalate") result = run_merge(BASE, OURS, THEIRS, engine) # ours changed a.py (base→ours delta), so a.py in result = ours version assert result.merged["files"]["a.py"] == OURS["a.py"] # b.py: ours unchanged, theirs changed → theirs' version in merged base (theirs state kept) assert result.merged["files"]["b.py"] == THEIRS["b.py"] # d.py: ours added (not in base) → in result assert result.merged["files"]["d.py"] == OURS["d.py"] # ───────────────────────────────────────────────────────────────────────────── # ME_07 — replay_theirs: theirs delta applied onto ours state # ───────────────────────────────────────────────────────────────────────────── def test_ME_07_replay_theirs(): engine = MergeEngine(diff_unit="replay_theirs", resolution="escalate") result = run_merge(BASE, OURS, THEIRS, engine) # theirs changed a.py → a.py in result = theirs version assert result.merged["files"]["a.py"] == THEIRS["a.py"] # d.py: ours added → preserved in ours state base assert result.merged["files"]["d.py"] == OURS["d.py"] # e.py: theirs added (base→theirs delta) → in result assert result.merged["files"]["e.py"] == THEIRS["e.py"] # ───────────────────────────────────────────────────────────────────────────── # ME_08 — STRATEGY_MAP entries produce correct engines # ───────────────────────────────────────────────────────────────────────────── def test_ME_08_strategy_map_entries(): assert STRATEGY_MAP["recursive"] == MergeEngine("three_way", "escalate") assert STRATEGY_MAP["overlay"] == MergeEngine("snapshot", "prefer_theirs") assert STRATEGY_MAP["snapshot"] == MergeEngine("snapshot", "escalate") assert STRATEGY_MAP["replay"] == MergeEngine("replay_ours", "escalate") assert STRATEGY_MAP["ours"] == MergeEngine("three_way", "prefer_ours") assert STRATEGY_MAP["theirs"] == MergeEngine("three_way", "prefer_theirs") # ───────────────────────────────────────────────────────────────────────────── # ME_09 — clean merge: no conflicts regardless of diff_unit # ───────────────────────────────────────────────────────────────────────────── def test_ME_09_clean_merge_no_conflicts(): base_m = {"a.py": _oid("b1")} ours_m = {"a.py": _oid("b1"), "b.py": _oid("o2")} # only ours adds b.py theirs_m = {"a.py": _oid("b1"), "c.py": _oid("t3")} # only theirs adds c.py for diff_unit in ("three_way", "snapshot", "replay_ours", "replay_theirs"): engine = MergeEngine(diff_unit=diff_unit, resolution="escalate") result = run_merge(base_m, ours_m, theirs_m, engine) assert result.conflicts == [], f"{diff_unit} produced unexpected conflicts" # ───────────────────────────────────────────────────────────────────────────── # ME_10 — musehub execute_merge_strategy delegates canonical routes to run_merge # ───────────────────────────────────────────────────────────────────────────── def test_ME_10_musehub_overlay_via_run_merge(): """execute_merge_strategy('overlay') uses run_merge and produces correct manifest.""" sys.path.insert(0, "/Users/gabriel/ecosystem/musehub") from musehub.services.proposal_merge_strategies import execute_merge_strategy to_m = {"a.py": _oid("t1"), "b.py": _oid("t2")} from_m = {"a.py": _oid("f1"), "c.py": _oid("f3")} result = execute_merge_strategy("overlay", to_m, from_m) # overlay = snapshot + prefer_theirs → from_branch wins on a.py assert result.manifest["a.py"] == _oid("f1") # b.py kept from to_branch (ours) assert result.manifest["b.py"] == _oid("t2") # c.py from from_branch added assert result.manifest["c.py"] == _oid("f3") assert result.conflicts == [] def test_ME_11_musehub_recursive_surfaces_conflicts(): """execute_merge_strategy('recursive') uses run_merge and surfaces conflicts.""" sys.path.insert(0, "/Users/gabriel/ecosystem/musehub") from musehub.services.proposal_merge_strategies import execute_merge_strategy, ConflictEntry base_m = {"a.py": _oid("b1")} to_m = {"a.py": _oid("t1")} # to_branch changed a.py from_m = {"a.py": _oid("f1")} # from_branch also changed a.py → conflict result = execute_merge_strategy("recursive", to_m, from_m, ancestor_manifest=base_m) assert len(result.conflicts) == 1 assert result.conflicts[0].path == "a.py" assert result.conflicts[0].resolution == "manual_required" def test_ME_12_musehub_replay_applies_from_delta(): """execute_merge_strategy('replay') applies from_branch delta onto to_branch state.""" sys.path.insert(0, "/Users/gabriel/ecosystem/musehub") from musehub.services.proposal_merge_strategies import execute_merge_strategy base_m = {"a.py": _oid("b1"), "b.py": _oid("b2")} to_m = {"a.py": _oid("b1"), "b.py": _oid("t2")} # to changed b.py only from_m = {"a.py": _oid("f1"), "b.py": _oid("b2")} # from changed a.py only result = execute_merge_strategy("replay", to_m, from_m, ancestor_manifest=base_m) # replay_ours: ours is to_m, theirs is from_m → apply ours-delta (base→to) onto theirs state # ours delta: b.py changed → apply b.py change onto from_m state assert result.conflicts == []