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