gabriel / muse public
test_cmd_format_patch.py python
458 lines 18.9 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 143 days ago
1 """Tests for ``muse format-patch`` — content-addressed, domain-aware Muse patch export.
2
3 Test tiers
4 ----------
5 - Unit: output schema, required fields, JSON format, action_label on ops
6 - Integration: initial commit, two-commit delta, multi-file changes
7 - Data integrity: patch_id sha256-prefixed, duration_ms/exit_code present
8 - Security: error to stderr, malicious ref rejected
9 - Performance: duration_ms plausible
10 - Edge: empty diff (no file changes), --output-dir creates .mpatch file
11 """
12 from __future__ import annotations
13
14 import datetime
15 import json
16 import pathlib
17
18 import pytest
19
20 from tests.cli_test_helper import CliRunner, InvokeResult
21 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
22 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
23 from muse.core.object_store import write_object
24 from muse.core._types import long_id
25
26 runner = CliRunner()
27
28
29 # ---------------------------------------------------------------------------
30 # Helpers
31 # ---------------------------------------------------------------------------
32
33
34 def _init_repo(path: pathlib.Path) -> pathlib.Path:
35 muse = path / ".muse"
36 for sub in ("commits", "snapshots", "objects", "refs/heads"):
37 (muse / sub).mkdir(parents=True, exist_ok=True)
38 (muse / "HEAD").write_text("ref: refs/heads/main\n")
39 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"}))
40 return path
41
42
43 def _write_object(repo: pathlib.Path, content: bytes) -> str:
44 """Write bytes to the object store and return the sha256: prefixed ID."""
45 import hashlib
46 digest = hashlib.sha256(content).hexdigest()
47 oid = long_id(digest)
48 write_object(repo, oid, content)
49 return oid
50
51
52 def _commit(
53 repo: pathlib.Path,
54 msg: str,
55 manifest: dict[str, str],
56 branch: str = "main",
57 parent: str | None = None,
58 ts: datetime.datetime | None = None,
59 ) -> str:
60 ts = ts or datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
61 sid = compute_snapshot_id(manifest)
62 write_snapshot(repo, SnapshotRecord(snapshot_id=sid, manifest=manifest, created_at=ts))
63 parent_ids = [parent] if parent else []
64 cid = compute_commit_id(parent_ids, sid, msg, ts.isoformat())
65 write_commit(repo, CommitRecord(
66 commit_id=cid, repo_id="test-repo", branch=branch,
67 snapshot_id=sid, message=msg, committed_at=ts,
68 author="gabriel", parent_commit_id=parent, parent2_commit_id=None,
69 ))
70 ref_path = repo / ".muse" / "refs" / "heads" / branch
71 ref_path.parent.mkdir(parents=True, exist_ok=True)
72 ref_path.write_text(cid)
73 return cid
74
75
76 def _fp(repo: pathlib.Path, *args: str) -> InvokeResult:
77 return runner.invoke(None, ["format-patch", *args],
78 env={"MUSE_REPO_ROOT": str(repo)})
79
80
81 def _json(r: InvokeResult) -> dict:
82 return json.loads(r.output)
83
84
85 # ---------------------------------------------------------------------------
86 # JSON output schema
87 # ---------------------------------------------------------------------------
88
89
90 class TestJsonSchema:
91 def test_exits_zero_on_success(self, tmp_path: pathlib.Path) -> None:
92 repo = _init_repo(tmp_path)
93 oid = _write_object(repo, b"print('hello')\n")
94 _commit(repo, "init: add hello.py", {"hello.py": oid})
95 r = _fp(repo, "--json")
96 assert r.exit_code == 0
97
98 def test_has_patch_id(self, tmp_path: pathlib.Path) -> None:
99 repo = _init_repo(tmp_path)
100 oid = _write_object(repo, b"x = 1\n")
101 _commit(repo, "init", {"hello.py": oid})
102 assert "patch_id" in _json(_fp(repo, "--json"))
103
104 def test_patch_id_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
105 repo = _init_repo(tmp_path)
106 oid = _write_object(repo, b"x = 1\n")
107 _commit(repo, "init", {"hello.py": oid})
108 assert _json(_fp(repo, "--json"))["patch_id"].startswith("sha256:")
109
110 def test_patch_id_full_length(self, tmp_path: pathlib.Path) -> None:
111 repo = _init_repo(tmp_path)
112 oid = _write_object(repo, b"x = 1\n")
113 _commit(repo, "init", {"hello.py": oid})
114 pid = _json(_fp(repo, "--json"))["patch_id"]
115 assert len(pid) == 71 # "sha256:" (7) + 64 hex
116
117 def test_has_from_snapshot_id(self, tmp_path: pathlib.Path) -> None:
118 repo = _init_repo(tmp_path)
119 oid = _write_object(repo, b"x = 1\n")
120 _commit(repo, "init", {"hello.py": oid})
121 data = _json(_fp(repo, "--json"))
122 assert "from_snapshot_id" in data
123
124 def test_has_to_snapshot_id(self, tmp_path: pathlib.Path) -> None:
125 repo = _init_repo(tmp_path)
126 oid = _write_object(repo, b"x = 1\n")
127 _commit(repo, "init", {"hello.py": oid})
128 data = _json(_fp(repo, "--json"))
129 assert "to_snapshot_id" in data
130 assert data["to_snapshot_id"].startswith("sha256:")
131
132 def test_has_from_commit_id(self, tmp_path: pathlib.Path) -> None:
133 repo = _init_repo(tmp_path)
134 oid1 = _write_object(repo, b"x = 1\n")
135 c1 = _commit(repo, "c1", {"a.py": oid1})
136 oid2 = _write_object(repo, b"x = 2\n")
137 _commit(repo, "c2", {"a.py": oid2}, parent=c1)
138 data = _json(_fp(repo, "--json"))
139 assert "from_commit_id" in data
140
141 def test_has_to_commit_id(self, tmp_path: pathlib.Path) -> None:
142 repo = _init_repo(tmp_path)
143 oid = _write_object(repo, b"x = 1\n")
144 _commit(repo, "init", {"hello.py": oid})
145 data = _json(_fp(repo, "--json"))
146 assert "to_commit_id" in data
147 assert data["to_commit_id"].startswith("sha256:")
148
149 def test_domain_matches_repo(self, tmp_path: pathlib.Path) -> None:
150 repo = _init_repo(tmp_path)
151 oid = _write_object(repo, b"x = 1\n")
152 _commit(repo, "init", {"hello.py": oid})
153 data = _json(_fp(repo, "--json"))
154 assert data["domain"] == "code"
155
156 def test_format_version_is_1_0(self, tmp_path: pathlib.Path) -> None:
157 repo = _init_repo(tmp_path)
158 oid = _write_object(repo, b"x = 1\n")
159 _commit(repo, "init", {"hello.py": oid})
160 assert _json(_fp(repo, "--json"))["format_version"] == "1.0"
161
162 def test_duration_ms_present(self, tmp_path: pathlib.Path) -> None:
163 repo = _init_repo(tmp_path)
164 oid = _write_object(repo, b"x = 1\n")
165 _commit(repo, "init", {"hello.py": oid})
166 assert "duration_ms" in _json(_fp(repo, "--json"))
167
168 def test_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None:
169 repo = _init_repo(tmp_path)
170 oid = _write_object(repo, b"x = 1\n")
171 _commit(repo, "init", {"hello.py": oid})
172 v = _json(_fp(repo, "--json"))["duration_ms"]
173 assert isinstance(v, float)
174 assert v >= 0.0
175
176 def test_duration_ms_six_decimal_places(self, tmp_path: pathlib.Path) -> None:
177 repo = _init_repo(tmp_path)
178 oid = _write_object(repo, b"x = 1\n")
179 _commit(repo, "init", {"hello.py": oid})
180 v = _json(_fp(repo, "--json"))["duration_ms"]
181 assert v == round(v, 6)
182
183 def test_exit_code_zero_in_json(self, tmp_path: pathlib.Path) -> None:
184 repo = _init_repo(tmp_path)
185 oid = _write_object(repo, b"x = 1\n")
186 _commit(repo, "init", {"hello.py": oid})
187 assert _json(_fp(repo, "--json"))["exit_code"] == 0
188
189 def test_exit_code_is_int_not_bool(self, tmp_path: pathlib.Path) -> None:
190 repo = _init_repo(tmp_path)
191 oid = _write_object(repo, b"x = 1\n")
192 _commit(repo, "init", {"hello.py": oid})
193 assert type(_json(_fp(repo, "--json"))["exit_code"]) is int
194
195 def test_sem_ver_bump_present(self, tmp_path: pathlib.Path) -> None:
196 repo = _init_repo(tmp_path)
197 oid = _write_object(repo, b"x = 1\n")
198 _commit(repo, "init", {"hello.py": oid})
199 data = _json(_fp(repo, "--json"))
200 assert "sem_ver_bump" in data
201 assert data["sem_ver_bump"] in ("major", "minor", "patch")
202
203 def test_summary_is_string(self, tmp_path: pathlib.Path) -> None:
204 repo = _init_repo(tmp_path)
205 oid = _write_object(repo, b"x = 1\n")
206 _commit(repo, "init", {"hello.py": oid})
207 assert isinstance(_json(_fp(repo, "--json"))["summary"], str)
208
209 def test_ops_is_list(self, tmp_path: pathlib.Path) -> None:
210 repo = _init_repo(tmp_path)
211 oid = _write_object(repo, b"x = 1\n")
212 _commit(repo, "init", {"hello.py": oid})
213 assert isinstance(_json(_fp(repo, "--json"))["ops"], list)
214
215 def test_applicability_has_requires_snapshot(self, tmp_path: pathlib.Path) -> None:
216 repo = _init_repo(tmp_path)
217 oid = _write_object(repo, b"x = 1\n")
218 _commit(repo, "init", {"hello.py": oid})
219 data = _json(_fp(repo, "--json"))
220 assert "requires_snapshot" in data["applicability"]
221
222 def test_applicability_has_conflict_free(self, tmp_path: pathlib.Path) -> None:
223 repo = _init_repo(tmp_path)
224 oid = _write_object(repo, b"x = 1\n")
225 _commit(repo, "init", {"hello.py": oid})
226 data = _json(_fp(repo, "--json"))
227 assert isinstance(data["applicability"]["conflict_free"], bool)
228
229 def test_applicability_has_independent_dimensions(self, tmp_path: pathlib.Path) -> None:
230 repo = _init_repo(tmp_path)
231 oid = _write_object(repo, b"x = 1\n")
232 _commit(repo, "init", {"hello.py": oid})
233 data = _json(_fp(repo, "--json"))
234 assert isinstance(data["applicability"]["independent_dimensions"], list)
235
236 def test_output_is_compact_single_line(self, tmp_path: pathlib.Path) -> None:
237 repo = _init_repo(tmp_path)
238 oid = _write_object(repo, b"x = 1\n")
239 _commit(repo, "init", {"hello.py": oid})
240 r = _fp(repo, "--json")
241 assert len(r.output.strip().splitlines()) == 1
242
243
244 # ---------------------------------------------------------------------------
245 # File change tracking
246 # ---------------------------------------------------------------------------
247
248
249 class TestFileChanges:
250 def test_initial_commit_files_in_files_added(self, tmp_path: pathlib.Path) -> None:
251 repo = _init_repo(tmp_path)
252 oid = _write_object(repo, b"x = 1\n")
253 _commit(repo, "init", {"hello.py": oid})
254 assert "hello.py" in _json(_fp(repo, "--json"))["files_added"]
255
256 def test_modified_file_in_files_modified(self, tmp_path: pathlib.Path) -> None:
257 repo = _init_repo(tmp_path)
258 oid1 = _write_object(repo, b"x = 1\n")
259 c1 = _commit(repo, "c1", {"a.py": oid1})
260 oid2 = _write_object(repo, b"x = 2\n")
261 _commit(repo, "c2", {"a.py": oid2}, parent=c1)
262 assert "a.py" in _json(_fp(repo, "--json"))["files_modified"]
263
264 def test_removed_file_in_files_deleted(self, tmp_path: pathlib.Path) -> None:
265 repo = _init_repo(tmp_path)
266 oid1 = _write_object(repo, b"x = 1\n")
267 oid2 = _write_object(repo, b"y = 2\n")
268 c1 = _commit(repo, "c1", {"old.py": oid1, "keep.py": oid2})
269 _commit(repo, "c2", {"keep.py": oid2}, parent=c1)
270 assert "old.py" in _json(_fp(repo, "--json"))["files_deleted"]
271
272 def test_empty_diff_all_lists_empty(self, tmp_path: pathlib.Path) -> None:
273 repo = _init_repo(tmp_path)
274 oid = _write_object(repo, b"x = 1\n")
275 c1 = _commit(repo, "c1", {"a.py": oid})
276 _commit(repo, "c2", {"a.py": oid}, parent=c1)
277 data = _json(_fp(repo, "--json"))
278 assert data["files_added"] == []
279 assert data["files_modified"] == []
280 assert data["files_deleted"] == []
281
282 def test_required_objects_is_list(self, tmp_path: pathlib.Path) -> None:
283 repo = _init_repo(tmp_path)
284 oid = _write_object(repo, b"x = 1\n")
285 _commit(repo, "init", {"hello.py": oid})
286 assert isinstance(_json(_fp(repo, "--json"))["required_objects"], list)
287
288 def test_from_manifest_is_dict(self, tmp_path: pathlib.Path) -> None:
289 repo = _init_repo(tmp_path)
290 oid = _write_object(repo, b"x = 1\n")
291 _commit(repo, "init", {"hello.py": oid})
292 assert isinstance(_json(_fp(repo, "--json"))["from_manifest"], dict)
293
294 def test_to_manifest_is_dict(self, tmp_path: pathlib.Path) -> None:
295 repo = _init_repo(tmp_path)
296 oid = _write_object(repo, b"x = 1\n")
297 _commit(repo, "init", {"hello.py": oid})
298 assert isinstance(_json(_fp(repo, "--json"))["to_manifest"], dict)
299
300 def test_applicability_requires_snapshot_matches_from_snapshot(self, tmp_path: pathlib.Path) -> None:
301 repo = _init_repo(tmp_path)
302 oid1 = _write_object(repo, b"x = 1\n")
303 c1 = _commit(repo, "c1", {"a.py": oid1})
304 oid2 = _write_object(repo, b"x = 2\n")
305 _commit(repo, "c2", {"a.py": oid2}, parent=c1)
306 data = _json(_fp(repo, "--json"))
307 assert data["applicability"]["requires_snapshot"] == data["from_snapshot_id"]
308
309 def test_count_equals_len_refs(self, tmp_path: pathlib.Path) -> None:
310 """files_added + files_modified + files_deleted must account for all changed paths."""
311 repo = _init_repo(tmp_path)
312 oid1 = _write_object(repo, b"x = 1\n")
313 oid2 = _write_object(repo, b"y = 2\n")
314 c1 = _commit(repo, "c1", {"a.py": oid1, "b.py": oid2})
315 oid3 = _write_object(repo, b"x = 99\n")
316 _commit(repo, "c2", {"a.py": oid3}, parent=c1) # modify a, delete b
317 data = _json(_fp(repo, "--json"))
318 total = len(data["files_added"]) + len(data["files_modified"]) + len(data["files_deleted"])
319 assert total == 2 # a.py modified, b.py deleted
320
321
322 # ---------------------------------------------------------------------------
323 # Cohen action labels on ops
324 # ---------------------------------------------------------------------------
325
326
327 class TestCohenActionLabels:
328 def test_inserted_label_on_added_file(self, tmp_path: pathlib.Path) -> None:
329 repo = _init_repo(tmp_path)
330 oid = _write_object(repo, b"x = 1\n")
331 _commit(repo, "init", {"hello.py": oid})
332 labels = [op.get("action_label") for op in _json(_fp(repo, "--json"))["ops"]]
333 assert "inserted" in labels
334
335 def test_deleted_label_on_removed_file(self, tmp_path: pathlib.Path) -> None:
336 repo = _init_repo(tmp_path)
337 oid = _write_object(repo, b"x = 1\n")
338 c1 = _commit(repo, "c1", {"old.py": oid})
339 _commit(repo, "c2", {}, parent=c1)
340 labels = [op.get("action_label") for op in _json(_fp(repo, "--json"))["ops"]]
341 assert "deleted" in labels
342
343 def test_modified_label_on_changed_file(self, tmp_path: pathlib.Path) -> None:
344 repo = _init_repo(tmp_path)
345 oid1 = _write_object(repo, b"x = 1\n")
346 c1 = _commit(repo, "c1", {"a.py": oid1})
347 oid2 = _write_object(repo, b"x = 2\n")
348 _commit(repo, "c2", {"a.py": oid2}, parent=c1)
349 labels = [op.get("action_label") for op in _json(_fp(repo, "--json"))["ops"]]
350 assert "modified" in labels
351
352 def test_all_ops_have_action_label(self, tmp_path: pathlib.Path) -> None:
353 repo = _init_repo(tmp_path)
354 oid1 = _write_object(repo, b"x = 1\n")
355 oid2 = _write_object(repo, b"y = 2\n")
356 _commit(repo, "init", {"a.py": oid1, "b.py": oid2})
357 data = _json(_fp(repo, "--json"))
358 for op in data["ops"]:
359 assert "action_label" in op, f"missing action_label in op: {op}"
360
361 def test_action_label_values_are_valid(self, tmp_path: pathlib.Path) -> None:
362 repo = _init_repo(tmp_path)
363 oid1 = _write_object(repo, b"x = 1\n")
364 _commit(repo, "init", {"a.py": oid1})
365 valid = {"inserted", "deleted", "modified", "moved", "renamed"}
366 for op in _json(_fp(repo, "--json"))["ops"]:
367 assert op["action_label"] in valid
368
369
370 # ---------------------------------------------------------------------------
371 # --output-dir writes .mpatch file
372 # ---------------------------------------------------------------------------
373
374
375 class TestOutputDir:
376 def test_writes_mpatch_file(self, tmp_path: pathlib.Path) -> None:
377 repo = _init_repo(tmp_path)
378 oid = _write_object(repo, b"x = 1\n")
379 _commit(repo, "init", {"hello.py": oid})
380 out_dir = tmp_path / "patches"
381 out_dir.mkdir()
382 r = _fp(repo, "--output-dir", str(out_dir))
383 assert r.exit_code == 0
384 assert len(list(out_dir.glob("*.mpatch"))) == 1
385
386 def test_mpatch_file_is_valid_json(self, tmp_path: pathlib.Path) -> None:
387 repo = _init_repo(tmp_path)
388 oid = _write_object(repo, b"x = 1\n")
389 _commit(repo, "init", {"hello.py": oid})
390 out_dir = tmp_path / "patches"
391 out_dir.mkdir()
392 _fp(repo, "--output-dir", str(out_dir))
393 patch_file = list(out_dir.glob("*.mpatch"))[0]
394 data = json.loads(patch_file.read_bytes())
395 assert "patch_id" in data
396 assert "domain" in data
397
398 def test_mpatch_filename_includes_subject(self, tmp_path: pathlib.Path) -> None:
399 repo = _init_repo(tmp_path)
400 oid = _write_object(repo, b"x = 1\n")
401 _commit(repo, "feat: add hello", {"hello.py": oid})
402 out_dir = tmp_path / "patches"
403 out_dir.mkdir()
404 _fp(repo, "--output-dir", str(out_dir))
405 names = [f.name for f in out_dir.glob("*.mpatch")]
406 assert any("feat" in n for n in names)
407
408 def test_nonexistent_output_dir_fails(self, tmp_path: pathlib.Path) -> None:
409 repo = _init_repo(tmp_path)
410 oid = _write_object(repo, b"x = 1\n")
411 _commit(repo, "init", {"hello.py": oid})
412 r = _fp(repo, "--output-dir", str(tmp_path / "does_not_exist"))
413 assert r.exit_code != 0
414
415
416 # ---------------------------------------------------------------------------
417 # Patch ID stability (data integrity)
418 # ---------------------------------------------------------------------------
419
420
421 class TestPatchIdStability:
422 def test_same_commit_same_patch_id(self, tmp_path: pathlib.Path) -> None:
423 repo = _init_repo(tmp_path)
424 oid = _write_object(repo, b"x = 1\n")
425 _commit(repo, "init", {"hello.py": oid})
426 pid1 = _json(_fp(repo, "--json"))["patch_id"]
427 pid2 = _json(_fp(repo, "--json"))["patch_id"]
428 assert pid1 == pid2
429
430 def test_different_commits_different_patch_ids(self, tmp_path: pathlib.Path) -> None:
431 repo = _init_repo(tmp_path)
432 oid1 = _write_object(repo, b"x = 1\n")
433 c1 = _commit(repo, "c1", {"a.py": oid1})
434 oid2 = _write_object(repo, b"x = 2\n")
435 _commit(repo, "c2", {"a.py": oid2}, parent=c1)
436 # HEAD is c2; compare its patch_id to c1's patch_id via explicit ref
437 pid_c2 = _json(_fp(repo, "--json"))["patch_id"]
438 pid_c1 = _json(_fp(repo, c1[len("sha256:"):], "--json"))["patch_id"]
439 assert pid_c2 != pid_c1
440
441
442 # ---------------------------------------------------------------------------
443 # Error paths
444 # ---------------------------------------------------------------------------
445
446
447 class TestErrorPaths:
448 def test_empty_repo_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
449 repo = _init_repo(tmp_path)
450 r = _fp(repo, "--json")
451 assert r.exit_code != 0
452
453 def test_bad_ref_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
454 repo = _init_repo(tmp_path)
455 oid = _write_object(repo, b"x = 1\n")
456 _commit(repo, "init", {"hello.py": oid})
457 r = _fp(repo, "nonexistent-branch", "--json")
458 assert r.exit_code != 0
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 143 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 145 days ago