gabriel / muse public
test_resolve_phase1.py python
223 lines 8.7 KB
Raw
sha256:84df9126d09aeec0b8f1b908f0b06c10913feec28f3514b382efb1ba6d619385 refactor: rename StructuredMergePlugin to AddressedMergePlu… Sonnet 4.6 minor ⚠ breaking 23 days ago
1 """Tests for Phase 1 of issue #8: resolve_path and resolve_symbol primitives.
2
3 Coverage
4 --------
5 - resolve_path: removes all conflict entries whose file portion matches path
6 - resolve_path: leaves original_conflict_paths untouched
7 - resolve_path: idempotent when path already resolved
8 - resolve_path: returns empty list when path not in conflict_paths
9 - resolve_path: raises ValueError when no merge in progress
10 - resolve_symbol: removes exactly one matching entry
11 - resolve_symbol: returns False when symbol not present (idempotent)
12 - resolve_symbol: leaves original_conflict_paths untouched
13 - resolve_symbol: raises ValueError when no merge in progress
14 - Mixed symbol/file entries: resolve_path clears only matching file portion
15 - After resolve_path clears all conflicts, conflict_paths is empty
16 - write_merge_state / read_merge_state round-trip preserves all fields
17 """
18
19 from __future__ import annotations
20
21 import json
22 import pathlib
23
24 import pytest
25
26 from muse.core.merge_engine import (
27 MergeState,
28 clear_merge_state,
29 read_merge_state,
30 resolve_path,
31 resolve_symbol,
32 write_merge_state,
33 )
34 from muse.core.types import MUSE_DIR, fake_id
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41 _BASE = fake_id("base")
42 _OURS = fake_id("ours")
43 _THEIRS = fake_id("theirs")
44
45
46 def _repo(tmp_path: pathlib.Path) -> pathlib.Path:
47 (tmp_path / MUSE_DIR).mkdir()
48 return tmp_path
49
50
51 def _write(root: pathlib.Path, conflicts: list[str]) -> None:
52 write_merge_state(
53 root,
54 base_commit=_BASE,
55 ours_commit=_OURS,
56 theirs_commit=_THEIRS,
57 conflict_paths=conflicts,
58 other_branch="feat/x",
59 )
60
61
62 def _state(root: pathlib.Path) -> MergeState:
63 s = read_merge_state(root)
64 assert s is not None
65 return s
66
67
68 # ---------------------------------------------------------------------------
69 # resolve_path
70 # ---------------------------------------------------------------------------
71
72 class TestResolvePath:
73 def test_clears_plain_file_entry(self, tmp_path: pathlib.Path) -> None:
74 repo = _repo(tmp_path)
75 _write(repo, ["hello.md", "world.md"])
76 cleared = resolve_path(repo, "hello.md")
77 assert cleared == ["hello.md"]
78 assert _state(repo).conflict_paths == ["world.md"]
79
80 def test_clears_symbol_level_entries_for_file(self, tmp_path: pathlib.Path) -> None:
81 repo = _repo(tmp_path)
82 _write(repo, ["hello.md::Hello World", "hello.md::Subtitle", "other.py"])
83 cleared = resolve_path(repo, "hello.md")
84 assert sorted(cleared) == ["hello.md::Hello World", "hello.md::Subtitle"]
85 assert _state(repo).conflict_paths == ["other.py"]
86
87 def test_clears_mixed_plain_and_symbol_entries(self, tmp_path: pathlib.Path) -> None:
88 repo = _repo(tmp_path)
89 _write(repo, ["hello.md", "hello.md::Section", "readme.txt"])
90 cleared = resolve_path(repo, "hello.md")
91 assert sorted(cleared) == ["hello.md", "hello.md::Section"]
92 assert _state(repo).conflict_paths == ["readme.txt"]
93
94 def test_returns_empty_when_path_not_conflicted(self, tmp_path: pathlib.Path) -> None:
95 repo = _repo(tmp_path)
96 _write(repo, ["other.py"])
97 cleared = resolve_path(repo, "hello.md")
98 assert cleared == []
99 assert _state(repo).conflict_paths == ["other.py"]
100
101 def test_idempotent_second_call_returns_empty(self, tmp_path: pathlib.Path) -> None:
102 repo = _repo(tmp_path)
103 _write(repo, ["hello.md"])
104 resolve_path(repo, "hello.md")
105 cleared2 = resolve_path(repo, "hello.md")
106 assert cleared2 == []
107
108 def test_does_not_mutate_original_conflict_paths(self, tmp_path: pathlib.Path) -> None:
109 repo = _repo(tmp_path)
110 _write(repo, ["hello.md", "world.md"])
111 original_before = _state(repo).original_conflict_paths[:]
112 resolve_path(repo, "hello.md")
113 assert _state(repo).original_conflict_paths == original_before
114
115 def test_empties_conflict_paths_when_all_resolved(self, tmp_path: pathlib.Path) -> None:
116 repo = _repo(tmp_path)
117 _write(repo, ["hello.md::A", "hello.md::B"])
118 resolve_path(repo, "hello.md")
119 assert _state(repo).conflict_paths == []
120
121 def test_raises_when_no_merge_in_progress(self, tmp_path: pathlib.Path) -> None:
122 repo = _repo(tmp_path)
123 with pytest.raises(ValueError, match="No merge in progress"):
124 resolve_path(repo, "hello.md")
125
126 def test_preserves_other_merge_state_fields(self, tmp_path: pathlib.Path) -> None:
127 repo = _repo(tmp_path)
128 _write(repo, ["hello.md", "other.py"])
129 resolve_path(repo, "hello.md")
130 s = _state(repo)
131 assert s.base_commit == _BASE
132 assert s.ours_commit == _OURS
133 assert s.theirs_commit == _THEIRS
134 assert s.other_branch == "feat/x"
135
136 def test_does_not_clear_entries_with_similar_prefix(self, tmp_path: pathlib.Path) -> None:
137 """resolve_path("hello.md") must not clear "hello.md.bak" or "hello.md2"."""
138 repo = _repo(tmp_path)
139 _write(repo, ["hello.md", "hello.md.bak", "hello.md2"])
140 cleared = resolve_path(repo, "hello.md")
141 assert cleared == ["hello.md"]
142 remaining = _state(repo).conflict_paths
143 assert "hello.md.bak" in remaining
144 assert "hello.md2" in remaining
145
146
147 # ---------------------------------------------------------------------------
148 # resolve_symbol
149 # ---------------------------------------------------------------------------
150
151 class TestResolveSymbol:
152 def test_removes_plain_path_entry(self, tmp_path: pathlib.Path) -> None:
153 repo = _repo(tmp_path)
154 _write(repo, ["hello.md", "world.md"])
155 found = resolve_symbol(repo, "hello.md")
156 assert found is True
157 assert _state(repo).conflict_paths == ["world.md"]
158
159 def test_removes_symbol_address_entry(self, tmp_path: pathlib.Path) -> None:
160 repo = _repo(tmp_path)
161 _write(repo, ["hello.md::Hello World", "hello.md::Subtitle"])
162 found = resolve_symbol(repo, "hello.md::Hello World")
163 assert found is True
164 assert _state(repo).conflict_paths == ["hello.md::Subtitle"]
165
166 def test_returns_false_when_not_present(self, tmp_path: pathlib.Path) -> None:
167 repo = _repo(tmp_path)
168 _write(repo, ["other.py"])
169 found = resolve_symbol(repo, "hello.md::Missing")
170 assert found is False
171 assert _state(repo).conflict_paths == ["other.py"]
172
173 def test_idempotent_second_call_returns_false(self, tmp_path: pathlib.Path) -> None:
174 repo = _repo(tmp_path)
175 _write(repo, ["hello.md"])
176 resolve_symbol(repo, "hello.md")
177 found2 = resolve_symbol(repo, "hello.md")
178 assert found2 is False
179
180 def test_does_not_mutate_original_conflict_paths(self, tmp_path: pathlib.Path) -> None:
181 repo = _repo(tmp_path)
182 _write(repo, ["hello.md", "world.md"])
183 original_before = _state(repo).original_conflict_paths[:]
184 resolve_symbol(repo, "hello.md")
185 assert _state(repo).original_conflict_paths == original_before
186
187 def test_raises_when_no_merge_in_progress(self, tmp_path: pathlib.Path) -> None:
188 repo = _repo(tmp_path)
189 with pytest.raises(ValueError, match="No merge in progress"):
190 resolve_symbol(repo, "hello.md")
191
192 def test_removes_only_exact_match(self, tmp_path: pathlib.Path) -> None:
193 """resolve_symbol must not remove "hello.md::A" when asked for "hello.md"."""
194 repo = _repo(tmp_path)
195 _write(repo, ["hello.md", "hello.md::A"])
196 resolve_symbol(repo, "hello.md")
197 assert _state(repo).conflict_paths == ["hello.md::A"]
198
199 def test_preserves_other_merge_state_fields(self, tmp_path: pathlib.Path) -> None:
200 repo = _repo(tmp_path)
201 _write(repo, ["hello.md"])
202 resolve_symbol(repo, "hello.md")
203 s = _state(repo)
204 assert s.base_commit == _BASE
205 assert s.ours_commit == _OURS
206 assert s.theirs_commit == _THEIRS
207 assert s.other_branch == "feat/x"
208
209 def test_empties_conflict_paths_when_last_entry_resolved(self, tmp_path: pathlib.Path) -> None:
210 repo = _repo(tmp_path)
211 _write(repo, ["only.py"])
212 resolve_symbol(repo, "only.py")
213 assert _state(repo).conflict_paths == []
214
215 def test_original_paths_persisted_across_multiple_resolves(self, tmp_path: pathlib.Path) -> None:
216 repo = _repo(tmp_path)
217 _write(repo, ["a.py", "b.py", "c.py"])
218 original = _state(repo).original_conflict_paths[:]
219 resolve_symbol(repo, "a.py")
220 resolve_symbol(repo, "b.py")
221 resolve_symbol(repo, "c.py")
222 assert _state(repo).original_conflict_paths == original
223 assert _state(repo).conflict_paths == []
File History 1 commit
sha256:84df9126d09aeec0b8f1b908f0b06c10913feec28f3514b382efb1ba6d619385 refactor: rename StructuredMergePlugin to AddressedMergePlu… Sonnet 4.6 minor 23 days ago