cgcardona / muse public
test_muse_rerere.py python
230 lines 8.0 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """Tests for Muse Rerere — reuse recorded resolutions.
2
3 Verifies:
4 - record_conflict stores the conflict fingerprint hash correctly.
5 - record_resolution stores the postimage.
6 - apply_rerere returns the applied count and resolution on cache hit.
7 - apply_rerere returns (0, None) on cache miss.
8 - list_rerere returns all cached hashes.
9 - forget_rerere removes a single hash.
10 - clear_rerere empties the entire cache.
11 - Fingerprint is transposition-invariant (same shape at different pitches → same hash).
12 """
13 from __future__ import annotations
14
15 import json
16 from pathlib import Path
17
18 import pytest
19
20 from maestro.contracts.json_types import JSONObject
21 from maestro.services.muse_rerere import (
22 ConflictDict,
23 apply_rerere,
24 clear_rerere,
25 forget_rerere,
26 list_rerere,
27 record_conflict,
28 record_resolution,
29 )
30
31
32 # ---------------------------------------------------------------------------
33 # Fixtures
34 # ---------------------------------------------------------------------------
35
36
37 @pytest.fixture()
38 def repo_root(tmp_path: Path) -> Path:
39 """Return a temporary directory that looks like a Muse repo root."""
40 muse_dir = tmp_path / ".muse"
41 muse_dir.mkdir()
42 return tmp_path
43
44
45 def _make_conflicts(region_id: str = "region-1", pitch: int = 60) -> list[ConflictDict]:
46 """Helper: build a minimal conflict list."""
47 return [
48 ConflictDict(
49 region_id=region_id,
50 type="note",
51 description=f"Both sides modified note at pitch={pitch} beat=1.0",
52 )
53 ]
54
55
56 # ---------------------------------------------------------------------------
57 # record_conflict
58 # ---------------------------------------------------------------------------
59
60
61 def test_record_conflict_stores_hash_correctly(repo_root: Path) -> None:
62 """record_conflict returns a consistent hex SHA-256 hash."""
63 conflicts = _make_conflicts()
64 h = record_conflict(repo_root, conflicts)
65
66 assert isinstance(h, str)
67 assert len(h) == 64 # SHA-256 hex digest
68 assert (repo_root / ".muse" / "rr-cache" / h / "conflict").exists()
69
70
71 def test_record_conflict_is_idempotent(repo_root: Path) -> None:
72 """Calling record_conflict twice with the same conflicts yields the same hash."""
73 conflicts = _make_conflicts()
74 h1 = record_conflict(repo_root, conflicts)
75 h2 = record_conflict(repo_root, conflicts)
76 assert h1 == h2
77
78
79 def test_record_conflict_transposition_invariant(repo_root: Path) -> None:
80 """Conflicts at pitch=60 and pitch=72 (same interval pattern) → same hash."""
81 c_at_c4 = _make_conflicts(pitch=60)
82 c_at_c5 = _make_conflicts(pitch=72)
83 h1 = record_conflict(repo_root, c_at_c4)
84 h2 = record_conflict(repo_root, c_at_c5)
85 # Both have a single conflict with relative pitch offset 0 → same fingerprint.
86 assert h1 == h2
87
88
89 def test_record_conflict_different_structure_gives_different_hash(repo_root: Path) -> None:
90 """Structurally different conflict sets produce different hashes."""
91 c1 = _make_conflicts(region_id="region-1")
92 c2 = _make_conflicts(region_id="region-2")
93 h1 = record_conflict(repo_root, c1)
94 h2 = record_conflict(repo_root, c2)
95 assert h1 != h2
96
97
98 # ---------------------------------------------------------------------------
99 # record_resolution
100 # ---------------------------------------------------------------------------
101
102
103 def test_record_resolution_stores_postimage(repo_root: Path) -> None:
104 """record_resolution writes the postimage file for an existing conflict hash."""
105 conflicts = _make_conflicts()
106 h = record_conflict(repo_root, conflicts)
107 resolution: JSONObject = {"strategy": "ours", "region_id": "region-1"}
108
109 record_resolution(repo_root, h, resolution)
110
111 postimage = repo_root / ".muse" / "rr-cache" / h / "postimage"
112 assert postimage.exists()
113 stored = json.loads(postimage.read_text())
114 assert stored == resolution
115
116
117 def test_record_resolution_raises_for_unknown_hash(repo_root: Path) -> None:
118 """record_resolution raises FileNotFoundError for an unrecognised hash."""
119 with pytest.raises(FileNotFoundError, match="not found in rr-cache"):
120 record_resolution(repo_root, "a" * 64, {"foo": "bar"})
121
122
123 # ---------------------------------------------------------------------------
124 # apply_rerere
125 # ---------------------------------------------------------------------------
126
127
128 def test_apply_rerere_returns_applied_count_on_cache_hit(repo_root: Path) -> None:
129 """apply_rerere returns (len(conflicts), resolution) when a postimage exists."""
130 conflicts = _make_conflicts()
131 h = record_conflict(repo_root, conflicts)
132 resolution: JSONObject = {"strategy": "ours"}
133 record_resolution(repo_root, h, resolution)
134
135 applied, returned_resolution = apply_rerere(repo_root, conflicts)
136
137 assert applied == len(conflicts)
138 assert returned_resolution == resolution
139
140
141 def test_apply_rerere_returns_zero_on_cache_miss(repo_root: Path) -> None:
142 """apply_rerere returns (0, None) when no postimage is cached."""
143 conflicts = _make_conflicts()
144 # Record conflict but NOT the resolution → no postimage.
145 record_conflict(repo_root, conflicts)
146
147 applied, resolution = apply_rerere(repo_root, conflicts)
148
149 assert applied == 0
150 assert resolution is None
151
152
153 def test_apply_rerere_returns_zero_for_unknown_conflicts(repo_root: Path) -> None:
154 """apply_rerere returns (0, None) for conflicts with no rr-cache entry at all."""
155 conflicts = _make_conflicts(region_id="never-seen-region")
156 applied, resolution = apply_rerere(repo_root, conflicts)
157 assert applied == 0
158 assert resolution is None
159
160
161 def test_apply_rerere_empty_conflicts_returns_zero(repo_root: Path) -> None:
162 """apply_rerere short-circuits and returns (0, None) when conflict list is empty."""
163 applied, resolution = apply_rerere(repo_root, [])
164 assert applied == 0
165 assert resolution is None
166
167
168 # ---------------------------------------------------------------------------
169 # list_rerere
170 # ---------------------------------------------------------------------------
171
172
173 def test_list_rerere_returns_all_cached_hashes(repo_root: Path) -> None:
174 """list_rerere returns a sorted list of all conflict hashes in the cache."""
175 h1 = record_conflict(repo_root, _make_conflicts(region_id="r1"))
176 h2 = record_conflict(repo_root, _make_conflicts(region_id="r2"))
177
178 hashes = list_rerere(repo_root)
179
180 assert sorted([h1, h2]) == hashes
181
182
183 def test_list_rerere_empty_cache(repo_root: Path) -> None:
184 """list_rerere returns an empty list when the cache is empty."""
185 assert list_rerere(repo_root) == []
186
187
188 # ---------------------------------------------------------------------------
189 # forget_rerere
190 # ---------------------------------------------------------------------------
191
192
193 def test_forget_rerere_removes_one_hash(repo_root: Path) -> None:
194 """forget_rerere removes exactly the specified entry."""
195 h1 = record_conflict(repo_root, _make_conflicts(region_id="r1"))
196 h2 = record_conflict(repo_root, _make_conflicts(region_id="r2"))
197
198 removed = forget_rerere(repo_root, h1)
199
200 assert removed is True
201 hashes = list_rerere(repo_root)
202 assert h1 not in hashes
203 assert h2 in hashes
204
205
206 def test_forget_rerere_returns_false_for_unknown_hash(repo_root: Path) -> None:
207 """forget_rerere returns False (not an error) for an unknown hash."""
208 assert forget_rerere(repo_root, "b" * 64) is False
209
210
211 # ---------------------------------------------------------------------------
212 # clear_rerere
213 # ---------------------------------------------------------------------------
214
215
216 def test_clear_rerere_empties_cache(repo_root: Path) -> None:
217 """clear_rerere removes all entries from the rr-cache."""
218 record_conflict(repo_root, _make_conflicts(region_id="r1"))
219 record_conflict(repo_root, _make_conflicts(region_id="r2"))
220 record_conflict(repo_root, _make_conflicts(region_id="r3"))
221
222 removed = clear_rerere(repo_root)
223
224 assert removed == 3
225 assert list_rerere(repo_root) == []
226
227
228 def test_clear_rerere_empty_cache_returns_zero(repo_root: Path) -> None:
229 """clear_rerere on an already empty cache returns 0."""
230 assert clear_rerere(repo_root) == 0