gabriel / muse public

test_range_diff_supercharge.py file-level

at sha256:c · View file ↗ · Intel ↗

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