gabriel / muse public
test_core_pack.py python
292 lines 10.7 KB
Raw
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 154 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 PackBundle,
15 apply_pack,
16 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 import hashlib
53 oid = hashlib.sha256(content).hexdigest()
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(parent_ids, snapshot_id, message, committed_at.isoformat())
75 c = CommitRecord(
76 commit_id=commit_id,
77 repo_id="test-repo",
78 branch="main",
79 snapshot_id=snapshot_id,
80 message=message,
81 committed_at=committed_at,
82 parent_commit_id=parent,
83 )
84 write_commit(root, c)
85 return commit_id
86
87
88 # ---------------------------------------------------------------------------
89 # build_pack tests
90 # ---------------------------------------------------------------------------
91
92
93 class TestBuildPack:
94 def test_single_commit_no_history(self, repo: pathlib.Path) -> None:
95 content = b"hello world"
96 oid = _make_object(repo, content)
97 snap_id = _make_snapshot(repo, {"file.txt": oid})
98 c1_id = _make_commit(repo, snap_id)
99
100 bundle = build_pack(repo, [c1_id])
101
102 assert len(bundle.get("commits") or []) == 1
103 assert len(bundle.get("snapshots") or []) == 1
104 assert len(bundle.get("objects") or []) == 1
105 assert (bundle.get("objects") or [{}])[0]["object_id"] == oid
106
107 def test_object_content_is_raw_bytes(self, repo: pathlib.Path) -> None:
108 content = b"\x00\x01\x02\x03"
109 oid = _make_object(repo, content)
110 snap_id = _make_snapshot(repo, {"bin.dat": oid})
111 c1_id = _make_commit(repo, snap_id)
112
113 bundle = build_pack(repo, [c1_id])
114
115 objs = bundle.get("objects") or []
116 assert len(objs) == 1
117 assert objs[0]["content"] == content
118
119 def test_multi_commit_chain(self, repo: pathlib.Path) -> None:
120 oid1 = _make_object(repo, b"v1")
121 oid2 = _make_object(repo, b"v2")
122 snap1_id = _make_snapshot(repo, {"f.txt": oid1})
123 snap2_id = _make_snapshot(repo, {"f.txt": oid2})
124 c1_id = _make_commit(repo, snap1_id)
125 c2_id = _make_commit(repo, snap2_id, parent=c1_id)
126
127 bundle = build_pack(repo, [c2_id])
128
129 assert len(bundle.get("commits") or []) == 2
130 assert len(bundle.get("snapshots") or []) == 2
131 assert len(bundle.get("objects") or []) == 2
132
133 def test_have_excludes_ancestor_commits(self, repo: pathlib.Path) -> None:
134 oid1 = _make_object(repo, b"v1")
135 oid2 = _make_object(repo, b"v2")
136 snap1_id = _make_snapshot(repo, {"f.txt": oid1})
137 snap2_id = _make_snapshot(repo, {"f.txt": oid2})
138 c1_id = _make_commit(repo, snap1_id)
139 c2_id = _make_commit(repo, snap2_id, parent=c1_id)
140
141 bundle = build_pack(repo, [c2_id], have=[c1_id])
142
143 # Only c2 should be in the bundle; c1 is in have.
144 commit_ids = [c["commit_id"] for c in (bundle.get("commits") or [])]
145 assert c2_id in commit_ids
146 assert c1_id not in commit_ids
147
148 def test_deduplicates_shared_objects(self, repo: pathlib.Path) -> None:
149 shared_oid = _make_object(repo, b"shared")
150 snap1_id = _make_snapshot(repo, {"a.txt": shared_oid})
151 snap2_id = _make_snapshot(repo, {"b.txt": shared_oid})
152 c1_id = _make_commit(repo, snap1_id)
153 c2_id = _make_commit(repo, snap2_id, parent=c1_id)
154
155 bundle = build_pack(repo, [c2_id])
156
157 # Shared object should appear only once.
158 object_ids = [o["object_id"] for o in (bundle.get("objects") or [])]
159 assert object_ids.count(shared_oid) == 1
160
161 def test_empty_commit_ids_returns_empty_bundle(self, repo: pathlib.Path) -> None:
162 bundle = build_pack(repo, [])
163 assert (bundle.get("commits") or []) == []
164 assert (bundle.get("objects") or []) == []
165
166 def test_missing_commit_skipped_gracefully(self, repo: pathlib.Path) -> None:
167 # Should not raise even if a commit_id does not exist.
168 bundle = build_pack(repo, ["nonexistent"])
169 assert (bundle.get("commits") or []) == []
170
171 def test_merge_commit_includes_both_parents(self, repo: pathlib.Path) -> None:
172 oid_a = _make_object(repo, b"branch-a")
173 oid_b = _make_object(repo, b"branch-b")
174 snap_a_id = _make_snapshot(repo, {"a.txt": oid_a})
175 snap_b_id = _make_snapshot(repo, {"b.txt": oid_b})
176 snap_m_id = _make_snapshot(repo, {"a.txt": oid_a, "b.txt": oid_b})
177 c_a_id = _make_commit(repo, snap_a_id)
178 c_b_id = _make_commit(repo, snap_b_id)
179 # Merge commit with two parents — compute its ID from both parent hashes.
180 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
181 c_merge_id = compute_commit_id([c_a_id, c_b_id], snap_m_id, "merge", committed_at.isoformat())
182 c_merge = CommitRecord(
183 commit_id=c_merge_id,
184 repo_id="test-repo",
185 branch="main",
186 snapshot_id=snap_m_id,
187 message="merge",
188 committed_at=committed_at,
189 parent_commit_id=c_a_id,
190 parent2_commit_id=c_b_id,
191 )
192 write_commit(repo, c_merge)
193
194 bundle = build_pack(repo, [c_merge_id])
195 commit_ids = {c["commit_id"] for c in (bundle.get("commits") or [])}
196 assert {c_merge_id, c_a_id, c_b_id}.issubset(commit_ids)
197
198
199 # ---------------------------------------------------------------------------
200 # apply_pack tests
201 # ---------------------------------------------------------------------------
202
203
204 class TestApplyPack:
205 def test_round_trip(self, repo: pathlib.Path, tmp_path: pathlib.Path) -> None:
206 """build_pack → apply_pack in a fresh repo produces identical data."""
207 content = b"round trip"
208 oid = _make_object(repo, content)
209 snap_id = _make_snapshot(repo, {"f.txt": oid})
210 c1_id = _make_commit(repo, snap_id, message="initial")
211
212 bundle = build_pack(repo, [c1_id])
213
214 # Apply into a fresh repo.
215 dest = tmp_path / "dest"
216 muse_dir = dest / ".muse"
217 (muse_dir / "commits").mkdir(parents=True)
218 (muse_dir / "snapshots").mkdir(parents=True)
219 (muse_dir / "objects").mkdir(parents=True)
220
221 result = apply_pack(dest, bundle)
222
223 assert result["objects_written"] == 1
224 assert has_object(dest, oid)
225 assert read_object(dest, oid) == content
226 assert read_snapshot(dest, snap_id) is not None
227 assert read_commit(dest, c1_id) is not None
228
229 def test_idempotent_apply(self, repo: pathlib.Path) -> None:
230 """Applying the same bundle twice does not raise and new_count = 0."""
231 content = b"idempotent"
232 oid = _make_object(repo, content)
233 snap_id = _make_snapshot(repo, {"f.txt": oid})
234 c1_id = _make_commit(repo, snap_id)
235
236 bundle = build_pack(repo, [c1_id])
237 apply_pack(repo, bundle)
238 result = apply_pack(repo, bundle)
239
240 assert result["objects_written"] == 0 # All already present.
241
242 def test_malformed_object_skipped(self, repo: pathlib.Path) -> None:
243 # content must be bytes; passing wrong type is caught gracefully
244 bundle: PackBundle = {
245 "commits": [],
246 "snapshots": [],
247 "objects": [ObjectPayload(object_id="abc123", content=b"")],
248 }
249 result = apply_pack(repo, bundle)
250 assert result["objects_written"] == 0
251
252 def test_empty_bundle_is_noop(self, repo: pathlib.Path) -> None:
253 bundle: PackBundle = {}
254 result = apply_pack(repo, bundle)
255 assert result["objects_written"] == 0
256
257 def test_apply_preserves_commit_metadata(
258 self, repo: pathlib.Path, tmp_path: pathlib.Path
259 ) -> None:
260 oid = _make_object(repo, b"data")
261 snap_id = _make_snapshot(repo, {"data.bin": oid})
262 c1_id = _make_commit(repo, snap_id, message="preserve me")
263
264 bundle = build_pack(repo, [c1_id])
265
266 dest = tmp_path / "d"
267 (dest / ".muse" / "commits").mkdir(parents=True)
268 (dest / ".muse" / "snapshots").mkdir(parents=True)
269 (dest / ".muse" / "objects").mkdir(parents=True)
270 apply_pack(dest, bundle)
271
272 commit = read_commit(dest, c1_id)
273 assert commit is not None
274 assert commit.message == "preserve me"
275 assert commit.snapshot_id == snap_id
276
277 def test_apply_returns_new_object_count(
278 self, repo: pathlib.Path, tmp_path: pathlib.Path
279 ) -> None:
280 oid1 = _make_object(repo, b"obj1")
281 oid2 = _make_object(repo, b"obj2")
282 snap_id = _make_snapshot(repo, {"a": oid1, "b": oid2})
283 c1_id = _make_commit(repo, snap_id)
284
285 bundle = build_pack(repo, [c1_id])
286 dest = tmp_path / "d"
287 (dest / ".muse" / "commits").mkdir(parents=True)
288 (dest / ".muse" / "snapshots").mkdir(parents=True)
289 (dest / ".muse" / "objects").mkdir(parents=True)
290
291 result = apply_pack(dest, bundle)
292 assert result["objects_written"] == 2
File History 1 commit
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 154 days ago