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