gabriel / muse public
test_pack_store.py python
324 lines 12.9 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """TDD tests for muse.core.pack_store — MPack local object store.
2
3 Phase 1 of issue #70: MPack unified binary format.
4
5 Layout:
6 .muse/objects/pack/sha256/<64hex>.mpack ← pack data
7 .muse/objects/pack/sha256/<64hex>.idx ← seek index
8 """
9
10 from __future__ import annotations
11
12 import hashlib
13 import json
14 import pathlib
15 import struct
16
17 import pytest
18
19 from muse.core.paths import muse_dir, packs_dir
20 from muse.core.types import blob_id
21
22
23 # ---------------------------------------------------------------------------
24 # Fixtures
25 # ---------------------------------------------------------------------------
26
27
28 @pytest.fixture
29 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
30 """Minimal .muse/ repo structure."""
31 dot_muse = muse_dir(tmp_path)
32 (dot_muse / "commits").mkdir(parents=True)
33 (dot_muse / "snapshots").mkdir(parents=True)
34 (dot_muse / "objects").mkdir(parents=True)
35 (dot_muse / "refs" / "heads").mkdir(parents=True)
36 (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
37 (dot_muse / "HEAD").write_text("ref: refs/heads/main\n")
38 (dot_muse / "refs" / "heads" / "main").write_text("")
39 return tmp_path
40
41
42 def _make_objects(n: int, prefix: str = "obj") -> list[tuple[str, bytes]]:
43 """Return n (object_id, content) pairs with deterministic content."""
44 objects = []
45 for i in range(n):
46 content = f"{prefix}-{i}".encode() * 16
47 oid = blob_id(content)
48 objects.append((oid, content))
49 return objects
50
51
52 # ---------------------------------------------------------------------------
53 # Path helpers
54 # ---------------------------------------------------------------------------
55
56
57 class TestPacksDir:
58 def test_packs_dir_is_under_objects(self, repo: pathlib.Path) -> None:
59 from muse.core.paths import objects_dir
60 p = packs_dir(repo)
61 assert str(p).startswith(str(objects_dir(repo)))
62
63 def test_packs_dir_algo_subdir_is_sha256(self, repo: pathlib.Path) -> None:
64 p = packs_dir(repo)
65 assert p.name == "sha256"
66 assert p.parent.name == "pack"
67
68
69 # ---------------------------------------------------------------------------
70 # write_pack
71 # ---------------------------------------------------------------------------
72
73
74 class TestWritePack:
75 def test_write_pack_creates_mpack_and_idx(self, repo: pathlib.Path) -> None:
76 from muse.core.pack_store import write_pack
77 objects = _make_objects(3)
78 pack_id = write_pack(repo, objects)
79 _, hex_id = pack_id.split(":")
80 base = packs_dir(repo) / hex_id
81 assert base.with_suffix(".mpack").exists()
82 assert base.with_suffix(".idx").exists()
83
84 def test_pack_path_contains_sha256_algo_dir(self, repo: pathlib.Path) -> None:
85 from muse.core.pack_store import write_pack
86 objects = _make_objects(2)
87 pack_id = write_pack(repo, objects)
88 _, hex_id = pack_id.split(":")
89 mpack_path = packs_dir(repo) / f"{hex_id}.mpack"
90 assert "pack/sha256" in str(mpack_path)
91
92 def test_pack_id_is_sha256_of_pack_content(self, repo: pathlib.Path) -> None:
93 from muse.core.pack_store import write_pack
94 objects = _make_objects(3)
95 pack_id = write_pack(repo, objects)
96 _, hex_id = pack_id.split(":")
97 mpack_path = packs_dir(repo) / f"{hex_id}.mpack"
98 actual_hex = hashlib.sha256(mpack_path.read_bytes()).hexdigest()
99 assert actual_hex == hex_id
100
101 def test_pack_file_starts_with_muse_magic(self, repo: pathlib.Path) -> None:
102 from muse.core.pack_store import write_pack
103 objects = _make_objects(2)
104 pack_id = write_pack(repo, objects)
105 _, hex_id = pack_id.split(":")
106 mpack_path = packs_dir(repo) / f"{hex_id}.mpack"
107 assert mpack_path.read_bytes()[:4] == b"MUSE"
108
109 def test_idx_file_starts_with_musi_magic(self, repo: pathlib.Path) -> None:
110 from muse.core.pack_store import write_pack
111 objects = _make_objects(2)
112 pack_id = write_pack(repo, objects)
113 _, hex_id = pack_id.split(":")
114 idx_path = packs_dir(repo) / f"{hex_id}.idx"
115 assert idx_path.read_bytes()[:4] == b"MUSI"
116
117 def test_pack_file_encodes_object_count(self, repo: pathlib.Path) -> None:
118 from muse.core.pack_store import write_pack
119 objects = _make_objects(7)
120 pack_id = write_pack(repo, objects)
121 _, hex_id = pack_id.split(":")
122 mpack_path = packs_dir(repo) / f"{hex_id}.mpack"
123 data = mpack_path.read_bytes()
124 # magic(4) + version(1) + object_count(8)
125 count = struct.unpack_from("<Q", data, 5)[0]
126 assert count == 7
127
128 def test_write_pack_empty_object_list_is_noop(self, repo: pathlib.Path) -> None:
129 from muse.core.pack_store import write_pack
130 result = write_pack(repo, [])
131 assert result is None
132 assert not any(packs_dir(repo).rglob("*.mpack")) if packs_dir(repo).exists() else True
133
134 def test_write_pack_returns_prefixed_pack_id(self, repo: pathlib.Path) -> None:
135 from muse.core.pack_store import write_pack
136 objects = _make_objects(1)
137 pack_id = write_pack(repo, objects)
138 assert pack_id.startswith("sha256:")
139 assert len(pack_id) == 7 + 64
140
141 def test_write_pack_idempotent(self, repo: pathlib.Path) -> None:
142 from muse.core.pack_store import write_pack
143 objects = _make_objects(3)
144 pack_id_1 = write_pack(repo, objects)
145 pack_id_2 = write_pack(repo, objects)
146 assert pack_id_1 == pack_id_2
147 # Only one .mpack file should exist
148 mpack_files = list(packs_dir(repo).glob("*.mpack"))
149 assert len(mpack_files) == 1
150
151
152 # ---------------------------------------------------------------------------
153 # read_object_from_packs
154 # ---------------------------------------------------------------------------
155
156
157 class TestReadObjectFromPacks:
158 def test_returns_correct_bytes(self, repo: pathlib.Path) -> None:
159 from muse.core.pack_store import write_pack, read_object_from_packs
160 content = b"hello mpack world"
161 oid = blob_id(content)
162 write_pack(repo, [(oid, content)])
163 assert read_object_from_packs(repo, oid) == content
164
165 def test_returns_none_for_missing(self, repo: pathlib.Path) -> None:
166 from muse.core.pack_store import write_pack, read_object_from_packs
167 write_pack(repo, _make_objects(2))
168 missing = blob_id(b"not in any pack")
169 assert read_object_from_packs(repo, missing) is None
170
171 def test_returns_none_when_no_packs_exist(self, repo: pathlib.Path) -> None:
172 from muse.core.pack_store import read_object_from_packs
173 oid = blob_id(b"anything")
174 assert read_object_from_packs(repo, oid) is None
175
176 def test_byte_exact_round_trip(self, repo: pathlib.Path) -> None:
177 from muse.core.pack_store import write_pack, read_object_from_packs
178 objects = _make_objects(10)
179 write_pack(repo, objects)
180 for oid, content in objects:
181 assert read_object_from_packs(repo, oid) == content
182
183 def test_binary_search_finds_object_in_large_pack(self, repo: pathlib.Path) -> None:
184 from muse.core.pack_store import write_pack, read_object_from_packs
185 objects = _make_objects(500)
186 write_pack(repo, objects)
187 # Pick objects from start, middle, and end of sorted order
188 sorted_objects = sorted(objects, key=lambda x: x[0])
189 for oid, content in [sorted_objects[0], sorted_objects[250], sorted_objects[-1]]:
190 assert read_object_from_packs(repo, oid) == content
191
192 def test_reads_across_multiple_packs(self, repo: pathlib.Path) -> None:
193 from muse.core.pack_store import write_pack, read_object_from_packs
194 pack1 = _make_objects(3, prefix="pack1")
195 pack2 = _make_objects(3, prefix="pack2")
196 pack3 = _make_objects(3, prefix="pack3")
197 write_pack(repo, pack1)
198 write_pack(repo, pack2)
199 write_pack(repo, pack3)
200 for oid, content in pack1 + pack2 + pack3:
201 assert read_object_from_packs(repo, oid) == content
202
203
204 # ---------------------------------------------------------------------------
205 # has_object_in_packs
206 # ---------------------------------------------------------------------------
207
208
209 class TestHasObjectInPacks:
210 def test_finds_written_object(self, repo: pathlib.Path) -> None:
211 from muse.core.pack_store import write_pack, has_object_in_packs
212 content = b"find me"
213 oid = blob_id(content)
214 write_pack(repo, [(oid, content)])
215 assert has_object_in_packs(repo, oid) is True
216
217 def test_returns_false_for_missing(self, repo: pathlib.Path) -> None:
218 from muse.core.pack_store import write_pack, has_object_in_packs
219 write_pack(repo, _make_objects(2))
220 missing = blob_id(b"not here")
221 assert has_object_in_packs(repo, missing) is False
222
223 def test_returns_false_when_no_packs_exist(self, repo: pathlib.Path) -> None:
224 from muse.core.pack_store import has_object_in_packs
225 assert has_object_in_packs(repo, blob_id(b"x")) is False
226
227 def test_finds_objects_across_multiple_packs(self, repo: pathlib.Path) -> None:
228 from muse.core.pack_store import write_pack, has_object_in_packs
229 pack1 = _make_objects(3, prefix="a")
230 pack2 = _make_objects(3, prefix="b")
231 write_pack(repo, pack1)
232 write_pack(repo, pack2)
233 for oid, _ in pack1 + pack2:
234 assert has_object_in_packs(repo, oid) is True
235
236
237 # ---------------------------------------------------------------------------
238 # list_packs
239 # ---------------------------------------------------------------------------
240
241
242 class TestListPacks:
243 def test_empty_when_no_packs(self, repo: pathlib.Path) -> None:
244 from muse.core.pack_store import list_packs
245 assert list_packs(repo) == []
246
247 def test_returns_all_pack_ids(self, repo: pathlib.Path) -> None:
248 from muse.core.pack_store import write_pack, list_packs
249 id1 = write_pack(repo, _make_objects(2, prefix="a"))
250 id2 = write_pack(repo, _make_objects(2, prefix="b"))
251 result = list_packs(repo)
252 assert sorted(result) == sorted([id1, id2])
253
254
255 # ---------------------------------------------------------------------------
256 # verify_pack
257 # ---------------------------------------------------------------------------
258
259
260 class TestVerifyPack:
261 def test_passes_on_valid_pack(self, repo: pathlib.Path) -> None:
262 from muse.core.pack_store import write_pack, verify_pack
263 pack_id = write_pack(repo, _make_objects(5))
264 assert verify_pack(repo, pack_id) is True
265
266 def test_raises_on_corrupted_mpack(self, repo: pathlib.Path) -> None:
267 from muse.core.pack_store import write_pack, verify_pack
268 pack_id = write_pack(repo, _make_objects(3))
269 _, hex_id = pack_id.split(":")
270 mpack_path = packs_dir(repo) / f"{hex_id}.mpack"
271 data = bytearray(mpack_path.read_bytes())
272 data[20] ^= 0xFF # flip a byte in the object data
273 mpack_path.write_bytes(bytes(data))
274 with pytest.raises(OSError):
275 verify_pack(repo, pack_id)
276
277 def test_raises_on_corrupted_idx(self, repo: pathlib.Path) -> None:
278 from muse.core.pack_store import write_pack, verify_pack
279 pack_id = write_pack(repo, _make_objects(3))
280 _, hex_id = pack_id.split(":")
281 idx_path = packs_dir(repo) / f"{hex_id}.idx"
282 data = bytearray(idx_path.read_bytes())
283 data[20] ^= 0xFF
284 idx_path.write_bytes(bytes(data))
285 with pytest.raises(OSError):
286 verify_pack(repo, pack_id)
287
288
289 # ---------------------------------------------------------------------------
290 # object_store fallthrough
291 # ---------------------------------------------------------------------------
292
293
294 class TestObjectStoreFallthrough:
295 def test_read_object_finds_pack_objects(self, repo: pathlib.Path) -> None:
296 from muse.core.pack_store import write_pack
297 from muse.core.object_store import read_object
298 content = b"packed content"
299 oid = blob_id(content)
300 write_pack(repo, [(oid, content)])
301 assert read_object(repo, oid) == content
302
303 def test_has_object_finds_pack_objects(self, repo: pathlib.Path) -> None:
304 from muse.core.pack_store import write_pack
305 from muse.core.object_store import has_object
306 content = b"packed object"
307 oid = blob_id(content)
308 write_pack(repo, [(oid, content)])
309 assert has_object(repo, oid) is True
310
311 def test_loose_object_takes_priority_over_pack(self, repo: pathlib.Path) -> None:
312 from muse.core.pack_store import write_pack
313 from muse.core.object_store import read_object, write_object
314 content = b"real content"
315 oid = blob_id(content)
316 write_object(repo, oid, content)
317 # Pack with same oid is irrelevant — loose wins
318 write_pack(repo, [(oid, content)])
319 assert read_object(repo, oid) == content
320
321 def test_read_object_returns_none_when_absent_everywhere(self, repo: pathlib.Path) -> None:
322 from muse.core.object_store import read_object
323 missing = blob_id(b"nowhere")
324 assert read_object(repo, missing) is None
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago