gabriel / muse public

test_snapshot_content_migrate_cascade.py file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:2 chore: bump version to 0.2.0.dev4 for nightly.4 · gabriel · Jul 9, 2026
1 """TDD -- muse#83 Phase 2: cascading commit rewrite + re-sign.
2
3 migrate_commit_ids_cascade() must, given a snapshot_id_map from Phase 1:
4 - rewrite the commit_id of any commit whose OWN snapshot_id is a key in
5 the map, AND every descendant (transitively, since a descendant's
6 commit_id embeds its parent's commit_id),
7 - leave every commit outside that blast radius completely untouched,
8 - re-sign every rewritten commit with the current signing identity (a
9 stale signature over a superseded commit_id is a silent authenticity
10 regression -- this is the one real gap in reusing
11 muse.core.migrate.migrate_commit_ids as-is, which never re-signs).
12 """
13 from __future__ import annotations
14
15 import datetime
16 import json
17 import pathlib
18
19 import pytest
20 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
21
22 from muse.core.ids import hash_commit, hash_snapshot
23 from muse.core.object_store import object_path
24 from muse.core.paths import objects_dir
25 from muse.core.provenance import (
26 encode_public_key,
27 provenance_payload,
28 sign_commit_ed25519,
29 verify_commit_ed25519,
30 )
31 from muse.core.snapshot_content_migrate import migrate_commit_ids_cascade
32 from muse.core.transport import SigningIdentity
33 from muse.core.types import decode_pubkey
34
35 _AT_ISO = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc).isoformat()
36
37
38 # ---------------------------------------------------------------------------
39 # Fixtures / helpers
40 # ---------------------------------------------------------------------------
41
42
43 @pytest.fixture
44 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
45 objects_dir(tmp_path).mkdir(parents=True, exist_ok=True)
46 return tmp_path
47
48
49 def _make_identity(handle: str = "gabriel") -> SigningIdentity:
50 return SigningIdentity(handle=handle, private_key=Ed25519PrivateKey.generate())
51
52
53 def _sign(commit_id: str, private_key: Ed25519PrivateKey, author: str = "gabriel") -> str:
54 payload = provenance_payload(commit_id=commit_id, author=author, committed_at=_AT_ISO)
55 return sign_commit_ed25519(payload, private_key)
56
57
58 def _write_unified_commit(
59 repo: pathlib.Path,
60 commit_id: str,
61 *,
62 parent_commit_id: str | None,
63 snapshot_id: str,
64 message: str,
65 signer_public_key: str = "",
66 signature: str = "",
67 ) -> None:
68 raw = {
69 "commit_id": commit_id,
70 "branch": "main",
71 "snapshot_id": snapshot_id,
72 "message": message,
73 "committed_at": _AT_ISO,
74 "parent_commit_id": parent_commit_id,
75 "parent2_commit_id": None,
76 "author": "gabriel",
77 "agent_id": "",
78 "model_id": "",
79 "toolchain_id": "",
80 "prompt_hash": "",
81 "signature": signature,
82 "signer_public_key": signer_public_key,
83 "signer_key_id": "",
84 "format_version": 8,
85 "sem_ver_bump": "none",
86 "breaking_changes": [],
87 }
88 payload = json.dumps(raw, separators=(",", ":")).encode()
89 header = f"commit {len(payload)}\0".encode()
90 path = object_path(repo, commit_id)
91 path.parent.mkdir(parents=True, exist_ok=True)
92 path.write_bytes(header + payload)
93
94
95 def _read_unified_commit(repo: pathlib.Path, commit_id: str) -> dict:
96 path = object_path(repo, commit_id)
97 data = path.read_bytes()
98 null_idx = data.index(b"\x00")
99 return json.loads(data[null_idx + 1:])
100
101
102 # ---------------------------------------------------------------------------
103 # Build the A -> B(bad snapshot) -> C -> D fixture chain from the plan doc
104 # ---------------------------------------------------------------------------
105
106
107 def _build_chain(repo: pathlib.Path, identity: SigningIdentity) -> dict[str, str]:
108 manifest_a = {"a.txt": "sha256:" + "1" * 64}
109 snap_a = hash_snapshot(manifest_a, None)
110
111 manifest_b = {"a.txt": "sha256:" + "1" * 64, "b.txt": "sha256:" + "2" * 64}
112 wrong_snap_b = "sha256:" + "0" * 64 # deliberately wrong, like the real bug
113
114 manifest_c = {**manifest_b, "c.txt": "sha256:" + "3" * 64}
115 snap_c = hash_snapshot(manifest_c, None)
116
117 manifest_d = {**manifest_c, "d.txt": "sha256:" + "4" * 64}
118 snap_d = hash_snapshot(manifest_d, None)
119
120 id_a = hash_commit(parent_ids=[], snapshot_id=snap_a, message="A",
121 committed_at_iso=_AT_ISO, author="gabriel", signer_public_key="")
122 id_b = hash_commit(parent_ids=[id_a], snapshot_id=wrong_snap_b, message="B",
123 committed_at_iso=_AT_ISO, author="gabriel", signer_public_key="")
124 id_c = hash_commit(parent_ids=[id_b], snapshot_id=snap_c, message="C",
125 committed_at_iso=_AT_ISO, author="gabriel", signer_public_key="")
126 id_d = hash_commit(parent_ids=[id_c], snapshot_id=snap_d, message="D",
127 committed_at_iso=_AT_ISO, author="gabriel", signer_public_key="")
128
129 _write_unified_commit(repo, id_a, parent_commit_id=None, snapshot_id=snap_a, message="A",
130 signature=_sign(id_a, identity.private_key))
131 _write_unified_commit(repo, id_b, parent_commit_id=id_a, snapshot_id=wrong_snap_b, message="B",
132 signature=_sign(id_b, identity.private_key))
133 _write_unified_commit(repo, id_c, parent_commit_id=id_b, snapshot_id=snap_c, message="C",
134 signature=_sign(id_c, identity.private_key))
135 _write_unified_commit(repo, id_d, parent_commit_id=id_c, snapshot_id=snap_d, message="D",
136 signature=_sign(id_d, identity.private_key))
137
138 return {
139 "id_a": id_a, "id_b": id_b, "id_c": id_c, "id_d": id_d,
140 "wrong_snap_b": wrong_snap_b, "correct_snap_b": hash_snapshot(manifest_b, None),
141 }
142
143
144 # ---------------------------------------------------------------------------
145 # CC_01 -- the cascade rewrites B, C, D and leaves A untouched
146 # ---------------------------------------------------------------------------
147
148
149 def test_cc_01_cascade_rewrites_descendants_and_spares_ancestor(repo: pathlib.Path) -> None:
150 identity = _make_identity()
151 ids = _build_chain(repo, identity)
152 snapshot_id_map = {ids["wrong_snap_b"]: ids["correct_snap_b"]}
153
154 a_bytes_before = object_path(repo, ids["id_a"]).read_bytes()
155
156 result = migrate_commit_ids_cascade(repo, snapshot_id_map, identity, dry_run=False)
157
158 assert ids["id_a"] not in result.id_map, "A has no bad snapshot and no rewritten parent -- must be untouched"
159 assert ids["id_b"] in result.id_map
160 assert ids["id_c"] in result.id_map
161 assert ids["id_d"] in result.id_map
162 assert len({result.id_map[ids["id_b"]], result.id_map[ids["id_c"]], result.id_map[ids["id_d"]]}) == 3
163
164 # A is byte-identical -- provably outside the blast radius.
165 assert object_path(repo, ids["id_a"]).read_bytes() == a_bytes_before
166
167
168 # ---------------------------------------------------------------------------
169 # CC_02 -- the new B correctly carries the corrected snapshot_id, and C/D's
170 # new records point at B/C's NEW commit_id (the cascade actually threads
171 # through, not just independently rewritten in isolation)
172 # ---------------------------------------------------------------------------
173
174
175 def test_cc_02_cascade_threads_new_parent_ids_through(repo: pathlib.Path) -> None:
176 identity = _make_identity()
177 ids = _build_chain(repo, identity)
178 snapshot_id_map = {ids["wrong_snap_b"]: ids["correct_snap_b"]}
179
180 result = migrate_commit_ids_cascade(repo, snapshot_id_map, identity, dry_run=False)
181
182 new_b = result.id_map[ids["id_b"]]
183 new_c = result.id_map[ids["id_c"]]
184 new_d = result.id_map[ids["id_d"]]
185
186 b_record = _read_unified_commit(repo, new_b)
187 assert b_record["snapshot_id"] == ids["correct_snap_b"]
188 assert b_record["parent_commit_id"] == ids["id_a"], "A was not rewritten, so B's parent pointer is unchanged"
189
190 c_record = _read_unified_commit(repo, new_c)
191 assert c_record["parent_commit_id"] == new_b, "C must point at B's NEW commit_id, not the old one"
192
193 d_record = _read_unified_commit(repo, new_d)
194 assert d_record["parent_commit_id"] == new_c, "D must point at C's NEW commit_id, not the old one"
195
196
197 # ---------------------------------------------------------------------------
198 # CC_03 -- every rewritten commit is re-signed and the new signature verifies
199 # against the NEW commit_id (not the old one)
200 # ---------------------------------------------------------------------------
201
202
203 def test_cc_03_rewritten_commits_are_resigned_and_verify(repo: pathlib.Path) -> None:
204 identity = _make_identity()
205 ids = _build_chain(repo, identity)
206 snapshot_id_map = {ids["wrong_snap_b"]: ids["correct_snap_b"]}
207
208 result = migrate_commit_ids_cascade(repo, snapshot_id_map, identity, dry_run=False)
209
210 _, pub_str = encode_public_key(identity.private_key)
211 _, pub_bytes = decode_pubkey(pub_str)
212
213 for old_id in (ids["id_b"], ids["id_c"], ids["id_d"]):
214 new_id = result.id_map[old_id]
215 record = _read_unified_commit(repo, new_id)
216 assert record["signer_public_key"] == pub_str
217
218 payload = provenance_payload(
219 commit_id=new_id,
220 author=record["author"],
221 agent_id=record.get("agent_id", ""),
222 model_id=record.get("model_id", ""),
223 toolchain_id=record.get("toolchain_id", ""),
224 prompt_hash=record.get("prompt_hash", ""),
225 committed_at=record["committed_at"],
226 )
227 assert verify_commit_ed25519(payload, record["signature"], pub_bytes), (
228 f"re-signed commit {new_id[:18]} does not verify against its own new commit_id"
229 )
230
231
232 # ---------------------------------------------------------------------------
233 # CC_04 -- a sibling branch untouched by the corruption is provably unaffected
234 # ---------------------------------------------------------------------------
235
236
237 def test_cc_04_unrelated_sibling_commit_is_byte_identical(repo: pathlib.Path) -> None:
238 identity = _make_identity()
239 ids = _build_chain(repo, identity)
240
241 # An unrelated commit forking from A, nothing to do with B/C/D at all.
242 manifest_e = {"a.txt": "sha256:" + "1" * 64, "e.txt": "sha256:" + "9" * 64}
243 snap_e = hash_snapshot(manifest_e, None)
244 id_e = hash_commit(parent_ids=[ids["id_a"]], snapshot_id=snap_e, message="E",
245 committed_at_iso=_AT_ISO, author="gabriel", signer_public_key="")
246 _write_unified_commit(repo, id_e, parent_commit_id=ids["id_a"], snapshot_id=snap_e, message="E",
247 signature=_sign(id_e, identity.private_key))
248 e_bytes_before = object_path(repo, id_e).read_bytes()
249
250 snapshot_id_map = {ids["wrong_snap_b"]: ids["correct_snap_b"]}
251 result = migrate_commit_ids_cascade(repo, snapshot_id_map, identity, dry_run=False)
252
253 assert id_e not in result.id_map
254 assert object_path(repo, id_e).read_bytes() == e_bytes_before
255
256
257 # ---------------------------------------------------------------------------
258 # CC_05 -- dry_run computes the id_map but writes nothing
259 # ---------------------------------------------------------------------------
260
261
262 def test_cc_05_dry_run_writes_nothing(repo: pathlib.Path) -> None:
263 identity = _make_identity()
264 ids = _build_chain(repo, identity)
265 snapshot_id_map = {ids["wrong_snap_b"]: ids["correct_snap_b"]}
266
267 result = migrate_commit_ids_cascade(repo, snapshot_id_map, identity, dry_run=True)
268
269 assert set(result.id_map) == {ids["id_b"], ids["id_c"], ids["id_d"]}
270 for old_id in (ids["id_b"], ids["id_c"], ids["id_d"]):
271 assert not object_path(repo, result.id_map[old_id]).exists()
272
273
274 # ---------------------------------------------------------------------------
275 # CC_06 -- running the cascade twice with the same snapshot_id_map is
276 # deterministic (same id_map both times). Old (superseded) objects are
277 # never deleted per the non-destructive invariant, so a second run over the
278 # same raw objects necessarily rediscovers the same mapping -- this is
279 # stability, not "second run finds nothing" (that would require scoping the
280 # scan to ref-reachable objects only, which is a Phase 3/4 concern, not
281 # this function's).
282 # ---------------------------------------------------------------------------
283
284
285 def test_cc_06_running_twice_is_deterministic(repo: pathlib.Path) -> None:
286 identity = _make_identity()
287 ids = _build_chain(repo, identity)
288 snapshot_id_map = {ids["wrong_snap_b"]: ids["correct_snap_b"]}
289
290 result1 = migrate_commit_ids_cascade(repo, snapshot_id_map, identity, dry_run=False)
291 result2 = migrate_commit_ids_cascade(repo, snapshot_id_map, identity, dry_run=False)
292
293 assert result1.id_map == result2.id_map
294 assert result2.commits_written == 0, "second run must skip -- the new objects already exist"
295 assert result2.commits_skipped == len(result1.id_map)