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