cgcardona / muse public
test_cli_log.py python
292 lines 10.5 KB
dfaf1b77 refactor: rename muse-work/ → state/ Gabriel Cardona <gabriel@tellurstori.com> 8h ago
1 """Tests for muse log — commit history display and filters."""
2
3 import pathlib
4
5 import pytest
6 from typer.testing import CliRunner
7
8 from muse.cli.app import cli
9 from muse.cli.commands.log import _parse_date
10
11 runner = CliRunner()
12
13
14 @pytest.fixture
15 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
16 monkeypatch.chdir(tmp_path)
17 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
18 result = runner.invoke(cli, ["init"])
19 assert result.exit_code == 0, result.output
20 return tmp_path
21
22
23 def _write(repo: pathlib.Path, filename: str, content: str = "data") -> None:
24 (repo / "state" / filename).write_text(content)
25
26
27 def _commit(msg: str, **flags: str) -> None:
28 args = ["commit", "-m", msg]
29 for k, v in flags.items():
30 args += [f"--{k}", v]
31 result = runner.invoke(cli, args)
32 assert result.exit_code == 0, result.output
33
34
35 # ---------------------------------------------------------------------------
36 # _parse_date unit tests
37 # ---------------------------------------------------------------------------
38
39
40 class TestParseDate:
41 def test_today(self) -> None:
42 from datetime import datetime, timezone
43 dt = _parse_date("today")
44 assert dt.tzinfo == timezone.utc
45 now = datetime.now(timezone.utc)
46 assert dt.date() == now.date()
47
48 def test_yesterday(self) -> None:
49 from datetime import datetime, timedelta, timezone
50 dt = _parse_date("yesterday")
51 expected = (datetime.now(timezone.utc) - timedelta(days=1)).date()
52 assert dt.date() == expected
53
54 def test_n_days_ago(self) -> None:
55 from datetime import datetime, timedelta, timezone
56 dt = _parse_date("3 days ago")
57 expected = (datetime.now(timezone.utc) - timedelta(days=3)).date()
58 assert dt.date() == expected
59
60 def test_n_weeks_ago(self) -> None:
61 from datetime import datetime, timedelta, timezone
62 dt = _parse_date("2 weeks ago")
63 expected = (datetime.now(timezone.utc) - timedelta(weeks=2)).date()
64 assert dt.date() == expected
65
66 def test_n_months_ago(self) -> None:
67 from datetime import datetime, timedelta, timezone
68 dt = _parse_date("1 month ago")
69 expected = (datetime.now(timezone.utc) - timedelta(days=30)).date()
70 assert dt.date() == expected
71
72 def test_n_years_ago(self) -> None:
73 from datetime import datetime, timedelta, timezone
74 dt = _parse_date("1 year ago")
75 expected = (datetime.now(timezone.utc) - timedelta(days=365)).date()
76 assert dt.date() == expected
77
78 def test_iso_date(self) -> None:
79 dt = _parse_date("2025-01-15")
80 assert dt.year == 2025
81 assert dt.month == 1
82 assert dt.day == 15
83
84 def test_iso_datetime(self) -> None:
85 dt = _parse_date("2025-01-15T10:30:00")
86 assert dt.hour == 10
87 assert dt.minute == 30
88
89 def test_iso_datetime_space(self) -> None:
90 dt = _parse_date("2025-01-15 10:30:00")
91 assert dt.hour == 10
92
93 def test_invalid_raises(self) -> None:
94 with pytest.raises(ValueError, match="Cannot parse date"):
95 _parse_date("not-a-date")
96
97
98 # ---------------------------------------------------------------------------
99 # Log output modes
100 # ---------------------------------------------------------------------------
101
102
103 class TestLogEmpty:
104 def test_empty_repo_shows_no_commits(self, repo: pathlib.Path) -> None:
105 result = runner.invoke(cli, ["log"])
106 assert result.exit_code == 0
107 assert "no commits" in result.output.lower() or "(no commits)" in result.output
108
109
110 class TestLogDefault:
111 def test_shows_commit_line(self, repo: pathlib.Path) -> None:
112 _write(repo, "beat.mid")
113 _commit("first commit")
114 result = runner.invoke(cli, ["log"])
115 assert result.exit_code == 0
116 assert "commit" in result.output
117 assert "first commit" in result.output
118
119 def test_shows_date(self, repo: pathlib.Path) -> None:
120 _write(repo, "beat.mid")
121 _commit("dated")
122 result = runner.invoke(cli, ["log"])
123 assert "Date:" in result.output
124
125 def test_shows_author_when_set(self, repo: pathlib.Path) -> None:
126 _write(repo, "beat.mid")
127 _commit("authored", author="Gabriel")
128 result = runner.invoke(cli, ["log"])
129 assert "Gabriel" in result.output
130
131 def test_multiple_commits_newest_first(self, repo: pathlib.Path) -> None:
132 _write(repo, "a.mid")
133 _commit("first")
134 _write(repo, "b.mid")
135 _commit("second")
136 result = runner.invoke(cli, ["log"])
137 assert result.output.index("second") < result.output.index("first")
138
139 def test_shows_head_label(self, repo: pathlib.Path) -> None:
140 _write(repo, "beat.mid")
141 _commit("only")
142 result = runner.invoke(cli, ["log"])
143 assert "HEAD" in result.output
144
145 def test_shows_metadata(self, repo: pathlib.Path) -> None:
146 _write(repo, "beat.mid")
147 _commit("versed", section="verse")
148 result = runner.invoke(cli, ["log"])
149 assert "verse" in result.output
150 assert "Meta:" in result.output
151
152
153 class TestLogOneline:
154 def test_one_line_per_commit(self, repo: pathlib.Path) -> None:
155 _write(repo, "a.mid")
156 _commit("first")
157 _write(repo, "b.mid")
158 _commit("second")
159 result = runner.invoke(cli, ["log", "--oneline"])
160 assert result.exit_code == 0
161 lines = [l for l in result.output.strip().splitlines() if l.strip()]
162 assert len(lines) == 2
163
164 def test_oneline_format(self, repo: pathlib.Path) -> None:
165 _write(repo, "beat.mid")
166 _commit("a message")
167 result = runner.invoke(cli, ["log", "--oneline"])
168 # short id + message on one line
169 assert "a message" in result.output
170 lines = [l for l in result.output.strip().splitlines() if l]
171 assert len(lines) == 1
172
173 def test_oneline_shows_head_label(self, repo: pathlib.Path) -> None:
174 _write(repo, "beat.mid")
175 _commit("only")
176 result = runner.invoke(cli, ["log", "--oneline"])
177 assert "HEAD" in result.output
178
179
180 class TestLogGraph:
181 def test_graph_prefix(self, repo: pathlib.Path) -> None:
182 _write(repo, "beat.mid")
183 _commit("graphed")
184 result = runner.invoke(cli, ["log", "--graph"])
185 assert result.exit_code == 0
186 assert "* " in result.output
187
188 def test_graph_shows_message(self, repo: pathlib.Path) -> None:
189 _write(repo, "beat.mid")
190 _commit("graph msg")
191 result = runner.invoke(cli, ["log", "--graph"])
192 assert "graph msg" in result.output
193
194
195 class TestLogStat:
196 def test_stat_shows_added_files(self, repo: pathlib.Path) -> None:
197 _write(repo, "beat.mid")
198 _commit("add beat")
199 result = runner.invoke(cli, ["log", "--stat"])
200 assert result.exit_code == 0
201 assert "beat.mid" in result.output
202 assert "+" in result.output
203
204 def test_stat_shows_summary_line(self, repo: pathlib.Path) -> None:
205 _write(repo, "beat.mid")
206 _commit("add")
207 result = runner.invoke(cli, ["log", "--stat"])
208 assert "added" in result.output
209
210 def test_patch_shows_files(self, repo: pathlib.Path) -> None:
211 _write(repo, "beat.mid")
212 _commit("patched")
213 result = runner.invoke(cli, ["log", "--patch"])
214 assert result.exit_code == 0
215 assert "beat.mid" in result.output
216
217
218 class TestLogFilters:
219 def test_limit_n(self, repo: pathlib.Path) -> None:
220 for i in range(5):
221 _write(repo, f"f{i}.mid", str(i))
222 _commit(f"msg-{i}")
223 result = runner.invoke(cli, ["log", "-n", "2"])
224 assert result.exit_code == 0
225 # With limit 2, we should see the 2 newest but not the oldest
226 assert "msg-0" not in result.output
227 assert "msg-1" not in result.output
228 assert "msg-2" not in result.output
229
230 def test_filter_author(self, repo: pathlib.Path) -> None:
231 _write(repo, "a.mid")
232 _commit("by gabriel", author="Gabriel")
233 _write(repo, "b.mid")
234 _commit("by alice", author="Alice")
235 result = runner.invoke(cli, ["log", "--author", "Gabriel"])
236 assert result.exit_code == 0
237 assert "by gabriel" in result.output
238 assert "by alice" not in result.output
239
240 def test_filter_author_case_insensitive(self, repo: pathlib.Path) -> None:
241 _write(repo, "a.mid")
242 _commit("authored", author="Gabriel")
243 result = runner.invoke(cli, ["log", "--author", "gabriel"])
244 assert "authored" in result.output
245
246 def test_filter_section(self, repo: pathlib.Path) -> None:
247 _write(repo, "a.mid")
248 _commit("verse part", section="verse")
249 _write(repo, "b.mid")
250 _commit("chorus part", section="chorus")
251 result = runner.invoke(cli, ["log", "--section", "verse"])
252 assert result.exit_code == 0
253 assert "verse part" in result.output
254 assert "chorus part" not in result.output
255
256 def test_filter_track(self, repo: pathlib.Path) -> None:
257 _write(repo, "a.mid")
258 _commit("drums commit", track="drums")
259 _write(repo, "b.mid")
260 _commit("bass commit", track="bass")
261 result = runner.invoke(cli, ["log", "--track", "drums"])
262 assert "drums commit" in result.output
263 assert "bass commit" not in result.output
264
265 def test_filter_emotion(self, repo: pathlib.Path) -> None:
266 _write(repo, "a.mid")
267 _commit("happy commit", emotion="joyful")
268 _write(repo, "b.mid")
269 _commit("sad commit", emotion="melancholic")
270 result = runner.invoke(cli, ["log", "--emotion", "joyful"])
271 assert "happy commit" in result.output
272 assert "sad commit" not in result.output
273
274 def test_filter_since_future_returns_nothing(self, repo: pathlib.Path) -> None:
275 _write(repo, "a.mid")
276 _commit("old commit")
277 result = runner.invoke(cli, ["log", "--since", "2099-01-01"])
278 assert result.exit_code == 0
279 assert "no commits" in result.output.lower() or "(no commits)" in result.output
280
281 def test_filter_until_past_returns_nothing(self, repo: pathlib.Path) -> None:
282 _write(repo, "a.mid")
283 _commit("recent commit")
284 result = runner.invoke(cli, ["log", "--until", "2000-01-01"])
285 assert result.exit_code == 0
286 assert "no commits" in result.output.lower() or "(no commits)" in result.output
287
288 def test_no_matches_shows_no_commits(self, repo: pathlib.Path) -> None:
289 _write(repo, "a.mid")
290 _commit("only commit", author="Gabriel")
291 result = runner.invoke(cli, ["log", "--author", "nobody"])
292 assert "(no commits)" in result.output