cgcardona / muse public
test_cli_coverage_gaps.py python
287 lines 10.9 KB
dfaf1b77 refactor: rename muse-work/ → state/ Gabriel Cardona <gabriel@tellurstori.com> 8h ago
1 """Tests targeting specific coverage gaps in checkout, tag, commit, diff, stash, branch."""
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.core.store import get_head_commit_id
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 = "commit") -> str | None:
28 result = runner.invoke(cli, ["commit", "-m", msg])
29 assert result.exit_code == 0, result.output
30 return get_head_commit_id(pathlib.Path("."), "main")
31
32
33 # ---------------------------------------------------------------------------
34 # checkout gaps
35 # ---------------------------------------------------------------------------
36
37
38 class TestCheckoutGaps:
39 def test_create_branch_already_exists_errors(self, repo: pathlib.Path) -> None:
40 result = runner.invoke(cli, ["checkout", "-b", "main"])
41 assert result.exit_code != 0
42 assert "already exists" in result.output
43
44 def test_unknown_ref_errors(self, repo: pathlib.Path) -> None:
45 result = runner.invoke(cli, ["checkout", "no-such-branch-or-commit"])
46 assert result.exit_code != 0
47 assert "not a branch" in result.output.lower() or "not found" in result.output.lower() or "no-such" in result.output
48
49 def test_checkout_by_commit_id_detaches_head(self, repo: pathlib.Path) -> None:
50 _write(repo, "beat.mid")
51 result = runner.invoke(cli, ["commit", "-m", "root"])
52 assert result.exit_code == 0
53 commit_id = get_head_commit_id(repo, "main")
54 assert commit_id is not None
55
56 _write(repo, "lead.mid")
57 runner.invoke(cli, ["commit", "-m", "second"])
58
59 result = runner.invoke(cli, ["checkout", commit_id])
60 assert result.exit_code == 0
61 assert "HEAD is now at" in result.output
62
63 def test_checkout_restores_workdir_to_target_snapshot(self, repo: pathlib.Path) -> None:
64 _write(repo, "beat.mid", "v1")
65 result = runner.invoke(cli, ["commit", "-m", "first"])
66 assert result.exit_code == 0
67 first_id = get_head_commit_id(repo, "main")
68 assert first_id is not None
69
70 _write(repo, "lead.mid", "new")
71 runner.invoke(cli, ["commit", "-m", "second"])
72
73 runner.invoke(cli, ["checkout", first_id])
74 assert (repo / "state" / "beat.mid").exists()
75 assert not (repo / "state" / "lead.mid").exists()
76
77
78 # ---------------------------------------------------------------------------
79 # tag gaps
80 # ---------------------------------------------------------------------------
81
82
83 class TestTagGaps:
84 def test_tag_add_unknown_ref_errors(self, repo: pathlib.Path) -> None:
85 _write(repo, "beat.mid")
86 runner.invoke(cli, ["commit", "-m", "base"])
87 result = runner.invoke(cli, ["tag", "add", "emotion:joyful", "deadbeef"])
88 assert result.exit_code != 0
89
90 def test_tag_list_for_specific_commit(self, repo: pathlib.Path) -> None:
91 _write(repo, "beat.mid")
92 runner.invoke(cli, ["commit", "-m", "base"])
93 commit_id = get_head_commit_id(repo, "main")
94 assert commit_id is not None
95
96 runner.invoke(cli, ["tag", "add", "emotion:joyful", commit_id[:8]])
97 result = runner.invoke(cli, ["tag", "list", commit_id[:8]])
98 assert result.exit_code == 0
99 assert "joyful" in result.output
100
101 def test_tag_list_for_unknown_ref_errors(self, repo: pathlib.Path) -> None:
102 _write(repo, "beat.mid")
103 runner.invoke(cli, ["commit", "-m", "base"])
104 result = runner.invoke(cli, ["tag", "list", "deadbeef"])
105 assert result.exit_code != 0
106
107 def test_tag_list_all_shows_all_tags(self, repo: pathlib.Path) -> None:
108 _write(repo, "beat.mid")
109 runner.invoke(cli, ["commit", "-m", "base"])
110 commit_id = get_head_commit_id(repo, "main")
111 assert commit_id is not None
112
113 runner.invoke(cli, ["tag", "add", "section:verse", commit_id[:8]])
114 runner.invoke(cli, ["tag", "add", "emotion:joyful", commit_id[:8]])
115 result = runner.invoke(cli, ["tag", "list"])
116 assert result.exit_code == 0
117 assert "section:verse" in result.output
118 assert "emotion:joyful" in result.output
119
120
121 # ---------------------------------------------------------------------------
122 # commit gaps
123 # ---------------------------------------------------------------------------
124
125
126 class TestCommitGaps:
127 def test_no_message_without_allow_empty_errors(self, repo: pathlib.Path) -> None:
128 _write(repo, "beat.mid")
129 result = runner.invoke(cli, ["commit"])
130 assert result.exit_code != 0
131
132 def test_no_muse_work_dir_errors(self, repo: pathlib.Path) -> None:
133 import shutil
134 shutil.rmtree(repo / "state")
135 result = runner.invoke(cli, ["commit", "-m", "no workdir"])
136 assert result.exit_code != 0
137 assert "state" in result.output
138
139 def test_empty_workdir_without_allow_empty_errors(self, repo: pathlib.Path) -> None:
140 result = runner.invoke(cli, ["commit", "-m", "empty"])
141 assert result.exit_code != 0
142
143 def test_nothing_to_commit_clean_tree(self, repo: pathlib.Path) -> None:
144 _write(repo, "beat.mid")
145 runner.invoke(cli, ["commit", "-m", "first"])
146 result = runner.invoke(cli, ["commit", "-m", "second"])
147 assert result.exit_code == 0
148 assert "Nothing to commit" in result.output or "clean" in result.output
149
150 def test_commit_with_pending_conflicts_errors(self, repo: pathlib.Path) -> None:
151 _write(repo, "beat.mid", "base")
152 runner.invoke(cli, ["commit", "-m", "base"])
153
154 runner.invoke(cli, ["branch", "feature"])
155 runner.invoke(cli, ["checkout", "feature"])
156 _write(repo, "beat.mid", "feature-v")
157 runner.invoke(cli, ["commit", "-m", "feature changes"])
158
159 runner.invoke(cli, ["checkout", "main"])
160 _write(repo, "beat.mid", "main-v")
161 runner.invoke(cli, ["commit", "-m", "main changes"])
162
163 runner.invoke(cli, ["merge", "feature"])
164
165 # Now try to commit — should fail because of unresolved conflicts
166 _write(repo, "new.mid")
167 result = runner.invoke(cli, ["commit", "-m", "during conflict"])
168 assert result.exit_code != 0
169 assert "conflict" in result.output.lower()
170
171
172 # ---------------------------------------------------------------------------
173 # diff gaps
174 # ---------------------------------------------------------------------------
175
176
177 class TestDiffGaps:
178 def test_diff_two_commits(self, repo: pathlib.Path) -> None:
179 _write(repo, "a.mid", "v1")
180 runner.invoke(cli, ["commit", "-m", "first"])
181 first_id = get_head_commit_id(repo, "main")
182
183 _write(repo, "a.mid", "v2")
184 runner.invoke(cli, ["commit", "-m", "second"])
185 second_id = get_head_commit_id(repo, "main")
186
187 assert first_id is not None
188 assert second_id is not None
189 result = runner.invoke(cli, ["diff", first_id, second_id])
190 assert result.exit_code == 0
191 assert "a.mid" in result.output
192
193 def test_diff_commit_vs_head(self, repo: pathlib.Path) -> None:
194 _write(repo, "a.mid", "v1")
195 runner.invoke(cli, ["commit", "-m", "first"])
196 first_id = get_head_commit_id(repo, "main")
197
198 _write(repo, "a.mid", "v2")
199 runner.invoke(cli, ["commit", "-m", "second"])
200 assert first_id is not None
201
202 result = runner.invoke(cli, ["diff", first_id])
203 assert result.exit_code == 0
204 assert "a.mid" in result.output
205
206 def test_diff_stat_flag(self, repo: pathlib.Path) -> None:
207 _write(repo, "a.mid")
208 runner.invoke(cli, ["commit", "-m", "base"])
209 _write(repo, "b.mid")
210 result = runner.invoke(cli, ["diff", "--stat"])
211 assert result.exit_code == 0
212 # --stat prints the structured delta summary line.
213 assert "added" in result.output or "No differences" in result.output
214
215
216 # ---------------------------------------------------------------------------
217 # stash gaps
218 # ---------------------------------------------------------------------------
219
220
221 class TestStashGaps:
222 def test_stash_pop_restores_files(self, repo: pathlib.Path) -> None:
223 _write(repo, "beat.mid")
224 runner.invoke(cli, ["commit", "-m", "base"])
225 _write(repo, "unsaved.mid", "wip")
226
227 runner.invoke(cli, ["stash"])
228 assert not (repo / "state" / "unsaved.mid").exists()
229
230 runner.invoke(cli, ["stash", "pop"])
231 assert (repo / "state" / "unsaved.mid").exists()
232
233 def test_stash_drop_removes_entry(self, repo: pathlib.Path) -> None:
234 _write(repo, "beat.mid")
235 runner.invoke(cli, ["commit", "-m", "base"])
236 _write(repo, "unsaved.mid", "wip")
237
238 runner.invoke(cli, ["stash"])
239 result = runner.invoke(cli, ["stash", "drop"])
240 assert result.exit_code == 0
241
242 result = runner.invoke(cli, ["stash", "list"])
243 assert "No stash entries" in result.output
244
245 def test_stash_pop_empty_errors(self, repo: pathlib.Path) -> None:
246 result = runner.invoke(cli, ["stash", "pop"])
247 assert result.exit_code != 0
248 assert "No stash" in result.output
249
250 def test_stash_drop_empty_errors(self, repo: pathlib.Path) -> None:
251 result = runner.invoke(cli, ["stash", "drop"])
252 assert result.exit_code != 0
253
254 def test_stash_nothing_to_stash(self, repo: pathlib.Path) -> None:
255 result = runner.invoke(cli, ["stash"])
256 assert result.exit_code == 0
257 assert "Nothing to stash" in result.output
258
259
260 # ---------------------------------------------------------------------------
261 # branch gaps
262 # ---------------------------------------------------------------------------
263
264
265 class TestBranchGaps:
266 def test_delete_branch_with_d_flag(self, repo: pathlib.Path) -> None:
267 runner.invoke(cli, ["branch", "to-delete"])
268 result = runner.invoke(cli, ["branch", "-d", "to-delete"])
269 assert result.exit_code == 0
270
271 def test_delete_current_branch_errors(self, repo: pathlib.Path) -> None:
272 result = runner.invoke(cli, ["branch", "-d", "main"])
273 assert result.exit_code != 0
274 assert "current" in result.output.lower() or "main" in result.output
275
276 def test_delete_nonexistent_branch_errors(self, repo: pathlib.Path) -> None:
277 result = runner.invoke(cli, ["branch", "-d", "no-such-branch"])
278 assert result.exit_code != 0
279
280 def test_create_branch_with_b_flag(self, repo: pathlib.Path) -> None:
281 result = runner.invoke(cli, ["branch", "new-feature"])
282 assert result.exit_code == 0
283
284 def test_branch_with_slash_shown_in_list(self, repo: pathlib.Path) -> None:
285 runner.invoke(cli, ["branch", "feature/my-thing"])
286 result = runner.invoke(cli, ["branch"])
287 assert "feature/my-thing" in result.output