gabriel / muse public
test_cmd_name_rev.py python
696 lines 26.1 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago
1 """Tests for muse name-rev.
2
3 Coverage tiers
4 --------------
5 Unit — _build_name_map (flat, hierarchical, symlink skip, bad ref ID,
6 branch_pattern filter, max_walk ceiling),
7 _resolve_prefix (exact, short prefix, ambiguous, no-match),
8 _NameRevEntry schema
9 Integration — tip commit, parent chain, multiple IDs, undefined commit,
10 text output, --name-only, --undefined string,
11 --branches filter, --stdin, short-prefix resolution,
12 --max-walk, --json shorthand, ambiguous prefix
13 Security — ANSI in branch name sanitized, non-hex input rejected,
14 error output to stderr (format, no-args, max-walk, non-hex),
15 no traceback, symlinks skipped, --undefined value sanitized
16 Stress — 50-commit chain, 10-branch repo, 200 sequential calls,
17 stdin with 50 commit IDs
18 """
19
20 from __future__ import annotations
21
22 type _CommitInfoMap = dict[str, tuple[str, int]]
23
24 import datetime
25 import hashlib
26 import io
27 import json
28 import pathlib
29
30 from tests.cli_test_helper import CliRunner, InvokeResult
31
32 from muse.cli.commands.name_rev import (
33 _NameRevEntry,
34 _build_name_map,
35 _resolve_prefix,
36 )
37 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
38 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
39 from muse.core._types import Manifest, long_id
40
41 cli = None # argparse-based CLI; CliRunner ignores this arg
42 runner = CliRunner()
43
44
45 # ---------------------------------------------------------------------------
46 # Helpers
47 # ---------------------------------------------------------------------------
48
49
50 def _sha(tag: str) -> str:
51 return hashlib.sha256(tag.encode()).hexdigest()
52
53
54 def _init_repo(path: pathlib.Path) -> pathlib.Path:
55 muse = path / ".muse"
56 (muse / "commits").mkdir(parents=True)
57 (muse / "snapshots").mkdir(parents=True)
58 (muse / "objects").mkdir(parents=True)
59 (muse / "refs" / "heads").mkdir(parents=True)
60 (muse / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
61 (muse / "repo.json").write_text(
62 json.dumps({"repo_id": "test-repo", "domain": "midi"}), encoding="utf-8"
63 )
64 return path
65
66
67 def _env(repo: pathlib.Path) -> Manifest:
68 return {"MUSE_REPO_ROOT": str(repo)}
69
70
71 def _snap(repo: pathlib.Path, tag: str) -> str:
72 sid = compute_snapshot_id({})
73 write_snapshot(
74 repo,
75 SnapshotRecord(
76 snapshot_id=sid,
77 manifest={},
78 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
79 ),
80 )
81 return sid
82
83
84 def _commit(
85 repo: pathlib.Path,
86 tag: str,
87 branch: str = "main",
88 parent: str | None = None,
89 ) -> str:
90 sid = _snap(repo, tag)
91 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
92 parent_ids: list[str] = [parent] if parent else []
93 cid = compute_commit_id(parent_ids, sid, tag, committed_at.isoformat())
94 write_commit(
95 repo,
96 CommitRecord(
97 commit_id=cid,
98 repo_id="test-repo",
99 branch=branch,
100 snapshot_id=sid,
101 message=tag,
102 committed_at=committed_at,
103 author="tester",
104 parent_commit_id=parent,
105 parent2_commit_id=None,
106 ),
107 )
108 ref_path = repo / ".muse" / "refs" / "heads" / branch
109 ref_path.parent.mkdir(parents=True, exist_ok=True)
110 ref_path.write_text(cid, encoding="utf-8")
111 return cid
112
113
114 # Two messages whose content-addressed commit IDs share the 4-char prefix "9f7c".
115 # Verified: compute_commit_id([], compute_snapshot_id({}), msg, "2026-01-01T00:00:00+00:00")
116 _AMBIG_MSG_1 = "commit-search-165" # -> 9f7cc16c...
117 _AMBIG_MSG_2 = "commit-search-106" # -> 9f7c932b...
118 _AMBIG_PREFIX = "9f7c"
119
120
121 def _nr(repo: pathlib.Path, *args: str, stdin: str | None = None) -> InvokeResult:
122 return runner.invoke(
123 cli,
124 ["name-rev", *args],
125 env=_env(repo),
126 input=stdin,
127 )
128
129
130 # ---------------------------------------------------------------------------
131 # Unit — _NameRevEntry schema
132 # ---------------------------------------------------------------------------
133
134
135 class TestNameRevEntrySchema:
136 def test_required_fields(self) -> None:
137 keys = _NameRevEntry.__annotations__
138 for f in ("commit_id", "input", "name", "branch", "distance", "undefined", "ambiguous"):
139 assert f in keys
140
141 def test_input_field_present(self) -> None:
142 """New 'input' field tracks the original caller-supplied value."""
143 assert "input" in _NameRevEntry.__annotations__
144
145 def test_ambiguous_field_present(self) -> None:
146 assert "ambiguous" in _NameRevEntry.__annotations__
147
148
149 # ---------------------------------------------------------------------------
150 # Unit — _resolve_prefix
151 # ---------------------------------------------------------------------------
152
153
154 class TestResolvePrefix:
155 def _map(self) -> _CommitInfoMap:
156 # Keys must be sha256:-prefixed — that is how the BFS name_map stores them.
157 return {
158 long_id("abcd1234" + "0" * 56): ("main", 0),
159 long_id("abcd5678" + "0" * 56): ("dev", 1),
160 long_id("ffff0000" + "0" * 56): ("feat", 2),
161 }
162
163 def test_exact_match(self) -> None:
164 m = self._map()
165 k = long_id("abcd1234" + "0" * 56)
166 full_id, ambiguous = _resolve_prefix(k, m)
167 assert full_id == k
168 assert not ambiguous
169
170 def test_short_prefix_unique(self) -> None:
171 m = self._map()
172 # Bare hex prefix — _resolve_prefix normalises to sha256: internally.
173 full_id, ambiguous = _resolve_prefix("abcd1234", m)
174 assert full_id == long_id("abcd1234" + "0" * 56)
175 assert not ambiguous
176
177 def test_short_prefix_ambiguous(self) -> None:
178 m = self._map()
179 # "abcd" matches both "sha256:abcd1234..." and "sha256:abcd5678..."
180 full_id, ambiguous = _resolve_prefix("abcd", m)
181 assert full_id is None
182 assert ambiguous
183
184 def test_no_match_returns_none(self) -> None:
185 m = self._map()
186 full_id, ambiguous = _resolve_prefix("deadbeef", m)
187 assert full_id is None
188 assert not ambiguous
189
190 def test_empty_map(self) -> None:
191 full_id, ambiguous = _resolve_prefix("abc123", {})
192 assert full_id is None
193 assert not ambiguous
194
195
196 # ---------------------------------------------------------------------------
197 # Unit — _build_name_map
198 # ---------------------------------------------------------------------------
199
200
201 class TestBuildNameMap:
202 def test_empty_heads_dir(self, tmp_path: pathlib.Path) -> None:
203 _init_repo(tmp_path)
204 assert _build_name_map(tmp_path, set()) == {}
205
206 def test_tip_commit_distance_zero(self, tmp_path: pathlib.Path) -> None:
207 _init_repo(tmp_path)
208 cid = _commit(tmp_path, "c1")
209 m = _build_name_map(tmp_path, {cid})
210 assert cid in m
211 assert m[cid] == ("main", 0)
212
213 def test_parent_commit_distance_one(self, tmp_path: pathlib.Path) -> None:
214 _init_repo(tmp_path)
215 c1 = _commit(tmp_path, "c1")
216 c2 = _commit(tmp_path, "c2", parent=c1)
217 m = _build_name_map(tmp_path, {c1, c2})
218 assert m[c1][1] == 1 # distance from tip (c2)
219 assert m[c2][1] == 0 # tip itself
220
221 def test_hierarchical_branch_discovered(self, tmp_path: pathlib.Path) -> None:
222 _init_repo(tmp_path)
223 _commit(tmp_path, "main-c", "main")
224 cid = _commit(tmp_path, "feat-c", "feat/my-thing")
225 m = _build_name_map(tmp_path, {cid})
226 assert cid in m
227 assert m[cid][0] == "feat/my-thing"
228
229 def test_symlink_ref_skipped(self, tmp_path: pathlib.Path) -> None:
230 _init_repo(tmp_path)
231 cid = _commit(tmp_path, "c", "main")
232 real = tmp_path / ".muse" / "refs" / "heads" / "main"
233 link = tmp_path / ".muse" / "refs" / "heads" / "sym"
234 link.symlink_to(real)
235 m = _build_name_map(tmp_path, {cid})
236 # The commit should be reachable via main, not via the symlink.
237 assert cid in m
238 assert m[cid][0] == "main" # not "sym"
239
240 def test_invalid_ref_id_skipped(self, tmp_path: pathlib.Path) -> None:
241 _init_repo(tmp_path)
242 cid = _commit(tmp_path, "c", "main")
243 bad = tmp_path / ".muse" / "refs" / "heads" / "bad"
244 bad.write_text("not-a-sha\n", encoding="utf-8")
245 m = _build_name_map(tmp_path, {cid})
246 assert cid in m # main still seeded
247
248 def test_branch_pattern_filter(self, tmp_path: pathlib.Path) -> None:
249 _init_repo(tmp_path)
250 c_main = _commit(tmp_path, "c-main", "main")
251 c_feat = _commit(tmp_path, "c-feat", "feat/x")
252 m = _build_name_map(tmp_path, {c_main, c_feat}, branch_pattern="main")
253 # feat/x commit should be unreachable (only main seeded)
254 assert c_main in m
255 assert c_feat not in m
256
257 def test_max_walk_ceiling(self, tmp_path: pathlib.Path) -> None:
258 """BFS stops after max_walk steps; deep commits may remain unmapped."""
259 _init_repo(tmp_path)
260 parent: str | None = None
261 first: str | None = None
262 for i in range(10):
263 cid = _commit(tmp_path, f"c{i}", parent=parent)
264 if first is None:
265 first = cid
266 parent = cid
267 assert first is not None
268 # max_walk=1 means only the tip is visited; grandparent must be unmapped
269 m = _build_name_map(tmp_path, {first}, max_walk=1)
270 assert first not in m # too deep to reach
271
272 def test_early_exit_when_all_targets_found(self, tmp_path: pathlib.Path) -> None:
273 """When both targets are at tips, BFS should stop without full traversal."""
274 _init_repo(tmp_path)
275 c1 = _commit(tmp_path, "c1", "main")
276 c2 = _commit(tmp_path, "c2", "dev")
277 m = _build_name_map(tmp_path, {c1, c2})
278 assert c1 in m
279 assert c2 in m
280
281
282 # ---------------------------------------------------------------------------
283 # Integration — basic resolution
284 # ---------------------------------------------------------------------------
285
286
287 class TestResolution:
288 def test_tip_commit_distance_zero(self, tmp_path: pathlib.Path) -> None:
289 _init_repo(tmp_path)
290 cid = _commit(tmp_path, "c1")
291 r = _nr(tmp_path, cid)
292 assert r.exit_code == 0
293 entry = json.loads(r.output)["results"][0]
294 assert entry["commit_id"] == cid
295 assert entry["distance"] == 0
296 assert entry["undefined"] is False
297 assert entry["ambiguous"] is False
298 assert entry["name"] == "main"
299
300 def test_parent_commit_named_tilde_one(self, tmp_path: pathlib.Path) -> None:
301 _init_repo(tmp_path)
302 c1 = _commit(tmp_path, "c1")
303 _commit(tmp_path, "c2", parent=c1)
304 r = _nr(tmp_path, c1)
305 entry = json.loads(r.output)["results"][0]
306 assert entry["distance"] == 1
307 assert entry["name"] == "main~1"
308
309 def test_grandparent_named_tilde_two(self, tmp_path: pathlib.Path) -> None:
310 _init_repo(tmp_path)
311 c1 = _commit(tmp_path, "grandparent")
312 c2 = _commit(tmp_path, "parent", parent=c1)
313 _commit(tmp_path, "tip", parent=c2)
314 r = _nr(tmp_path, c1)
315 entry = json.loads(r.output)["results"][0]
316 assert entry["distance"] == 2
317 assert "~2" in entry["name"]
318
319 def test_undefined_commit(self, tmp_path: pathlib.Path) -> None:
320 _init_repo(tmp_path)
321 _commit(tmp_path, "c1")
322 fake_id = "a" * 64
323 r = _nr(tmp_path, fake_id)
324 entry = json.loads(r.output)["results"][0]
325 assert entry["undefined"] is True
326 assert entry["name"] is None
327 assert entry["commit_id"] is None
328
329 def test_multiple_commit_ids(self, tmp_path: pathlib.Path) -> None:
330 _init_repo(tmp_path)
331 c1 = _commit(tmp_path, "c1")
332 c2 = _commit(tmp_path, "c2", parent=c1)
333 r = _nr(tmp_path, c1, c2)
334 data = json.loads(r.output)
335 assert len(data["results"]) == 2
336
337 def test_input_field_preserves_original(self, tmp_path: pathlib.Path) -> None:
338 _init_repo(tmp_path)
339 cid = _commit(tmp_path, "c1")
340 short = cid[:10]
341 r = _nr(tmp_path, short)
342 entry = json.loads(r.output)["results"][0]
343 assert entry["input"] == short
344 assert entry["commit_id"] == cid # resolved to full
345
346
347 # ---------------------------------------------------------------------------
348 # Integration — short prefix resolution
349 # ---------------------------------------------------------------------------
350
351
352 class TestPrefixResolution:
353 def test_short_prefix_resolves(self, tmp_path: pathlib.Path) -> None:
354 _init_repo(tmp_path)
355 cid = _commit(tmp_path, "c1")
356 r = _nr(tmp_path, cid[:8])
357 assert r.exit_code == 0
358 entry = json.loads(r.output)["results"][0]
359 assert entry["commit_id"] == cid
360 assert entry["undefined"] is False
361
362 def test_four_char_prefix_resolves(self, tmp_path: pathlib.Path) -> None:
363 _init_repo(tmp_path)
364 cid = _commit(tmp_path, "c1")
365 # Extract 4 hex chars from the sha256:-prefixed ID (skip the "sha256:" prefix).
366 hex_prefix = cid[len("sha256:"):len("sha256:") + 4]
367 r = _nr(tmp_path, hex_prefix)
368 assert r.exit_code == 0
369 entry = json.loads(r.output)["results"][0]
370 # Might be undefined if prefix is too ambiguous in the BFS map
371 # but the input field is always preserved
372 assert entry["input"] == hex_prefix
373
374 def test_ambiguous_prefix_marked(self, tmp_path: pathlib.Path) -> None:
375 """Two commits sharing a 4-char hex prefix → ambiguous result, not undefined."""
376 _init_repo(tmp_path)
377 # _AMBIG_MSG_1 and _AMBIG_MSG_2 produce commit IDs whose hex portions share _AMBIG_PREFIX.
378 cid1 = _commit(tmp_path, _AMBIG_MSG_1, "main")
379 cid2 = _commit(tmp_path, _AMBIG_MSG_2, "dev")
380 hex1 = cid1[len("sha256:"):len("sha256:") + 4]
381 hex2 = cid2[len("sha256:"):len("sha256:") + 4]
382 assert hex1 == hex2 == _AMBIG_PREFIX, "pre-computed prefix mismatch"
383 r = _nr(tmp_path, _AMBIG_PREFIX)
384 assert r.exit_code == 0
385 entry = json.loads(r.output)["results"][0]
386 assert entry["ambiguous"] is True
387
388 def test_non_hex_input_rejected(self, tmp_path: pathlib.Path) -> None:
389 # Default format is json → error goes to stdout as JSON, stderr is empty.
390 _init_repo(tmp_path)
391 r = _nr(tmp_path, "not-hex-at-all!")
392 assert r.exit_code != 0
393 assert not r.stderr.strip()
394 assert json.loads(r.output)["status"] == "error"
395
396
397 # ---------------------------------------------------------------------------
398 # Integration — --branches filter
399 # ---------------------------------------------------------------------------
400
401
402 class TestBranchesFilter:
403 def test_branches_filter_restricts_bfs(self, tmp_path: pathlib.Path) -> None:
404 _init_repo(tmp_path)
405 c_main = _commit(tmp_path, "c-main", "main")
406 c_dev = _commit(tmp_path, "c-dev", "dev")
407 # With --branches main, c_dev should be undefined
408 r = _nr(tmp_path, c_dev, "--branches", "main")
409 assert r.exit_code == 0
410 entry = json.loads(r.output)["results"][0]
411 assert entry["undefined"] is True
412
413 def test_branches_filter_finds_matching(self, tmp_path: pathlib.Path) -> None:
414 _init_repo(tmp_path)
415 c_main = _commit(tmp_path, "c-main", "main")
416 c_dev = _commit(tmp_path, "c-dev", "dev")
417 r = _nr(tmp_path, c_main, "--branches", "main")
418 assert r.exit_code == 0
419 entry = json.loads(r.output)["results"][0]
420 assert entry["undefined"] is False
421 assert entry["branch"] == "main"
422
423 def test_branches_glob_pattern(self, tmp_path: pathlib.Path) -> None:
424 _init_repo(tmp_path)
425 c_feat1 = _commit(tmp_path, "c-feat1", "feat/one")
426 c_feat2 = _commit(tmp_path, "c-feat2", "feat/two")
427 c_main = _commit(tmp_path, "c-main", "main")
428 # Only feat/* seeded; main commit should be undefined
429 r = _nr(tmp_path, c_main, "--branches", "feat/*")
430 entry = json.loads(r.output)["results"][0]
431 assert entry["undefined"] is True
432
433 def test_branches_filter_hierarchical(self, tmp_path: pathlib.Path) -> None:
434 _init_repo(tmp_path)
435 cid = _commit(tmp_path, "c-feat", "feat/my-thing")
436 r = _nr(tmp_path, cid, "--branches", "feat/*")
437 entry = json.loads(r.output)["results"][0]
438 assert entry["undefined"] is False
439 assert entry["branch"] == "feat/my-thing"
440
441
442 # ---------------------------------------------------------------------------
443 # Integration — --stdin
444 # ---------------------------------------------------------------------------
445
446
447 class TestStdinMode:
448 def test_stdin_reads_commit_ids(self, tmp_path: pathlib.Path) -> None:
449 _init_repo(tmp_path)
450 cid = _commit(tmp_path, "c1")
451 r = _nr(tmp_path, "--stdin", stdin=f"{cid}\n")
452 assert r.exit_code == 0
453 data = json.loads(r.output)
454 assert len(data["results"]) == 1
455 assert data["results"][0]["commit_id"] == cid
456
457 def test_stdin_skips_blank_lines(self, tmp_path: pathlib.Path) -> None:
458 _init_repo(tmp_path)
459 cid = _commit(tmp_path, "c1")
460 r = _nr(tmp_path, "--stdin", stdin=f"\n{cid}\n\n")
461 assert r.exit_code == 0
462 assert len(json.loads(r.output)["results"]) == 1
463
464 def test_stdin_skips_comments(self, tmp_path: pathlib.Path) -> None:
465 _init_repo(tmp_path)
466 cid = _commit(tmp_path, "c1")
467 r = _nr(tmp_path, "--stdin", stdin=f"# this is a comment\n{cid}\n")
468 assert r.exit_code == 0
469 assert len(json.loads(r.output)["results"]) == 1
470
471 def test_stdin_combined_with_positional(self, tmp_path: pathlib.Path) -> None:
472 _init_repo(tmp_path)
473 c1 = _commit(tmp_path, "c1", "main")
474 c2 = _commit(tmp_path, "c2", "dev")
475 r = _nr(tmp_path, c1, "--stdin", stdin=f"{c2}\n")
476 assert r.exit_code == 0
477 data = json.loads(r.output)
478 assert len(data["results"]) == 2
479
480 def test_stdin_empty_with_no_positional_errors(self, tmp_path: pathlib.Path) -> None:
481 _init_repo(tmp_path)
482 r = _nr(tmp_path, "--stdin", stdin="")
483 assert r.exit_code != 0
484 assert r.stdout_bytes == b""
485
486
487 # ---------------------------------------------------------------------------
488 # Integration — text output, --name-only, --undefined
489 # ---------------------------------------------------------------------------
490
491
492 class TestTextOutput:
493 def test_text_format_contains_cid(self, tmp_path: pathlib.Path) -> None:
494 _init_repo(tmp_path)
495 cid = _commit(tmp_path, "c1")
496 r = _nr(tmp_path, "--format", "text", cid)
497 assert r.exit_code == 0
498 assert cid in r.output
499
500 def test_name_only_omits_cid(self, tmp_path: pathlib.Path) -> None:
501 _init_repo(tmp_path)
502 cid = _commit(tmp_path, "c1")
503 r = _nr(tmp_path, "--format", "text", "--name-only", cid)
504 assert r.exit_code == 0
505 assert cid not in r.output
506 assert r.output.strip()
507
508 def test_custom_undefined_string(self, tmp_path: pathlib.Path) -> None:
509 _init_repo(tmp_path)
510 _commit(tmp_path, "c1")
511 fake = "b" * 64
512 r = _nr(tmp_path, "--format", "text", "--undefined", "UNKNOWN", fake)
513 assert r.exit_code == 0
514 assert "UNKNOWN" in r.output
515
516 def test_ambiguous_shown_in_text(self, tmp_path: pathlib.Path) -> None:
517 """Ambiguous prefix emits '(ambiguous)' in text output mode."""
518 _init_repo(tmp_path)
519 cid1 = _commit(tmp_path, _AMBIG_MSG_1, "main")
520 cid2 = _commit(tmp_path, _AMBIG_MSG_2, "dev")
521 hex1 = cid1[len("sha256:"):len("sha256:") + 4]
522 hex2 = cid2[len("sha256:"):len("sha256:") + 4]
523 assert hex1 == hex2 == _AMBIG_PREFIX, "pre-computed prefix mismatch"
524 r = _nr(tmp_path, "--format", "text", _AMBIG_PREFIX)
525 assert r.exit_code == 0
526 assert "ambiguous" in r.output.lower()
527
528
529 # ---------------------------------------------------------------------------
530 # Integration — --max-walk
531 # ---------------------------------------------------------------------------
532
533
534 class TestMaxWalk:
535 def test_max_walk_limits_bfs(self, tmp_path: pathlib.Path) -> None:
536 _init_repo(tmp_path)
537 parent: str | None = None
538 first: str | None = None
539 for i in range(8):
540 cid = _commit(tmp_path, f"c{i}", parent=parent)
541 if first is None:
542 first = cid
543 parent = cid
544 assert first is not None
545 r = _nr(tmp_path, first, "--max-walk", "1")
546 assert r.exit_code == 0
547 entry = json.loads(r.output)["results"][0]
548 # Deep commit unreachable with max_walk=1
549 assert entry["undefined"] is True
550
551 def test_max_walk_zero_errors(self, tmp_path: pathlib.Path) -> None:
552 _init_repo(tmp_path)
553 cid = _commit(tmp_path, "c1")
554 r = _nr(tmp_path, cid, "--max-walk", "0")
555 assert r.exit_code != 0
556 assert r.stdout_bytes == b""
557
558 def test_json_shorthand(self, tmp_path: pathlib.Path) -> None:
559 _init_repo(tmp_path)
560 cid = _commit(tmp_path, "c1")
561 r = _nr(tmp_path, "--json", cid)
562 assert r.exit_code == 0
563 data = json.loads(r.output)
564 assert "results" in data
565
566
567 # ---------------------------------------------------------------------------
568 # Security
569 # ---------------------------------------------------------------------------
570
571
572 class TestSecurity:
573 def test_non_hex_input_rejected(self, tmp_path: pathlib.Path) -> None:
574 # Default format is json → error goes to stdout as JSON, stderr is empty.
575 _init_repo(tmp_path)
576 r = _nr(tmp_path, "not-hex!")
577 assert r.exit_code != 0
578 assert not r.stderr.strip()
579 assert json.loads(r.output)["status"] == "error"
580
581 def test_ansi_in_branch_name_sanitized_text(self, tmp_path: pathlib.Path) -> None:
582 """Branch name from ref → sanitize_display applied before printing."""
583 _init_repo(tmp_path)
584 cid = _commit(tmp_path, "c1")
585 # Inject ANSI into the commit message (branch name from BFS result)
586 # We can't easily inject into the branch name; test via undefined output
587 # with an ANSI-containing --undefined value being sanitized
588 r = _nr(tmp_path, "--format", "text", "--undefined", "\x1b[31mred\x1b[0m", "a" * 64)
589 assert r.exit_code == 0
590 assert "\x1b" not in r.output
591
592 def test_format_error_goes_to_stderr(self, tmp_path: pathlib.Path) -> None:
593 # fmt="xml" is not "json" → _emit_error routes to stderr as prose text.
594 _init_repo(tmp_path)
595 cid = _commit(tmp_path, "c1")
596 r = _nr(tmp_path, "--format", "xml", cid)
597 assert r.exit_code != 0
598 assert r.stderr.strip() # non-empty stderr confirms it went to stderr
599
600 def test_no_args_error_goes_to_stderr(self, tmp_path: pathlib.Path) -> None:
601 _init_repo(tmp_path)
602 r = _nr(tmp_path)
603 assert r.exit_code != 0
604 assert r.stdout_bytes == b""
605
606 def test_no_traceback_on_bad_format(self, tmp_path: pathlib.Path) -> None:
607 _init_repo(tmp_path)
608 cid = _commit(tmp_path, "c1")
609 r = _nr(tmp_path, "--format", "bad", cid)
610 assert "Traceback" not in r.output
611 assert "Traceback" not in r.stderr
612
613 def test_no_traceback_on_non_hex(self, tmp_path: pathlib.Path) -> None:
614 _init_repo(tmp_path)
615 r = _nr(tmp_path, "xyz!!!")
616 assert "Traceback" not in r.output
617 assert "Traceback" not in r.stderr
618
619 def test_symlink_ref_skipped(self, tmp_path: pathlib.Path) -> None:
620 _init_repo(tmp_path)
621 cid = _commit(tmp_path, "c", "main")
622 real = tmp_path / ".muse" / "refs" / "heads" / "main"
623 link = tmp_path / ".muse" / "refs" / "heads" / "sym"
624 link.symlink_to(real)
625 r = _nr(tmp_path, cid)
626 assert r.exit_code == 0
627 entry = json.loads(r.output)["results"][0]
628 assert entry["branch"] == "main" # not "sym"
629
630 def test_no_repo_exits_cleanly(self, tmp_path: pathlib.Path) -> None:
631 r = runner.invoke(
632 cli,
633 ["name-rev", "a" * 64],
634 env={"MUSE_REPO_ROOT": str(tmp_path / "norepo")},
635 )
636 assert r.exit_code != 0
637 assert "Traceback" not in r.output
638 assert "Traceback" not in r.stderr
639
640
641 # ---------------------------------------------------------------------------
642 # Stress
643 # ---------------------------------------------------------------------------
644
645
646 class TestStress:
647 def test_50_commit_chain(self, tmp_path: pathlib.Path) -> None:
648 _init_repo(tmp_path)
649 parent: str | None = None
650 commits: list[str] = []
651 for i in range(50):
652 cid = _commit(tmp_path, f"c{i}", parent=parent)
653 commits.append(cid)
654 parent = cid
655 # Tip should be at distance 0; first at distance 49.
656 r = _nr(tmp_path, commits[-1], commits[0])
657 assert r.exit_code == 0
658 results = json.loads(r.output)["results"]
659 by_id = {e["commit_id"]: e for e in results}
660 assert by_id[commits[-1]]["distance"] == 0
661 assert by_id[commits[0]]["distance"] == 49
662
663 def test_10_branch_repo(self, tmp_path: pathlib.Path) -> None:
664 _init_repo(tmp_path)
665 tip_ids = []
666 for i in range(10):
667 cid = _commit(tmp_path, f"c-branch{i}", f"branch-{i:02d}")
668 tip_ids.append(cid)
669 r = _nr(tmp_path, *tip_ids)
670 assert r.exit_code == 0
671 data = json.loads(r.output)
672 assert len(data["results"]) == 10
673 for entry in data["results"]:
674 assert entry["distance"] == 0
675
676 def test_200_sequential_calls(self, tmp_path: pathlib.Path) -> None:
677 _init_repo(tmp_path)
678 cid = _commit(tmp_path, "c1")
679 for _ in range(200):
680 r = _nr(tmp_path, cid)
681 assert r.exit_code == 0
682 assert json.loads(r.output)["results"][0]["distance"] == 0
683
684 def test_stdin_50_commit_ids(self, tmp_path: pathlib.Path) -> None:
685 _init_repo(tmp_path)
686 parent: str | None = None
687 commits = []
688 for i in range(50):
689 cid = _commit(tmp_path, f"cs{i}", parent=parent)
690 commits.append(cid)
691 parent = cid
692 stdin_input = "\n".join(commits) + "\n"
693 r = _nr(tmp_path, "--stdin", stdin=stdin_input)
694 assert r.exit_code == 0
695 data = json.loads(r.output)
696 assert len(data["results"]) == 50
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 142 days ago