cgcardona / muse public
test_cli_workflow.py python
257 lines 9.8 KB
1d9234e8 Replace Maestro-coupled tests with new architecture test suite Gabriel Cardona <gabriel@tellurstori.com> 3d ago
1 """End-to-end CLI workflow tests — init, commit, log, status, branch, merge."""
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
11 runner = CliRunner()
12
13
14 @pytest.fixture
15 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
16 """Initialise a fresh Muse repo in tmp_path and set it as cwd."""
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 class TestInit:
29 def test_creates_muse_dir(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
30 monkeypatch.chdir(tmp_path)
31 result = runner.invoke(cli, ["init"])
32 assert result.exit_code == 0
33 assert (tmp_path / ".muse").is_dir()
34 assert (tmp_path / ".muse" / "HEAD").exists()
35 assert (tmp_path / ".muse" / "repo.json").exists()
36 assert (tmp_path / "muse-work").is_dir()
37
38 def test_reinit_requires_force(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
39 monkeypatch.chdir(tmp_path)
40 runner.invoke(cli, ["init"])
41 result = runner.invoke(cli, ["init"])
42 assert result.exit_code != 0
43 assert "force" in result.output.lower()
44
45 def test_bare_repo(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
46 monkeypatch.chdir(tmp_path)
47 result = runner.invoke(cli, ["init", "--bare"])
48 assert result.exit_code == 0
49 assert not (tmp_path / "muse-work").exists()
50
51
52 class TestCommit:
53 def test_commit_with_message(self, repo: pathlib.Path) -> None:
54 _write(repo, "beat.mid")
55 result = runner.invoke(cli, ["commit", "-m", "Initial commit"])
56 assert result.exit_code == 0
57 assert "Initial commit" in result.output
58
59 def test_nothing_to_commit(self, repo: pathlib.Path) -> None:
60 _write(repo, "beat.mid")
61 runner.invoke(cli, ["commit", "-m", "First"])
62 result = runner.invoke(cli, ["commit", "-m", "Second"])
63 assert result.exit_code == 0
64 assert "Nothing to commit" in result.output
65
66 def test_allow_empty(self, repo: pathlib.Path) -> None:
67 result = runner.invoke(cli, ["commit", "-m", "Empty", "--allow-empty"])
68 assert result.exit_code == 0
69
70 def test_message_required(self, repo: pathlib.Path) -> None:
71 _write(repo, "beat.mid")
72 result = runner.invoke(cli, ["commit"])
73 assert result.exit_code != 0
74
75 def test_section_metadata(self, repo: pathlib.Path) -> None:
76 _write(repo, "beat.mid")
77 result = runner.invoke(cli, ["commit", "-m", "Chorus take", "--section", "chorus"])
78 assert result.exit_code == 0
79
80 from muse.core.store import get_head_commit_id, read_commit
81 import json
82 repo_id = json.loads((repo / ".muse" / "repo.json").read_text())["repo_id"]
83 commit_id = get_head_commit_id(repo, "main")
84 commit = read_commit(repo, commit_id)
85 assert commit is not None
86 assert commit.metadata.get("section") == "chorus"
87
88
89 class TestStatus:
90 def test_clean_after_commit(self, repo: pathlib.Path) -> None:
91 _write(repo, "beat.mid")
92 runner.invoke(cli, ["commit", "-m", "First"])
93 result = runner.invoke(cli, ["status"])
94 assert result.exit_code == 0
95 assert "Nothing to commit" in result.output
96
97 def test_shows_new_file(self, repo: pathlib.Path) -> None:
98 _write(repo, "beat.mid")
99 result = runner.invoke(cli, ["status"])
100 assert result.exit_code == 0
101 assert "beat.mid" in result.output
102
103 def test_short_flag(self, repo: pathlib.Path) -> None:
104 _write(repo, "beat.mid")
105 result = runner.invoke(cli, ["status", "--short"])
106 assert result.exit_code == 0
107 assert "A " in result.output
108
109 def test_porcelain_flag(self, repo: pathlib.Path) -> None:
110 _write(repo, "beat.mid")
111 result = runner.invoke(cli, ["status", "--porcelain"])
112 assert result.exit_code == 0
113 assert "## main" in result.output
114
115
116 class TestLog:
117 def test_empty_log(self, repo: pathlib.Path) -> None:
118 result = runner.invoke(cli, ["log"])
119 assert result.exit_code == 0
120 assert "no commits" in result.output
121
122 def test_shows_commit(self, repo: pathlib.Path) -> None:
123 _write(repo, "beat.mid")
124 runner.invoke(cli, ["commit", "-m", "First take"])
125 result = runner.invoke(cli, ["log"])
126 assert result.exit_code == 0
127 assert "First take" in result.output
128
129 def test_oneline(self, repo: pathlib.Path) -> None:
130 _write(repo, "beat.mid")
131 runner.invoke(cli, ["commit", "-m", "First take"])
132 result = runner.invoke(cli, ["log", "--oneline"])
133 assert result.exit_code == 0
134 assert "First take" in result.output
135 assert "Author:" not in result.output
136
137 def test_multiple_commits_newest_first(self, repo: pathlib.Path) -> None:
138 _write(repo, "a.mid")
139 runner.invoke(cli, ["commit", "-m", "First"])
140 _write(repo, "b.mid")
141 runner.invoke(cli, ["commit", "-m", "Second"])
142 result = runner.invoke(cli, ["log", "--oneline"])
143 lines = [l for l in result.output.strip().splitlines() if l.strip()]
144 assert "Second" in lines[0]
145 assert "First" in lines[1]
146
147
148 class TestBranch:
149 def test_list_shows_main(self, repo: pathlib.Path) -> None:
150 result = runner.invoke(cli, ["branch"])
151 assert result.exit_code == 0
152 assert "main" in result.output
153 assert "* " in result.output
154
155 def test_create_branch(self, repo: pathlib.Path) -> None:
156 result = runner.invoke(cli, ["branch", "feature/chorus"])
157 assert result.exit_code == 0
158 result = runner.invoke(cli, ["branch"])
159 assert "feature/chorus" in result.output
160
161 def test_delete_branch(self, repo: pathlib.Path) -> None:
162 runner.invoke(cli, ["branch", "feature/x"])
163 result = runner.invoke(cli, ["branch", "--delete", "feature/x"])
164 assert result.exit_code == 0
165 result = runner.invoke(cli, ["branch"])
166 assert "feature/x" not in result.output
167
168
169 class TestCheckout:
170 def test_create_and_switch(self, repo: pathlib.Path) -> None:
171 result = runner.invoke(cli, ["checkout", "-b", "feature/chorus"])
172 assert result.exit_code == 0
173 assert "feature/chorus" in result.output
174 status = runner.invoke(cli, ["status"])
175 assert "feature/chorus" in status.output
176
177 def test_switch_existing_branch(self, repo: pathlib.Path) -> None:
178 runner.invoke(cli, ["checkout", "-b", "feature/chorus"])
179 runner.invoke(cli, ["checkout", "main"])
180 result = runner.invoke(cli, ["status"])
181 assert "main" in result.output
182
183 def test_already_on_branch(self, repo: pathlib.Path) -> None:
184 result = runner.invoke(cli, ["checkout", "main"])
185 assert result.exit_code == 0
186 assert "Already on" in result.output
187
188
189 class TestMerge:
190 def test_fast_forward(self, repo: pathlib.Path) -> None:
191 _write(repo, "verse.mid")
192 runner.invoke(cli, ["commit", "-m", "Verse"])
193 runner.invoke(cli, ["checkout", "-b", "feature/chorus"])
194 _write(repo, "chorus.mid")
195 runner.invoke(cli, ["commit", "-m", "Add chorus"])
196 runner.invoke(cli, ["checkout", "main"])
197 result = runner.invoke(cli, ["merge", "feature/chorus"])
198 assert result.exit_code == 0
199 assert "Fast-forward" in result.output
200
201 def test_clean_three_way_merge(self, repo: pathlib.Path) -> None:
202 _write(repo, "base.mid")
203 runner.invoke(cli, ["commit", "-m", "Base"])
204 runner.invoke(cli, ["checkout", "-b", "branch-a"])
205 _write(repo, "a.mid")
206 runner.invoke(cli, ["commit", "-m", "Add A"])
207 runner.invoke(cli, ["checkout", "main"])
208 runner.invoke(cli, ["checkout", "-b", "branch-b"])
209 _write(repo, "b.mid")
210 runner.invoke(cli, ["commit", "-m", "Add B"])
211 runner.invoke(cli, ["checkout", "main"])
212 result = runner.invoke(cli, ["merge", "branch-a"])
213 assert result.exit_code == 0
214
215 def test_cannot_merge_self(self, repo: pathlib.Path) -> None:
216 result = runner.invoke(cli, ["merge", "main"])
217 assert result.exit_code != 0
218
219
220 class TestDiff:
221 def test_no_diff_clean(self, repo: pathlib.Path) -> None:
222 _write(repo, "beat.mid")
223 runner.invoke(cli, ["commit", "-m", "First"])
224 result = runner.invoke(cli, ["diff"])
225 assert result.exit_code == 0
226 assert "No differences" in result.output
227
228 def test_shows_new_file(self, repo: pathlib.Path) -> None:
229 _write(repo, "beat.mid")
230 runner.invoke(cli, ["commit", "-m", "First"])
231 _write(repo, "lead.mid")
232 result = runner.invoke(cli, ["diff"])
233 assert result.exit_code == 0
234 assert "lead.mid" in result.output
235
236
237 class TestTag:
238 def test_add_and_list(self, repo: pathlib.Path) -> None:
239 _write(repo, "beat.mid")
240 runner.invoke(cli, ["commit", "-m", "Tagged take"])
241 result = runner.invoke(cli, ["tag", "add", "emotion:joyful"])
242 assert result.exit_code == 0
243 result = runner.invoke(cli, ["tag", "list"])
244 assert "emotion:joyful" in result.output
245
246
247 class TestStash:
248 def test_stash_and_pop(self, repo: pathlib.Path) -> None:
249 _write(repo, "beat.mid")
250 runner.invoke(cli, ["commit", "-m", "First"])
251 _write(repo, "lead.mid")
252 result = runner.invoke(cli, ["stash"])
253 assert result.exit_code == 0
254 assert not (repo / "muse-work" / "lead.mid").exists()
255 result = runner.invoke(cli, ["stash", "pop"])
256 assert result.exit_code == 0
257 assert (repo / "muse-work" / "lead.mid").exists()