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