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