gabriel / muse public
test_mpack_core.py python
348 lines 12.9 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 121 days ago
1 """Tests for muse.core.pack — MPackBundle build and apply operations."""
2
3 from __future__ import annotations
4
5 import datetime
6 import json
7 import pathlib
8
9 import pytest
10
11 from muse.core.object_store import has_object, read_object, write_object
12 from muse.core.pack import (
13 ObjectPayload,
14 MPackBundle,
15 apply_mpack,
16 build_mpack,
17 )
18 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
19
20 from muse.core.types import Manifest, blob_id, fake_id
21 from muse.core.store import (
22 CommitRecord,
23 SnapshotRecord,
24 read_commit,
25 read_snapshot,
26 write_commit,
27 write_snapshot,
28 )
29 from muse.core.paths import commits_dir, objects_dir, snapshots_dir, muse_dir
30
31
32 # ---------------------------------------------------------------------------
33 # Fixtures
34 # ---------------------------------------------------------------------------
35
36
37 @pytest.fixture
38 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
39 """Minimal .muse/ repo structure."""
40 dot_muse = muse_dir(tmp_path)
41 (dot_muse / "commits").mkdir(parents=True)
42 (dot_muse / "snapshots").mkdir(parents=True)
43 (dot_muse / "objects").mkdir(parents=True)
44 (dot_muse / "refs" / "heads").mkdir(parents=True)
45 (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
46 (dot_muse / "HEAD").write_text("ref: refs/heads/main\n")
47 (dot_muse / "refs" / "heads" / "main").write_text("")
48 return tmp_path
49
50
51 def _make_object(root: pathlib.Path, content: bytes) -> str:
52 """Write raw bytes into the object store; return the object_id."""
53 oid = blob_id(content)
54 write_object(root, oid, content)
55 return oid
56
57
58 def _make_snapshot(root: pathlib.Path, manifest: Manifest) -> str:
59 """Write a snapshot with a valid content-hash snapshot_id. Returns the snapshot_id."""
60 snap_id = compute_snapshot_id(manifest)
61 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
62 return snap_id
63
64
65 def _make_commit(
66 root: pathlib.Path,
67 snapshot_id: str,
68 message: str = "test",
69 parent: str | None = None,
70 ) -> str:
71 """Write a commit with a valid content-hash commit_id. Returns the commit_id."""
72 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
73 parent_ids = [parent] if parent else []
74 commit_id = compute_commit_id(
75 parent_ids=parent_ids,
76 snapshot_id=snapshot_id,
77 message=message,
78 committed_at_iso=committed_at.isoformat(),
79 )
80 c = CommitRecord(
81 repo_id="test-repo",
82 commit_id=commit_id,
83 branch="main",
84 snapshot_id=snapshot_id,
85 message=message,
86 committed_at=committed_at,
87 parent_commit_id=parent,
88 )
89 write_commit(root, c)
90 return commit_id
91
92
93 # ---------------------------------------------------------------------------
94 # build_mpack tests
95 # ---------------------------------------------------------------------------
96
97
98 class TestBuildPack:
99 def test_single_commit_no_history(self, repo: pathlib.Path) -> None:
100 content = b"hello world"
101 oid = _make_object(repo, content)
102 snap_id = _make_snapshot(repo, {"file.txt": oid})
103 c1_id = _make_commit(repo, snap_id)
104
105 bundle = build_mpack(repo, [c1_id])
106
107 assert len(bundle.get("commits") or []) == 1
108 assert len(bundle.get("snapshots") or []) == 1
109 assert len(bundle.get("objects") or []) == 1
110 assert (bundle.get("objects") or [{}])[0]["object_id"] == oid
111
112 def test_object_content_is_raw_bytes(self, repo: pathlib.Path) -> None:
113 content = b"\x00\x01\x02\x03"
114 oid = _make_object(repo, content)
115 snap_id = _make_snapshot(repo, {"bin.dat": oid})
116 c1_id = _make_commit(repo, snap_id)
117
118 bundle = build_mpack(repo, [c1_id])
119
120 objs = bundle.get("objects") or []
121 assert len(objs) == 1
122 assert objs[0]["content"] == content
123
124 def test_multi_commit_chain(self, repo: pathlib.Path) -> None:
125 oid1 = _make_object(repo, b"v1")
126 oid2 = _make_object(repo, b"v2")
127 snap1_id = _make_snapshot(repo, {"f.txt": oid1})
128 snap2_id = _make_snapshot(repo, {"f.txt": oid2})
129 c1_id = _make_commit(repo, snap1_id)
130 c2_id = _make_commit(repo, snap2_id, parent=c1_id)
131
132 bundle = build_mpack(repo, [c2_id])
133
134 assert len(bundle.get("commits") or []) == 2
135 assert len(bundle.get("snapshots") or []) == 2
136 assert len(bundle.get("objects") or []) == 2
137
138 def test_have_excludes_ancestor_commits(self, repo: pathlib.Path) -> None:
139 oid1 = _make_object(repo, b"v1")
140 oid2 = _make_object(repo, b"v2")
141 snap1_id = _make_snapshot(repo, {"f.txt": oid1})
142 snap2_id = _make_snapshot(repo, {"f.txt": oid2})
143 c1_id = _make_commit(repo, snap1_id)
144 c2_id = _make_commit(repo, snap2_id, parent=c1_id)
145
146 bundle = build_mpack(repo, [c2_id], have=[c1_id])
147
148 # Only c2 should be in the bundle; c1 is in have.
149 commit_ids = [c["commit_id"] for c in (bundle.get("commits") or [])]
150 assert c2_id in commit_ids
151 assert c1_id not in commit_ids
152
153 def test_deduplicates_shared_objects(self, repo: pathlib.Path) -> None:
154 shared_oid = _make_object(repo, b"shared")
155 snap1_id = _make_snapshot(repo, {"a.txt": shared_oid})
156 snap2_id = _make_snapshot(repo, {"b.txt": shared_oid})
157 c1_id = _make_commit(repo, snap1_id)
158 c2_id = _make_commit(repo, snap2_id, parent=c1_id)
159
160 bundle = build_mpack(repo, [c2_id])
161
162 # Shared object should appear only once.
163 object_ids = [o["object_id"] for o in (bundle.get("objects") or [])]
164 assert object_ids.count(shared_oid) == 1
165
166 def test_empty_commit_ids_returns_empty_bundle(self, repo: pathlib.Path) -> None:
167 bundle = build_mpack(repo, [])
168 assert (bundle.get("commits") or []) == []
169 assert (bundle.get("objects") or []) == []
170
171 def test_missing_commit_skipped_gracefully(self, repo: pathlib.Path) -> None:
172 # Should not raise even if a commit_id does not exist.
173 bundle = build_mpack(repo, [fake_id("nonexistent")])
174 assert (bundle.get("commits") or []) == []
175
176 def test_snapshot_always_included_for_every_commit(self, repo: pathlib.Path) -> None:
177 """Every commit in the pack must have its snapshot included.
178
179 This is the data-integrity invariant that prevents the corruption
180 pattern where commits arrive on the remote without their snapshots,
181 making them permanently unreadable after a local .muse wipe.
182 """
183 oid = _make_object(repo, b"content")
184 snap_id = _make_snapshot(repo, {"a.txt": oid})
185 c_id = _make_commit(repo, snap_id)
186
187 bundle = build_mpack(repo, [c_id])
188
189 commit_snap_ids = {c["snapshot_id"] for c in (bundle.get("commits") or [])}
190 bundled_snap_ids = {s["snapshot_id"] for s in (bundle.get("snapshots") or [])}
191
192 assert commit_snap_ids == bundled_snap_ids, (
193 "Every commit's snapshot_id must appear in the bundle's snapshots list"
194 )
195
196 def test_missing_snapshot_raises_not_skips(self, repo: pathlib.Path) -> None:
197 """build_mpack must raise ValueError when a commit's snapshot is absent.
198
199 Silently skipping was the root cause of the recurring snapshot
200 corruption: commits reached the remote without their snapshots, and
201 subsequent pulls restored commits but not snapshots.
202 """
203 # Write commit record directly — no snapshot written
204 import datetime
205 from muse.core.snapshot import compute_commit_id
206 snap_id = "ab" * 32 # valid hex, but no snapshot file exists
207 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
208 c_id = compute_commit_id(
209 parent_ids=[],
210 snapshot_id=snap_id,
211 message="orphan",
212 committed_at_iso=committed_at.isoformat(),
213 )
214 write_commit(repo, CommitRecord(
215 commit_id=c_id, repo_id="test-repo", branch="main",
216 snapshot_id=snap_id, message="orphan", committed_at=committed_at,
217 ))
218
219 with pytest.raises(ValueError, match="Push aborted"):
220 build_mpack(repo, [c_id])
221
222 def test_merge_commit_includes_both_parents(self, repo: pathlib.Path) -> None:
223 oid_a = _make_object(repo, b"branch-a")
224 oid_b = _make_object(repo, b"branch-b")
225 snap_a_id = _make_snapshot(repo, {"a.txt": oid_a})
226 snap_b_id = _make_snapshot(repo, {"b.txt": oid_b})
227 snap_m_id = _make_snapshot(repo, {"a.txt": oid_a, "b.txt": oid_b})
228 c_a_id = _make_commit(repo, snap_a_id)
229 c_b_id = _make_commit(repo, snap_b_id)
230 # Merge commit with two parents — compute its ID from both parent hashes.
231 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
232 c_merge_id = compute_commit_id(
233 parent_ids=[c_a_id, c_b_id],
234 snapshot_id=snap_m_id,
235 message="merge",
236 committed_at_iso=committed_at.isoformat(),
237 )
238 c_merge = CommitRecord(
239 repo_id="test-repo",
240 commit_id=c_merge_id,
241 branch="main",
242 snapshot_id=snap_m_id,
243 message="merge",
244 committed_at=committed_at,
245 parent_commit_id=c_a_id,
246 parent2_commit_id=c_b_id,
247 )
248 write_commit(repo, c_merge)
249
250 bundle = build_mpack(repo, [c_merge_id])
251 commit_ids = {c["commit_id"] for c in (bundle.get("commits") or [])}
252 assert {c_merge_id, c_a_id, c_b_id}.issubset(commit_ids)
253
254
255 # ---------------------------------------------------------------------------
256 # apply_mpack tests
257 # ---------------------------------------------------------------------------
258
259
260 class TestApplyPack:
261 def test_round_trip(self, repo: pathlib.Path, tmp_path: pathlib.Path) -> None:
262 """build_mpack → apply_mpack in a fresh repo produces identical data."""
263 content = b"round trip"
264 oid = _make_object(repo, content)
265 snap_id = _make_snapshot(repo, {"f.txt": oid})
266 c1_id = _make_commit(repo, snap_id, message="initial")
267
268 bundle = build_mpack(repo, [c1_id])
269
270 # Apply into a fresh repo.
271 dest = tmp_path / "dest"
272 dot_muse = muse_dir(dest)
273 (dot_muse / "commits").mkdir(parents=True)
274 (dot_muse / "snapshots").mkdir(parents=True)
275 (dot_muse / "objects").mkdir(parents=True)
276
277 result = apply_mpack(dest, bundle)
278
279 assert result["objects_written"] == 1
280 assert has_object(dest, oid)
281 assert read_object(dest, oid) == content
282 assert read_snapshot(dest, snap_id) is not None
283 assert read_commit(dest, c1_id) is not None
284
285 def test_idempotent_apply(self, repo: pathlib.Path) -> None:
286 """Applying the same bundle twice does not raise and new_count = 0."""
287 content = b"idempotent"
288 oid = _make_object(repo, content)
289 snap_id = _make_snapshot(repo, {"f.txt": oid})
290 c1_id = _make_commit(repo, snap_id)
291
292 bundle = build_mpack(repo, [c1_id])
293 apply_mpack(repo, bundle)
294 result = apply_mpack(repo, bundle)
295
296 assert result["objects_written"] == 0 # All already present.
297
298 def test_malformed_object_skipped(self, repo: pathlib.Path) -> None:
299 # content must be bytes; passing wrong type is caught gracefully
300 bundle: MPackBundle = {
301 "commits": [],
302 "snapshots": [],
303 "objects": [ObjectPayload(object_id="abc123", content=b"")],
304 }
305 result = apply_mpack(repo, bundle)
306 assert result["objects_written"] == 0
307
308 def test_empty_bundle_is_noop(self, repo: pathlib.Path) -> None:
309 bundle: MPackBundle = {}
310 result = apply_mpack(repo, bundle)
311 assert result["objects_written"] == 0
312
313 def test_apply_preserves_commit_metadata(
314 self, repo: pathlib.Path, tmp_path: pathlib.Path
315 ) -> None:
316 oid = _make_object(repo, b"data")
317 snap_id = _make_snapshot(repo, {"data.bin": oid})
318 c1_id = _make_commit(repo, snap_id, message="preserve me")
319
320 bundle = build_mpack(repo, [c1_id])
321
322 dest = tmp_path / "d"
323 (commits_dir(dest)).mkdir(parents=True)
324 (snapshots_dir(dest)).mkdir(parents=True)
325 (objects_dir(dest)).mkdir(parents=True)
326 apply_mpack(dest, bundle)
327
328 commit = read_commit(dest, c1_id)
329 assert commit is not None
330 assert commit.message == "preserve me"
331 assert commit.snapshot_id == snap_id
332
333 def test_apply_returns_new_object_count(
334 self, repo: pathlib.Path, tmp_path: pathlib.Path
335 ) -> None:
336 oid1 = _make_object(repo, b"obj1")
337 oid2 = _make_object(repo, b"obj2")
338 snap_id = _make_snapshot(repo, {"a": oid1, "b": oid2})
339 c1_id = _make_commit(repo, snap_id)
340
341 bundle = build_mpack(repo, [c1_id])
342 dest = tmp_path / "d"
343 (commits_dir(dest)).mkdir(parents=True)
344 (snapshots_dir(dest)).mkdir(parents=True)
345 (objects_dir(dest)).mkdir(parents=True)
346
347 result = apply_mpack(dest, bundle)
348 assert result["objects_written"] == 2
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 121 days ago