gabriel / muse public
test_mpack_bundle.py python
407 lines 16.4 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 127 days ago
1 """Tests for MPackBundle — the MPack wire format replacing PackBundle.
2
3 Coverage tiers
4 --------------
5 - MPackBundle TypedDict: fields, total=False (partial bundles valid)
6 - MPackSummary TypedDict: all advisory summary fields
7 - build_mpack: produces MPackBundle with commits, snapshots, objects
8 - build_mpack: have-set exclusion (only sends delta)
9 - build_mpack: summary field populated correctly
10 - build_mpack: raises on missing snapshot
11 - apply_mpack: writes objects → snapshots → commits in order
12 - apply_mpack: idempotent (safe to call twice)
13 - apply_mpack: pack-bomb guard respected
14 - apply_mpack: returns MPackApplyResult with counts
15 - Round-trip: build_mpack → apply_mpack recovers all data
16 - MPackBundle replaces PackBundle everywhere — PackBundle no longer exists
17 - Phase 2 — BundleMeta: mode, base_commits, created_at always present
18 """
19 from __future__ import annotations
20 from collections.abc import Mapping
21
22 import datetime
23 import json
24 import pathlib
25
26 import pytest
27
28 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
29 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
30 from muse.core.object_store import write_object
31 from muse.core.types import blob_id, long_id
32 from muse.core.paths import heads_dir, muse_dir, snapshots_dir
33
34 _DT = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
35
36
37
38
39 def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path:
40 muse = muse_dir(tmp_path)
41 for d in ("commits", "snapshots", "objects", "refs/heads"):
42 (muse / d).mkdir(parents=True, exist_ok=True)
43 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
44 (muse / "repo.json").write_text(
45 json.dumps({"repo_id": "mpack-bundle-test", "domain": "code"}),
46 encoding="utf-8",
47 )
48 return tmp_path
49
50
51 def _commit(root: pathlib.Path, files: Mapping[str, bytes]) -> str:
52 manifest = {}
53 for path, content in files.items():
54 oid = blob_id(content)
55 write_object(root, oid, content)
56 manifest[path] = oid
57 snap_id = compute_snapshot_id(manifest)
58 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest, created_at=_DT))
59 parent_ref = heads_dir(root) / "main"
60 parent = parent_ref.read_text().strip() if parent_ref.exists() else None
61 parent_ids = [parent] if parent else []
62 cid = compute_commit_id( parent_ids=parent_ids,
63 snapshot_id=snap_id,
64 message="test",
65 committed_at_iso=_DT.isoformat(),
66 )
67 write_commit(root, CommitRecord(
68 commit_id=cid, repo_id="mpack-bundle-test", branch="main",
69 snapshot_id=snap_id, message="test", committed_at=_DT,
70 parent_commit_id=parent,
71 ))
72 parent_ref.write_text(cid, encoding="utf-8")
73 return cid
74
75
76 # ---------------------------------------------------------------------------
77 # MPackBundle TypedDict
78 # ---------------------------------------------------------------------------
79
80
81 class TestMPackBundleTypedDict:
82 def test_exists(self) -> None:
83 from muse.core.pack import MPackBundle
84 assert MPackBundle is not None
85
86 def test_has_commits_field(self) -> None:
87 from muse.core.pack import MPackBundle
88 from typing import get_type_hints
89 hints = get_type_hints(MPackBundle)
90 assert "commits" in hints
91
92 def test_has_snapshots_field(self) -> None:
93 from muse.core.pack import MPackBundle
94 from typing import get_type_hints
95 hints = get_type_hints(MPackBundle)
96 assert "snapshots" in hints
97
98 def test_has_objects_field(self) -> None:
99 from muse.core.pack import MPackBundle
100 from typing import get_type_hints
101 hints = get_type_hints(MPackBundle)
102 assert "objects" in hints
103
104 def test_has_tags_field(self) -> None:
105 from muse.core.pack import MPackBundle
106 from typing import get_type_hints
107 hints = get_type_hints(MPackBundle)
108 assert "tags" in hints
109
110 def test_has_branch_heads_field(self) -> None:
111 from muse.core.pack import MPackBundle
112 from typing import get_type_hints
113 hints = get_type_hints(MPackBundle)
114 assert "branch_heads" in hints
115
116 def test_pack_bundle_no_longer_exists(self) -> None:
117 """PackBundle is replaced by MPackBundle — no backward compat."""
118 with pytest.raises(ImportError):
119 from muse.core.pack import PackBundle # noqa: F401
120
121
122 # ---------------------------------------------------------------------------
123 # MPackSummary TypedDict
124 # ---------------------------------------------------------------------------
125
126
127 class TestMPackSummary:
128 def test_exists(self) -> None:
129 from muse.core.pack import MPackSummary
130 assert MPackSummary is not None
131
132 def test_has_required_advisory_fields(self) -> None:
133 from muse.core.pack import MPackSummary
134 from typing import get_type_hints
135 hints = get_type_hints(MPackSummary)
136 for field in ("commits_count", "objects_count", "objects_bytes", "agent_ids"):
137 assert field in hints, f"MPackSummary missing field {field!r}"
138
139 def test_has_branch_fields(self) -> None:
140 from muse.core.pack import MPackSummary
141 from typing import get_type_hints
142 hints = get_type_hints(MPackSummary)
143 assert "branches" in hints
144
145
146 # ---------------------------------------------------------------------------
147 # build_mpack
148 # ---------------------------------------------------------------------------
149
150
151 class TestBuildMpack:
152 def test_single_commit(self, tmp_path: pathlib.Path) -> None:
153 from muse.core.pack import build_mpack
154 root = _init_repo(tmp_path)
155 cid = _commit(root, {"a.py": b"# a\n"})
156 bundle = build_mpack(root, [cid])
157 assert len(bundle["commits"]) == 1
158 assert len(bundle["snapshots"]) >= 1
159 assert len(bundle["objects"]) >= 1
160
161 def test_have_exclusion(self, tmp_path: pathlib.Path) -> None:
162 from muse.core.pack import build_mpack
163 root = _init_repo(tmp_path)
164 c1 = _commit(root, {"a.py": b"# a\n"})
165 c2 = _commit(root, {"b.py": b"# b\n"})
166 bundle = build_mpack(root, [c2], have=[c1])
167 commit_ids = [c["commit_id"] for c in bundle["commits"]]
168 assert c2 in commit_ids
169 assert c1 not in commit_ids
170
171 def test_raises_on_missing_snapshot(self, tmp_path: pathlib.Path) -> None:
172 from muse.core.pack import build_mpack
173 root = _init_repo(tmp_path)
174 cid = _commit(root, {"a.py": b"# a\n"})
175 # Delete the snapshot file to simulate corruption
176 snap_dir = snapshots_dir(root)
177 for f in snap_dir.rglob("*.msgpack"):
178 f.unlink()
179 with pytest.raises(ValueError, match="snapshot"):
180 build_mpack(root, [cid])
181
182 def test_empty_commit_ids_returns_empty_bundle(self, tmp_path: pathlib.Path) -> None:
183 from muse.core.pack import build_mpack
184 root = _init_repo(tmp_path)
185 bundle = build_mpack(root, [])
186 assert bundle.get("commits", []) == []
187 assert bundle.get("objects", []) == []
188
189 def test_objects_have_sha256_prefixed_ids(self, tmp_path: pathlib.Path) -> None:
190 from muse.core.pack import build_mpack
191 root = _init_repo(tmp_path)
192 cid = _commit(root, {"main.py": b"print('hello')\n"})
193 bundle = build_mpack(root, [cid])
194 for obj in bundle["objects"]:
195 assert obj["object_id"].startswith("sha256:"), (
196 f"object_id not sha256-prefixed: {obj['object_id']!r}"
197 )
198
199 def test_multi_commit_chain(self, tmp_path: pathlib.Path) -> None:
200 from muse.core.pack import build_mpack
201 root = _init_repo(tmp_path)
202 c1 = _commit(root, {"a.py": b"v1\n"})
203 c2 = _commit(root, {"a.py": b"v2\n"})
204 c3 = _commit(root, {"a.py": b"v3\n"})
205 bundle = build_mpack(root, [c3])
206 commit_ids = {c["commit_id"] for c in bundle["commits"]}
207 assert c1 in commit_ids
208 assert c2 in commit_ids
209 assert c3 in commit_ids
210
211 def test_summary_populated(self, tmp_path: pathlib.Path) -> None:
212 from muse.core.pack import build_mpack
213 root = _init_repo(tmp_path)
214 _commit(root, {"a.py": b"# a\n"})
215 cid = _commit(root, {"b.py": b"# b\n"})
216 bundle = build_mpack(root, [cid])
217 summary = bundle.get("summary")
218 assert summary is not None
219 assert summary["commits_count"] >= 1
220 assert summary["objects_count"] >= 1
221 assert summary["objects_bytes"] >= 0
222
223
224 # ---------------------------------------------------------------------------
225 # apply_mpack
226 # ---------------------------------------------------------------------------
227
228
229 class TestApplyMpack:
230 def test_round_trip(self, tmp_path: pathlib.Path) -> None:
231 from muse.core.pack import build_mpack, apply_mpack
232 src = _init_repo(tmp_path / "src")
233 dst = _init_repo(tmp_path / "dst")
234 cid = _commit(src, {"a.py": b"# hello\n"})
235 bundle = build_mpack(src, [cid])
236 result = apply_mpack(dst, bundle)
237 assert result["commits_written"] >= 1
238 assert result["objects_written"] >= 1
239
240 def test_idempotent(self, tmp_path: pathlib.Path) -> None:
241 from muse.core.pack import build_mpack, apply_mpack
242 root = _init_repo(tmp_path)
243 cid = _commit(root, {"x.py": b"# x\n"})
244 bundle = build_mpack(root, [cid])
245 apply_mpack(root, bundle)
246 result2 = apply_mpack(root, bundle)
247 assert result2["commits_written"] == 0
248 assert result2["objects_skipped"] >= 1
249
250 def test_empty_bundle_is_noop(self, tmp_path: pathlib.Path) -> None:
251 from muse.core.pack import MPackBundle, apply_mpack
252 root = _init_repo(tmp_path)
253 empty: MPackBundle = {}
254 result = apply_mpack(root, empty)
255 assert result["commits_written"] == 0
256 assert result["objects_written"] == 0
257
258 def test_pack_bomb_rejected(self, tmp_path: pathlib.Path) -> None:
259 from muse.core.pack import MPackBundle, apply_mpack
260 root = _init_repo(tmp_path)
261 # Create a bundle claiming 100k objects (far exceeds MAX_PACK_OBJECTS)
262 fake_objects = [{"object_id": long_id('a'*64), "content": b"x"}] * 100_001
263 bundle: MPackBundle = {"objects": fake_objects} # type: ignore[typeddict-item]
264 with pytest.raises(ValueError, match="limit"):
265 apply_mpack(root, bundle)
266
267 def test_returns_apply_result(self, tmp_path: pathlib.Path) -> None:
268 from muse.core.pack import build_mpack, apply_mpack
269 src = _init_repo(tmp_path / "src")
270 dst = _init_repo(tmp_path / "dst")
271 cid = _commit(src, {"f.py": b"# f\n"})
272 bundle = build_mpack(src, [cid])
273 result = apply_mpack(dst, bundle)
274 for field in ("commits_written", "snapshots_written", "objects_written", "objects_skipped"):
275 assert field in result, f"apply_mpack result missing {field!r}"
276
277
278 # ---------------------------------------------------------------------------
279 # Phase 2 — BundleMeta
280 # ---------------------------------------------------------------------------
281
282 class TestBundleMeta:
283 """build_mpack always writes a self-describing ``meta`` field."""
284
285 # -----------------------------------------------------------------
286 # BundleMeta TypedDict exists and is annotated
287 # -----------------------------------------------------------------
288
289 def test_bundle_meta_typeddict_exists(self) -> None:
290 from muse.core.pack import BundleMeta
291 assert BundleMeta is not None
292
293 def test_bundle_meta_has_mode_annotation(self) -> None:
294 from muse.core.pack import BundleMeta
295 from typing import get_type_hints
296 hints = get_type_hints(BundleMeta)
297 assert "mode" in hints
298
299 def test_bundle_meta_has_base_commits_annotation(self) -> None:
300 from muse.core.pack import BundleMeta
301 from typing import get_type_hints
302 hints = get_type_hints(BundleMeta)
303 assert "base_commits" in hints
304
305 def test_bundle_meta_has_created_at_annotation(self) -> None:
306 from muse.core.pack import BundleMeta
307 from typing import get_type_hints
308 hints = get_type_hints(BundleMeta)
309 assert "created_at" in hints
310
311 def test_mpack_bundle_has_meta_field(self) -> None:
312 from muse.core.pack import MPackBundle
313 from typing import get_type_hints
314 hints = get_type_hints(MPackBundle)
315 assert "meta" in hints
316
317 # -----------------------------------------------------------------
318 # Full bundle (no have) — mode == "full", base_commits == []
319 # -----------------------------------------------------------------
320
321 def test_full_bundle_has_meta(self, tmp_path: pathlib.Path) -> None:
322 from muse.core.pack import build_mpack
323 repo = _init_repo(tmp_path)
324 cid = _commit(repo, {"a.py": b"content"})
325 bundle = build_mpack(repo, [cid])
326 assert "meta" in bundle
327
328 def test_full_bundle_mode_is_full(self, tmp_path: pathlib.Path) -> None:
329 from muse.core.pack import build_mpack
330 repo = _init_repo(tmp_path)
331 cid = _commit(repo, {"a.py": b"content"})
332 bundle = build_mpack(repo, [cid])
333 assert bundle["meta"]["mode"] == "full"
334
335 def test_full_bundle_base_commits_empty(self, tmp_path: pathlib.Path) -> None:
336 from muse.core.pack import build_mpack
337 repo = _init_repo(tmp_path)
338 cid = _commit(repo, {"a.py": b"content"})
339 bundle = build_mpack(repo, [cid])
340 assert bundle["meta"]["base_commits"] == []
341
342 def test_full_bundle_created_at_is_str(self, tmp_path: pathlib.Path) -> None:
343 from muse.core.pack import build_mpack
344 repo = _init_repo(tmp_path)
345 cid = _commit(repo, {"a.py": b"content"})
346 bundle = build_mpack(repo, [cid])
347 assert isinstance(bundle["meta"]["created_at"], str)
348 assert len(bundle["meta"]["created_at"]) > 10 # not empty
349
350 def test_full_bundle_created_at_is_iso(self, tmp_path: pathlib.Path) -> None:
351 from muse.core.pack import build_mpack
352 import datetime
353 repo = _init_repo(tmp_path)
354 cid = _commit(repo, {"a.py": b"content"})
355 bundle = build_mpack(repo, [cid])
356 # Must be parseable as ISO 8601
357 dt = datetime.datetime.fromisoformat(bundle["meta"]["created_at"].rstrip("Z"))
358 assert dt.year >= 2024
359
360 # -----------------------------------------------------------------
361 # Incremental bundle (have set) — mode == "incremental"
362 # -----------------------------------------------------------------
363
364 def test_incremental_bundle_mode_is_incremental(self, tmp_path: pathlib.Path) -> None:
365 from muse.core.pack import build_mpack
366 repo = _init_repo(tmp_path)
367 base_cid = _commit(repo, {"a.py": b"v1"})
368 tip_cid = _commit(repo, {"a.py": b"v2"})
369 bundle = build_mpack(repo, [tip_cid], have=[base_cid])
370 assert bundle["meta"]["mode"] == "incremental"
371
372 def test_incremental_bundle_base_commits_populated(self, tmp_path: pathlib.Path) -> None:
373 from muse.core.pack import build_mpack
374 repo = _init_repo(tmp_path)
375 base_cid = _commit(repo, {"a.py": b"v1"})
376 tip_cid = _commit(repo, {"a.py": b"v2"})
377 bundle = build_mpack(repo, [tip_cid], have=[base_cid])
378 assert base_cid in bundle["meta"]["base_commits"]
379
380 def test_incremental_bundle_multiple_have(self, tmp_path: pathlib.Path) -> None:
381 from muse.core.pack import build_mpack
382 repo = _init_repo(tmp_path)
383 c1 = _commit(repo, {"a.py": b"v1"})
384 c2 = _commit(repo, {"a.py": b"v2"})
385 c3 = _commit(repo, {"a.py": b"v3"})
386 bundle = build_mpack(repo, [c3], have=[c1, c2])
387 assert set(bundle["meta"]["base_commits"]) == {c1, c2}
388
389 def test_incremental_bundle_has_created_at(self, tmp_path: pathlib.Path) -> None:
390 from muse.core.pack import build_mpack
391 repo = _init_repo(tmp_path)
392 base = _commit(repo, {"a.py": b"base"})
393 tip = _commit(repo, {"a.py": b"tip"})
394 bundle = build_mpack(repo, [tip], have=[base])
395 assert isinstance(bundle["meta"]["created_at"], str)
396
397 # -----------------------------------------------------------------
398 # Empty have list treated as full
399 # -----------------------------------------------------------------
400
401 def test_empty_have_list_means_full(self, tmp_path: pathlib.Path) -> None:
402 from muse.core.pack import build_mpack
403 repo = _init_repo(tmp_path)
404 cid = _commit(repo, {"f.py": b"data"})
405 bundle = build_mpack(repo, [cid], have=[])
406 assert bundle["meta"]["mode"] == "full"
407 assert bundle["meta"]["base_commits"] == []
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 127 days ago