gabriel / muse public
test_pack_missing_snapshot_integrity.py python
325 lines 13.2 KB
Raw
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 151 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 included in a push bundle MUST have its snapshot included in
13 the same bundle. If the snapshot file is missing locally:
14
15 * The commit MUST be excluded from the bundle.
16 * A clear warning is emitted identifying the broken commit.
17 * ``walk_commits`` result must carry a non-empty ``missing_snapshots`` set
18 so callers can surface the issue before building the pack.
19
20 These tests drive the fix in ``muse/core/pack.py``.
21 """
22
23 from __future__ import annotations
24
25 import datetime
26 import hashlib
27 import pathlib
28
29 import pytest
30
31 from muse.core.object_store import write_object
32 from muse.core.pack import PackBundle, build_pack_from_walk, walk_commits
33 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
34 from muse.core.store import (
35 CommitRecord,
36 SnapshotRecord,
37 write_commit,
38 write_snapshot,
39 )
40
41 # ---------------------------------------------------------------------------
42 # Helpers
43 # ---------------------------------------------------------------------------
44
45 _REPO_ID = "integrity-test"
46
47
48 def _sha(data: bytes) -> str:
49 return hashlib.sha256(data).hexdigest()
50
51
52 def _init_repo(root: pathlib.Path) -> None:
53 import json as _json
54 muse = root / ".muse"
55 for d in ("commits", "snapshots", "objects", "refs/heads"):
56 (muse / d).mkdir(parents=True, exist_ok=True)
57 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
58 (muse / "repo.json").write_text(
59 _json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
60 )
61
62
63 def _make_commit(
64 root: pathlib.Path,
65 files: dict[str, bytes],
66 message: str,
67 parent_id: str | None = None,
68 branch: str = "main",
69 write_snap: bool = True,
70 ) -> CommitRecord:
71 """Create a commit, optionally skipping snapshot write to simulate corruption."""
72 manifest = {}
73 for path, content in files.items():
74 oid = _sha(content)
75 write_object(root, oid, content)
76 manifest[path] = oid
77
78 snap_id = compute_snapshot_id(manifest)
79 now = datetime.datetime.now(datetime.timezone.utc)
80 commit_id = compute_commit_id(
81 parent_ids=[parent_id] if parent_id else [],
82 snapshot_id=snap_id,
83 message=message,
84 committed_at_iso=now.isoformat(),
85 )
86
87 if write_snap:
88 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
89
90 record = CommitRecord(
91 commit_id=commit_id,
92 repo_id=_REPO_ID,
93 branch=branch,
94 snapshot_id=snap_id,
95 message=message,
96 committed_at=now,
97 parent_commit_id=parent_id,
98 )
99 write_commit(root, record)
100 (root / ".muse" / "refs" / "heads" / branch).write_text(commit_id, encoding="utf-8")
101 return record
102
103
104 # ---------------------------------------------------------------------------
105 # I — walk_commits exposes missing_snapshots
106 # ---------------------------------------------------------------------------
107
108 class TestWalkCommitsMissingSnapshotDetection:
109 """walk_commits must report commits whose snapshot files are absent."""
110
111 def test_walk_commits_no_missing_snapshots_when_all_present(
112 self, tmp_path: pathlib.Path
113 ) -> None:
114 _init_repo(tmp_path)
115 c = _make_commit(tmp_path, {"a.py": b"x"}, "first", write_snap=True)
116 result = walk_commits(tmp_path, [c.commit_id])
117 assert not result["missing_snapshots"], (
118 "No snapshots are missing — missing_snapshots should be empty"
119 )
120
121 def test_walk_commits_detects_single_missing_snapshot(
122 self, tmp_path: pathlib.Path
123 ) -> None:
124 _init_repo(tmp_path)
125 c1 = _make_commit(tmp_path, {"a.py": b"v1"}, "first", write_snap=True)
126 # Second commit: snapshot file deliberately not written
127 c2 = _make_commit(tmp_path, {"a.py": b"v2"}, "second",
128 parent_id=c1.commit_id, write_snap=False)
129
130 result = walk_commits(tmp_path, [c2.commit_id])
131 assert c2.snapshot_id in result["missing_snapshots"], (
132 "walk_commits must expose the missing snapshot_id"
133 )
134
135 def test_walk_commits_detects_multiple_missing_snapshots_in_chain(
136 self, tmp_path: pathlib.Path
137 ) -> None:
138 _init_repo(tmp_path)
139 c1 = _make_commit(tmp_path, {"f.py": b"v1"}, "A", write_snap=True)
140 c2 = _make_commit(tmp_path, {"f.py": b"v2"}, "B",
141 parent_id=c1.commit_id, write_snap=False)
142 c3 = _make_commit(tmp_path, {"f.py": b"v3"}, "C",
143 parent_id=c2.commit_id, write_snap=False)
144 c4 = _make_commit(tmp_path, {"f.py": b"v4"}, "D",
145 parent_id=c3.commit_id, write_snap=True)
146
147 result = walk_commits(tmp_path, [c4.commit_id])
148 assert c2.snapshot_id in result["missing_snapshots"]
149 assert c3.snapshot_id in result["missing_snapshots"]
150 assert c1.snapshot_id not in result["missing_snapshots"]
151 assert c4.snapshot_id not in result["missing_snapshots"]
152
153 def test_walk_commits_missing_snapshots_not_in_have_are_excluded(
154 self, tmp_path: pathlib.Path
155 ) -> None:
156 """Commits in the have-set are never walked so their snapshots don't matter."""
157 _init_repo(tmp_path)
158 c1 = _make_commit(tmp_path, {"f.py": b"v1"}, "A", write_snap=False)
159 c2 = _make_commit(tmp_path, {"f.py": b"v2"}, "B",
160 parent_id=c1.commit_id, write_snap=True)
161
162 # c1 is in have — BFS stops before it; its missing snapshot is irrelevant.
163 result = walk_commits(tmp_path, [c2.commit_id], have=[c1.commit_id])
164 assert not result["missing_snapshots"], (
165 "Commits in have are not walked — their snapshots should not be flagged"
166 )
167
168
169 # ---------------------------------------------------------------------------
170 # II — build_pack_from_walk excludes commits with missing snapshots
171 # ---------------------------------------------------------------------------
172
173 class TestBuildPackExcludesCommitsWithMissingSnapshot:
174 """A pack bundle must never contain a commit whose snapshot is absent."""
175
176 def test_pack_excludes_commit_when_snapshot_missing(
177 self, tmp_path: pathlib.Path
178 ) -> None:
179 _init_repo(tmp_path)
180 c1 = _make_commit(tmp_path, {"a.py": b"v1"}, "good", write_snap=True)
181 c2 = _make_commit(tmp_path, {"a.py": b"v2"}, "broken",
182 parent_id=c1.commit_id, write_snap=False)
183
184 walk = walk_commits(tmp_path, [c2.commit_id])
185 bundle = build_pack_from_walk(tmp_path, walk)
186
187 commit_ids_in_pack = {c["commit_id"] for c in bundle["commits"]}
188 assert c2.commit_id not in commit_ids_in_pack, (
189 "Commit with missing snapshot must not appear in the pack"
190 )
191
192 def test_pack_includes_commit_when_snapshot_present(
193 self, tmp_path: pathlib.Path
194 ) -> None:
195 _init_repo(tmp_path)
196 c1 = _make_commit(tmp_path, {"a.py": b"v1"}, "good", write_snap=True)
197
198 walk = walk_commits(tmp_path, [c1.commit_id])
199 bundle = build_pack_from_walk(tmp_path, walk)
200
201 commit_ids_in_pack = {c["commit_id"] for c in bundle["commits"]}
202 assert c1.commit_id in commit_ids_in_pack
203
204 def test_pack_excludes_only_broken_commit_leaves_others(
205 self, tmp_path: pathlib.Path
206 ) -> None:
207 """Good commits in the same pack are not affected by one broken commit."""
208 _init_repo(tmp_path)
209 c1 = _make_commit(tmp_path, {"f.py": b"v1"}, "A", write_snap=True)
210 c2 = _make_commit(tmp_path, {"f.py": b"v2"}, "B",
211 parent_id=c1.commit_id, write_snap=False)
212 c3 = _make_commit(tmp_path, {"f.py": b"v3"}, "C",
213 parent_id=c2.commit_id, write_snap=True)
214
215 walk = walk_commits(tmp_path, [c3.commit_id])
216 bundle = build_pack_from_walk(tmp_path, walk)
217
218 ids = {c["commit_id"] for c in bundle["commits"]}
219 assert c1.commit_id in ids
220 assert c3.commit_id in ids
221 assert c2.commit_id not in ids
222
223 def test_pack_bundle_snapshot_list_and_commit_list_are_consistent(
224 self, tmp_path: pathlib.Path
225 ) -> None:
226 """Every snapshot_id referenced by a commit in the bundle must be present
227 in bundle['snapshots']."""
228 _init_repo(tmp_path)
229 c1 = _make_commit(tmp_path, {"a.py": b"v1"}, "A", write_snap=True)
230 c2 = _make_commit(tmp_path, {"a.py": b"v2"}, "B",
231 parent_id=c1.commit_id, write_snap=False)
232 c3 = _make_commit(tmp_path, {"a.py": b"v3"}, "C",
233 parent_id=c2.commit_id, write_snap=True)
234
235 walk = walk_commits(tmp_path, [c3.commit_id])
236 bundle = build_pack_from_walk(tmp_path, walk)
237
238 snap_ids_in_bundle = {s["snapshot_id"] for s in bundle["snapshots"]}
239 for commit_dict in bundle["commits"]:
240 sid = commit_dict["snapshot_id"]
241 assert sid in snap_ids_in_bundle, (
242 f"Commit {commit_dict['commit_id'][:8]} references snapshot "
243 f"{sid[:8]} which is not in the bundle — dangling reference"
244 )
245
246 def test_no_warning_when_all_snapshots_present(
247 self, tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture
248 ) -> None:
249 _init_repo(tmp_path)
250 c = _make_commit(tmp_path, {"x.py": b"ok"}, "clean", write_snap=True)
251 walk = walk_commits(tmp_path, [c.commit_id])
252 import logging
253 with caplog.at_level(logging.WARNING, logger="muse.core.pack"):
254 build_pack_from_walk(tmp_path, walk)
255 assert "not found" not in caplog.text
256
257 def test_warning_emitted_when_snapshot_missing(
258 self, tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture
259 ) -> None:
260 _init_repo(tmp_path)
261 c = _make_commit(tmp_path, {"x.py": b"broken"}, "oops", write_snap=False)
262 walk = walk_commits(tmp_path, [c.commit_id])
263 import logging
264 with caplog.at_level(logging.WARNING, logger="muse.core.pack"):
265 build_pack_from_walk(tmp_path, walk)
266 assert c.snapshot_id[:8] in caplog.text
267
268
269 # ---------------------------------------------------------------------------
270 # III — regression: the real muse repo's 3 broken commits
271 # ---------------------------------------------------------------------------
272
273 class TestMissingSnapshotRegressionInvariant:
274 """Verify the invariant holds end-to-end: every reachable commit in a repo
275 that we attempt to push must either have its snapshot present, or be
276 excluded from the pack with a warning — never silently sent as a dangling
277 reference."""
278
279 def test_pack_bundle_is_always_internally_consistent(
280 self, tmp_path: pathlib.Path
281 ) -> None:
282 """Build a chain with gaps, push the full chain, assert the bundle is
283 self-consistent (no commit in bundle lacks its snapshot in 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 bundle = build_pack_from_walk(tmp_path, walk)
296
297 snap_ids = {s["snapshot_id"] for s in bundle["snapshots"]}
298 for commit_dict in bundle["commits"]:
299 assert commit_dict["snapshot_id"] in snap_ids, (
300 "Bundle consistency violated: commit references snapshot not in bundle"
301 )
302
303 def test_reachable_commits_with_missing_snapshots_are_reported(
304 self, tmp_path: pathlib.Path
305 ) -> None:
306 """walk_commits must expose all missing snapshot_ids so callers can
307 surface the issue before attempting a push."""
308 _init_repo(tmp_path)
309 c1 = _make_commit(tmp_path, {"f": b"1"}, "root", write_snap=True)
310 c2 = _make_commit(tmp_path, {"f": b"2"}, "broken-1",
311 parent_id=c1.commit_id, write_snap=False)
312 c3 = _make_commit(tmp_path, {"f": b"3"}, "broken-2",
313 parent_id=c2.commit_id, write_snap=False)
314 c4 = _make_commit(tmp_path, {"f": b"4"}, "broken-3",
315 parent_id=c3.commit_id, write_snap=False)
316 c5 = _make_commit(tmp_path, {"f": b"5"}, "good",
317 parent_id=c4.commit_id, write_snap=True)
318
319 result = walk_commits(tmp_path, [c5.commit_id])
320 missing = result["missing_snapshots"]
321 assert c2.snapshot_id in missing
322 assert c3.snapshot_id in missing
323 assert c4.snapshot_id in missing
324 assert c1.snapshot_id not in missing
325 assert c5.snapshot_id not in missing
File History 1 commit
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 151 days ago