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