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