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