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