gabriel / muse public
test_cmd_log.py python
204 lines 8.3 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 """Comprehensive tests for ``muse log``.
2
3 Covers:
4 - Unit: _parse_date helper
5 - Integration: log with history, filters, authors
6 - E2E: full CLI via CliRunner
7 - Security: no path traversal via --ref, sanitized output
8 - Stress: long commit history with limit
9 """
10
11 from __future__ import annotations
12
13 import datetime
14 import json
15 import pathlib
16 import uuid
17
18 import pytest
19 from tests.cli_test_helper import CliRunner
20
21 cli = None # argparse migration — CliRunner ignores this arg
22
23 runner = CliRunner()
24
25
26 # ---------------------------------------------------------------------------
27 # Helpers
28 # ---------------------------------------------------------------------------
29
30 def _env(root: pathlib.Path) -> dict[str, str]:
31 return {"MUSE_REPO_ROOT": str(root)}
32
33
34 def _init_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]:
35 muse_dir = tmp_path / ".muse"
36 muse_dir.mkdir()
37 repo_id = str(uuid.uuid4())
38 (muse_dir / "repo.json").write_text(json.dumps({
39 "repo_id": repo_id,
40 "domain": "midi",
41 "default_branch": "main",
42 "created_at": "2025-01-01T00:00:00+00:00",
43 }), encoding="utf-8")
44 (muse_dir / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
45 (muse_dir / "refs" / "heads").mkdir(parents=True)
46 (muse_dir / "snapshots").mkdir()
47 (muse_dir / "commits").mkdir()
48 (muse_dir / "objects").mkdir()
49 return tmp_path, repo_id
50
51
52 def _make_commit(
53 root: pathlib.Path, repo_id: str, message: str = "commit",
54 author: str = "Alice",
55 ) -> str:
56 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
57 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
58
59 ref_file = root / ".muse" / "refs" / "heads" / "main"
60 parent_id = ref_file.read_text().strip() if ref_file.exists() else None
61 manifest: dict[str, str] = {}
62 snap_id = compute_snapshot_id(manifest)
63 committed_at = datetime.datetime.now(datetime.timezone.utc)
64 commit_id = compute_commit_id(
65 parent_ids=[parent_id] if parent_id else [],
66 snapshot_id=snap_id, message=message,
67 committed_at_iso=committed_at.isoformat(),
68 )
69 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
70 write_commit(root, CommitRecord(
71 commit_id=commit_id, repo_id=repo_id, branch="main",
72 snapshot_id=snap_id, message=message, committed_at=committed_at,
73 parent_commit_id=parent_id, author=author,
74 ))
75 ref_file.parent.mkdir(parents=True, exist_ok=True)
76 ref_file.write_text(commit_id, encoding="utf-8")
77 return commit_id
78
79
80 # ---------------------------------------------------------------------------
81 # Unit tests
82 # ---------------------------------------------------------------------------
83
84 class TestLogUnit:
85 def test_parse_date_iso(self) -> None:
86 from muse.cli.commands.log import _parse_date
87 dt = _parse_date("2025-01-15")
88 assert dt.year == 2025
89 assert dt.month == 1
90 assert dt.day == 15
91
92 def test_parse_date_relative_days(self) -> None:
93 from muse.cli.commands.log import _parse_date
94 dt = _parse_date("7 days ago")
95 assert isinstance(dt, datetime.datetime)
96
97
98 # ---------------------------------------------------------------------------
99 # Integration tests
100 # ---------------------------------------------------------------------------
101
102 class TestLogIntegration:
103 def test_log_empty_repo_shows_nothing(self, tmp_path: pathlib.Path) -> None:
104 root, repo_id = _init_repo(tmp_path)
105 result = runner.invoke(cli, ["log"], env=_env(root), catch_exceptions=False)
106 assert result.exit_code == 0
107
108 def test_log_single_commit(self, tmp_path: pathlib.Path) -> None:
109 root, repo_id = _init_repo(tmp_path)
110 _make_commit(root, repo_id, message="first commit")
111 result = runner.invoke(cli, ["log"], env=_env(root), catch_exceptions=False)
112 assert result.exit_code == 0
113 assert "first commit" in result.output
114
115 def test_log_multiple_commits_newest_first(self, tmp_path: pathlib.Path) -> None:
116 root, repo_id = _init_repo(tmp_path)
117 _make_commit(root, repo_id, message="commit one")
118 _make_commit(root, repo_id, message="commit two")
119 _make_commit(root, repo_id, message="commit three")
120 result = runner.invoke(cli, ["log"], env=_env(root), catch_exceptions=False)
121 pos_one = result.output.find("commit one")
122 pos_three = result.output.find("commit three")
123 assert pos_three < pos_one # newest appears first
124
125 def test_log_oneline_format(self, tmp_path: pathlib.Path) -> None:
126 root, repo_id = _init_repo(tmp_path)
127 _make_commit(root, repo_id, message="oneline test")
128 result = runner.invoke(cli, ["log", "--oneline"], env=_env(root), catch_exceptions=False)
129 assert result.exit_code == 0
130 lines = [l for l in result.output.splitlines() if l.strip()]
131 assert len(lines) == 1
132 assert "oneline test" in lines[0]
133
134 def test_log_max_count_limits_output(self, tmp_path: pathlib.Path) -> None:
135 root, repo_id = _init_repo(tmp_path)
136 for i in range(5):
137 _make_commit(root, repo_id, message=f"commit {i}")
138 result = runner.invoke(cli, ["log", "-n", "2"], env=_env(root), catch_exceptions=False)
139 assert result.exit_code == 0
140 # Count distinct commit IDs in output (each is 64 chars long)
141 import re
142 commit_ids = re.findall(r"commit [0-9a-f]{64}", result.output)
143 assert len(commit_ids) <= 2
144
145 def test_log_filter_by_author(self, tmp_path: pathlib.Path) -> None:
146 root, repo_id = _init_repo(tmp_path)
147 _make_commit(root, repo_id, message="by alice", author="Alice")
148 _make_commit(root, repo_id, message="by bob", author="Bob")
149 result = runner.invoke(cli, ["log", "--author", "Alice"], env=_env(root), catch_exceptions=False)
150 assert "by alice" in result.output
151 assert "by bob" not in result.output
152
153 def test_log_stat_flag(self, tmp_path: pathlib.Path) -> None:
154 root, repo_id = _init_repo(tmp_path)
155 _make_commit(root, repo_id, message="stat commit")
156 result = runner.invoke(cli, ["log", "--stat"], env=_env(root), catch_exceptions=False)
157 assert result.exit_code == 0
158
159 def test_log_max_count_zero_rejected(self, tmp_path: pathlib.Path) -> None:
160 root, repo_id = _init_repo(tmp_path)
161 result = runner.invoke(cli, ["log", "-n", "0"], env=_env(root))
162 assert result.exit_code != 0
163
164 def test_log_short_flags(self, tmp_path: pathlib.Path) -> None:
165 root, repo_id = _init_repo(tmp_path)
166 _make_commit(root, repo_id, message="short flag")
167 result = runner.invoke(cli, ["log", "-n", "1", "-p"], env=_env(root), catch_exceptions=False)
168 assert result.exit_code == 0
169
170
171 # ---------------------------------------------------------------------------
172 # Security tests
173 # ---------------------------------------------------------------------------
174
175 class TestLogSecurity:
176 def test_log_invalid_branch_name_handled(self, tmp_path: pathlib.Path) -> None:
177 root, repo_id = _init_repo(tmp_path)
178 _make_commit(root, repo_id)
179 result = runner.invoke(cli, ["log", "../../../etc/passwd"], env=_env(root))
180 assert "\x1b" not in result.output
181
182
183 # ---------------------------------------------------------------------------
184 # Stress tests
185 # ---------------------------------------------------------------------------
186
187 class TestLogStress:
188 def test_long_history_with_limit(self, tmp_path: pathlib.Path) -> None:
189 root, repo_id = _init_repo(tmp_path)
190 for i in range(200):
191 _make_commit(root, repo_id, message=f"commit {i:03d}")
192 result = runner.invoke(cli, ["log", "-n", "10", "--oneline"], env=_env(root), catch_exceptions=False)
193 assert result.exit_code == 0
194 lines = [l for l in result.output.splitlines() if l.strip()]
195 assert 1 <= len(lines) <= 10
196
197 def test_many_commits_author_filter_performance(self, tmp_path: pathlib.Path) -> None:
198 root, repo_id = _init_repo(tmp_path)
199 for i in range(100):
200 author = "Alice" if i % 2 == 0 else "Bob"
201 _make_commit(root, repo_id, message=f"msg {i}", author=author)
202 result = runner.invoke(cli, ["log", "--author", "Alice", "-n", "50"], env=_env(root), catch_exceptions=False)
203 assert result.exit_code == 0
204 assert "Bob" not in result.output