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