gabriel / muse public

test_resolve_phase4.py file-level

at sha256:c · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:b adding issues docs to bust staging mpack prebuild cache. · gabriel · Jun 20, 2026
1 """Phase 4 of issue #8: Harmony integration β€” manually_resolved tracking.
2
3 Coverage
4 --------
5 MERGE_STATE:
6 - resolve_path records cleared entries in manually_resolved
7 - resolve_symbol records the cleared entry in manually_resolved
8 - manually_resolved accumulates across multiple resolve calls
9 - manually_resolved is preserved through _write_conflict_paths round-trips
10 - original_conflict_paths is never affected by resolve
11 - read_merge_state round-trips manually_resolved correctly
12 - legacy MERGE_STATE (no manually_resolved key) defaults to empty list
13
14 record_resolutions:
15 - manually_resolved=None β†’ legacy: all paths get confidence=1.0, human_verified=True
16 - manually_resolved=set() β†’ tracking active, none manually resolved β†’ confidence=0.8, human_verified=False
17 - path IN manually_resolved β†’ confidence=1.0, human_verified=True
18 - path NOT IN manually_resolved β†’ confidence=0.8, human_verified=False
19 - mixed: some manual, some side-picked β†’ each gets correct values
20 - rationale text reflects manual vs side-picked
21
22 End-to-end:
23 - full flow: muse resolve β†’ commit β†’ harmony patterns have correct confidence
24 - checkout --ours side-pick then commit β†’ lower confidence when muse resolve also used
25 """
26
27 from __future__ import annotations
28
29 import json
30 import os
31 import pathlib
32
33 import pytest
34
35 from muse.core.harmony import (
36 list_patterns,
37 list_resolutions,
38 record_resolutions,
39 )
40 from muse.core.merge_engine import (
41 MergeState,
42 read_merge_state,
43 resolve_path,
44 resolve_symbol,
45 write_merge_state,
46 )
47 from muse.core.object_store import write_object
48 from muse.core.paths import muse_dir
49 from muse.core.types import MUSE_DIR, Manifest, blob_id, fake_id
50 from tests.cli_test_helper import CliRunner
51
52 runner = CliRunner()
53
54 _BASE = fake_id("base")
55 _OURS = fake_id("ours")
56 _THEIRS = fake_id("theirs")
57
58
59 # ---------------------------------------------------------------------------
60 # Helpers
61 # ---------------------------------------------------------------------------
62
63 def _repo(tmp_path: pathlib.Path) -> pathlib.Path:
64 (tmp_path / MUSE_DIR).mkdir()
65 return tmp_path
66
67
68 def _harmony_repo(tmp_path: pathlib.Path) -> pathlib.Path:
69 muse_dir(tmp_path).mkdir()
70 return tmp_path
71
72
73 def _write(root: pathlib.Path, conflicts: list[str]) -> None:
74 write_merge_state(
75 root,
76 base_commit=_BASE,
77 ours_commit=_OURS,
78 theirs_commit=_THEIRS,
79 conflict_paths=conflicts,
80 other_branch="feat/x",
81 )
82
83
84 def _state(root: pathlib.Path) -> MergeState:
85 s = read_merge_state(root)
86 assert s is not None
87 return s
88
89
90 def _write_object(root: pathlib.Path, content: bytes) -> str:
91 oid = blob_id(content)
92 write_object(root, oid, content)
93 return oid
94
95
96 class _FakePlugin:
97 name = "test"
98 def schema(self) -> "dict[str, object]": return {}
99
100
101 def _invoke(repo: pathlib.Path, args: list[str]) -> "InvokeResult": # type: ignore[name-defined]
102 saved = os.getcwd()
103 try:
104 os.chdir(repo)
105 return runner.invoke(None, args)
106 finally:
107 os.chdir(saved)
108
109
110 # ---------------------------------------------------------------------------
111 # MERGE_STATE β€” manually_resolved tracking
112 # ---------------------------------------------------------------------------
113
114 class TestManuallyResolvedTracking:
115 def test_resolve_path_records_in_manually_resolved(
116 self, tmp_path: pathlib.Path
117 ) -> None:
118 repo = _repo(tmp_path)
119 _write(repo, ["hello.md", "world.md"])
120 resolve_path(repo, "hello.md")
121 assert _state(repo).manually_resolved == ["hello.md"]
122
123 def test_resolve_path_symbol_entries_recorded(
124 self, tmp_path: pathlib.Path
125 ) -> None:
126 repo = _repo(tmp_path)
127 _write(repo, ["hello.md::A", "hello.md::B", "other.py"])
128 resolve_path(repo, "hello.md")
129 mr = _state(repo).manually_resolved
130 assert "hello.md::A" in mr
131 assert "hello.md::B" in mr
132 assert "other.py" not in mr
133
134 def test_resolve_symbol_records_in_manually_resolved(
135 self, tmp_path: pathlib.Path
136 ) -> None:
137 repo = _repo(tmp_path)
138 _write(repo, ["hello.md::A", "hello.md::B"])
139 resolve_symbol(repo, "hello.md::A")
140 assert "hello.md::A" in _state(repo).manually_resolved
141 assert "hello.md::B" not in _state(repo).manually_resolved
142
143 def test_manually_resolved_accumulates_across_calls(
144 self, tmp_path: pathlib.Path
145 ) -> None:
146 repo = _repo(tmp_path)
147 _write(repo, ["a.py", "b.py", "c.py"])
148 resolve_symbol(repo, "a.py")
149 resolve_symbol(repo, "b.py")
150 mr = _state(repo).manually_resolved
151 assert "a.py" in mr
152 assert "b.py" in mr
153 assert "c.py" not in mr
154
155 def test_resolve_path_noop_does_not_add_to_manually_resolved(
156 self, tmp_path: pathlib.Path
157 ) -> None:
158 repo = _repo(tmp_path)
159 _write(repo, ["other.py"])
160 resolve_path(repo, "hello.md") # not in conflict_paths
161 assert _state(repo).manually_resolved == []
162
163 def test_resolve_symbol_noop_does_not_add_to_manually_resolved(
164 self, tmp_path: pathlib.Path
165 ) -> None:
166 repo = _repo(tmp_path)
167 _write(repo, ["other.py"])
168 resolve_symbol(repo, "hello.md") # not in conflict_paths
169 assert _state(repo).manually_resolved == []
170
171 def test_manually_resolved_does_not_affect_original_conflict_paths(
172 self, tmp_path: pathlib.Path
173 ) -> None:
174 repo = _repo(tmp_path)
175 _write(repo, ["a.py", "b.py"])
176 original = _state(repo).original_conflict_paths[:]
177 resolve_path(repo, "a.py")
178 assert _state(repo).original_conflict_paths == original
179
180 def test_read_write_round_trip_manually_resolved(
181 self, tmp_path: pathlib.Path
182 ) -> None:
183 repo = _repo(tmp_path)
184 _write(repo, ["a.py::X", "b.py"])
185 resolve_symbol(repo, "a.py::X")
186 resolve_symbol(repo, "b.py")
187 state = _state(repo)
188 assert sorted(state.manually_resolved) == ["a.py::X", "b.py"]
189 assert state.conflict_paths == []
190 assert sorted(state.original_conflict_paths) == ["a.py::X", "b.py"]
191
192 def test_legacy_merge_state_no_manually_resolved_key(
193 self, tmp_path: pathlib.Path
194 ) -> None:
195 """MERGE_STATE without manually_resolved key (pre-Phase-4) β†’ empty list."""
196 repo = _repo(tmp_path)
197 merge_state_path = repo / MUSE_DIR / "MERGE_STATE.json"
198 merge_state_path.write_text(json.dumps({
199 "base_commit": _BASE,
200 "ours_commit": _OURS,
201 "theirs_commit": _THEIRS,
202 "conflict_paths": ["hello.md"],
203 "original_conflict_paths": ["hello.md"],
204 }), encoding="utf-8")
205 state = _state(repo)
206 assert state.manually_resolved == []
207
208
209 # ---------------------------------------------------------------------------
210 # record_resolutions β€” manually_resolved parameter
211 # ---------------------------------------------------------------------------
212
213 class TestRecordResolutionsManuallyResolved:
214 def test_none_gives_legacy_human_verified_true(
215 self, tmp_path: pathlib.Path
216 ) -> None:
217 """manually_resolved=None β†’ legacy: confidence=1.0, human_verified=True."""
218 repo = _harmony_repo(tmp_path)
219 ours_id = _write_object(repo, b"ours")
220 theirs_id = _write_object(repo, b"theirs")
221 res_id = _write_object(repo, b"resolved")
222 ours_m: Manifest = {"f.py": ours_id}
223 theirs_m: Manifest = {"f.py": theirs_id}
224 new_m: Manifest = {"f.py": res_id}
225 plugin = _FakePlugin()
226 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", plugin, manually_resolved=None)
227 res = list_resolutions(repo, list_patterns(repo)[0].pattern_id)
228 assert res[0].human_verified is True
229 assert res[0].confidence == 1.0
230
231 def test_empty_set_gives_low_confidence(
232 self, tmp_path: pathlib.Path
233 ) -> None:
234 """manually_resolved={} β†’ tracking active, path not manually resolved β†’ confidence=0.8."""
235 repo = _harmony_repo(tmp_path)
236 ours_id = _write_object(repo, b"ours")
237 theirs_id = _write_object(repo, b"theirs")
238 res_id = _write_object(repo, b"resolved")
239 ours_m: Manifest = {"f.py": ours_id}
240 theirs_m: Manifest = {"f.py": theirs_id}
241 new_m: Manifest = {"f.py": res_id}
242 plugin = _FakePlugin()
243 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", plugin, manually_resolved=set())
244 res = list_resolutions(repo, list_patterns(repo)[0].pattern_id)
245 assert res[0].human_verified is False
246 assert res[0].confidence == 0.8
247
248 def test_path_in_manually_resolved_gets_high_confidence(
249 self, tmp_path: pathlib.Path
250 ) -> None:
251 repo = _harmony_repo(tmp_path)
252 ours_id = _write_object(repo, b"ours")
253 theirs_id = _write_object(repo, b"theirs")
254 res_id = _write_object(repo, b"resolved")
255 ours_m: Manifest = {"f.py": ours_id}
256 theirs_m: Manifest = {"f.py": theirs_id}
257 new_m: Manifest = {"f.py": res_id}
258 plugin = _FakePlugin()
259 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", plugin, manually_resolved={"f.py"})
260 res = list_resolutions(repo, list_patterns(repo)[0].pattern_id)
261 assert res[0].human_verified is True
262 assert res[0].confidence == 1.0
263
264 def test_path_not_in_manually_resolved_gets_low_confidence(
265 self, tmp_path: pathlib.Path
266 ) -> None:
267 repo = _harmony_repo(tmp_path)
268 ours_id = _write_object(repo, b"ours")
269 theirs_id = _write_object(repo, b"theirs")
270 res_id = _write_object(repo, b"resolved")
271 ours_m: Manifest = {"f.py": ours_id}
272 theirs_m: Manifest = {"f.py": theirs_id}
273 new_m: Manifest = {"f.py": res_id}
274 plugin = _FakePlugin()
275 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", plugin, manually_resolved={"other.py"})
276 res = list_resolutions(repo, list_patterns(repo)[0].pattern_id)
277 assert res[0].human_verified is False
278 assert res[0].confidence == 0.8
279
280 def test_mixed_paths_get_different_confidence(
281 self, tmp_path: pathlib.Path
282 ) -> None:
283 """Two paths: one manually resolved, one side-picked β†’ different confidence."""
284 repo = _harmony_repo(tmp_path)
285 a_ours = _write_object(repo, b"a-ours")
286 a_theirs = _write_object(repo, b"a-theirs")
287 a_res = _write_object(repo, b"a-resolved")
288 b_ours = _write_object(repo, b"b-ours")
289 b_theirs = _write_object(repo, b"b-theirs")
290 b_res = _write_object(repo, b"b-resolved")
291 ours_m: Manifest = {"a.py": a_ours, "b.py": b_ours}
292 theirs_m: Manifest = {"a.py": a_theirs, "b.py": b_theirs}
293 new_m: Manifest = {"a.py": a_res, "b.py": b_res}
294 plugin = _FakePlugin()
295 record_resolutions(
296 repo, ["a.py", "b.py"], ours_m, theirs_m, new_m, "code", plugin,
297 manually_resolved={"a.py"}, # only a.py was manually resolved
298 )
299 patterns = list_patterns(repo)
300 by_path = {p.path: list_resolutions(repo, p.pattern_id)[0] for p in patterns}
301 assert by_path["a.py"].human_verified is True
302 assert by_path["a.py"].confidence == 1.0
303 assert by_path["b.py"].human_verified is False
304 assert by_path["b.py"].confidence == 0.8
305
306 def test_manual_rationale_text(self, tmp_path: pathlib.Path) -> None:
307 repo = _harmony_repo(tmp_path)
308 ours_id = _write_object(repo, b"ours")
309 theirs_id = _write_object(repo, b"theirs")
310 res_id = _write_object(repo, b"resolved")
311 ours_m: Manifest = {"f.py": ours_id}
312 theirs_m: Manifest = {"f.py": theirs_id}
313 new_m: Manifest = {"f.py": res_id}
314 plugin = _FakePlugin()
315 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", plugin, manually_resolved={"f.py"})
316 res = list_resolutions(repo, list_patterns(repo)[0].pattern_id)
317 assert "muse resolve" in res[0].rationale
318
319 def test_side_pick_rationale_text(self, tmp_path: pathlib.Path) -> None:
320 repo = _harmony_repo(tmp_path)
321 ours_id = _write_object(repo, b"ours")
322 theirs_id = _write_object(repo, b"theirs")
323 res_id = _write_object(repo, b"resolved")
324 ours_m: Manifest = {"f.py": ours_id}
325 theirs_m: Manifest = {"f.py": theirs_id}
326 new_m: Manifest = {"f.py": res_id}
327 plugin = _FakePlugin()
328 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", plugin, manually_resolved=set())
329 res = list_resolutions(repo, list_patterns(repo)[0].pattern_id)
330 assert "checkout" in res[0].rationale
331
332 def test_symbol_level_path_in_manually_resolved(
333 self, tmp_path: pathlib.Path
334 ) -> None:
335 """Symbol-level paths (file::Symbol) are matched exactly in manually_resolved."""
336 repo = _harmony_repo(tmp_path)
337 ours_id = _write_object(repo, b"ours")
338 theirs_id = _write_object(repo, b"theirs")
339 res_id = _write_object(repo, b"resolved")
340 ours_m: Manifest = {"f.py": ours_id}
341 theirs_m: Manifest = {"f.py": theirs_id}
342 new_m: Manifest = {"f.py": res_id}
343 plugin = _FakePlugin()
344 record_resolutions(
345 repo, ["f.py::MyFunc"], ours_m, theirs_m, new_m, "code", plugin,
346 manually_resolved={"f.py::MyFunc"},
347 )
348 res = list_resolutions(repo, list_patterns(repo)[0].pattern_id)
349 assert res[0].human_verified is True
350 assert res[0].confidence == 1.0
351
352
353 # ---------------------------------------------------------------------------
354 # End-to-end: muse resolve β†’ muse commit β†’ Harmony patterns
355 # ---------------------------------------------------------------------------
356
357 class TestResolveToHarmonyEndToEnd:
358 @pytest.fixture()
359 def repo(self, tmp_path: pathlib.Path) -> pathlib.Path:
360 _invoke(tmp_path, ["init"])
361 (tmp_path / "hello.md").write_text("# Hello\n")
362 _invoke(tmp_path, ["code", "add", "."])
363 _invoke(tmp_path, ["commit", "-m", "initial"])
364 return tmp_path
365
366 def test_muse_resolve_sets_manually_resolved_in_merge_state(
367 self, repo: pathlib.Path
368 ) -> None:
369 write_merge_state(
370 repo,
371 base_commit=_BASE,
372 ours_commit=_OURS,
373 theirs_commit=_THEIRS,
374 conflict_paths=["hello.md"],
375 other_branch="feat/x",
376 )
377 _invoke(repo, ["resolve", "hello.md"])
378 state = _state(repo)
379 assert "hello.md" in state.manually_resolved
380
381 def test_muse_resolve_all_sets_all_in_manually_resolved(
382 self, repo: pathlib.Path
383 ) -> None:
384 write_merge_state(
385 repo,
386 base_commit=_BASE,
387 ours_commit=_OURS,
388 theirs_commit=_THEIRS,
389 conflict_paths=["hello.md::A", "hello.md::B"],
390 other_branch="feat/x",
391 )
392 _invoke(repo, ["resolve", "--all"])
393 state = _state(repo)
394 assert "hello.md::A" in state.manually_resolved
395 assert "hello.md::B" in state.manually_resolved