gabriel / muse public
test_range_diff_supercharge.py python
864 lines 43.2 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
1 """Tests for ``muse range-diff`` — supercharged coverage.
2
3 Coverage tiers
4 --------------
5 - Unit: _parse_range, _resolve_ref (sha256: prefix), _compute_patch_id,
6 _patch_id_for_commit, _pair_series pairing logic
7 - Integration: identical series, changed/dropped/added, JSON schema,
8 text output, creation-factor variants, error cases
9 - End-to-end: full CLI via CliRunner
10 - Data integrity: old_count/new_count match pair list; files_changed per commit;
11 patch_id has sha256: prefix; duration_ms is numeric
12 - Performance: 50-commit series completes under 2 seconds
13 - Security: ANSI injection rejected; no control characters in output
14 - Stress: 50-commit mixed series (equivalent + changed + added)
15
16 Supercharged JSON schema (all ``--json`` outputs)
17 --------------------------------------------------
18
19 ::
20
21 {
22 "old_range": "base..old",
23 "new_range": "base..new",
24 "trivially_equivalent": true,
25 "old_count": 3,
26 "new_count": 3,
27 "stable": false,
28 "creation_factor": 0.6,
29 "pairs": [
30 {
31 "old": {
32 "commit_id": "sha256:...",
33 "patch_id": "sha256:...",
34 "subject": "feat: add foo",
35 "files_changed": 2
36 },
37 "new": { ... },
38 "status": "equivalent"
39 }
40 ],
41 "duration_ms": 12.3,
42 "exit_code": 0
43 }
44 """
45
46 from __future__ import annotations
47
48 import datetime
49 import hashlib
50 import json
51 import pathlib
52 import re
53 import time
54
55 import pytest
56
57 from tests.cli_test_helper import CliRunner
58 from muse.core.object_store import write_object
59 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
60 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
61 from muse.core._types import Manifest, long_id
62
63 runner = CliRunner()
64
65 _REPO_ID = "range-diff-super"
66 _counter = 0
67
68
69 # ---------------------------------------------------------------------------
70 # Helpers
71 # ---------------------------------------------------------------------------
72
73
74 def _oid(content: bytes) -> str:
75 """sha256:-prefixed object ID — correct for all Muse APIs."""
76 return long_id(hashlib.sha256(content).hexdigest())
77
78
79 def _init_repo(path: pathlib.Path) -> pathlib.Path:
80 muse = path / ".muse"
81 for d in ("commits", "snapshots", "objects", "refs/heads", "code"):
82 (muse / d).mkdir(parents=True, exist_ok=True)
83 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
84 (muse / "repo.json").write_text(
85 json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
86 )
87 return path
88
89
90 def _env(repo: pathlib.Path) -> dict[str, str]:
91 return {"MUSE_REPO_ROOT": str(repo)}
92
93
94 def _write_files(root: pathlib.Path, files: dict[str, bytes]) -> Manifest:
95 manifest: Manifest = {}
96 for rel, content in files.items():
97 oid = _oid(content)
98 write_object(root, oid, content)
99 manifest[rel] = oid
100 p = root / rel
101 p.parent.mkdir(parents=True, exist_ok=True)
102 p.write_bytes(content)
103 return manifest
104
105
106 def _commit(
107 root: pathlib.Path,
108 files: dict[str, bytes],
109 branch: str = "main",
110 parent_id: str | None = None,
111 message: str | None = None,
112 ) -> str:
113 global _counter
114 _counter += 1
115 manifest = _write_files(root, files)
116 snap_id = compute_snapshot_id(manifest)
117 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
118 committed_at = datetime.datetime.now(datetime.timezone.utc)
119 msg = message or f"commit {_counter}"
120 commit_id = compute_commit_id(
121 [parent_id] if parent_id else [], snap_id, msg, committed_at.isoformat()
122 )
123 write_commit(root, CommitRecord(
124 commit_id=commit_id, repo_id=_REPO_ID, branch=branch,
125 snapshot_id=snap_id, message=msg, committed_at=committed_at,
126 parent_commit_id=parent_id,
127 ))
128 ref_path = root / ".muse" / "refs" / "heads" / branch
129 ref_path.parent.mkdir(parents=True, exist_ok=True)
130 ref_path.write_text(commit_id, encoding="utf-8")
131 return commit_id
132
133
134 def _invoke(repo: pathlib.Path, *args: str):
135 from muse.cli.app import main as cli
136 return runner.invoke(cli, ["range-diff", *args], env=_env(repo))
137
138
139 # ---------------------------------------------------------------------------
140 # Unit — _parse_range
141 # ---------------------------------------------------------------------------
142
143
144 class TestParseRange:
145 def test_with_dotdot(self) -> None:
146 from muse.cli.commands.range_diff import _parse_range
147 base, tip = _parse_range("abc..def")
148 assert base == "abc"
149 assert tip == "def"
150
151 def test_no_dotdot(self) -> None:
152 from muse.cli.commands.range_diff import _parse_range
153 base, tip = _parse_range("main")
154 assert base is None
155 assert tip == "main"
156
157 def test_strips_whitespace(self) -> None:
158 from muse.cli.commands.range_diff import _parse_range
159 base, tip = _parse_range("base .. tip")
160 assert base == "base"
161 assert tip == "tip"
162
163 def test_sha256_prefixed_base(self) -> None:
164 from muse.cli.commands.range_diff import _parse_range
165 sha = long_id("a" * 64)
166 base, tip = _parse_range(f"{sha}..main")
167 assert base == sha
168 assert tip == "main"
169
170
171 # ---------------------------------------------------------------------------
172 # Unit — _resolve_ref with sha256: prefix
173 # ---------------------------------------------------------------------------
174
175
176 class TestResolveRef:
177 def test_resolves_branch_name(self, tmp_path: pathlib.Path) -> None:
178 from muse.cli.commands.range_diff import _resolve_ref
179 root = _init_repo(tmp_path)
180 cid = _commit(root, {"a.py": b"x\n"}, branch="main")
181 resolved = _resolve_ref(root, "main")
182 assert resolved == cid
183
184 def test_resolves_sha256_prefixed_commit_id(self, tmp_path: pathlib.Path) -> None:
185 """RED: _resolve_ref must accept sha256:-prefixed commit IDs."""
186 from muse.cli.commands.range_diff import _resolve_ref
187 root = _init_repo(tmp_path)
188 cid = _commit(root, {"a.py": b"x\n"}, branch="main")
189 # cid from compute_commit_id is sha256:-prefixed
190 assert cid.startswith("sha256:")
191 resolved = _resolve_ref(root, cid)
192 assert resolved is not None, (
193 f"_resolve_ref could not resolve sha256:-prefixed commit ID {cid[:20]}..."
194 )
195
196 def test_resolves_head(self, tmp_path: pathlib.Path) -> None:
197 from muse.cli.commands.range_diff import _resolve_ref
198 root = _init_repo(tmp_path)
199 cid = _commit(root, {"a.py": b"x\n"}, branch="main")
200 assert _resolve_ref(root, "HEAD") == cid
201
202 def test_nonexistent_branch_returns_none(self, tmp_path: pathlib.Path) -> None:
203 from muse.cli.commands.range_diff import _resolve_ref
204 root = _init_repo(tmp_path)
205 assert _resolve_ref(root, "no-such-branch") is None
206
207 def test_nonexistent_sha_returns_none(self, tmp_path: pathlib.Path) -> None:
208 from muse.cli.commands.range_diff import _resolve_ref
209 root = _init_repo(tmp_path)
210 fake = long_id("f" * 64)
211 assert _resolve_ref(root, fake) is None
212
213
214 # ---------------------------------------------------------------------------
215 # Unit — _compute_patch_id / _patch_id_for_commit
216 # ---------------------------------------------------------------------------
217
218
219 class TestPatchId:
220 def test_returns_sha256_prefixed_patch_id(self, tmp_path: pathlib.Path) -> None:
221 """RED: patch_id must have sha256: prefix — consistent with muse patch-id --json."""
222 from muse.cli.commands.range_diff import _patch_id_for_commit
223 root = _init_repo(tmp_path)
224 base = _commit(root, {"base.py": b"base\n"}, branch="main")
225 cid = _commit(root, {"base.py": b"base\n", "a.py": b"a=1\n"}, branch="feat", parent_id=base)
226 pid, _ = _patch_id_for_commit(root, cid, stable=False)
227 assert pid.startswith("sha256:"), (
228 f"patch_id should be sha256:-prefixed but got: {pid[:20]!r}"
229 )
230
231 def test_returns_files_changed_count(self, tmp_path: pathlib.Path) -> None:
232 """RED: _patch_id_for_commit must return (patch_id, files_changed) tuple."""
233 from muse.cli.commands.range_diff import _patch_id_for_commit
234 root = _init_repo(tmp_path)
235 base = _commit(root, {"base.py": b"base\n"}, branch="main")
236 # commit adds 2 new files relative to parent
237 cid = _commit(root, {"base.py": b"base\n", "a.py": b"a\n", "b.py": b"b\n"},
238 branch="feat", parent_id=base)
239 pid, fc = _patch_id_for_commit(root, cid, stable=False)
240 assert fc == 2, f"Expected 2 files_changed, got {fc}"
241
242 def test_same_content_same_patch_id(self, tmp_path: pathlib.Path) -> None:
243 from muse.cli.commands.range_diff import _patch_id_for_commit
244 root = _init_repo(tmp_path)
245 base = _commit(root, {"base.py": b"base\n"}, branch="main")
246 c1 = _commit(root, {"base.py": b"base\n", "a.py": b"a=1\n"}, branch="b1", parent_id=base)
247 c2 = _commit(root, {"base.py": b"base\n", "a.py": b"a=1\n"}, branch="b2", parent_id=base)
248 pid1, _ = _patch_id_for_commit(root, c1, stable=False)
249 pid2, _ = _patch_id_for_commit(root, c2, stable=False)
250 assert pid1 == pid2
251
252 def test_different_content_different_patch_id(self, tmp_path: pathlib.Path) -> None:
253 from muse.cli.commands.range_diff import _patch_id_for_commit
254 root = _init_repo(tmp_path)
255 base = _commit(root, {"base.py": b"base\n"}, branch="main")
256 c1 = _commit(root, {"base.py": b"base\n", "a.py": b"v1\n"}, branch="b1", parent_id=base)
257 c2 = _commit(root, {"base.py": b"base\n", "a.py": b"v2\n"}, branch="b2", parent_id=base)
258 pid1, _ = _patch_id_for_commit(root, c1, stable=False)
259 pid2, _ = _patch_id_for_commit(root, c2, stable=False)
260 assert pid1 != pid2
261
262 def test_stable_ignores_trailing_whitespace(self, tmp_path: pathlib.Path) -> None:
263 from muse.cli.commands.range_diff import _patch_id_for_commit
264 root = _init_repo(tmp_path)
265 base = _commit(root, {"base.py": b"base\n"}, branch="main")
266 c1 = _commit(root, {"base.py": b"base\n", "a.py": b"a=1\n"}, branch="b1", parent_id=base)
267 c2 = _commit(root, {"base.py": b"base\n", "a.py": b"a=1 \n"}, branch="b2", parent_id=base)
268 pid1, _ = _patch_id_for_commit(root, c1, stable=True)
269 pid2, _ = _patch_id_for_commit(root, c2, stable=True)
270 assert pid1 == pid2
271
272 def test_first_commit_zero_files_changed(self, tmp_path: pathlib.Path) -> None:
273 from muse.cli.commands.range_diff import _patch_id_for_commit
274 root = _init_repo(tmp_path)
275 # First commit has no parent — base_manifest is empty, so all files are "added"
276 cid = _commit(root, {"a.py": b"x\n", "b.py": b"y\n"}, branch="main")
277 _, fc = _patch_id_for_commit(root, cid, stable=False)
278 assert fc == 2
279
280
281 # ---------------------------------------------------------------------------
282 # Integration — trivially equivalent
283 # ---------------------------------------------------------------------------
284
285
286 class TestTriviallyEquivalent:
287 def test_identical_series_exit_zero(self, tmp_path: pathlib.Path) -> None:
288 root = _init_repo(tmp_path)
289 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
290 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
291 c2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=c1)
292 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
293 n2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="new", parent_id=n1)
294 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
295 assert result.exit_code == 0
296 data = json.loads(result.stdout)
297 assert data["trivially_equivalent"] is True
298 assert all(p["status"] == "equivalent" for p in data["pairs"])
299
300 def test_both_empty_trivially_equivalent(self, tmp_path: pathlib.Path) -> None:
301 root = _init_repo(tmp_path)
302 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
303 result = _invoke(root, f"{base}..{base}", f"{base}..{base}", "--json")
304 assert result.exit_code == 0
305 data = json.loads(result.stdout)
306 assert data["trivially_equivalent"] is True
307 assert data["pairs"] == []
308
309 def test_empty_old_all_added(self, tmp_path: pathlib.Path) -> None:
310 root = _init_repo(tmp_path)
311 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
312 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
313 result = _invoke(root, f"{base}..{base}", f"{base}..new", "--json")
314 data = json.loads(result.stdout)
315 assert all(p["status"] == "added" for p in data["pairs"])
316
317 def test_empty_new_all_dropped(self, tmp_path: pathlib.Path) -> None:
318 root = _init_repo(tmp_path)
319 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
320 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
321 result = _invoke(root, f"{base}..old", f"{base}..{base}", "--json")
322 data = json.loads(result.stdout)
323 assert all(p["status"] == "dropped" for p in data["pairs"])
324
325
326 # ---------------------------------------------------------------------------
327 # Integration — differences
328 # ---------------------------------------------------------------------------
329
330
331 class TestDifferences:
332 def test_single_commit_changed(self, tmp_path: pathlib.Path) -> None:
333 root = _init_repo(tmp_path)
334 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
335 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base)
336 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base)
337 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
338 data = json.loads(result.stdout)
339 assert data["trivially_equivalent"] is False
340 assert len(data["pairs"]) == 1
341 assert data["pairs"][0]["status"] == "changed"
342
343 def test_commit_added(self, tmp_path: pathlib.Path) -> None:
344 root = _init_repo(tmp_path)
345 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
346 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
347 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
348 n2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "extra.py": b"e=9\n"}, branch="new", parent_id=n1)
349 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
350 data = json.loads(result.stdout)
351 statuses = [p["status"] for p in data["pairs"]]
352 assert "added" in statuses
353 assert "equivalent" in statuses
354
355 def test_commit_dropped(self, tmp_path: pathlib.Path) -> None:
356 root = _init_repo(tmp_path)
357 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
358 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="add a")
359 c2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=c1, message="add b")
360 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="new", parent_id=base, message="add a and b squashed")
361 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
362 data = json.loads(result.stdout)
363 statuses = {p["status"] for p in data["pairs"]}
364 assert "dropped" in statuses or "changed" in statuses
365
366 def test_exit_zero_when_equivalent(self, tmp_path: pathlib.Path) -> None:
367 root = _init_repo(tmp_path)
368 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
369 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
370 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
371 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
372 assert result.exit_code == 0
373
374 def test_exit_nonzero_when_differs(self, tmp_path: pathlib.Path) -> None:
375 root = _init_repo(tmp_path)
376 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
377 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base)
378 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base)
379 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
380 assert result.exit_code != 0
381
382
383 # ---------------------------------------------------------------------------
384 # Integration — JSON schema supercharge (RED tests)
385 # ---------------------------------------------------------------------------
386
387
388 class TestJsonSchema:
389 def test_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
390 """RED: duration_ms must be present in JSON output."""
391 root = _init_repo(tmp_path)
392 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
393 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
394 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
395 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
396 data = json.loads(result.stdout)
397 assert "duration_ms" in data, "duration_ms missing from JSON"
398 assert isinstance(data["duration_ms"], (int, float))
399 assert data["duration_ms"] >= 0
400
401 def test_has_exit_code(self, tmp_path: pathlib.Path) -> None:
402 """RED: exit_code must be present in JSON output."""
403 root = _init_repo(tmp_path)
404 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
405 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
406 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
407 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
408 data = json.loads(result.stdout)
409 assert "exit_code" in data, "exit_code missing from JSON"
410 assert data["exit_code"] == 0
411
412 def test_exit_code_reflects_differences(self, tmp_path: pathlib.Path) -> None:
413 """exit_code in JSON must be 1 when series differ."""
414 root = _init_repo(tmp_path)
415 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
416 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base)
417 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base)
418 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
419 data = json.loads(result.stdout)
420 assert "exit_code" in data
421 assert data["exit_code"] == 1
422
423 def test_has_old_count(self, tmp_path: pathlib.Path) -> None:
424 """RED: old_count must reflect the number of commits in the old range."""
425 root = _init_repo(tmp_path)
426 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
427 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
428 c2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=c1)
429 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
430 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
431 data = json.loads(result.stdout)
432 assert "old_count" in data, "old_count missing from JSON"
433 assert data["old_count"] == 2
434
435 def test_has_new_count(self, tmp_path: pathlib.Path) -> None:
436 """RED: new_count must reflect the number of commits in the new range."""
437 root = _init_repo(tmp_path)
438 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
439 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
440 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
441 n2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "c.py": b"c=3\n"}, branch="new", parent_id=n1)
442 n3 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "c.py": b"c=3\n", "d.py": b"d=4\n"}, branch="new", parent_id=n2)
443 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
444 data = json.loads(result.stdout)
445 assert "new_count" in data, "new_count missing from JSON"
446 assert data["new_count"] == 3
447
448 def test_has_stable_field(self, tmp_path: pathlib.Path) -> None:
449 """RED: stable must appear in JSON output reflecting the --stable flag."""
450 root = _init_repo(tmp_path)
451 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
452 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
453 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
454 result = _invoke(root, f"{base}..old", f"{base}..new", "--json", "--stable")
455 data = json.loads(result.stdout)
456 assert "stable" in data, "stable missing from JSON"
457 assert data["stable"] is True
458
459 def test_stable_false_by_default(self, tmp_path: pathlib.Path) -> None:
460 root = _init_repo(tmp_path)
461 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
462 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
463 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
464 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
465 data = json.loads(result.stdout)
466 assert "stable" in data
467 assert data["stable"] is False
468
469 def test_has_creation_factor(self, tmp_path: pathlib.Path) -> None:
470 """RED: creation_factor must appear in JSON, reflecting the value used."""
471 root = _init_repo(tmp_path)
472 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
473 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
474 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
475 result = _invoke(root, f"{base}..old", f"{base}..new", "--json", "--creation-factor", "0.3")
476 data = json.loads(result.stdout)
477 assert "creation_factor" in data, "creation_factor missing from JSON"
478 assert abs(data["creation_factor"] - 0.3) < 0.01
479
480 def test_patch_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
481 """RED: patch_id in pair commit info must be sha256:-prefixed."""
482 root = _init_repo(tmp_path)
483 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
484 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="add a")
485 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base, message="add a")
486 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
487 data = json.loads(result.stdout)
488 for pair in data["pairs"]:
489 for side in ("old", "new"):
490 info = pair.get(side)
491 if info is not None:
492 assert info["patch_id"].startswith("sha256:"), (
493 f"pair[{side}].patch_id lacks sha256: prefix: {info['patch_id'][:20]!r}"
494 )
495
496 def test_pair_has_files_changed(self, tmp_path: pathlib.Path) -> None:
497 """RED: each pair commit info must include files_changed count."""
498 root = _init_repo(tmp_path)
499 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
500 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=base)
501 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="new", parent_id=base)
502 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
503 data = json.loads(result.stdout)
504 for pair in data["pairs"]:
505 for side in ("old", "new"):
506 info = pair.get(side)
507 if info is not None:
508 assert "files_changed" in info, (
509 f"pair[{side}] missing files_changed: {info}"
510 )
511 assert isinstance(info["files_changed"], int)
512 assert info["files_changed"] >= 0
513
514 def test_old_count_new_count_match_pairs(self, tmp_path: pathlib.Path) -> None:
515 """old_count + new_count must be consistent with actual range walks."""
516 root = _init_repo(tmp_path)
517 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
518 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
519 c2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=c1)
520 c3 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n", "c.py": b"c=3\n"}, branch="old", parent_id=c2)
521 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
522 n2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="new", parent_id=n1)
523 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
524 data = json.loads(result.stdout)
525 assert data["old_count"] == 3
526 assert data["new_count"] == 2
527
528 def test_complete_schema_keys(self, tmp_path: pathlib.Path) -> None:
529 root = _init_repo(tmp_path)
530 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
531 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
532 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
533 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
534 data = json.loads(result.stdout)
535 for key in ("old_range", "new_range", "trivially_equivalent",
536 "old_count", "new_count", "stable", "creation_factor",
537 "pairs", "duration_ms", "exit_code"):
538 assert key in data, f"key {key!r} missing from JSON output"
539
540
541 # ---------------------------------------------------------------------------
542 # Integration — creation-factor
543 # ---------------------------------------------------------------------------
544
545
546 class TestCreationFactor:
547 def test_zero_no_fuzzy_pairing(self, tmp_path: pathlib.Path) -> None:
548 root = _init_repo(tmp_path)
549 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
550 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base)
551 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base)
552 result = _invoke(root, f"{base}..old", f"{base}..new", "--creation-factor", "0.0", "--json")
553 data = json.loads(result.stdout)
554 statuses = {p["status"] for p in data["pairs"]}
555 assert "changed" not in statuses
556 assert "dropped" in statuses or "added" in statuses
557
558 def test_one_all_positionally_paired(self, tmp_path: pathlib.Path) -> None:
559 root = _init_repo(tmp_path)
560 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
561 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base)
562 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base)
563 result = _invoke(root, f"{base}..old", f"{base}..new", "--creation-factor", "1.0", "--json")
564 data = json.loads(result.stdout)
565 statuses = {p["status"] for p in data["pairs"]}
566 assert "changed" in statuses
567
568 def test_creation_factor_clamped_to_range(self, tmp_path: pathlib.Path) -> None:
569 """creation_factor in JSON must be clamped to [0.0, 1.0]."""
570 root = _init_repo(tmp_path)
571 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
572 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
573 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
574 result = _invoke(root, f"{base}..old", f"{base}..new", "--creation-factor", "99.0", "--json")
575 data = json.loads(result.stdout)
576 assert data["creation_factor"] <= 1.0
577
578
579 # ---------------------------------------------------------------------------
580 # Integration — text output
581 # ---------------------------------------------------------------------------
582
583
584 class TestTextOutput:
585 def test_equivalent_shows_equals_symbol(self, tmp_path: pathlib.Path) -> None:
586 root = _init_repo(tmp_path)
587 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
588 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="add a")
589 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base, message="add a")
590 result = _invoke(root, f"{base}..old", f"{base}..new")
591 assert result.exit_code == 0
592 assert "=" in result.stdout
593
594 def test_text_short_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
595 """Short IDs in pair lines must be ``sha256:<8 hex chars>`` — prefix is canonical."""
596 root = _init_repo(tmp_path)
597 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
598 _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="add a")
599 _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base, message="add a")
600 result = _invoke(root, f"{base}..old", f"{base}..new")
601 # Only inspect pair lines (start with =, !, <, >)
602 pair_lines = [l for l in result.stdout.splitlines() if l and l[0] in "=!<>"]
603 assert pair_lines, "No pair lines in text output"
604 sha256_short = re.compile(r"^sha256:[0-9a-f]{8}$")
605 found = [tok for line in pair_lines for tok in line.split() if sha256_short.match(tok)]
606 assert found, (
607 f"No sha256:<8-hex> short IDs found in pair lines.\n"
608 f"Pair lines: {pair_lines}"
609 )
610
611 def test_text_short_ids_are_sha256_plus_8_hex(self, tmp_path: pathlib.Path) -> None:
612 """Short IDs must be sha256: + exactly 8 lowercase hex chars (15 total)."""
613 root = _init_repo(tmp_path)
614 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
615 _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base, message="changed commit")
616 _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base, message="changed commit")
617 result = _invoke(root, f"{base}..old", f"{base}..new")
618 sha256_short = re.compile(r"^sha256:[0-9a-f]{8}$")
619 found = []
620 for line in result.stdout.splitlines():
621 for token in line.split():
622 if sha256_short.match(token):
623 found.append(token)
624 assert found, f"No sha256:<8-hex> tokens found in output:\n{result.stdout}"
625 for tok in found:
626 assert len(tok) == 15, f"Expected 15 chars (sha256: + 8 hex), got {len(tok)}: {tok!r}"
627
628 def test_changed_shows_exclamation_symbol(self, tmp_path: pathlib.Path) -> None:
629 root = _init_repo(tmp_path)
630 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
631 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v1\n"}, branch="old", parent_id=base, message="add a v1")
632 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"v2\n"}, branch="new", parent_id=base, message="add a v2")
633 result = _invoke(root, f"{base}..old", f"{base}..new")
634 assert result.exit_code != 0
635 assert "!" in result.stdout
636
637 def test_added_shows_gt_symbol(self, tmp_path: pathlib.Path) -> None:
638 root = _init_repo(tmp_path)
639 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
640 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
641 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
642 n2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "x.py": b"x\n"}, branch="new", parent_id=n1)
643 result = _invoke(root, f"{base}..old", f"{base}..new")
644 assert ">" in result.stdout
645
646 def test_dropped_shows_lt_symbol(self, tmp_path: pathlib.Path) -> None:
647 root = _init_repo(tmp_path)
648 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
649 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
650 c2 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="old", parent_id=c1)
651 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n"}, branch="new", parent_id=base, message="squashed")
652 result = _invoke(root, f"{base}..old", f"{base}..new", "--creation-factor", "0.0")
653 assert "<" in result.stdout
654
655
656 # ---------------------------------------------------------------------------
657 # Integration — error cases
658 # ---------------------------------------------------------------------------
659
660
661 class TestErrors:
662 def test_nonexistent_old_ref(self, tmp_path: pathlib.Path) -> None:
663 root = _init_repo(tmp_path)
664 _commit(root, {"a.py": b"x\n"}, branch="main")
665 result = _invoke(root, "ghost..no-such", "main..main")
666 assert result.exit_code != 0
667
668 def test_nonexistent_new_ref(self, tmp_path: pathlib.Path) -> None:
669 root = _init_repo(tmp_path)
670 base = _commit(root, {"a.py": b"x\n"}, branch="main")
671 result = _invoke(root, f"{base}..main", "ghost..no-such")
672 assert result.exit_code != 0
673
674
675 # ---------------------------------------------------------------------------
676 # Data integrity
677 # ---------------------------------------------------------------------------
678
679
680 class TestDataIntegrity:
681 def test_files_changed_accurate(self, tmp_path: pathlib.Path) -> None:
682 """files_changed on a pair entry must match the actual diff size."""
683 root = _init_repo(tmp_path)
684 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
685 # commit changes exactly 3 files relative to parent
686 c1 = _commit(root, {
687 "readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n", "c.py": b"c=3\n"
688 }, branch="old", parent_id=base)
689 n1 = _commit(root, {
690 "readme.txt": b"base\n", "a.py": b"a=1\n", "b.py": b"b=2\n", "c.py": b"c=3\n"
691 }, branch="new", parent_id=base)
692 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
693 data = json.loads(result.stdout)
694 pair = data["pairs"][0]
695 # 3 files added relative to base
696 assert pair["old"]["files_changed"] == 3
697 assert pair["new"]["files_changed"] == 3
698
699 def test_old_count_zero_when_old_range_empty(self, tmp_path: pathlib.Path) -> None:
700 root = _init_repo(tmp_path)
701 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
702 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
703 result = _invoke(root, f"{base}..{base}", f"{base}..new", "--json")
704 data = json.loads(result.stdout)
705 assert data["old_count"] == 0
706 assert data["new_count"] == 1
707
708 def test_commit_ids_are_sha256_prefixed(self, tmp_path: pathlib.Path) -> None:
709 root = _init_repo(tmp_path)
710 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
711 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="add a")
712 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base, message="add a")
713 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
714 data = json.loads(result.stdout)
715 for pair in data["pairs"]:
716 for side in ("old", "new"):
717 info = pair.get(side)
718 if info is not None:
719 assert info["commit_id"].startswith("sha256:"), (
720 f"commit_id lacks sha256: prefix: {info['commit_id']!r}"
721 )
722
723 def test_pair_subject_matches_commit_message(self, tmp_path: pathlib.Path) -> None:
724 root = _init_repo(tmp_path)
725 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
726 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base, message="feat: add alpha")
727 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base, message="feat: add alpha")
728 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
729 data = json.loads(result.stdout)
730 pair = data["pairs"][0]
731 assert pair["old"]["subject"] == "feat: add alpha"
732 assert pair["new"]["subject"] == "feat: add alpha"
733
734 def test_duration_ms_is_plausible(self, tmp_path: pathlib.Path) -> None:
735 root = _init_repo(tmp_path)
736 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
737 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
738 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
739 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
740 data = json.loads(result.stdout)
741 assert 0 <= data["duration_ms"] < 10_000
742
743
744 # ---------------------------------------------------------------------------
745 # Security
746 # ---------------------------------------------------------------------------
747
748
749 class TestSecurity:
750 def test_ansi_in_old_range_rejected(self, tmp_path: pathlib.Path) -> None:
751 root = _init_repo(tmp_path)
752 result = _invoke(root, "\x1b[31mbad\x1b[0m..main", "main..main")
753 assert result.exit_code != 0
754
755 def test_ansi_in_new_range_rejected(self, tmp_path: pathlib.Path) -> None:
756 root = _init_repo(tmp_path)
757 result = _invoke(root, "main..main", "\x1b[31mbad\x1b[0m..main")
758 assert result.exit_code != 0
759
760 def test_control_char_in_range_rejected(self, tmp_path: pathlib.Path) -> None:
761 root = _init_repo(tmp_path)
762 result = _invoke(root, "main\x00trick..main", "main..main")
763 assert result.exit_code != 0
764
765 def test_no_ansi_in_json_output(self, tmp_path: pathlib.Path) -> None:
766 root = _init_repo(tmp_path)
767 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
768 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
769 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
770 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
771 assert "\x1b[" not in result.stdout
772
773 def test_no_ansi_in_text_output(self, tmp_path: pathlib.Path) -> None:
774 root = _init_repo(tmp_path)
775 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
776 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
777 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
778 result = _invoke(root, f"{base}..old", f"{base}..new")
779 assert "\x1b[" not in result.stdout
780
781
782 # ---------------------------------------------------------------------------
783 # Performance
784 # ---------------------------------------------------------------------------
785
786
787 class TestPerformance:
788 def test_50_commit_equivalent_series_under_2_seconds(self, tmp_path: pathlib.Path) -> None:
789 root = _init_repo(tmp_path)
790 base = _commit(root, {"base.py": b"base\n"}, branch="main")
791 old_id = base
792 new_id = base
793 for i in range(50):
794 content = f"v = {i}\n".encode()
795 old_id = _commit(root, {f"f{i}.py": content}, branch="old", parent_id=old_id, message=f"add f{i}")
796 new_id = _commit(root, {f"f{i}.py": content}, branch="new", parent_id=new_id, message=f"add f{i}")
797 t0 = time.monotonic()
798 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
799 elapsed = time.monotonic() - t0
800 assert result.exit_code == 0
801 assert elapsed < 2.0, f"range-diff took {elapsed:.2f}s — expected < 2s"
802 data = json.loads(result.stdout)
803 assert data["trivially_equivalent"] is True
804 assert len(data["pairs"]) == 50
805
806 def test_duration_ms_under_threshold(self, tmp_path: pathlib.Path) -> None:
807 root = _init_repo(tmp_path)
808 base = _commit(root, {"readme.txt": b"base\n"}, branch="main")
809 c1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="old", parent_id=base)
810 n1 = _commit(root, {"readme.txt": b"base\n", "a.py": b"a=1\n"}, branch="new", parent_id=base)
811 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
812 data = json.loads(result.stdout)
813 assert data["duration_ms"] < 2_000
814
815
816 # ---------------------------------------------------------------------------
817 # Stress
818 # ---------------------------------------------------------------------------
819
820
821 class TestStress:
822 def test_50_commits_all_equivalent(self, tmp_path: pathlib.Path) -> None:
823 root = _init_repo(tmp_path)
824 base = _commit(root, {"base.py": b"base\n"}, branch="main")
825 old_id = base
826 new_id = base
827 for i in range(50):
828 content = f"v = {i}\n".encode()
829 old_id = _commit(root, {f"f{i}.py": content}, branch="old", parent_id=old_id, message=f"add f{i}")
830 new_id = _commit(root, {f"f{i}.py": content}, branch="new", parent_id=new_id, message=f"add f{i}")
831 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
832 assert result.exit_code == 0
833 data = json.loads(result.stdout)
834 assert data["trivially_equivalent"] is True
835 assert len(data["pairs"]) == 50
836 assert data["old_count"] == 50
837 assert data["new_count"] == 50
838
839 def test_50_commits_mixed(self, tmp_path: pathlib.Path) -> None:
840 root = _init_repo(tmp_path)
841 base = _commit(root, {"base.py": b"base\n"}, branch="main")
842 old_id = base
843 new_id = base
844 # First 25: identical
845 for i in range(25):
846 content = f"v = {i}\n".encode()
847 old_id = _commit(root, {f"f{i}.py": content}, branch="old", parent_id=old_id)
848 new_id = _commit(root, {f"f{i}.py": content}, branch="new", parent_id=new_id)
849 # Next 25: different content
850 for i in range(25, 50):
851 old_id = _commit(root, {f"f{i}.py": f"old_{i}\n".encode()}, branch="old", parent_id=old_id)
852 new_id = _commit(root, {f"f{i}.py": f"new_{i}\n".encode()}, branch="new", parent_id=new_id)
853 # New has 10 extra commits
854 for i in range(50, 60):
855 new_id = _commit(root, {f"extra{i}.py": b"extra\n"}, branch="new", parent_id=new_id)
856 result = _invoke(root, f"{base}..old", f"{base}..new", "--json")
857 assert result.exit_code != 0
858 data = json.loads(result.stdout)
859 equivalent = [p for p in data["pairs"] if p["status"] == "equivalent"]
860 assert len(equivalent) == 25
861 added = [p for p in data["pairs"] if p["status"] == "added"]
862 assert len(added) == 10
863 assert data["old_count"] == 50
864 assert data["new_count"] == 60
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago