gabriel / muse public
test_apply_mpack_partial_failure.py python
293 lines 11.1 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 20 days ago
1 """TDD — apply_mpack partial failure atomicity.
2
3 Invariant: a commit must never be written that creates a dangling reference chain.
4
5 Write order is objects → snapshots → commits. This ordering is necessary but
6 not sufficient. If an object fails to write (content/ID mismatch, OSError),
7 any snapshot referencing that object must also be skipped, and any commit
8 referencing that snapshot must also be skipped.
9
10 Without this, a poisoned object entry in an mpack produces:
11 commit (written) → snapshot (written) → object (NOT written)
12 — a dangling reference that silently corrupts the local store.
13
14 Tests
15 -----
16 PF-1 Poisoned object (content/ID mismatch) → its snapshot and commit are
17 not written.
18 PF-2 OSError mid-object-write → no snapshots or commits are written.
19 PF-3 Unaffected commits (different object chain) still write successfully
20 when one object in the mpack is poisoned.
21 PF-4 All objects succeed → all snapshots and commits write normally
22 (regression guard — the fix must not break the happy path).
23 """
24 from __future__ import annotations
25
26 import datetime
27 import json
28 import pathlib
29 from unittest.mock import patch
30
31 import pytest
32
33 from muse.core.mpack import MPack, apply_mpack, build_mpack
34 from muse.core.object_store import has_object, write_object
35 from muse.core.paths import muse_dir
36 from muse.core.ids import hash_commit, hash_snapshot
37 from muse.core.refs import write_branch_ref
38 from muse.core.commits import (
39 CommitRecord,
40 read_commit,
41 write_commit,
42 )
43 from muse.core.snapshots import (
44 SnapshotRecord,
45 read_snapshot,
46 write_snapshot,
47 )
48 from muse.core.types import blob_id
49
50
51 _DT = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
52
53
54 def _init_repo(root: pathlib.Path) -> pathlib.Path:
55 dot = muse_dir(root)
56 dot.mkdir(parents=True)
57 (dot / "repo.json").write_text(json.dumps({"repo_id": "pf-test"}))
58 for d in ("commits", "snapshots", "objects", "refs/heads"):
59 (dot / d).mkdir(parents=True, exist_ok=True)
60 (dot / "HEAD").write_text("ref: refs/heads/main\n")
61 (dot / "config.toml").write_text("")
62 return root
63
64
65 def _make_mpack_with_poisoned_object(
66 good_content: bytes,
67 poisoned_oid: str,
68 poisoned_content: bytes,
69 ) -> tuple[MPack, str, str]:
70 """Build a minimal MPack where one object has a content/ID mismatch.
71
72 Returns (mpack, snapshot_id, commit_id).
73 The poisoned object has poisoned_oid as its declared ID but
74 poisoned_content whose sha256 does NOT equal poisoned_oid.
75 """
76 good_oid = blob_id(good_content)
77 manifest = {"src/good.py": good_oid, "src/bad.py": poisoned_oid}
78 sid = hash_snapshot(manifest)
79 cid = hash_commit(
80 parent_ids=[],
81 snapshot_id=sid,
82 message="poisoned",
83 committed_at_iso=_DT.isoformat(),
84 author="gabriel",
85 )
86 mpack: MPack = {
87 "commits": [CommitRecord(
88 commit_id=cid, branch="main",
89 snapshot_id=sid, message="poisoned", committed_at=_DT,
90 parent_commit_id=None, parent2_commit_id=None,
91 author="gabriel", metadata={}, structured_delta=None,
92 sem_ver_bump="none", breaking_changes=[],
93 agent_id="", model_id="", toolchain_id="",
94 prompt_hash="", signature="", signer_key_id="",
95 ).to_dict()],
96 "snapshots": [{
97 "snapshot_id": sid,
98 "parent_snapshot_id": None,
99 "delta_upsert": manifest,
100 "delta_remove": [],
101 }],
102 "blobs": [
103 {"object_id": good_oid, "content": good_content},
104 {"object_id": poisoned_oid, "content": poisoned_content},
105 ],
106 }
107 return mpack, sid, cid
108
109
110 # ---------------------------------------------------------------------------
111 # PF-1 Poisoned object → snapshot and commit not written
112 # ---------------------------------------------------------------------------
113
114 def test_pf1_poisoned_object_prevents_commit_write(tmp_path: pathlib.Path) -> None:
115 """A content/ID mismatch on an object must prevent its commit from being written.
116
117 When write_object raises ValueError (hash mismatch), any snapshot that
118 references that object_id must be skipped, and any commit referencing
119 that snapshot must also be skipped — no dangling references.
120 """
121 dst = _init_repo(tmp_path / "dst")
122
123 good_content = b"def good(): pass\n"
124 # poisoned: declare a sha256:aaa... ID but send different bytes
125 poisoned_oid = "sha256:" + "a" * 64
126 poisoned_content = b"this does not hash to aaa..."
127
128 mpack, sid, cid = _make_mpack_with_poisoned_object(
129 good_content, poisoned_oid, poisoned_content
130 )
131
132 apply_mpack(dst, mpack)
133
134 assert read_commit(dst, cid) is None, (
135 "commit was written despite its snapshot referencing a poisoned object — "
136 "dangling reference chain created"
137 )
138 assert read_snapshot(dst, sid) is None, (
139 "snapshot was written despite referencing a poisoned object"
140 )
141
142
143 # ---------------------------------------------------------------------------
144 # PF-2 OSError mid-object-write → no snapshots or commits written
145 # ---------------------------------------------------------------------------
146
147 def test_pf2_oserror_on_object_write_aborts_cleanly(tmp_path: pathlib.Path) -> None:
148 """OSError during object write must not leave any commits or snapshots written."""
149 dst = _init_repo(tmp_path / "dst")
150
151 content = b"print('hello')\n"
152 oid = blob_id(content)
153 manifest = {"src/hello.py": oid}
154 sid = hash_snapshot(manifest)
155 cid = hash_commit(
156 parent_ids=[], snapshot_id=sid, message="oserror test",
157 committed_at_iso=_DT.isoformat(), author="gabriel",
158 )
159 mpack: MPack = {
160 "commits": [CommitRecord(
161 commit_id=cid, branch="main",
162 snapshot_id=sid, message="oserror test", committed_at=_DT,
163 parent_commit_id=None, parent2_commit_id=None,
164 author="gabriel", metadata={}, structured_delta=None,
165 sem_ver_bump="none", breaking_changes=[],
166 agent_id="", model_id="", toolchain_id="",
167 prompt_hash="", signature="", signer_key_id="",
168 ).to_dict()],
169 "snapshots": [{
170 "snapshot_id": sid,
171 "parent_snapshot_id": None,
172 "delta_upsert": manifest,
173 "delta_remove": [],
174 }],
175 "blobs": [{"object_id": oid, "content": content}],
176 }
177
178 with patch("muse.core.mpack.write_pack", side_effect=OSError("disk full")):
179 with pytest.raises(OSError, match="disk full"):
180 apply_mpack(dst, mpack)
181
182 assert read_commit(dst, cid) is None, "commit written after OSError on object write"
183 assert read_snapshot(dst, sid) is None, "snapshot written after OSError on object write"
184
185
186 # ---------------------------------------------------------------------------
187 # PF-3 Unaffected commits still write when one object chain is poisoned
188 # ---------------------------------------------------------------------------
189
190 def test_pf3_clean_commits_write_when_one_chain_is_poisoned(tmp_path: pathlib.Path) -> None:
191 """A poisoned object must not prevent unrelated commits from being written."""
192 dst = _init_repo(tmp_path / "dst")
193
194 # Clean chain
195 clean_content = b"def clean(): pass\n"
196 clean_oid = blob_id(clean_content)
197 clean_manifest = {"src/clean.py": clean_oid}
198 clean_sid = hash_snapshot(clean_manifest)
199 clean_cid = hash_commit(
200 parent_ids=[], snapshot_id=clean_sid, message="clean",
201 committed_at_iso=_DT.isoformat(), author="gabriel",
202 )
203
204 # Poisoned chain
205 poisoned_oid = "sha256:" + "b" * 64
206 poisoned_content = b"wrong bytes"
207 poisoned_manifest = {"src/bad.py": poisoned_oid}
208 poisoned_sid = hash_snapshot(poisoned_manifest)
209 poisoned_cid = hash_commit(
210 parent_ids=[], snapshot_id=poisoned_sid, message="poisoned",
211 committed_at_iso=_DT.isoformat(), author="gabriel",
212 )
213
214 mpack: MPack = {
215 "commits": [
216 CommitRecord(
217 commit_id=clean_cid, branch="main",
218 snapshot_id=clean_sid, message="clean", committed_at=_DT,
219 parent_commit_id=None, parent2_commit_id=None,
220 author="gabriel", metadata={}, structured_delta=None,
221 sem_ver_bump="none", breaking_changes=[],
222 agent_id="", model_id="", toolchain_id="",
223 prompt_hash="", signature="", signer_key_id="",
224 ).to_dict(),
225 CommitRecord(
226 commit_id=poisoned_cid, branch="main",
227 snapshot_id=poisoned_sid, message="poisoned", committed_at=_DT,
228 parent_commit_id=None, parent2_commit_id=None,
229 author="gabriel", metadata={}, structured_delta=None,
230 sem_ver_bump="none", breaking_changes=[],
231 agent_id="", model_id="", toolchain_id="",
232 prompt_hash="", signature="", signer_key_id="",
233 ).to_dict(),
234 ],
235 "snapshots": [
236 {"snapshot_id": clean_sid, "parent_snapshot_id": None,
237 "delta_upsert": clean_manifest, "delta_remove": []},
238 {"snapshot_id": poisoned_sid, "parent_snapshot_id": None,
239 "delta_upsert": poisoned_manifest, "delta_remove": []},
240 ],
241 "blobs": [
242 {"object_id": clean_oid, "content": clean_content},
243 {"object_id": poisoned_oid, "content": poisoned_content},
244 ],
245 }
246
247 apply_mpack(dst, mpack)
248
249 assert read_commit(dst, clean_cid) is not None, "clean commit was not written"
250 assert read_commit(dst, poisoned_cid) is None, (
251 "poisoned commit was written despite referencing a missing object"
252 )
253
254
255 # ---------------------------------------------------------------------------
256 # PF-4 Happy path regression guard
257 # ---------------------------------------------------------------------------
258
259 def test_pf4_happy_path_unaffected(tmp_path: pathlib.Path) -> None:
260 """All objects succeed → all snapshots and commits write normally."""
261 src = _init_repo(tmp_path / "src")
262 dst = _init_repo(tmp_path / "dst")
263
264 content = b"def hello(): return 42\n"
265 oid = blob_id(content)
266 write_object(src, oid, content)
267 manifest = {"src/hello.py": oid}
268 sid = hash_snapshot(manifest)
269 write_snapshot(src, SnapshotRecord(snapshot_id=sid, manifest=manifest))
270 cid = hash_commit(
271 parent_ids=[], snapshot_id=sid, message="happy",
272 committed_at_iso=_DT.isoformat(), author="gabriel",
273 )
274 write_commit(src, CommitRecord(
275 commit_id=cid, branch="main",
276 snapshot_id=sid, message="happy", committed_at=_DT,
277 parent_commit_id=None, parent2_commit_id=None,
278 author="gabriel", metadata={}, structured_delta=None,
279 sem_ver_bump="none", breaking_changes=[],
280 agent_id="", model_id="", toolchain_id="",
281 prompt_hash="", signature="", signer_key_id="",
282 ))
283 write_branch_ref(src, "main", cid)
284
285 mpack = build_mpack(src, [cid], have=[])
286 result = apply_mpack(dst, mpack)
287
288 assert result["commits_written"] == 1
289 assert result["snapshots_written"] == 1
290 assert result["blobs_written"] == 1
291 assert read_commit(dst, cid) is not None
292 assert read_snapshot(dst, sid) is not None
293 assert has_object(dst, oid)
File History 6 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 20 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:0313c134f0ef4518a9c3a0ec359ffdc42546dc720010730374edfe0857caf7ef rename: delta_add → delta_upsert across wire format, source… Sonnet 4.6 minor 22 days ago
sha256:fb19dc03703eb3fc11d016ea19f619eebfab7bde2acf247346dc0f032e65ff19 fix(push): step 0 log shows full /refs URL instead of misle… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 28 days ago