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