gabriel / muse public
test_patch_id_supercharge.py python
685 lines 27.4 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Supercharge tests for ``muse patch-id``.
2
3 TDD — [RED] tests fail until the feature lands; [GREEN] tests replace the
4 broken ``test_cmd_patch_id.py`` (which used raw hex object IDs without the
5 required ``sha256:`` prefix).
6
7 New features under test
8 -----------------------
9 - ``duration_ms`` [RED] — wall-clock ms in JSON output
10 - ``exit_code`` [RED] — always present in JSON output
11 - ``files_changed`` [RED] — count of files in the diff in JSON output
12 - ``stable`` [RED] — boolean reflecting --stable flag in JSON output
13
14 Gap-fill / regression coverage [GREEN]
15 ----------------------------------------
16 - _compute_patch_id unit tests (all using sha256: prefix)
17 - JSON schema keys present
18 - Text output format «<patch_id> <commit_id>»
19 - Same diff → same patch-id (cherry-pick detection)
20 - Different diff → different patch-id
21 - --stable whitespace normalisation via CLI and unit
22 - Initial commit (no parent) has deterministic patch-id
23 - Branch name ref, explicit commit ID ref
24 - Error paths: empty repo, bad ref
25 - Security: ANSI, null byte, path traversal, very long ref, no tracebacks
26 - Data integrity: patch_id changes when content changes
27 - Multi-file commit patch-id covers all changed files
28 - Binary file diffs included in patch-id
29 - Performance: duration_ms < 2000ms for 20-file commit
30 - Stress: 10 distinct commits → 10 distinct patch-ids
31 """
32 from __future__ import annotations
33 from collections.abc import Mapping
34
35 import datetime
36 import json
37 import pathlib
38
39 import pytest
40
41 from muse.cli.commands.patch_id import _compute_patch_id
42 from muse.core.object_store import write_object
43 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
44 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
45 from muse.core._types import Manifest, blob_id, split_id
46 from tests.cli_test_helper import CliRunner, InvokeResult
47
48 runner = CliRunner()
49
50 _TS = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
51 _REPO_ID = "patch-id-test"
52
53
54 # ---------------------------------------------------------------------------
55 # Helpers
56 # ---------------------------------------------------------------------------
57
58 def _oid(content: bytes) -> str:
59 """Return a sha256:-prefixed object ID for content."""
60 return blob_id(content)
61
62
63 def _init_repo(path: pathlib.Path) -> pathlib.Path:
64 muse = path / ".muse"
65 for d in ("commits", "snapshots", "objects", "refs/heads"):
66 (muse / d).mkdir(parents=True, exist_ok=True)
67 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
68 (muse / "repo.json").write_text(
69 json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
70 )
71 return path
72
73
74 def _write_obj(repo: pathlib.Path, content: bytes) -> str:
75 oid = _oid(content)
76 write_object(repo, oid, content)
77 return oid
78
79
80 def _commit(
81 repo: pathlib.Path,
82 msg: str,
83 manifest: dict[str, str],
84 *,
85 branch: str = "main",
86 parent: str | None = None,
87 ) -> str:
88 sid = compute_snapshot_id(manifest)
89 write_snapshot(repo, SnapshotRecord(snapshot_id=sid, manifest=manifest, created_at=_TS))
90 parent_ids = [parent] if parent else []
91 cid = compute_commit_id(
92 repo_id=_REPO_ID,
93 parent_ids=parent_ids,
94 snapshot_id=sid,
95 message=msg,
96 committed_at_iso=_TS.isoformat(),
97 author="gabriel",)
98 write_commit(repo, CommitRecord(
99 commit_id=cid, repo_id=_REPO_ID, created_on_branch=branch,
100 snapshot_id=sid, message=msg, committed_at=_TS,
101 author="gabriel", parent_commit_id=parent, parent2_commit_id=None,
102 ))
103 ref = repo / ".muse" / "refs" / "heads" / branch
104 ref.parent.mkdir(parents=True, exist_ok=True)
105 ref.write_text(cid)
106 return cid
107
108
109 def _pid(repo: pathlib.Path, *args: str) -> InvokeResult:
110 return runner.invoke(None, ["patch-id", *args], env={"MUSE_REPO_ROOT": str(repo)})
111
112
113 def _json_out(r: InvokeResult) -> Mapping[str, object]:
114 for line in r.output.splitlines():
115 line = line.strip()
116 if line.startswith("{"):
117 return json.loads(line)
118 raise ValueError(f"No JSON in output:\n{r.output!r}")
119
120
121 # ---------------------------------------------------------------------------
122 # _compute_patch_id unit tests [GREEN — fix sha256: prefix]
123 # ---------------------------------------------------------------------------
124
125 class TestComputePatchId:
126 def test_same_diff_same_id(self, tmp_path: pathlib.Path) -> None:
127 repo = _init_repo(tmp_path)
128 oid_a = _write_obj(repo, b"x = 1\n")
129 oid_b = _write_obj(repo, b"x = 2\n")
130 base = {"f.py": oid_a}
131 target = {"f.py": oid_b}
132 assert _compute_patch_id(repo, base, target, stable=False) == \
133 _compute_patch_id(repo, base, target, stable=False)
134
135 def test_result_is_64_hex_chars(self, tmp_path: pathlib.Path) -> None:
136 repo = _init_repo(tmp_path)
137 oid_a = _write_obj(repo, b"a")
138 oid_b = _write_obj(repo, b"b")
139 pid = _compute_patch_id(repo, {"f.py": oid_a}, {"f.py": oid_b}, stable=False)
140 assert pid.startswith("sha256:") and len(pid) == 71
141 assert all(c in "0123456789abcdef" for c in split_id(pid)[1])
142
143 def test_different_content_different_id(self, tmp_path: pathlib.Path) -> None:
144 repo = _init_repo(tmp_path)
145 oid_a = _write_obj(repo, b"v1\n")
146 oid_b = _write_obj(repo, b"v2\n")
147 oid_c = _write_obj(repo, b"v3\n")
148 id1 = _compute_patch_id(repo, {"f.py": oid_a}, {"f.py": oid_b}, stable=False)
149 id2 = _compute_patch_id(repo, {"f.py": oid_a}, {"f.py": oid_c}, stable=False)
150 assert id1 != id2
151
152 def test_no_op_commit_deterministic(self, tmp_path: pathlib.Path) -> None:
153 repo = _init_repo(tmp_path)
154 oid = _write_obj(repo, b"unchanged\n")
155 manifest = {"f.py": oid}
156 pid1 = _compute_patch_id(repo, manifest, manifest, stable=False)
157 pid2 = _compute_patch_id(repo, manifest, manifest, stable=False)
158 assert pid1 == pid2
159
160 def test_stable_normalizes_trailing_whitespace(self, tmp_path: pathlib.Path) -> None:
161 repo = _init_repo(tmp_path)
162 oid_base = _write_obj(repo, b"x = 1\n")
163 oid_clean = _write_obj(repo, b"x = 2\n")
164 oid_ws = _write_obj(repo, b"x = 2 \n")
165 base = {"f.py": oid_base}
166 id_clean = _compute_patch_id(repo, base, {"f.py": oid_clean}, stable=True)
167 id_ws = _compute_patch_id(repo, base, {"f.py": oid_ws}, stable=True)
168 assert id_clean == id_ws
169
170 def test_unstable_sensitive_to_whitespace(self, tmp_path: pathlib.Path) -> None:
171 repo = _init_repo(tmp_path)
172 oid_base = _write_obj(repo, b"x = 1\n")
173 oid_clean = _write_obj(repo, b"x = 2\n")
174 oid_ws = _write_obj(repo, b"x = 2 \n")
175 base = {"f.py": oid_base}
176 id_clean = _compute_patch_id(repo, base, {"f.py": oid_clean}, stable=False)
177 id_ws = _compute_patch_id(repo, base, {"f.py": oid_ws}, stable=False)
178 assert id_clean != id_ws
179
180 def test_file_order_does_not_affect_id(self, tmp_path: pathlib.Path) -> None:
181 """Files are sorted alphabetically so order of dict keys is irrelevant."""
182 repo = _init_repo(tmp_path)
183 oid_a = _write_obj(repo, b"a\n")
184 oid_b = _write_obj(repo, b"b\n")
185 oid_a2 = _write_obj(repo, b"a2\n")
186 oid_b2 = _write_obj(repo, b"b2\n")
187 base1 = {"a.py": oid_a, "b.py": oid_b}
188 base2 = {"b.py": oid_b, "a.py": oid_a}
189 target1 = {"a.py": oid_a2, "b.py": oid_b2}
190 target2 = {"b.py": oid_b2, "a.py": oid_a2}
191 assert _compute_patch_id(repo, base1, target1, stable=False) == \
192 _compute_patch_id(repo, base2, target2, stable=False)
193
194 def test_initial_commit_no_parent(self, tmp_path: pathlib.Path) -> None:
195 """Initial commit: base_manifest={}, target has files."""
196 repo = _init_repo(tmp_path)
197 oid = _write_obj(repo, b"hello\n")
198 pid = _compute_patch_id(repo, {}, {"f.py": oid}, stable=False)
199 assert pid.startswith("sha256:") and len(pid) == 71
200
201 def test_deleted_file_affects_id(self, tmp_path: pathlib.Path) -> None:
202 repo = _init_repo(tmp_path)
203 oid = _write_obj(repo, b"bye\n")
204 id_del = _compute_patch_id(repo, {"f.py": oid}, {}, stable=False)
205 id_add = _compute_patch_id(repo, {}, {"f.py": oid}, stable=False)
206 assert id_del != id_add
207
208 def test_binary_content_included(self, tmp_path: pathlib.Path) -> None:
209 """Binary files (non-UTF-8) still produce a stable patch-id."""
210 repo = _init_repo(tmp_path)
211 binary_v1 = bytes(range(256))
212 binary_v2 = bytes(range(255, -1, -1))
213 oid1 = _write_obj(repo, binary_v1)
214 oid2 = _write_obj(repo, binary_v2)
215 pid1 = _compute_patch_id(repo, {"img.bin": oid1}, {"img.bin": oid2}, stable=False)
216 pid2 = _compute_patch_id(repo, {"img.bin": oid1}, {"img.bin": oid2}, stable=False)
217 assert pid1 == pid2
218 assert pid1.startswith("sha256:") and len(pid1) == 71
219
220
221 # ---------------------------------------------------------------------------
222 # JSON output: duration_ms, exit_code, files_changed, stable [RED]
223 # ---------------------------------------------------------------------------
224
225 class TestJsonSupercharge:
226 """[RED] New fields in --json output."""
227
228 def test_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
229 repo = _init_repo(tmp_path)
230 oid = _write_obj(repo, b"x")
231 _commit(repo, "init", {"f.py": oid})
232 d = _json_out(_pid(repo, "--json"))
233 assert "duration_ms" in d
234
235 def test_json_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
236 repo = _init_repo(tmp_path)
237 oid = _write_obj(repo, b"x")
238 _commit(repo, "init", {"f.py": oid})
239 d = _json_out(_pid(repo, "--json"))
240 assert d["duration_ms"] >= 0.0
241
242 def test_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
243 repo = _init_repo(tmp_path)
244 oid = _write_obj(repo, b"x")
245 _commit(repo, "init", {"f.py": oid})
246 d = _json_out(_pid(repo, "--json"))
247 assert "exit_code" in d
248
249 def test_json_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
250 repo = _init_repo(tmp_path)
251 oid = _write_obj(repo, b"x")
252 _commit(repo, "init", {"f.py": oid})
253 d = _json_out(_pid(repo, "--json"))
254 assert d["exit_code"] == 0
255
256 def test_json_has_files_changed(self, tmp_path: pathlib.Path) -> None:
257 repo = _init_repo(tmp_path)
258 oid = _write_obj(repo, b"x")
259 _commit(repo, "init", {"f.py": oid})
260 d = _json_out(_pid(repo, "--json"))
261 assert "files_changed" in d
262
263 def test_json_files_changed_correct_count(self, tmp_path: pathlib.Path) -> None:
264 repo = _init_repo(tmp_path)
265 oid_a = _write_obj(repo, b"a")
266 oid_b = _write_obj(repo, b"b")
267 _commit(repo, "init", {"a.py": oid_a, "b.py": oid_b})
268 d = _json_out(_pid(repo, "--json"))
269 assert d["files_changed"] == 2
270
271 def test_json_files_changed_is_int(self, tmp_path: pathlib.Path) -> None:
272 repo = _init_repo(tmp_path)
273 oid = _write_obj(repo, b"x")
274 _commit(repo, "init", {"f.py": oid})
275 d = _json_out(_pid(repo, "--json"))
276 assert isinstance(d["files_changed"], int)
277
278 def test_json_has_stable_field(self, tmp_path: pathlib.Path) -> None:
279 repo = _init_repo(tmp_path)
280 oid = _write_obj(repo, b"x")
281 _commit(repo, "init", {"f.py": oid})
282 d = _json_out(_pid(repo, "--json"))
283 assert "stable" in d
284
285 def test_json_stable_false_by_default(self, tmp_path: pathlib.Path) -> None:
286 repo = _init_repo(tmp_path)
287 oid = _write_obj(repo, b"x")
288 _commit(repo, "init", {"f.py": oid})
289 d = _json_out(_pid(repo, "--json"))
290 assert d["stable"] is False
291
292 def test_json_stable_true_with_flag(self, tmp_path: pathlib.Path) -> None:
293 repo = _init_repo(tmp_path)
294 oid = _write_obj(repo, b"x")
295 _commit(repo, "init", {"f.py": oid})
296 d = _json_out(_pid(repo, "--json", "--stable"))
297 assert d["stable"] is True
298
299 def test_stable_flag_produces_different_patch_id_for_ws_diff(
300 self, tmp_path: pathlib.Path
301 ) -> None:
302 """--stable and no-flag produce different IDs for trailing-whitespace diff."""
303 repo = _init_repo(tmp_path)
304 oid_base = _write_obj(repo, b"x = 1\n")
305 c1 = _commit(repo, "c1", {"f.py": oid_base})
306 oid_ws = _write_obj(repo, b"x = 1 \n")
307 _commit(repo, "c2 ws", {"f.py": oid_ws}, parent=c1)
308 d_normal = _json_out(_pid(repo, "--json"))
309 d_stable = _json_out(_pid(repo, "--json", "--stable"))
310 assert d_normal["patch_id"] != d_stable["patch_id"]
311
312
313 # ---------------------------------------------------------------------------
314 # Existing JSON fields still present [GREEN]
315 # ---------------------------------------------------------------------------
316
317 class TestJsonGreen:
318 def test_commit_id_present(self, tmp_path: pathlib.Path) -> None:
319 repo = _init_repo(tmp_path)
320 oid = _write_obj(repo, b"x")
321 _commit(repo, "init", {"f.py": oid})
322 d = _json_out(_pid(repo, "--json"))
323 assert "commit_id" in d
324
325 def test_patch_id_present(self, tmp_path: pathlib.Path) -> None:
326 repo = _init_repo(tmp_path)
327 oid = _write_obj(repo, b"x")
328 _commit(repo, "init", {"f.py": oid})
329 d = _json_out(_pid(repo, "--json"))
330 assert "patch_id" in d
331
332 def test_patch_id_is_64_hex(self, tmp_path: pathlib.Path) -> None:
333 repo = _init_repo(tmp_path)
334 oid = _write_obj(repo, b"x")
335 _commit(repo, "init", {"f.py": oid})
336 d = _json_out(_pid(repo, "--json"))
337 assert d["patch_id"].startswith("sha256:") and len(d["patch_id"]) == 71
338 assert all(c in "0123456789abcdef" for c in split_id(d["patch_id"])[1])
339
340 def test_subject_present(self, tmp_path: pathlib.Path) -> None:
341 repo = _init_repo(tmp_path)
342 oid = _write_obj(repo, b"x")
343 _commit(repo, "feat: hello world", {"f.py": oid})
344 d = _json_out(_pid(repo, "--json"))
345 assert d["subject"] == "feat: hello world"
346
347 def test_commit_id_matches_head(self, tmp_path: pathlib.Path) -> None:
348 repo = _init_repo(tmp_path)
349 oid = _write_obj(repo, b"x")
350 cid = _commit(repo, "init", {"f.py": oid})
351 d = _json_out(_pid(repo, "--json"))
352 assert d["commit_id"] == cid
353
354 def test_same_diff_same_patch_id(self, tmp_path: pathlib.Path) -> None:
355 """Cherry-pick detection: same logical change → same patch_id."""
356 repo = _init_repo(tmp_path)
357 oid_a = _write_obj(repo, b"v1\n")
358 c1 = _commit(repo, "c1", {"f.py": oid_a})
359 oid_b = _write_obj(repo, b"v2\n")
360 _commit(repo, "c2", {"f.py": oid_b}, parent=c1)
361 d1 = _json_out(_pid(repo, "--json"))
362
363 # Second repo with identical diff
364 repo2 = _init_repo(tmp_path / "repo2")
365 _write_obj(repo2, b"v1\n")
366 c1b = _commit(repo2, "c1", {"f.py": oid_a})
367 _write_obj(repo2, b"v2\n")
368 _commit(repo2, "c2 clone", {"f.py": oid_b}, parent=c1b)
369 d2 = _json_out(_pid(repo2, "--json"))
370
371 assert d1["patch_id"] == d2["patch_id"]
372
373 def test_different_diff_different_patch_id(self, tmp_path: pathlib.Path) -> None:
374 repo = _init_repo(tmp_path)
375 oid_a = _write_obj(repo, b"v1\n")
376 c1 = _commit(repo, "c1", {"f.py": oid_a})
377 oid_b = _write_obj(repo, b"v2\n")
378 _commit(repo, "c2", {"f.py": oid_b}, parent=c1)
379 d1 = _json_out(_pid(repo, "--json"))
380
381 repo2 = _init_repo(tmp_path / "repo2")
382 _write_obj(repo2, b"v1\n")
383 c1b = _commit(repo2, "c1", {"f.py": oid_a})
384 oid_c = _write_obj(repo2, b"completely different content\n")
385 _commit(repo2, "c2 different", {"f.py": oid_c}, parent=c1b)
386 d2 = _json_out(_pid(repo2, "--json"))
387
388 assert d1["patch_id"] != d2["patch_id"]
389
390 def test_explicit_commit_id_ref(self, tmp_path: pathlib.Path) -> None:
391 repo = _init_repo(tmp_path)
392 oid = _write_obj(repo, b"x")
393 cid = _commit(repo, "init", {"f.py": oid})
394 d = _json_out(_pid(repo, cid, "--json"))
395 assert d["commit_id"] == cid
396
397 def test_branch_name_ref(self, tmp_path: pathlib.Path) -> None:
398 repo = _init_repo(tmp_path)
399 oid = _write_obj(repo, b"x")
400 _commit(repo, "init", {"f.py": oid})
401 d = _json_out(_pid(repo, "main", "--json"))
402 assert "patch_id" in d
403
404
405 # ---------------------------------------------------------------------------
406 # Text output format [GREEN]
407 # ---------------------------------------------------------------------------
408
409 class TestTextOutput:
410 def test_text_format_two_parts(self, tmp_path: pathlib.Path) -> None:
411 repo = _init_repo(tmp_path)
412 oid = _write_obj(repo, b"x")
413 _commit(repo, "init", {"f.py": oid})
414 r = _pid(repo)
415 assert r.exit_code == 0
416 parts = r.output.strip().split()
417 assert len(parts) == 2
418
419 def test_text_patch_id_is_hex(self, tmp_path: pathlib.Path) -> None:
420 repo = _init_repo(tmp_path)
421 oid = _write_obj(repo, b"x")
422 _commit(repo, "init", {"f.py": oid})
423 parts = _pid(repo).output.strip().split()
424 assert parts[0].startswith("sha256:") and len(parts[0]) == 71
425 assert all(c in "0123456789abcdef" for c in split_id(parts[0])[1])
426
427 def test_text_commit_id_matches_json(self, tmp_path: pathlib.Path) -> None:
428 repo = _init_repo(tmp_path)
429 oid = _write_obj(repo, b"x")
430 _commit(repo, "init", {"f.py": oid})
431 text_parts = _pid(repo).output.strip().split()
432 json_d = _json_out(_pid(repo, "--json"))
433 assert text_parts[1] == json_d["commit_id"]
434 assert text_parts[0] == json_d["patch_id"]
435
436
437 # ---------------------------------------------------------------------------
438 # files_changed correctness [RED]
439 # ---------------------------------------------------------------------------
440
441 class TestFilesChanged:
442 def test_initial_commit_all_files_counted(self, tmp_path: pathlib.Path) -> None:
443 repo = _init_repo(tmp_path)
444 oid_a = _write_obj(repo, b"a")
445 oid_b = _write_obj(repo, b"b")
446 oid_c = _write_obj(repo, b"c")
447 _commit(repo, "init", {"a.py": oid_a, "b.py": oid_b, "c.py": oid_c})
448 d = _json_out(_pid(repo, "--json"))
449 assert d["files_changed"] == 3
450
451 def test_deletion_counted(self, tmp_path: pathlib.Path) -> None:
452 repo = _init_repo(tmp_path)
453 oid = _write_obj(repo, b"gone")
454 c1 = _commit(repo, "c1", {"old.py": oid})
455 _commit(repo, "c2 delete", {}, parent=c1)
456 d = _json_out(_pid(repo, "--json"))
457 assert d["files_changed"] == 1
458
459 def test_modification_counted(self, tmp_path: pathlib.Path) -> None:
460 repo = _init_repo(tmp_path)
461 oid_v1 = _write_obj(repo, b"v1")
462 c1 = _commit(repo, "c1", {"f.py": oid_v1})
463 oid_v2 = _write_obj(repo, b"v2")
464 _commit(repo, "c2 mod", {"f.py": oid_v2}, parent=c1)
465 d = _json_out(_pid(repo, "--json"))
466 assert d["files_changed"] == 1
467
468 def test_unchanged_files_not_counted(self, tmp_path: pathlib.Path) -> None:
469 repo = _init_repo(tmp_path)
470 oid_keep = _write_obj(repo, b"keep")
471 oid_chg = _write_obj(repo, b"v1")
472 c1 = _commit(repo, "c1", {"keep.py": oid_keep, "chg.py": oid_chg})
473 oid_chg2 = _write_obj(repo, b"v2")
474 _commit(repo, "c2", {"keep.py": oid_keep, "chg.py": oid_chg2}, parent=c1)
475 d = _json_out(_pid(repo, "--json"))
476 assert d["files_changed"] == 1
477
478 def test_no_op_commit_zero_files_changed(self, tmp_path: pathlib.Path) -> None:
479 repo = _init_repo(tmp_path)
480 oid = _write_obj(repo, b"same")
481 c1 = _commit(repo, "c1", {"f.py": oid})
482 _commit(repo, "c2 noop", {"f.py": oid}, parent=c1)
483 d = _json_out(_pid(repo, "--json"))
484 assert d["files_changed"] == 0
485
486
487 # ---------------------------------------------------------------------------
488 # Error paths [GREEN]
489 # ---------------------------------------------------------------------------
490
491 class TestErrors:
492 def test_empty_repo_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
493 repo = _init_repo(tmp_path)
494 r = _pid(repo, "--json")
495 assert r.exit_code != 0
496
497 def test_bad_ref_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
498 repo = _init_repo(tmp_path)
499 oid = _write_obj(repo, b"x")
500 _commit(repo, "init", {"f.py": oid})
501 r = _pid(repo, "no-such-ref", "--json")
502 assert r.exit_code != 0
503
504 def test_no_traceback_on_bad_ref(self, tmp_path: pathlib.Path) -> None:
505 repo = _init_repo(tmp_path)
506 oid = _write_obj(repo, b"x")
507 _commit(repo, "init", {"f.py": oid})
508 r = _pid(repo, "no-such-ref")
509 assert "Traceback" not in r.output
510 assert "Traceback" not in r.stderr
511
512 def test_error_to_stderr_not_stdout(self, tmp_path: pathlib.Path) -> None:
513 repo = _init_repo(tmp_path)
514 r = _pid(repo, "--json")
515 assert r.exit_code != 0
516 assert "❌" in r.stderr or r.exit_code != 0
517
518
519 # ---------------------------------------------------------------------------
520 # Security [GREEN]
521 # ---------------------------------------------------------------------------
522
523 class TestSecurity:
524 def test_ansi_in_ref_rejected(self, tmp_path: pathlib.Path) -> None:
525 repo = _init_repo(tmp_path)
526 oid = _write_obj(repo, b"x")
527 _commit(repo, "init", {"f.py": oid})
528 assert _pid(repo, "\x1b[31mbad\x1b[0m").exit_code != 0
529
530 def test_null_byte_in_ref_rejected(self, tmp_path: pathlib.Path) -> None:
531 repo = _init_repo(tmp_path)
532 oid = _write_obj(repo, b"x")
533 _commit(repo, "init", {"f.py": oid})
534 assert _pid(repo, "main\x00evil").exit_code != 0
535
536 def test_path_traversal_in_ref_rejected(self, tmp_path: pathlib.Path) -> None:
537 repo = _init_repo(tmp_path)
538 oid = _write_obj(repo, b"x")
539 _commit(repo, "init", {"f.py": oid})
540 assert _pid(repo, "../../etc/passwd").exit_code != 0
541
542 def test_very_long_ref_rejected(self, tmp_path: pathlib.Path) -> None:
543 repo = _init_repo(tmp_path)
544 oid = _write_obj(repo, b"x")
545 _commit(repo, "init", {"f.py": oid})
546 assert _pid(repo, "a" * 300).exit_code != 0
547
548 def test_no_traceback_on_ansi_ref(self, tmp_path: pathlib.Path) -> None:
549 repo = _init_repo(tmp_path)
550 oid = _write_obj(repo, b"x")
551 _commit(repo, "init", {"f.py": oid})
552 r = _pid(repo, "\x1b[31mbad\x1b[0m")
553 assert "Traceback" not in r.output
554 assert "Traceback" not in r.stderr
555
556
557 # ---------------------------------------------------------------------------
558 # Data integrity [GREEN]
559 # ---------------------------------------------------------------------------
560
561 class TestDataIntegrity:
562 def test_patch_id_changes_when_content_changes(self, tmp_path: pathlib.Path) -> None:
563 repo = _init_repo(tmp_path)
564 oid_v1 = _write_obj(repo, b"version 1\n")
565 c1 = _commit(repo, "c1", {"f.py": oid_v1})
566 oid_v2 = _write_obj(repo, b"version 2\n")
567 _commit(repo, "c2", {"f.py": oid_v2}, parent=c1)
568 d1 = _json_out(_pid(repo, c1, "--json"))
569
570 # Change HEAD to c2 by making another commit
571 oid_v3 = _write_obj(repo, b"version 3\n")
572 c3 = _commit(repo, "c3", {"f.py": oid_v3}, parent=c1)
573 # re-point HEAD ref directly (two different commits from same parent)
574 (repo / ".muse" / "refs" / "heads" / "main").write_text(c3)
575 d3 = _json_out(_pid(repo, "--json"))
576 assert d1["patch_id"] != d3["patch_id"]
577
578 def test_adding_file_changes_patch_id(self, tmp_path: pathlib.Path) -> None:
579 repo = _init_repo(tmp_path)
580 oid_a = _write_obj(repo, b"a\n")
581 c1 = _commit(repo, "c1", {"a.py": oid_a})
582 oid_b = _write_obj(repo, b"b\n")
583 _commit(repo, "c2 add b", {"a.py": oid_a, "b.py": oid_b}, parent=c1)
584 d = _json_out(_pid(repo, "--json"))
585 assert d["files_changed"] == 1
586 assert d["patch_id"] is not None
587
588 def test_patch_id_stable_vs_unstable_differ_for_ws(self, tmp_path: pathlib.Path) -> None:
589 repo = _init_repo(tmp_path)
590 oid_base = _write_obj(repo, b"x = 1\n")
591 c1 = _commit(repo, "c1", {"f.py": oid_base})
592 oid_ws = _write_obj(repo, b"x = 1 \n")
593 _commit(repo, "c2", {"f.py": oid_ws}, parent=c1)
594 d_normal = _json_out(_pid(repo, "--json"))
595 d_stable = _json_out(_pid(repo, "--json", "--stable"))
596 assert d_normal["patch_id"] != d_stable["patch_id"]
597
598
599 # ---------------------------------------------------------------------------
600 # Performance [GREEN]
601 # ---------------------------------------------------------------------------
602
603 class TestPerformance:
604 def test_duration_ms_under_two_seconds(self, tmp_path: pathlib.Path) -> None:
605 repo = _init_repo(tmp_path)
606 manifest: dict[str, str] = {}
607 for i in range(20):
608 content = f"# module {i}\n" .encode() * 50
609 oid = _write_obj(repo, content)
610 manifest[f"src/file_{i:02d}.py"] = oid
611 _commit(repo, "feat: 20 files", manifest)
612 d = _json_out(_pid(repo, "--json"))
613 assert d["duration_ms"] < 2000.0
614
615 def test_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
616 repo = _init_repo(tmp_path)
617 oid = _write_obj(repo, b"x")
618 _commit(repo, "init", {"f.py": oid})
619 d = _json_out(_pid(repo, "--json"))
620 assert d["duration_ms"] >= 0.0
621
622
623 # ---------------------------------------------------------------------------
624 # Stress [GREEN]
625 # ---------------------------------------------------------------------------
626
627 class TestStress:
628 def test_10_distinct_commits_10_distinct_patch_ids(self, tmp_path: pathlib.Path) -> None:
629 repo = _init_repo(tmp_path)
630 patch_ids: set[str] = set()
631 parent: str | None = None
632 for i in range(10):
633 content = f"value = {i}\n".encode()
634 oid = _write_obj(repo, content)
635 cid = _commit(repo, f"c{i}", {"file.py": oid}, parent=parent)
636 d = _json_out(_pid(repo, cid, "--json"))
637 patch_ids.add(d["patch_id"])
638 parent = cid
639 assert len(patch_ids) == 10
640
641 def test_50_file_commit(self, tmp_path: pathlib.Path) -> None:
642 repo = _init_repo(tmp_path)
643 manifest: dict[str, str] = {}
644 for i in range(50):
645 oid = _write_obj(repo, f"file {i}\n".encode() * 20)
646 manifest[f"f{i:03d}.py"] = oid
647 _commit(repo, "feat: 50 files", manifest)
648 r = _pid(repo, "--json")
649 assert r.exit_code == 0
650 d = _json_out(r)
651 assert d["files_changed"] == 50
652
653
654 # ---------------------------------------------------------------------------
655 # TestRegisterFlags — argparse-level verification
656 # ---------------------------------------------------------------------------
657
658
659 class TestRegisterFlags:
660 """Verify that register() wires --json / -j correctly."""
661
662 def _make_parser(self):
663 import argparse
664 from muse.cli.commands.patch_id import register
665 ap = argparse.ArgumentParser()
666 subs = ap.add_subparsers()
667 register(subs)
668 return ap
669
670 def test_json_flag_long(self):
671 ns = self._make_parser().parse_args(["patch-id", "--json"])
672 assert ns.json_out is True
673
674 def test_j_alias(self):
675 ns = self._make_parser().parse_args(["patch-id", "-j"])
676 assert ns.json_out is True
677
678 def test_default_is_text(self):
679 ns = self._make_parser().parse_args(["patch-id"])
680 assert ns.json_out is False
681
682 def test_dest_is_json_out(self):
683 ns = self._make_parser().parse_args(["patch-id", "-j"])
684 assert hasattr(ns, "json_out")
685 assert not hasattr(ns, "fmt")
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago