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