gabriel / muse public
test_mpack_snapshot_integrity.py python
320 lines 13.2 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 120 days ago
1 """Tests for the missing-snapshot integrity invariant in pack building.
2
3 Root cause
4 ----------
5 ``build_mpack_from_walk`` silently skips a snapshot when its file is absent,
6 but still includes the commit that references it in the pack bundle. The
7 remote then receives a commit record pointing to a snapshot_id it will never
8 have — a dangling reference that silently corrupts the remote's history.
9
10 Invariant being enforced
11 ------------------------
12 Every commit in a push bundle MUST have its snapshot present in the local
13 store. If any snapshot file is missing, ``build_mpack_from_walk`` raises
14 ``ValueError`` ("Push aborted") rather than sending a commit with a dangling
15 snapshot reference. Behaviour:
16
17 * ``walk_commits`` detects missing snapshots and reports them in
18 ``missing_snapshots``; a WARNING is emitted for each.
19 * ``build_mpack_from_walk`` raises ``ValueError`` if ``missing_snapshots``
20 is non-empty — no partial bundle is ever returned.
21
22 These tests drive the implementation in ``muse/core/pack.py``.
23 """
24
25 from __future__ import annotations
26
27 import datetime
28 import pathlib
29
30 import pytest
31
32 from muse.core.types import Manifest, blob_id
33 from muse.core.object_store import write_object
34
35 type _FileBytes = dict[str, bytes]
36 from muse.core.pack import MPackBundle, build_mpack_from_walk, walk_commits
37 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
38 from muse.core.store import (
39 CommitRecord,
40 SnapshotRecord,
41 write_commit,
42 write_snapshot,
43 )
44 from muse.core.paths import ref_path, muse_dir
45
46 # ---------------------------------------------------------------------------
47 # Helpers
48 # ---------------------------------------------------------------------------
49
50 _REPO_ID = "integrity-test"
51
52
53
54
55 def _init_repo(root: pathlib.Path) -> None:
56 import json as _json
57 dot_muse = muse_dir(root)
58 for d in ("commits", "snapshots", "objects", "refs/heads"):
59 (dot_muse / d).mkdir(parents=True, exist_ok=True)
60 (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
61 (dot_muse / "repo.json").write_text(
62 _json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
63 )
64
65
66 def _make_commit(
67 root: pathlib.Path,
68 files: _FileBytes,
69 message: str,
70 parent_id: str | None = None,
71 branch: str = "main",
72 write_snap: bool = True,
73 ) -> CommitRecord:
74 """Create a commit, optionally skipping snapshot write to simulate corruption."""
75 manifest = {}
76 for path, content in files.items():
77 oid = blob_id(content)
78 write_object(root, oid, content)
79 manifest[path] = oid
80
81 snap_id = compute_snapshot_id(manifest)
82 now = datetime.datetime.now(datetime.timezone.utc)
83 commit_id = compute_commit_id(
84 parent_ids=[parent_id] if parent_id else [],
85 snapshot_id=snap_id,
86 message=message,
87 committed_at_iso=now.isoformat(),
88 )
89
90 if write_snap:
91 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
92
93 record = CommitRecord(
94 repo_id=_REPO_ID,
95 commit_id=commit_id,
96 branch=branch,
97 snapshot_id=snap_id,
98 message=message,
99 committed_at=now,
100 parent_commit_id=parent_id,
101 )
102 write_commit(root, record)
103 (ref_path(root, branch)).write_text(commit_id, encoding="utf-8")
104 return record
105
106
107 # ---------------------------------------------------------------------------
108 # I — walk_commits exposes missing_snapshots
109 # ---------------------------------------------------------------------------
110
111 class TestWalkCommitsMissingSnapshotDetection:
112 """walk_commits must report commits whose snapshot files are absent."""
113
114 def test_walk_commits_no_missing_snapshots_when_all_present(
115 self, tmp_path: pathlib.Path
116 ) -> None:
117 _init_repo(tmp_path)
118 c = _make_commit(tmp_path, {"a.py": b"x"}, "first", write_snap=True)
119 result = walk_commits(tmp_path, [c.commit_id])
120 assert not result["missing_snapshots"], (
121 "No snapshots are missing — missing_snapshots should be empty"
122 )
123
124 def test_walk_commits_detects_single_missing_snapshot(
125 self, tmp_path: pathlib.Path
126 ) -> None:
127 _init_repo(tmp_path)
128 c1 = _make_commit(tmp_path, {"a.py": b"v1"}, "first", write_snap=True)
129 # Second commit: snapshot file deliberately not written
130 c2 = _make_commit(tmp_path, {"a.py": b"v2"}, "second",
131 parent_id=c1.commit_id, write_snap=False)
132
133 result = walk_commits(tmp_path, [c2.commit_id])
134 assert c2.snapshot_id in result["missing_snapshots"], (
135 "walk_commits must expose the missing snapshot_id"
136 )
137
138 def test_walk_commits_detects_multiple_missing_snapshots_in_chain(
139 self, tmp_path: pathlib.Path
140 ) -> None:
141 _init_repo(tmp_path)
142 c1 = _make_commit(tmp_path, {"f.py": b"v1"}, "A", write_snap=True)
143 c2 = _make_commit(tmp_path, {"f.py": b"v2"}, "B",
144 parent_id=c1.commit_id, write_snap=False)
145 c3 = _make_commit(tmp_path, {"f.py": b"v3"}, "C",
146 parent_id=c2.commit_id, write_snap=False)
147 c4 = _make_commit(tmp_path, {"f.py": b"v4"}, "D",
148 parent_id=c3.commit_id, write_snap=True)
149
150 result = walk_commits(tmp_path, [c4.commit_id])
151 assert c2.snapshot_id in result["missing_snapshots"]
152 assert c3.snapshot_id in result["missing_snapshots"]
153 assert c1.snapshot_id not in result["missing_snapshots"]
154 assert c4.snapshot_id not in result["missing_snapshots"]
155
156 def test_walk_commits_missing_snapshots_not_in_have_are_excluded(
157 self, tmp_path: pathlib.Path
158 ) -> None:
159 """Commits in the have-set are never walked so their snapshots don't matter."""
160 _init_repo(tmp_path)
161 c1 = _make_commit(tmp_path, {"f.py": b"v1"}, "A", write_snap=False)
162 c2 = _make_commit(tmp_path, {"f.py": b"v2"}, "B",
163 parent_id=c1.commit_id, write_snap=True)
164
165 # c1 is in have — BFS stops before it; its missing snapshot is irrelevant.
166 result = walk_commits(tmp_path, [c2.commit_id], have=[c1.commit_id])
167 assert not result["missing_snapshots"], (
168 "Commits in have are not walked — their snapshots should not be flagged"
169 )
170
171
172 # ---------------------------------------------------------------------------
173 # II — build_mpack_from_walk raises when missing snapshots are present
174 # ---------------------------------------------------------------------------
175
176 class TestBuildPackExcludesCommitsWithMissingSnapshot:
177 """build_mpack_from_walk must raise ValueError when any snapshot is absent.
178
179 Silently skipping would push commits without their snapshots, creating
180 dangling references on the remote that can never be healed without
181 rewriting history. The strict raise forces the caller to either repair
182 the store (``muse verify``) or exclude the broken commits before pushing.
183 """
184
185 def test_pack_raises_when_snapshot_missing(
186 self, tmp_path: pathlib.Path
187 ) -> None:
188 _init_repo(tmp_path)
189 c1 = _make_commit(tmp_path, {"a.py": b"v1"}, "good", write_snap=True)
190 c2 = _make_commit(tmp_path, {"a.py": b"v2"}, "broken",
191 parent_id=c1.commit_id, write_snap=False)
192
193 walk = walk_commits(tmp_path, [c2.commit_id])
194 with pytest.raises(ValueError, match="Push aborted"):
195 build_mpack_from_walk(tmp_path, walk)
196
197 def test_pack_includes_commit_when_snapshot_present(
198 self, tmp_path: pathlib.Path
199 ) -> None:
200 _init_repo(tmp_path)
201 c1 = _make_commit(tmp_path, {"a.py": b"v1"}, "good", write_snap=True)
202
203 walk = walk_commits(tmp_path, [c1.commit_id])
204 bundle = build_mpack_from_walk(tmp_path, walk)
205
206 commit_ids_in_pack = {c["commit_id"] for c in bundle["commits"]}
207 assert c1.commit_id in commit_ids_in_pack
208
209 def test_pack_raises_when_any_snapshot_missing_in_chain(
210 self, tmp_path: pathlib.Path
211 ) -> None:
212 """A single missing snapshot in a chain aborts the entire pack."""
213 _init_repo(tmp_path)
214 c1 = _make_commit(tmp_path, {"f.py": b"v1"}, "A", write_snap=True)
215 c2 = _make_commit(tmp_path, {"f.py": b"v2"}, "B",
216 parent_id=c1.commit_id, write_snap=False)
217 c3 = _make_commit(tmp_path, {"f.py": b"v3"}, "C",
218 parent_id=c2.commit_id, write_snap=True)
219
220 walk = walk_commits(tmp_path, [c3.commit_id])
221 with pytest.raises(ValueError, match="Push aborted"):
222 build_mpack_from_walk(tmp_path, walk)
223
224 def test_pack_bundle_snapshot_list_and_commit_list_are_consistent(
225 self, tmp_path: pathlib.Path
226 ) -> None:
227 """Every snapshot_id referenced by a commit in the bundle must be present
228 in bundle['snapshots'] — verified on a fully intact chain."""
229 _init_repo(tmp_path)
230 c1 = _make_commit(tmp_path, {"a.py": b"v1"}, "A", write_snap=True)
231 c2 = _make_commit(tmp_path, {"a.py": b"v2"}, "B",
232 parent_id=c1.commit_id, write_snap=True)
233 c3 = _make_commit(tmp_path, {"a.py": b"v3"}, "C",
234 parent_id=c2.commit_id, write_snap=True)
235
236 walk = walk_commits(tmp_path, [c3.commit_id])
237 bundle = build_mpack_from_walk(tmp_path, walk)
238
239 snap_ids_in_bundle = {s["snapshot_id"] for s in bundle["snapshots"]}
240 for commit_dict in bundle["commits"]:
241 sid = commit_dict["snapshot_id"]
242 assert sid in snap_ids_in_bundle, (
243 f"Commit {commit_dict['commit_id'][:8]} references snapshot "
244 f"{sid[:8]} which is not in the bundle — dangling reference"
245 )
246
247 def test_no_warning_when_all_snapshots_present(
248 self, tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture
249 ) -> None:
250 _init_repo(tmp_path)
251 c = _make_commit(tmp_path, {"x.py": b"ok"}, "clean", write_snap=True)
252 import logging
253 with caplog.at_level(logging.WARNING, logger="muse.core.pack"):
254 walk = walk_commits(tmp_path, [c.commit_id])
255 build_mpack_from_walk(tmp_path, walk)
256 assert "not found" not in caplog.text
257
258 def test_warning_emitted_when_snapshot_missing(
259 self, tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture
260 ) -> None:
261 _init_repo(tmp_path)
262 c = _make_commit(tmp_path, {"x.py": b"broken"}, "oops", write_snap=False)
263 import logging
264 with caplog.at_level(logging.WARNING, logger="muse.core.pack"):
265 walk = walk_commits(tmp_path, [c.commit_id])
266 with pytest.raises(ValueError, match="Push aborted"):
267 build_mpack_from_walk(tmp_path, walk)
268 assert c.snapshot_id[:8] in caplog.text
269
270
271 # ---------------------------------------------------------------------------
272 # III — regression: the real muse repo's 3 broken commits
273 # ---------------------------------------------------------------------------
274
275 class TestMissingSnapshotRegressionInvariant:
276 """Verify the invariant holds end-to-end: every reachable commit in a repo
277 that we attempt to push must have its snapshot present — build_mpack_from_walk
278 raises ValueError rather than sending a commit with a dangling snapshot ref."""
279
280 def test_pack_aborts_on_chain_with_gaps(
281 self, tmp_path: pathlib.Path
282 ) -> None:
283 """A chain with missing snapshots raises ValueError, not a partial bundle."""
284 _init_repo(tmp_path)
285 # Build: A(good) → B(broken) → C(broken) → D(good)
286 c_a = _make_commit(tmp_path, {"f": b"a"}, "A", write_snap=True)
287 c_b = _make_commit(tmp_path, {"f": b"b"}, "B",
288 parent_id=c_a.commit_id, write_snap=False)
289 c_c = _make_commit(tmp_path, {"f": b"c"}, "C",
290 parent_id=c_b.commit_id, write_snap=False)
291 c_d = _make_commit(tmp_path, {"f": b"d"}, "D",
292 parent_id=c_c.commit_id, write_snap=True)
293
294 walk = walk_commits(tmp_path, [c_d.commit_id])
295 with pytest.raises(ValueError, match="Push aborted"):
296 build_mpack_from_walk(tmp_path, walk)
297
298 def test_reachable_commits_with_missing_snapshots_are_reported(
299 self, tmp_path: pathlib.Path
300 ) -> None:
301 """walk_commits must expose all missing snapshot_ids so callers can
302 surface the issue before attempting a push."""
303 _init_repo(tmp_path)
304 c1 = _make_commit(tmp_path, {"f": b"1"}, "root", write_snap=True)
305 c2 = _make_commit(tmp_path, {"f": b"2"}, "broken-1",
306 parent_id=c1.commit_id, write_snap=False)
307 c3 = _make_commit(tmp_path, {"f": b"3"}, "broken-2",
308 parent_id=c2.commit_id, write_snap=False)
309 c4 = _make_commit(tmp_path, {"f": b"4"}, "broken-3",
310 parent_id=c3.commit_id, write_snap=False)
311 c5 = _make_commit(tmp_path, {"f": b"5"}, "good",
312 parent_id=c4.commit_id, write_snap=True)
313
314 result = walk_commits(tmp_path, [c5.commit_id])
315 missing = result["missing_snapshots"]
316 assert c2.snapshot_id in missing
317 assert c3.snapshot_id in missing
318 assert c4.snapshot_id in missing
319 assert c1.snapshot_id not in missing
320 assert c5.snapshot_id not in missing
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 120 days ago