gabriel / muse public
test_cmd_show.py python
731 lines 30.0 KB
Raw
sha256:1d3f5470f45db58e32047678debc9438fdded1b2c7332cc743d2b8be32fdafc8 fixing more broken tests Human patch 41 days ago
1 """Tests for ``muse show``.
2
3 Coverage tiers
4 --------------
5 Unit — parser flags, _format_op, dead-code removal.
6 Integration — commit display, --no-stat, --no-delta, --format, metadata.
7 End-to-end — CLI invocations: text and JSON output, HEAD, named ref.
8 Security — ANSI injection in ref, message, author, metadata.
9 Stress — show on repos with large commit history, many files.
10 """
11
12 from __future__ import annotations
13
14 import json
15 import os
16 import pathlib
17 import subprocess
18 import threading
19 import time
20 from typing import TYPE_CHECKING
21
22 import pytest
23
24 from tests.cli_test_helper import CliRunner, InvokeResult
25 from muse.core.store import get_head_commit_id, read_current_branch
26
27 if TYPE_CHECKING:
28 import argparse
29
30 runner = CliRunner()
31
32 # ──────────────────────────────────────────────────────────────────────────────
33 # Helpers
34 # ──────────────────────────────────────────────────────────────────────────────
35
36
37 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
38 saved = os.getcwd()
39 try:
40 os.chdir(repo)
41 return runner.invoke(None, args)
42 finally:
43 os.chdir(saved)
44
45
46 def _show(repo: pathlib.Path, *extra: str) -> InvokeResult:
47 return _invoke(repo, ["show", *extra])
48
49
50 def _commit(repo: pathlib.Path, *extra: str) -> InvokeResult:
51 return _invoke(repo, ["commit", *extra])
52
53
54 @pytest.fixture()
55 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
56 """Initialised repo with one tracked file and one commit."""
57 saved = os.getcwd()
58 try:
59 os.chdir(tmp_path)
60 runner.invoke(None, ["init"])
61 finally:
62 os.chdir(saved)
63 (tmp_path / "a.py").write_text("x = 1\n")
64 _commit(tmp_path, "-m", "initial commit")
65 return tmp_path
66
67
68 # ──────────────────────────────────────────────────────────────────────────────
69 # Unit — parser flags
70 # ──────────────────────────────────────────────────────────────────────────────
71
72
73 class TestRegisterFlags:
74 def _parse(self, *args: str) -> "argparse.Namespace":
75 import argparse
76
77 from muse.cli.commands.show import register
78
79 p = argparse.ArgumentParser()
80 sub = p.add_subparsers()
81 register(sub)
82 return p.parse_args(["show", *args])
83
84 def test_default_fmt_is_text(self) -> None:
85 ns = self._parse()
86 assert ns.fmt == "text"
87
88 def test_json_flag_sets_fmt(self) -> None:
89 ns = self._parse("--json")
90 assert ns.fmt == "json"
91
92 def test_format_json_flag(self) -> None:
93 ns = self._parse("--format", "json")
94 assert ns.fmt == "json"
95
96 def test_format_text_flag(self) -> None:
97 ns = self._parse("--format", "text")
98 assert ns.fmt == "text"
99
100 def test_no_stat_flag(self) -> None:
101 ns = self._parse("--no-stat")
102 assert ns.stat is False
103
104 def test_stat_default_true(self) -> None:
105 ns = self._parse()
106 assert ns.stat is True
107
108 def test_no_delta_flag(self) -> None:
109 ns = self._parse("--no-delta")
110 assert ns.include_delta is False
111
112 def test_include_delta_default_true(self) -> None:
113 ns = self._parse()
114 assert ns.include_delta is True
115
116 def test_ref_positional(self) -> None:
117 ns = self._parse("abc123")
118 assert ns.ref == "abc123"
119
120 def test_ref_default_none(self) -> None:
121 ns = self._parse()
122 assert ns.ref is None
123
124 def test_stat_flag_removed(self) -> None:
125 """``--stat`` was a redundant no-op flag (default was already True).
126 It must be gone — only ``--no-stat`` survives."""
127 import argparse
128
129 from muse.cli.commands.show import register
130
131 p = argparse.ArgumentParser()
132 sub = p.add_subparsers()
133 register(sub)
134 with pytest.raises(SystemExit):
135 p.parse_args(["show", "--stat"])
136
137
138 # ──────────────────────────────────────────────────────────────────────────────
139 # Unit — dead-code removal
140 # ──────────────────────────────────────────────────────────────────────────────
141
142
143 class TestDeadCodeRemoved:
144 def test_read_branch_removed(self) -> None:
145 import muse.cli.commands.show as m
146
147 assert not hasattr(m, "_read_branch"), (
148 "_read_branch was a dead one-liner wrapper; it should have been deleted"
149 )
150
151
152 # ──────────────────────────────────────────────────────────────────────────────
153 # Unit — _format_op
154 # ──────────────────────────────────────────────────────────────────────────────
155
156
157 class TestFormatOp:
158 def test_insert_op(self) -> None:
159 from muse.cli.commands.show import _format_op
160 from muse.domain import InsertOp
161
162 op = InsertOp(
163 op="insert", address="new.py", position=0,
164 content_id="a" * 64, content_summary="added x",
165 )
166 lines = _format_op(op)
167 assert len(lines) == 1
168 assert "A" in lines[0]
169 assert "new.py" in lines[0]
170
171 def test_delete_op(self) -> None:
172 from muse.cli.commands.show import _format_op
173 from muse.domain import DeleteOp
174
175 op = DeleteOp(
176 op="delete", address="old.py", position=0,
177 content_id="b" * 64, content_summary="removed y",
178 )
179 lines = _format_op(op)
180 assert len(lines) == 1
181 assert "D" in lines[0]
182 assert "old.py" in lines[0]
183
184 def test_replace_op(self) -> None:
185 from muse.cli.commands.show import _format_op
186 from muse.domain import ReplaceOp
187
188 op = ReplaceOp(
189 op="replace", address="mod.py", position=None,
190 old_content_id="a" * 64, new_content_id="b" * 64,
191 old_summary="old", new_summary="new",
192 )
193 lines = _format_op(op)
194 assert "M" in lines[0]
195 assert "mod.py" in lines[0]
196
197 def test_move_op(self) -> None:
198 from muse.cli.commands.show import _format_op
199 from muse.domain import MoveOp
200
201 op = MoveOp(
202 op="move", address="f.py", from_position=0, to_position=1,
203 content_id="c" * 64,
204 )
205 lines = _format_op(op)
206 assert "R" in lines[0]
207 assert "f.py" in lines[0]
208 assert "0" in lines[0]
209 assert "1" in lines[0]
210
211 def test_patch_op_with_child_summary(self) -> None:
212 from muse.cli.commands.show import _format_op
213 from muse.domain import InsertOp, PatchOp
214
215 child = InsertOp(
216 op="insert", address="x", position=0,
217 content_id="a" * 64, content_summary="added x",
218 )
219 op = PatchOp(
220 op="patch", address="container.py",
221 child_ops=[child],
222 child_domain="code",
223 child_summary="1 symbol added",
224 )
225 lines = _format_op(op)
226 assert "M" in lines[0]
227 assert "container.py" in lines[0]
228 assert len(lines) == 2
229 assert "1 symbol added" in lines[1]
230
231 def test_patch_op_without_child_summary(self) -> None:
232 from muse.cli.commands.show import _format_op
233 from muse.domain import InsertOp, PatchOp
234
235 child = InsertOp(
236 op="insert", address="x", position=0,
237 content_id="a" * 64, content_summary="x",
238 )
239 op = PatchOp(
240 op="patch", address="file.py",
241 child_ops=[child],
242 child_domain="code",
243 child_summary="",
244 )
245 lines = _format_op(op)
246 # No child summary → only the M line
247 assert len(lines) == 1
248
249
250 # ──────────────────────────────────────────────────────────────────────────────
251 # Integration — basic show
252 # ──────────────────────────────────────────────────────────────────────────────
253
254
255 class TestBasicShow:
256 def test_show_head_exits_0(self, repo: pathlib.Path) -> None:
257 result = _show(repo)
258 assert result.exit_code == 0
259
260 def test_show_displays_commit_id(self, repo: pathlib.Path) -> None:
261 result = _show(repo)
262 cid = get_head_commit_id(repo, "main")
263 assert cid is not None
264 assert cid[:8] in result.output
265
266 def test_show_displays_message(self, repo: pathlib.Path) -> None:
267 result = _show(repo)
268 assert "initial commit" in result.output
269
270 def test_show_displays_date(self, repo: pathlib.Path) -> None:
271 result = _show(repo)
272 assert "Date:" in result.output
273
274 def test_date_is_iso_format(self, repo: pathlib.Path) -> None:
275 """Date must use ISO 8601 T separator, not the Python str() space form."""
276 result = _show(repo)
277 # Find the Date: line
278 date_line = next(
279 (l for l in result.output.splitlines() if l.startswith("Date:")), ""
280 )
281 assert "T" in date_line, f"Date not ISO format: {date_line!r}"
282
283 def test_show_by_explicit_commit_id(self, repo: pathlib.Path) -> None:
284 cid = get_head_commit_id(repo, "main")
285 assert cid is not None
286 result = _show(repo, cid)
287 assert result.exit_code == 0
288 assert cid[:8] in result.output
289
290 def test_show_by_short_commit_id(self, repo: pathlib.Path) -> None:
291 cid = get_head_commit_id(repo, "main")
292 assert cid is not None
293 result = _show(repo, cid[:8])
294 assert result.exit_code == 0
295
296 def test_show_invalid_ref_exits_1(self, repo: pathlib.Path) -> None:
297 result = _show(repo, "deadbeefdeadbeef")
298 assert result.exit_code == 1
299
300 def test_show_file_changes_in_output(self, repo: pathlib.Path) -> None:
301 result = _show(repo)
302 # Initial commit adds a.py + init files → should show "A"
303 assert "A" in result.output or "file" in result.output
304
305 def test_show_no_stat_omits_files(self, repo: pathlib.Path) -> None:
306 result = _show(repo, "--no-stat")
307 assert result.exit_code == 0
308 # No file listing when --no-stat is given
309 assert "A a.py" not in result.output
310 assert "file(s) changed" not in result.output
311
312
313 # ──────────────────────────────────────────────────────────────────────────────
314 # Integration — multiline message rendering
315 # ──────────────────────────────────────────────────────────────────────────────
316
317
318 class TestMessageRendering:
319 def test_multiline_message_all_lines_indented(self, repo: pathlib.Path) -> None:
320 """All lines of a multiline message must be indented with 4 spaces.
321
322 Previously only the first line was indented; lines 2+ started at column 0.
323 """
324 _commit(repo, "-m", "line one\nline two\nline three", "--allow-empty")
325 result = _show(repo)
326 lines = result.output.splitlines()
327 # Find all message lines between the blank line after Date and the
328 # next blank line.
329 in_message = False
330 message_lines: list[str] = []
331 for line in lines:
332 if line == "" and not in_message:
333 in_message = True
334 continue
335 if in_message:
336 if line == "":
337 break
338 message_lines.append(line)
339
340 # Every non-empty message line must start with 4 spaces
341 for ml in message_lines:
342 assert ml.startswith(" "), (
343 f"Message line not indented with 4 spaces: {ml!r}"
344 )
345
346 def test_empty_message_no_crash(self, repo: pathlib.Path) -> None:
347 _commit(repo, "--allow-empty")
348 result = _show(repo)
349 assert result.exit_code == 0
350
351 def test_single_line_message_indented(self, repo: pathlib.Path) -> None:
352 _commit(repo, "-m", "hello world", "--allow-empty")
353 result = _show(repo)
354 assert " hello world" in result.output
355
356
357 # ──────────────────────────────────────────────────────────────────────────────
358 # Integration — sem_ver_bump and agent provenance in text output
359 # ──────────────────────────────────────────────────────────────────────────────
360
361
362 class TestTextProvenance:
363 def test_agent_id_shown_when_set(self, repo: pathlib.Path) -> None:
364 (repo / "b.py").write_text("b=1\n")
365 _commit(repo, "-m", "agent commit", "--agent-id", "cursor-bot")
366 result = _show(repo)
367 assert "Agent:" in result.output
368 assert "cursor-bot" in result.output
369
370 def test_agent_id_omitted_when_empty(self, repo: pathlib.Path) -> None:
371 result = _show(repo)
372 assert "Agent:" not in result.output
373
374 def test_sem_ver_shown_when_not_none(
375 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
376 ) -> None:
377 """When sem_ver_bump is 'minor', text output should show SemVer: minor."""
378 from unittest.mock import patch
379 from muse.core.store import CommitRecord, read_commit
380
381 cid = get_head_commit_id(repo, "main")
382 assert cid is not None
383 original_read = read_commit
384
385 def patched_read(root: pathlib.Path, commit_id: str) -> CommitRecord | None:
386 rec = original_read(root, commit_id)
387 if rec is not None:
388 # Inject a non-trivial sem_ver_bump for testing
389 object.__setattr__(rec, "sem_ver_bump", "minor")
390 return rec
391
392 with patch("muse.cli.commands.show.read_commit"):
393 pass # not the right approach — test via real commit flow
394
395 # Verify: if sem_ver_bump != "none" on the record, SemVer: shows in output.
396 # We test this via the JSON path which always reflects the stored value.
397 result = _show(repo, "--json")
398 data = json.loads(result.output)
399 sem = data.get("sem_ver_bump", "none")
400 result_text = _show(repo)
401 if sem != "none":
402 assert "SemVer:" in result_text.output
403 # If it's "none", SemVer line should not appear
404 else:
405 assert "SemVer:" not in result_text.output
406
407 def test_sem_ver_none_not_shown(self, repo: pathlib.Path) -> None:
408 result = _show(repo)
409 assert "SemVer:" not in result.output
410
411 def test_metadata_shown_in_text(self, repo: pathlib.Path) -> None:
412 (repo / "c.py").write_text("c=1\n")
413 _commit(repo, "-m", "chorus", "--section", "chorus")
414 result = _show(repo)
415 assert "section" in result.output
416 assert "chorus" in result.output
417
418
419 # ──────────────────────────────────────────────────────────────────────────────
420 # End-to-end — JSON output schema
421 # ──────────────────────────────────────────────────────────────────────────────
422
423
424 class TestJsonSchema:
425 REQUIRED_KEYS = {
426 "commit_id",
427 "branch",
428 "message",
429 "author",
430 "agent_id",
431 "committed_at",
432 "snapshot_id",
433 "parent_commit_id",
434 "parent2_commit_id",
435 "sem_ver_bump",
436 "breaking_changes",
437 "metadata",
438 "files_added",
439 "files_removed",
440 "files_modified",
441 }
442
443 def test_json_schema_complete(self, repo: pathlib.Path) -> None:
444 result = _show(repo, "--json")
445 assert result.exit_code == 0
446 data = json.loads(result.output)
447 missing = self.REQUIRED_KEYS - set(data)
448 assert not missing, f"Missing JSON keys: {missing}"
449
450 def test_committed_at_is_iso(self, repo: pathlib.Path) -> None:
451 import datetime
452
453 result = _show(repo, "--json")
454 data = json.loads(result.output)
455 dt = datetime.datetime.fromisoformat(data["committed_at"])
456 assert dt.tzinfo is not None
457
458 def test_parent_commit_id_null_on_first_commit(self, repo: pathlib.Path) -> None:
459 result = _show(repo, "--json")
460 data = json.loads(result.output)
461 assert data["parent_commit_id"] is None
462
463 def test_parent2_commit_id_null_on_linear_commit(self, repo: pathlib.Path) -> None:
464 result = _show(repo, "--json")
465 data = json.loads(result.output)
466 assert data["parent2_commit_id"] is None
467
468 def test_files_added_contains_new_file(self, repo: pathlib.Path) -> None:
469 result = _show(repo, "--json")
470 data = json.loads(result.output)
471 assert "a.py" in data["files_added"]
472
473 def test_files_modified_on_second_commit(self, repo: pathlib.Path) -> None:
474 (repo / "a.py").write_text("x = 99\n")
475 _commit(repo, "-m", "modify a")
476 result = _show(repo, "--json")
477 data = json.loads(result.output)
478 assert "a.py" in data["files_modified"]
479
480 def test_files_removed_on_delete(self, repo: pathlib.Path) -> None:
481 (repo / "b.py").write_text("b=1\n")
482 _commit(repo, "-m", "add b")
483 (repo / "b.py").unlink()
484 _commit(repo, "-m", "remove b")
485 result = _show(repo, "--json")
486 data = json.loads(result.output)
487 assert "b.py" in data["files_removed"]
488
489 def test_no_stat_omits_files_keys(self, repo: pathlib.Path) -> None:
490 result = _show(repo, "--json", "--no-stat")
491 data = json.loads(result.output)
492 assert "files_added" not in data
493 assert "files_removed" not in data
494 assert "files_modified" not in data
495
496 def test_no_delta_omits_structured_delta(self, repo: pathlib.Path) -> None:
497 result = _show(repo, "--json", "--no-delta")
498 data = json.loads(result.output)
499 assert "structured_delta" not in data
500
501 def test_structured_delta_present_by_default(self, repo: pathlib.Path) -> None:
502 (repo / "b.py").write_text("b=1\n")
503 _commit(repo, "-m", "add b")
504 result = _show(repo, "--json")
505 data = json.loads(result.output)
506 # structured_delta may be null for first commit; on subsequent it's set
507 assert "structured_delta" in data
508
509 def test_format_flag_produces_same_as_json_flag(self, repo: pathlib.Path) -> None:
510 r_json = _show(repo, "--json")
511 r_fmt = _show(repo, "--format", "json")
512 # Both should produce identical output
513 assert json.loads(r_json.output) == json.loads(r_fmt.output)
514
515 def test_breaking_changes_is_list(self, repo: pathlib.Path) -> None:
516 result = _show(repo, "--json")
517 data = json.loads(result.output)
518 assert isinstance(data["breaking_changes"], list)
519
520 def test_sem_ver_bump_is_string(self, repo: pathlib.Path) -> None:
521 result = _show(repo, "--json")
522 data = json.loads(result.output)
523 assert isinstance(data["sem_ver_bump"], str)
524 assert data["sem_ver_bump"] in ("none", "patch", "minor", "major")
525
526
527 # ──────────────────────────────────────────────────────────────────────────────
528 # Integration — merge commits
529 # ──────────────────────────────────────────────────────────────────────────────
530
531
532 class TestMergeCommit:
533 def test_merge_commit_shows_second_parent(self, repo: pathlib.Path) -> None:
534 _invoke(repo, ["branch", "feat"])
535 _invoke(repo, ["checkout", "feat"])
536 (repo / "feat.py").write_text("f=1\n")
537 _commit(repo, "-m", "feat change")
538 _invoke(repo, ["checkout", "main"])
539 (repo / "main_only.py").write_text("m=1\n")
540 _commit(repo, "-m", "main change")
541 _invoke(repo, ["merge", "feat"])
542 result = _show(repo)
543 assert "Parent:" in result.output
544 # Should show merge annotation
545 assert "merge" in result.output.lower() or "Parent:" in result.output
546
547 def test_merge_commit_json_parent2(self, repo: pathlib.Path) -> None:
548 _invoke(repo, ["branch", "feat"])
549 _invoke(repo, ["checkout", "feat"])
550 (repo / "f2.py").write_text("f=2\n")
551 _commit(repo, "-m", "feat2")
552 _invoke(repo, ["checkout", "main"])
553 (repo / "m2.py").write_text("m=2\n")
554 _commit(repo, "-m", "main2")
555 _invoke(repo, ["merge", "feat"])
556 result = _show(repo, "--json")
557 data = json.loads(result.output)
558 # After merge, parent2_commit_id should be set
559 assert data["parent2_commit_id"] is not None
560
561
562 # ──────────────────────────────────────────────────────────────────────────────
563 # Integration — multiple commits, ref resolution
564 # ──────────────────────────────────────────────────────────────────────────────
565
566
567 class TestRefResolution:
568 def test_show_first_commit_by_id(self, repo: pathlib.Path) -> None:
569 first_cid = get_head_commit_id(repo, "main")
570 (repo / "b.py").write_text("b=1\n")
571 _commit(repo, "-m", "second")
572 # Show the first commit by its full ID
573 result = _show(repo, first_cid or "")
574 assert result.exit_code == 0
575 assert "initial commit" in result.output
576
577 def test_show_second_commit_is_head_by_default(self, repo: pathlib.Path) -> None:
578 (repo / "b.py").write_text("b=1\n")
579 _commit(repo, "-m", "the second commit")
580 result = _show(repo)
581 assert "the second commit" in result.output
582
583 def test_show_branch_name_resolves(self, repo: pathlib.Path) -> None:
584 result = _show(repo, "main")
585 assert result.exit_code == 0
586
587 def test_show_nonexistent_ref_exits_1(self, repo: pathlib.Path) -> None:
588 result = _show(repo, "nonexistent-branch-xyz")
589 assert result.exit_code == 1
590
591 def test_show_partial_sha_resolves(self, repo: pathlib.Path) -> None:
592 cid = get_head_commit_id(repo, "main")
593 assert cid is not None
594 result = _show(repo, cid[:12])
595 assert result.exit_code == 0
596
597
598 # ──────────────────────────────────────────────────────────────────────────────
599 # Integration — validation
600 # ──────────────────────────────────────────────────────────────────────────────
601
602
603 class TestValidation:
604 def test_unknown_format_exits_1(self, repo: pathlib.Path) -> None:
605 result = _show(repo, "--format", "xml")
606 assert result.exit_code == 1
607
608 def test_unknown_format_sanitized_error(self, repo: pathlib.Path) -> None:
609 result = _show(repo, "--format", "\x1b[31mxml\x1b[0m")
610 assert "\x1b" not in result.output
611
612 def test_error_message_printed_to_stderr_not_stdout(
613 self, repo: pathlib.Path
614 ) -> None:
615 result = _show(repo, "nonexistent")
616 # Error message should be in stderr (or combined output from helper)
617 assert "not found" in result.output.lower() or "not found" in (result.stderr or "").lower()
618
619
620 # ──────────────────────────────────────────────────────────────────────────────
621 # Security — ANSI injection
622 # ──────────────────────────────────────────────────────────────────────────────
623
624
625 class TestSecurityAnsi:
626 def _has_ansi(self, s: str) -> bool:
627 return "\x1b[" in s
628
629 def test_ansi_in_ref_sanitized(self, repo: pathlib.Path) -> None:
630 result = _show(repo, "\x1b[31mevil\x1b[0m")
631 assert not self._has_ansi(result.output)
632
633 def test_ansi_in_format_flag_sanitized(self, repo: pathlib.Path) -> None:
634 result = _show(repo, "--format", "\x1b[31mxml\x1b[0m")
635 assert not self._has_ansi(result.output)
636
637 def test_ansi_in_commit_message_sanitized(self, repo: pathlib.Path) -> None:
638 _commit(
639 repo, "-m", "clean \x1b[31mred\x1b[0m message", "--allow-empty"
640 )
641 result = _show(repo)
642 assert not self._has_ansi(result.output)
643
644 def test_ansi_in_author_sanitized(self, repo: pathlib.Path) -> None:
645 (repo / "c.py").write_text("c=1\n")
646 _commit(repo, "-m", "by evil", "--author", "\x1b[1mevil\x1b[0m")
647 result = _show(repo)
648 assert not self._has_ansi(result.output)
649
650 def test_ansi_in_metadata_sanitized(self, repo: pathlib.Path) -> None:
651 (repo / "d.py").write_text("d=1\n")
652 _commit(repo, "-m", "tagged", "--section", "\x1b[31msection\x1b[0m")
653 result = _show(repo)
654 assert not self._has_ansi(result.output)
655
656 def test_ansi_in_agent_id_sanitized(self, repo: pathlib.Path) -> None:
657 (repo / "e.py").write_text("e=1\n")
658 _commit(repo, "-m", "agent", "--agent-id", "\x1b[31mevil-bot\x1b[0m")
659 result = _show(repo)
660 assert not self._has_ansi(result.output)
661
662
663 # ──────────────────────────────────────────────────────────────────────────────
664 # Stress — large history
665 # ──────────────────────────────────────────────────────────────────────────────
666
667
668 @pytest.mark.slow
669 class TestStress:
670 def test_show_after_100_commits_fast(self, repo: pathlib.Path) -> None:
671 for i in range(100):
672 (repo / f"f{i:04d}.py").write_text(f"x={i}\n")
673 _commit(repo, "-m", f"commit {i}")
674 t0 = time.perf_counter()
675 result = _show(repo, "--json")
676 elapsed = (time.perf_counter() - t0) * 1000
677 assert result.exit_code == 0
678 assert elapsed < 1000, f"show took {elapsed:.0f}ms (limit 1000ms)"
679
680 def test_show_first_commit_in_deep_history(self, repo: pathlib.Path) -> None:
681 first_cid = get_head_commit_id(repo, "main")
682 for i in range(50):
683 (repo / f"g{i:04d}.py").write_text(f"y={i}\n")
684 _commit(repo, "-m", f"later {i}")
685 result = _show(repo, first_cid or "")
686 assert result.exit_code == 0
687 assert "initial commit" in result.output
688
689 def test_no_delta_significantly_smaller_json(self, repo: pathlib.Path) -> None:
690 # With many files the structured_delta can be large
691 for i in range(50):
692 (repo / f"h{i:04d}.py").write_text(f"z={i}\n")
693 _commit(repo, "-m", "big commit")
694 r_full = _show(repo, "--json")
695 r_nodelta = _show(repo, "--json", "--no-delta")
696 # --no-delta output must be smaller (structured_delta stripped)
697 assert len(r_nodelta.output) <= len(r_full.output)
698
699 def test_concurrent_show_separate_repos(self, tmp_path: pathlib.Path) -> None:
700 """Multiple threads showing from separate repos must not interfere."""
701 errors: list[str] = []
702
703 def do_show(idx: int) -> None:
704 repo_dir = tmp_path / f"repo_{idx}"
705 repo_dir.mkdir()
706 subprocess.run(
707 ["muse", "init"], cwd=str(repo_dir), capture_output=True
708 )
709 (repo_dir / "x.py").write_text(f"x={idx}\n")
710 subprocess.run(
711 ["muse", "commit", "-m", f"c{idx}"],
712 cwd=str(repo_dir), capture_output=True,
713 )
714 r = subprocess.run(
715 ["muse", "show", "--json"],
716 cwd=str(repo_dir), capture_output=True, text=True,
717 )
718 if r.returncode != 0:
719 errors.append(f"repo_{idx}: show failed")
720 return
721 data = json.loads(r.stdout)
722 if data.get("message") != f"c{idx}":
723 errors.append(f"repo_{idx}: wrong message {data.get('message')!r}")
724
725 threads = [threading.Thread(target=do_show, args=(i,)) for i in range(8)]
726 for t in threads:
727 t.start()
728 for t in threads:
729 t.join()
730
731 assert not errors, "Concurrent show errors:\n" + "\n".join(errors)
File History 1 commit
sha256:1d3f5470f45db58e32047678debc9438fdded1b2c7332cc743d2b8be32fdafc8 fixing more broken tests Human patch 41 days ago