gabriel / muse public
test_phase6_unified_merge_engine.py python
219 lines 13.1 KB
Raw
sha256:ecfc7b5d19db951f256942ac0908b53d55a2da37c6cd1e6cf85b4a6088870865 feat(phase6): unified MergeEngine code path via run_merge() Sonnet 4.6 patch 21 hours ago
1 """Phase 6 — Unified MergeEngine code path (issue #86).
2
3 Verifies that run_merge() produces correct results across all four diff_units
4 and all three resolution policies, both with and without a domain plugin,
5 and that the musehub execute_merge_strategy() canonical router delegates to
6 run_merge() correctly.
7 """
8 from __future__ import annotations
9
10 import sys
11 import types
12
13 import pytest
14
15 from muse.core.merge_engine import MergeEngine, STRATEGY_MAP, run_merge
16
17
18 # ─────────────────────────────────────────────────────────────────────────────
19 # Fixtures / helpers
20 # ─────────────────────────────────────────────────────────────────────────────
21
22 def _oid(tag: str) -> str:
23 return f"sha256:{'a' * 60}{tag.ljust(4, '0')[:4]}"
24
25
26 BASE = {
27 "a.py": _oid("b001"),
28 "b.py": _oid("b002"),
29 "c.py": _oid("b003"),
30 }
31
32 OURS = {
33 "a.py": _oid("o001"), # modified
34 "b.py": _oid("b002"), # unchanged
35 "c.py": _oid("b003"), # unchanged
36 "d.py": _oid("o004"), # ours-only addition
37 }
38
39 THEIRS = {
40 "a.py": _oid("t001"), # modified (conflict with ours)
41 "b.py": _oid("t002"), # modified (ours unchanged → clean take)
42 "c.py": _oid("b003"), # unchanged
43 "e.py": _oid("t005"), # theirs-only addition
44 }
45
46
47 # ─────────────────────────────────────────────────────────────────────────────
48 # ME_01 — three_way / escalate: conflicts surfaced, clean changes applied
49 # ─────────────────────────────────────────────────────────────────────────────
50
51 def test_ME_01_three_way_escalate_conflicts():
52 engine = MergeEngine(diff_unit="three_way", resolution="escalate")
53 result = run_merge(BASE, OURS, THEIRS, engine)
54 # a.py diverged on both sides → conflict
55 assert "a.py" in result.conflicts
56 # b.py only theirs changed → clean
57 assert "b.py" not in result.conflicts
58 assert result.merged["files"]["b.py"] == THEIRS["b.py"]
59 # ours-only addition present
60 assert result.merged["files"]["d.py"] == OURS["d.py"]
61 # theirs-only addition present
62 assert result.merged["files"]["e.py"] == THEIRS["e.py"]
63
64
65 # ─────────────────────────────────────────────────────────────────────────────
66 # ME_02 — three_way / prefer_ours: no conflicts, ours wins
67 # ─────────────────────────────────────────────────────────────────────────────
68
69 def test_ME_02_three_way_prefer_ours():
70 engine = MergeEngine(diff_unit="three_way", resolution="prefer_ours")
71 result = run_merge(BASE, OURS, THEIRS, engine)
72 assert result.conflicts == []
73 assert result.merged["files"]["a.py"] == OURS["a.py"] # conflict → ours wins
74 assert result.merged["files"]["b.py"] == THEIRS["b.py"] # clean theirs still applied
75
76
77 # ─────────────────────────────────────────────────────────────────────────────
78 # ME_03 — three_way / prefer_theirs: no conflicts, theirs wins
79 # ─────────────────────────────────────────────────────────────────────────────
80
81 def test_ME_03_three_way_prefer_theirs():
82 engine = MergeEngine(diff_unit="three_way", resolution="prefer_theirs")
83 result = run_merge(BASE, OURS, THEIRS, engine)
84 assert result.conflicts == []
85 assert result.merged["files"]["a.py"] == THEIRS["a.py"]
86
87
88 # ─────────────────────────────────────────────────────────────────────────────
89 # ME_04 — snapshot / prefer_theirs (overlay): theirs always wins
90 # ─────────────────────────────────────────────────────────────────────────────
91
92 def test_ME_04_snapshot_prefer_theirs():
93 engine = MergeEngine(diff_unit="snapshot", resolution="prefer_theirs")
94 result = run_merge(BASE, OURS, THEIRS, engine)
95 assert result.conflicts == []
96 # theirs wins on a.py (conflict in snapshot means same in snapshot = both have it)
97 assert result.merged["files"]["a.py"] == THEIRS["a.py"]
98 # ours-only d.py present (not in theirs, keep from ours via apply_merge)
99 assert "d.py" in result.merged["files"]
100 # theirs-only e.py present
101 assert "e.py" in result.merged["files"]
102
103
104 # ─────────────────────────────────────────────────────────────────────────────
105 # ME_05 — snapshot / escalate: conflicts surfaced at snapshot level (no base)
106 # ─────────────────────────────────────────────────────────────────────────────
107
108 def test_ME_05_snapshot_escalate():
109 engine = MergeEngine(diff_unit="snapshot", resolution="escalate")
110 result = run_merge(BASE, OURS, THEIRS, engine)
111 # a.py differs in ours vs theirs with empty base → every differing path conflicts
112 assert "a.py" in result.conflicts
113
114
115 # ─────────────────────────────────────────────────────────────────────────────
116 # ME_06 — replay_ours: ours delta applied onto theirs state
117 # ─────────────────────────────────────────────────────────────────────────────
118
119 def test_ME_06_replay_ours():
120 engine = MergeEngine(diff_unit="replay_ours", resolution="escalate")
121 result = run_merge(BASE, OURS, THEIRS, engine)
122 # ours changed a.py (base→ours delta), so a.py in result = ours version
123 assert result.merged["files"]["a.py"] == OURS["a.py"]
124 # b.py: ours unchanged, theirs changed → theirs' version in merged base (theirs state kept)
125 assert result.merged["files"]["b.py"] == THEIRS["b.py"]
126 # d.py: ours added (not in base) → in result
127 assert result.merged["files"]["d.py"] == OURS["d.py"]
128
129
130 # ─────────────────────────────────────────────────────────────────────────────
131 # ME_07 — replay_theirs: theirs delta applied onto ours state
132 # ─────────────────────────────────────────────────────────────────────────────
133
134 def test_ME_07_replay_theirs():
135 engine = MergeEngine(diff_unit="replay_theirs", resolution="escalate")
136 result = run_merge(BASE, OURS, THEIRS, engine)
137 # theirs changed a.py → a.py in result = theirs version
138 assert result.merged["files"]["a.py"] == THEIRS["a.py"]
139 # d.py: ours added → preserved in ours state base
140 assert result.merged["files"]["d.py"] == OURS["d.py"]
141 # e.py: theirs added (base→theirs delta) → in result
142 assert result.merged["files"]["e.py"] == THEIRS["e.py"]
143
144
145 # ─────────────────────────────────────────────────────────────────────────────
146 # ME_08 — STRATEGY_MAP entries produce correct engines
147 # ─────────────────────────────────────────────────────────────────────────────
148
149 def test_ME_08_strategy_map_entries():
150 assert STRATEGY_MAP["recursive"] == MergeEngine("three_way", "escalate")
151 assert STRATEGY_MAP["overlay"] == MergeEngine("snapshot", "prefer_theirs")
152 assert STRATEGY_MAP["snapshot"] == MergeEngine("snapshot", "escalate")
153 assert STRATEGY_MAP["replay"] == MergeEngine("replay_ours", "escalate")
154 assert STRATEGY_MAP["ours"] == MergeEngine("three_way", "prefer_ours")
155 assert STRATEGY_MAP["theirs"] == MergeEngine("three_way", "prefer_theirs")
156
157
158 # ─────────────────────────────────────────────────────────────────────────────
159 # ME_09 — clean merge: no conflicts regardless of diff_unit
160 # ─────────────────────────────────────────────────────────────────────────────
161
162 def test_ME_09_clean_merge_no_conflicts():
163 base_m = {"a.py": _oid("b1")}
164 ours_m = {"a.py": _oid("b1"), "b.py": _oid("o2")} # only ours adds b.py
165 theirs_m = {"a.py": _oid("b1"), "c.py": _oid("t3")} # only theirs adds c.py
166
167 for diff_unit in ("three_way", "snapshot", "replay_ours", "replay_theirs"):
168 engine = MergeEngine(diff_unit=diff_unit, resolution="escalate")
169 result = run_merge(base_m, ours_m, theirs_m, engine)
170 assert result.conflicts == [], f"{diff_unit} produced unexpected conflicts"
171
172
173 # ─────────────────────────────────────────────────────────────────────────────
174 # ME_10 — musehub execute_merge_strategy delegates canonical routes to run_merge
175 # ─────────────────────────────────────────────────────────────────────────────
176
177 def test_ME_10_musehub_overlay_via_run_merge():
178 """execute_merge_strategy('overlay') uses run_merge and produces correct manifest."""
179 sys.path.insert(0, "/Users/gabriel/ecosystem/musehub")
180 from musehub.services.proposal_merge_strategies import execute_merge_strategy
181
182 to_m = {"a.py": _oid("t1"), "b.py": _oid("t2")}
183 from_m = {"a.py": _oid("f1"), "c.py": _oid("f3")}
184 result = execute_merge_strategy("overlay", to_m, from_m)
185 # overlay = snapshot + prefer_theirs → from_branch wins on a.py
186 assert result.manifest["a.py"] == _oid("f1")
187 # b.py kept from to_branch (ours)
188 assert result.manifest["b.py"] == _oid("t2")
189 # c.py from from_branch added
190 assert result.manifest["c.py"] == _oid("f3")
191 assert result.conflicts == []
192
193
194 def test_ME_11_musehub_recursive_surfaces_conflicts():
195 """execute_merge_strategy('recursive') uses run_merge and surfaces conflicts."""
196 sys.path.insert(0, "/Users/gabriel/ecosystem/musehub")
197 from musehub.services.proposal_merge_strategies import execute_merge_strategy, ConflictEntry
198
199 base_m = {"a.py": _oid("b1")}
200 to_m = {"a.py": _oid("t1")} # to_branch changed a.py
201 from_m = {"a.py": _oid("f1")} # from_branch also changed a.py → conflict
202 result = execute_merge_strategy("recursive", to_m, from_m, ancestor_manifest=base_m)
203 assert len(result.conflicts) == 1
204 assert result.conflicts[0].path == "a.py"
205 assert result.conflicts[0].resolution == "manual_required"
206
207
208 def test_ME_12_musehub_replay_applies_from_delta():
209 """execute_merge_strategy('replay') applies from_branch delta onto to_branch state."""
210 sys.path.insert(0, "/Users/gabriel/ecosystem/musehub")
211 from musehub.services.proposal_merge_strategies import execute_merge_strategy
212
213 base_m = {"a.py": _oid("b1"), "b.py": _oid("b2")}
214 to_m = {"a.py": _oid("b1"), "b.py": _oid("t2")} # to changed b.py only
215 from_m = {"a.py": _oid("f1"), "b.py": _oid("b2")} # from changed a.py only
216 result = execute_merge_strategy("replay", to_m, from_m, ancestor_manifest=base_m)
217 # replay_ours: ours is to_m, theirs is from_m → apply ours-delta (base→to) onto theirs state
218 # ours delta: b.py changed → apply b.py change onto from_m state
219 assert result.conflicts == []
File History 1 commit
sha256:ecfc7b5d19db951f256942ac0908b53d55a2da37c6cd1e6cf85b4a6088870865 feat(phase6): unified MergeEngine code path via run_merge() Sonnet 4.6 patch 21 hours ago