gabriel / muse public
test_core_patch_record.py python
398 lines 13.7 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 120 days ago
1 """Unit tests for ``muse.core.patch_record`` — content-addressed Muse patch objects.
2
3 Test tiers
4 ----------
5 - Unit: PatchRecord dataclass, compute_patch_id, serialize/deserialize round-trip
6 - Data integrity: patch_id is stable, deterministic, and changes with content
7 - Security: patch_id forgery, tampered fields detected on re-verify
8 - Edge: empty diff, initial commit (no parent), binary objects skipped gracefully
9 """
10 from __future__ import annotations
11
12 import hashlib
13 import json
14 import pathlib
15
16 import pytest
17
18 from muse.core.patch_record import (
19 PatchRecord,
20 compute_patch_id,
21 deserialize_patch,
22 serialize_patch,
23 )
24 from muse.core.snapshot import compute_snapshot_id
25 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
26 from muse.core.object_store import write_object
27
28 import datetime
29 from muse.core.types import long_id, blob_id
30 from muse.core.paths import muse_dir
31
32
33 # ---------------------------------------------------------------------------
34 # Helpers
35 # ---------------------------------------------------------------------------
36
37
38 def _init_repo(path: pathlib.Path) -> pathlib.Path:
39 dot_muse = muse_dir(path)
40 for sub in ("commits", "snapshots", "objects", "refs/heads"):
41 (dot_muse / sub).mkdir(parents=True, exist_ok=True)
42 (dot_muse / "HEAD").write_text("ref: refs/heads/main\n")
43 (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "test", "domain": "code"}))
44 return path
45
46
47 def _make_object(repo: pathlib.Path, content: bytes) -> str:
48 """Write bytes to object store; return sha256:<hex> prefixed ID."""
49 oid = blob_id(content)
50 write_object(repo, oid, content)
51 return oid
52
53
54 def _ts() -> datetime.datetime:
55 return datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
56
57
58 # ---------------------------------------------------------------------------
59 # compute_patch_id
60 # ---------------------------------------------------------------------------
61
62
63 class TestComputePatchId:
64 def test_returns_sha256_prefixed_string(self, tmp_path: pathlib.Path) -> None:
65 repo = _init_repo(tmp_path)
66 rec = PatchRecord(
67 patch_id="",
68 from_snapshot_id=long_id("a" * 64),
69 to_snapshot_id=long_id("b" * 64),
70 from_commit_id=long_id("c" * 64),
71 to_commit_id=long_id("d" * 64),
72 domain="code",
73 format_version="1.0",
74 created_at="2026-01-01T00:00:00+00:00",
75 agent_id="",
76 model_id="",
77 signer_public_key="",
78 signature="",
79 intent="",
80 sem_ver_bump="patch",
81 breaking_changes=[],
82 summary="test",
83 ops=[],
84 files_added=[],
85 files_modified=[],
86 files_deleted=[],
87 files_renamed={},
88 required_objects=[],
89 from_manifest={},
90 to_manifest={},
91 applicability={
92 "requires_snapshot": long_id("a" * 64),
93 "independent_dimensions": [],
94 "conflict_free": True,
95 },
96 blobs={},
97 )
98 pid = compute_patch_id(rec)
99 assert pid.startswith("sha256:")
100 assert len(pid) == 71 # sha256: (7) + 64 hex
101
102 def test_deterministic_across_calls(self, tmp_path: pathlib.Path) -> None:
103 repo = _init_repo(tmp_path)
104 rec = PatchRecord(
105 patch_id="",
106 from_snapshot_id=long_id("a" * 64),
107 to_snapshot_id=long_id("b" * 64),
108 from_commit_id=long_id("c" * 64),
109 to_commit_id=long_id("d" * 64),
110 domain="code",
111 format_version="1.0",
112 created_at="2026-01-01T00:00:00+00:00",
113 agent_id="test-agent",
114 model_id="claude-sonnet-4-6",
115 signer_public_key="",
116 signature="",
117 intent="test intent",
118 sem_ver_bump="minor",
119 breaking_changes=[],
120 summary="2 modified files",
121 ops=[],
122 files_added=["new.py"],
123 files_modified=[],
124 files_deleted=[],
125 files_renamed={},
126 required_objects=[],
127 from_manifest={},
128 to_manifest={},
129 applicability={
130 "requires_snapshot": long_id("a" * 64),
131 "independent_dimensions": ["symbols"],
132 "conflict_free": True,
133 },
134 blobs={},
135 )
136 pid1 = compute_patch_id(rec)
137 pid2 = compute_patch_id(rec)
138 assert pid1 == pid2
139
140 def test_changes_with_different_content(self, tmp_path: pathlib.Path) -> None:
141 base = dict(
142 patch_id="",
143 from_snapshot_id=long_id("a" * 64),
144 to_snapshot_id=long_id("b" * 64),
145 from_commit_id=long_id("c" * 64),
146 to_commit_id=long_id("d" * 64),
147 domain="code",
148 format_version="1.0",
149 created_at="2026-01-01T00:00:00+00:00",
150 agent_id="",
151 model_id="",
152 signer_public_key="",
153 signature="",
154 intent="",
155 sem_ver_bump="patch",
156 breaking_changes=[],
157 summary="v1",
158 ops=[],
159 files_added=[],
160 files_modified=[],
161 files_deleted=[],
162 files_renamed={},
163 required_objects=[],
164 from_manifest={},
165 to_manifest={},
166 applicability={"requires_snapshot": long_id("a" * 64), "independent_dimensions": [], "conflict_free": True},
167 )
168 r1 = PatchRecord(**base)
169 r2 = PatchRecord(**{**base, "summary": "v2"})
170 assert compute_patch_id(r1) != compute_patch_id(r2)
171
172 def test_patch_id_excludes_signature_field(self, tmp_path: pathlib.Path) -> None:
173 """Signature must not influence patch_id (it signs the id, not the other way)."""
174 base = dict(
175 patch_id="",
176 from_snapshot_id=long_id("a" * 64),
177 to_snapshot_id=long_id("b" * 64),
178 from_commit_id=long_id("c" * 64),
179 to_commit_id=long_id("d" * 64),
180 domain="code",
181 format_version="1.0",
182 created_at="2026-01-01T00:00:00+00:00",
183 agent_id="",
184 model_id="",
185 signer_public_key="",
186 signature="",
187 intent="",
188 sem_ver_bump="patch",
189 breaking_changes=[],
190 summary="test",
191 ops=[],
192 files_added=[],
193 files_modified=[],
194 files_deleted=[],
195 files_renamed={},
196 required_objects=[],
197 from_manifest={},
198 to_manifest={},
199 applicability={"requires_snapshot": long_id("a" * 64), "independent_dimensions": [], "conflict_free": True},
200 )
201 r_no_sig = PatchRecord(**base)
202 r_with_sig = PatchRecord(**{**base, "signature": "abc123", "signer_public_key": "pubkey"})
203 assert compute_patch_id(r_no_sig) == compute_patch_id(r_with_sig)
204
205
206 # ---------------------------------------------------------------------------
207 # Serialization round-trip
208 # ---------------------------------------------------------------------------
209
210
211 class TestSerializeDeserialize:
212 def _make_record(self) -> PatchRecord:
213 rec = PatchRecord(
214 patch_id="",
215 from_snapshot_id=long_id("a" * 64),
216 to_snapshot_id=long_id("b" * 64),
217 from_commit_id=long_id("c" * 64),
218 to_commit_id=long_id("d" * 64),
219 domain="code",
220 format_version="1.0",
221 created_at="2026-01-01T00:00:00+00:00",
222 agent_id="claude-code",
223 model_id="claude-sonnet-4-6",
224 signer_public_key="",
225 signature="",
226 intent="improve merge logic",
227 sem_ver_bump="minor",
228 breaking_changes=[],
229 summary="1 modified file",
230 ops=[{"op": "insert", "address": "main.py", "position": 0, "content_id": long_id("e" * 64), "content_summary": "new file", "action_label": "inserted"}],
231 files_added=["main.py"],
232 files_modified=[],
233 files_deleted=[],
234 files_renamed={},
235 required_objects=[long_id("e" * 64)],
236 from_manifest={},
237 to_manifest={"main.py": long_id("e" * 64)},
238 applicability={
239 "requires_snapshot": long_id("a" * 64),
240 "independent_dimensions": ["symbols", "imports"],
241 "conflict_free": True,
242 },
243 blobs={},
244 )
245 rec.patch_id = compute_patch_id(rec)
246 return rec
247
248 def test_serialize_returns_bytes(self) -> None:
249 rec = self._make_record()
250 data = serialize_patch(rec)
251 assert isinstance(data, bytes)
252
253 def test_deserialize_round_trip(self) -> None:
254 rec = self._make_record()
255 data = serialize_patch(rec)
256 rec2 = deserialize_patch(data)
257 assert rec2.patch_id == rec.patch_id
258 assert rec2.domain == rec.domain
259 assert rec2.summary == rec.summary
260 assert rec2.ops == rec.ops
261 assert rec2.files_added == rec.files_added
262 assert rec2.from_manifest == rec.from_manifest
263 assert rec2.to_manifest == rec.to_manifest
264
265 def test_serialized_is_valid_json(self) -> None:
266 rec = self._make_record()
267 data = serialize_patch(rec)
268 parsed = json.loads(data)
269 assert "patch_id" in parsed
270 assert "domain" in parsed
271
272 def test_patch_id_preserved_through_round_trip(self) -> None:
273 rec = self._make_record()
274 data = serialize_patch(rec)
275 rec2 = deserialize_patch(data)
276 assert rec2.patch_id == rec.patch_id
277
278 def test_deserialize_rejects_garbage(self) -> None:
279 with pytest.raises(Exception):
280 deserialize_patch(b"not valid json at all !!!!")
281
282 def test_deserialize_rejects_missing_patch_id(self) -> None:
283 data = json.dumps({"domain": "code"}).encode()
284 with pytest.raises(Exception):
285 deserialize_patch(data)
286
287
288 # ---------------------------------------------------------------------------
289 # PatchRecord dataclass
290 # ---------------------------------------------------------------------------
291
292
293 class TestPatchRecord:
294 def test_has_required_fields(self) -> None:
295 rec = PatchRecord(
296 patch_id=long_id("a" * 64),
297 from_snapshot_id=long_id("b" * 64),
298 to_snapshot_id=long_id("c" * 64),
299 from_commit_id=long_id("d" * 64),
300 to_commit_id=long_id("e" * 64),
301 domain="code",
302 format_version="1.0",
303 created_at="2026-01-01T00:00:00+00:00",
304 agent_id="",
305 model_id="",
306 signer_public_key="",
307 signature="",
308 intent="",
309 sem_ver_bump="patch",
310 breaking_changes=[],
311 summary="",
312 ops=[],
313 files_added=[],
314 files_modified=[],
315 files_deleted=[],
316 files_renamed={},
317 required_objects=[],
318 from_manifest={},
319 to_manifest={},
320 applicability={"requires_snapshot": long_id("b" * 64), "independent_dimensions": [], "conflict_free": True},
321 )
322 assert rec.domain == "code"
323 assert rec.format_version == "1.0"
324 assert rec.sem_ver_bump == "patch"
325
326 def test_ops_with_action_label(self) -> None:
327 """Each op can carry an action_label — Cohen-transform extension."""
328 op = {
329 "op": "insert",
330 "address": "foo.py",
331 "position": 0,
332 "content_id": long_id("a" * 64),
333 "content_summary": "new function",
334 "action_label": "inserted",
335 }
336 rec = PatchRecord(
337 patch_id="",
338 from_snapshot_id=long_id("a" * 64),
339 to_snapshot_id=long_id("b" * 64),
340 from_commit_id=long_id("c" * 64),
341 to_commit_id=long_id("d" * 64),
342 domain="code",
343 format_version="1.0",
344 created_at="2026-01-01T00:00:00+00:00",
345 agent_id="",
346 model_id="",
347 signer_public_key="",
348 signature="",
349 intent="",
350 sem_ver_bump="patch",
351 breaking_changes=[],
352 summary="",
353 ops=[op],
354 files_added=[],
355 files_modified=[],
356 files_deleted=[],
357 files_renamed={},
358 required_objects=[],
359 from_manifest={},
360 to_manifest={},
361 applicability={"requires_snapshot": long_id("a" * 64), "independent_dimensions": [], "conflict_free": True},
362 )
363 assert rec.ops[0]["action_label"] == "inserted"
364
365 def test_applicability_has_requires_snapshot(self) -> None:
366 rec = PatchRecord(
367 patch_id="",
368 from_snapshot_id=long_id("a" * 64),
369 to_snapshot_id=long_id("b" * 64),
370 from_commit_id=long_id("c" * 64),
371 to_commit_id=long_id("d" * 64),
372 domain="code",
373 format_version="1.0",
374 created_at="2026-01-01T00:00:00+00:00",
375 agent_id="",
376 model_id="",
377 signer_public_key="",
378 signature="",
379 intent="",
380 sem_ver_bump="patch",
381 breaking_changes=[],
382 summary="",
383 ops=[],
384 files_added=[],
385 files_modified=[],
386 files_deleted=[],
387 files_renamed={},
388 required_objects=[],
389 from_manifest={},
390 to_manifest={},
391 applicability={
392 "requires_snapshot": long_id("a" * 64),
393 "independent_dimensions": ["symbols"],
394 "conflict_free": False,
395 },
396 )
397 assert rec.applicability["requires_snapshot"] == long_id("a" * 64)
398 assert rec.applicability["conflict_free"] is False
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 120 days ago