gabriel / muse public
test_indices.py python
289 lines 10.9 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Tests for muse/core/indices.py — optional local index layer.
2
3 Coverage
4 --------
5 SymbolHistoryEntry
6 - to_dict / from_dict round-trip.
7 - All six fields preserved.
8
9 symbol_history index
10 - save_symbol_history writes a valid JSON file.
11 - load_symbol_history reads it back correctly.
12 - load returns empty dict when file absent.
13 - load returns empty dict on corrupt JSON.
14 - Sorting: entries dict is sorted by address.
15 - Multiple addresses, multiple events per address.
16
17 hash_occurrence index
18 - save_hash_occurrence writes a valid JSON file.
19 - load_hash_occurrence reads it back correctly.
20 - load returns empty dict when file absent.
21 - load returns empty dict on corrupt JSON.
22 - Addresses within each hash entry are sorted.
23
24 index_info
25 - Reports "absent" for missing indexes.
26 - Reports "present" + correct entry count for existing indexes.
27 - Reports "corrupt" for malformed JSON.
28 - Reports both indexes.
29
30 Schema compliance
31 - schema_version == __version__.
32 - updated_at is present and is a non-empty string.
33 - index field matches the index name.
34 """
35
36 import pathlib
37
38 import msgpack
39 import pytest
40
41 from muse._version import __version__
42 from muse.core.indices import (
43 HashOccurrenceIndex,
44 SymbolHistoryEntry,
45 SymbolHistoryIndex,
46 index_info,
47 load_hash_occurrence,
48 load_symbol_history,
49 save_hash_occurrence,
50 save_symbol_history,
51 )
52
53
54 # ---------------------------------------------------------------------------
55 # SymbolHistoryEntry
56 # ---------------------------------------------------------------------------
57
58
59 class TestSymbolHistoryEntry:
60 def test_to_dict_from_dict_round_trip(self) -> None:
61 entry = SymbolHistoryEntry(
62 commit_id="abc123",
63 committed_at="2026-01-01T00:00:00+00:00",
64 op="insert",
65 content_id="content_abc",
66 body_hash="body_hash_xyz",
67 signature_id="sig_id_pqr",
68 )
69 d = entry.to_dict()
70 entry2 = SymbolHistoryEntry.from_dict(d)
71 assert entry2.commit_id == "abc123"
72 assert entry2.committed_at == "2026-01-01T00:00:00+00:00"
73 assert entry2.op == "insert"
74 assert entry2.content_id == "content_abc"
75 assert entry2.body_hash == "body_hash_xyz"
76 assert entry2.signature_id == "sig_id_pqr"
77
78 def test_all_ops_preserved(self) -> None:
79 for op in ("insert", "delete", "replace", "patch"):
80 e = SymbolHistoryEntry("c", "t", op, "cid", "bh", "sig")
81 assert SymbolHistoryEntry.from_dict(e.to_dict()).op == op
82
83
84 # ---------------------------------------------------------------------------
85 # symbol_history index — save / load
86 # ---------------------------------------------------------------------------
87
88
89 class TestSymbolHistoryIndex:
90 def _make_entry(self, op: str = "insert") -> SymbolHistoryEntry:
91 return SymbolHistoryEntry(
92 commit_id="commit1",
93 committed_at="2026-01-01T00:00:00+00:00",
94 op=op,
95 content_id="cid1",
96 body_hash="bh1",
97 signature_id="sig1",
98 )
99
100 def test_save_creates_file(self, tmp_path: pathlib.Path) -> None:
101 index: SymbolHistoryIndex = {
102 "src/a.py::f": [self._make_entry()],
103 }
104 save_symbol_history(tmp_path, index)
105 path = tmp_path / ".muse" / "indices" / "symbol_history.msgpack"
106 assert path.exists()
107
108 def test_round_trip(self, tmp_path: pathlib.Path) -> None:
109 entry = self._make_entry("replace")
110 index: SymbolHistoryIndex = {
111 "src/billing.py::compute_total": [entry],
112 }
113 save_symbol_history(tmp_path, index)
114 loaded = load_symbol_history(tmp_path)
115 assert "src/billing.py::compute_total" in loaded
116 entries = loaded["src/billing.py::compute_total"]
117 assert len(entries) == 1
118 assert entries[0].op == "replace"
119 assert entries[0].commit_id == "commit1"
120
121 def test_multiple_addresses(self, tmp_path: pathlib.Path) -> None:
122 index: SymbolHistoryIndex = {
123 "src/a.py::alpha": [self._make_entry("insert")],
124 "src/b.py::beta": [self._make_entry("insert"), self._make_entry("replace")],
125 }
126 save_symbol_history(tmp_path, index)
127 loaded = load_symbol_history(tmp_path)
128 assert len(loaded["src/a.py::alpha"]) == 1
129 assert len(loaded["src/b.py::beta"]) == 2
130
131 def test_load_absent_returns_empty(self, tmp_path: pathlib.Path) -> None:
132 result = load_symbol_history(tmp_path)
133 assert result == {}
134
135 def test_load_corrupt_returns_empty(self, tmp_path: pathlib.Path) -> None:
136 indices_dir = tmp_path / ".muse" / "indices"
137 indices_dir.mkdir(parents=True, exist_ok=True)
138 (indices_dir / "symbol_history.msgpack").write_bytes(b"\xff\xfe not valid msgpack")
139 result = load_symbol_history(tmp_path)
140 assert result == {}
141
142 def test_schema_compliance(self, tmp_path: pathlib.Path) -> None:
143 index: SymbolHistoryIndex = {"x.py::f": [self._make_entry()]}
144 save_symbol_history(tmp_path, index)
145 raw = msgpack.unpackb(
146 (tmp_path / ".muse" / "indices" / "symbol_history.msgpack").read_bytes(),
147 raw=False,
148 )
149 assert raw["schema_version"] == __version__
150 assert raw["index"] == "symbol_history"
151 assert raw["updated_at"] # non-empty string
152 assert "x.py::f" in raw["entries"]
153
154 def test_empty_index_saved(self, tmp_path: pathlib.Path) -> None:
155 save_symbol_history(tmp_path, {})
156 loaded = load_symbol_history(tmp_path)
157 assert loaded == {}
158
159 def test_entries_sorted_by_address(self, tmp_path: pathlib.Path) -> None:
160 index: SymbolHistoryIndex = {
161 "z.py::z": [self._make_entry()],
162 "a.py::a": [self._make_entry()],
163 "m.py::m": [self._make_entry()],
164 }
165 save_symbol_history(tmp_path, index)
166 raw = msgpack.unpackb(
167 (tmp_path / ".muse" / "indices" / "symbol_history.msgpack").read_bytes(),
168 raw=False,
169 )
170 keys = list(raw["entries"].keys())
171 assert keys == sorted(keys)
172
173
174 # ---------------------------------------------------------------------------
175 # hash_occurrence index — save / load
176 # ---------------------------------------------------------------------------
177
178
179 class TestHashOccurrenceIndex:
180 def test_save_creates_file(self, tmp_path: pathlib.Path) -> None:
181 index: HashOccurrenceIndex = {
182 "deadbeef": ["src/a.py::f", "src/b.py::g"],
183 }
184 save_hash_occurrence(tmp_path, index)
185 path = tmp_path / ".muse" / "indices" / "hash_occurrence.msgpack"
186 assert path.exists()
187
188 def test_round_trip(self, tmp_path: pathlib.Path) -> None:
189 index: HashOccurrenceIndex = {
190 "abc123": ["src/a.py::f", "src/b.py::g"],
191 "def456": ["src/c.py::h"],
192 }
193 save_hash_occurrence(tmp_path, index)
194 loaded = load_hash_occurrence(tmp_path)
195 assert "abc123" in loaded
196 assert set(loaded["abc123"]) == {"src/a.py::f", "src/b.py::g"}
197 assert loaded["def456"] == ["src/c.py::h"]
198
199 def test_addresses_sorted_within_hash(self, tmp_path: pathlib.Path) -> None:
200 index: HashOccurrenceIndex = {
201 "hash1": ["z.py::z", "a.py::a", "m.py::m"],
202 }
203 save_hash_occurrence(tmp_path, index)
204 raw = msgpack.unpackb(
205 (tmp_path / ".muse" / "indices" / "hash_occurrence.msgpack").read_bytes(),
206 raw=False,
207 )
208 addrs = raw["entries"]["hash1"]
209 assert addrs == sorted(addrs)
210
211 def test_hashes_sorted(self, tmp_path: pathlib.Path) -> None:
212 index: HashOccurrenceIndex = {
213 "zzz": ["a.py::f"],
214 "aaa": ["b.py::g"],
215 }
216 save_hash_occurrence(tmp_path, index)
217 raw = msgpack.unpackb(
218 (tmp_path / ".muse" / "indices" / "hash_occurrence.msgpack").read_bytes(),
219 raw=False,
220 )
221 keys = list(raw["entries"].keys())
222 assert keys == sorted(keys)
223
224 def test_load_absent_returns_empty(self, tmp_path: pathlib.Path) -> None:
225 assert load_hash_occurrence(tmp_path) == {}
226
227 def test_load_corrupt_returns_empty(self, tmp_path: pathlib.Path) -> None:
228 indices_dir = tmp_path / ".muse" / "indices"
229 indices_dir.mkdir(parents=True, exist_ok=True)
230 (indices_dir / "hash_occurrence.msgpack").write_bytes(b"\xff\xfe garbage bytes")
231 assert load_hash_occurrence(tmp_path) == {}
232
233 def test_schema_compliance(self, tmp_path: pathlib.Path) -> None:
234 save_hash_occurrence(tmp_path, {"h": ["a.py::f"]})
235 raw = msgpack.unpackb(
236 (tmp_path / ".muse" / "indices" / "hash_occurrence.msgpack").read_bytes(),
237 raw=False,
238 )
239 assert raw["schema_version"] == __version__
240 assert raw["index"] == "hash_occurrence"
241 assert raw["updated_at"]
242
243 def test_empty_index(self, tmp_path: pathlib.Path) -> None:
244 save_hash_occurrence(tmp_path, {})
245 assert load_hash_occurrence(tmp_path) == {}
246
247
248 # ---------------------------------------------------------------------------
249 # index_info
250 # ---------------------------------------------------------------------------
251
252
253 class TestIndexInfo:
254 def test_both_absent(self, tmp_path: pathlib.Path) -> None:
255 info = index_info(tmp_path)
256 assert len(info) == 2
257 names = {i["name"] for i in info}
258 assert names == {"symbol_history", "hash_occurrence"}
259 for item in info:
260 assert item["status"] == "absent"
261
262 def test_symbol_history_present(self, tmp_path: pathlib.Path) -> None:
263 entry = SymbolHistoryEntry("c", "t", "insert", "cid", "bh", "sig")
264 save_symbol_history(tmp_path, {"a.py::f": [entry], "b.py::g": [entry]})
265 info = index_info(tmp_path)
266 sh = next(i for i in info if i["name"] == "symbol_history")
267 assert sh["status"] == "present"
268 assert sh["entries"] == 2
269
270 def test_hash_occurrence_present(self, tmp_path: pathlib.Path) -> None:
271 save_hash_occurrence(tmp_path, {"h1": ["a.py::f"], "h2": ["b.py::g"]})
272 info = index_info(tmp_path)
273 ho = next(i for i in info if i["name"] == "hash_occurrence")
274 assert ho["status"] == "present"
275 assert ho["entries"] == 2
276
277 def test_corrupt_index_reported(self, tmp_path: pathlib.Path) -> None:
278 indices_dir = tmp_path / ".muse" / "indices"
279 indices_dir.mkdir(parents=True, exist_ok=True)
280 (indices_dir / "symbol_history.msgpack").write_bytes(b"\xff\xfe garbage")
281 info = index_info(tmp_path)
282 sh = next(i for i in info if i["name"] == "symbol_history")
283 assert sh["status"] == "corrupt"
284
285 def test_updated_at_present_when_index_exists(self, tmp_path: pathlib.Path) -> None:
286 save_hash_occurrence(tmp_path, {"h": ["f.py::x"]})
287 info = index_info(tmp_path)
288 ho = next(i for i in info if i["name"] == "hash_occurrence")
289 assert ho["updated_at"] # non-empty string
File History 5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 48 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago