cgcardona / muse public
test_cli_reset_revert.py python
235 lines 7.7 KB
e6786943 feat: upgrade to Python 3.14, drop from __future__ import annotations Gabriel Cardona <cgcardona@gmail.com> 1d ago
1 """Tests for muse reset and muse revert."""
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 / "muse-work" / filename).write_text(content)
25
26
27 def _commit(msg: str = "initial") -> None:
28 result = runner.invoke(cli, ["commit", "-m", msg])
29 assert result.exit_code == 0, result.output
30
31
32 def _head_id(repo: pathlib.Path) -> str | None:
33 return get_head_commit_id(repo, "main")
34
35
36 # ---------------------------------------------------------------------------
37 # reset
38 # ---------------------------------------------------------------------------
39
40
41 class TestResetSoft:
42 def test_moves_branch_pointer(self, repo: pathlib.Path) -> None:
43 _write(repo, "beat.mid", "v1")
44 _commit("first")
45 first_id = _head_id(repo)
46
47 _write(repo, "beat.mid", "v2")
48 _commit("second")
49 assert _head_id(repo) != first_id
50
51 result = runner.invoke(cli, ["reset", first_id])
52 assert result.exit_code == 0, result.output
53 assert _head_id(repo) == first_id
54
55 def test_soft_preserves_workdir(self, repo: pathlib.Path) -> None:
56 _write(repo, "beat.mid", "v1")
57 _commit("first")
58 first_id = _head_id(repo)
59
60 _write(repo, "lead.mid", "new")
61 _commit("second")
62
63 runner.invoke(cli, ["reset", first_id])
64 # workdir still has lead.mid from second commit (soft = no restore)
65 assert (repo / "muse-work" / "lead.mid").exists()
66
67 def test_soft_output_message(self, repo: pathlib.Path) -> None:
68 _write(repo, "beat.mid")
69 _commit("first")
70 first_id = _head_id(repo)
71 _write(repo, "lead.mid")
72 _commit("second")
73
74 result = runner.invoke(cli, ["reset", first_id])
75 assert "Moved" in result.output or first_id[:8] in result.output
76
77 def test_reset_unknown_ref_errors(self, repo: pathlib.Path) -> None:
78 _write(repo, "beat.mid")
79 _commit("only")
80 result = runner.invoke(cli, ["reset", "deadbeef"])
81 assert result.exit_code != 0
82 assert "not found" in result.output.lower() or "deadbeef" in result.output
83
84
85 class TestResetHard:
86 def test_moves_branch_pointer(self, repo: pathlib.Path) -> None:
87 _write(repo, "beat.mid", "v1")
88 _commit("first")
89 first_id = _head_id(repo)
90
91 _write(repo, "beat.mid", "v2")
92 _commit("second")
93
94 result = runner.invoke(cli, ["reset", "--hard", first_id])
95 assert result.exit_code == 0, result.output
96 assert _head_id(repo) == first_id
97
98 def test_restores_workdir(self, repo: pathlib.Path) -> None:
99 _write(repo, "beat.mid", "v1")
100 _commit("first")
101 first_id = _head_id(repo)
102
103 _write(repo, "lead.mid", "new")
104 _commit("second")
105
106 runner.invoke(cli, ["reset", "--hard", first_id])
107 # After hard reset, workdir should reflect first commit (no lead.mid)
108 assert not (repo / "muse-work" / "lead.mid").exists()
109 assert (repo / "muse-work" / "beat.mid").exists()
110
111 def test_restores_file_content(self, repo: pathlib.Path) -> None:
112 _write(repo, "beat.mid", "original")
113 _commit("first")
114 first_id = _head_id(repo)
115
116 _write(repo, "beat.mid", "modified")
117 _commit("second")
118
119 runner.invoke(cli, ["reset", "--hard", first_id])
120 assert (repo / "muse-work" / "beat.mid").read_text() == "original"
121
122 def test_hard_output_shows_commit(self, repo: pathlib.Path) -> None:
123 _write(repo, "beat.mid")
124 _commit("the target")
125 first_id = _head_id(repo)
126 _write(repo, "lead.mid")
127 _commit("second")
128
129 result = runner.invoke(cli, ["reset", "--hard", first_id])
130 assert result.exit_code == 0
131 assert "HEAD is now at" in result.output
132
133
134 # ---------------------------------------------------------------------------
135 # revert
136 # ---------------------------------------------------------------------------
137
138
139 class TestRevert:
140 def test_creates_new_commit(self, repo: pathlib.Path) -> None:
141 _write(repo, "beat.mid")
142 _commit("add beat")
143 before_id = _head_id(repo)
144
145 _write(repo, "lead.mid")
146 _commit("add lead")
147 after_id = _head_id(repo)
148
149 result = runner.invoke(cli, ["revert", after_id])
150 assert result.exit_code == 0, result.output
151 new_id = _head_id(repo)
152 assert new_id not in (before_id, after_id)
153
154 def test_revert_restores_parent_state(self, repo: pathlib.Path) -> None:
155 _write(repo, "beat.mid", "original")
156 _commit("first")
157
158 _write(repo, "beat.mid", "changed")
159 _commit("second")
160 second_id = _head_id(repo)
161
162 runner.invoke(cli, ["revert", second_id])
163 assert (repo / "muse-work" / "beat.mid").read_text() == "original"
164
165 def test_revert_default_message_includes_original(self, repo: pathlib.Path) -> None:
166 # Need a base commit first so "my change" is not the root
167 _write(repo, "base.mid", "base")
168 _commit("base")
169
170 _write(repo, "beat.mid")
171 _commit("my change")
172 commit_id = _head_id(repo)
173
174 _write(repo, "lead.mid")
175 _commit("third")
176
177 result = runner.invoke(cli, ["revert", commit_id])
178 assert result.exit_code == 0
179 assert "my change" in result.output or "Revert" in result.output
180
181 def test_revert_custom_message(self, repo: pathlib.Path) -> None:
182 _write(repo, "base.mid", "base")
183 _commit("base")
184 _write(repo, "beat.mid")
185 _commit("to revert")
186 commit_id = _head_id(repo)
187 _write(repo, "lead.mid")
188 _commit("third")
189
190 result = runner.invoke(cli, ["revert", "-m", "undo that change", commit_id])
191 assert result.exit_code == 0, result.output
192 assert "undo that change" in result.output
193
194 def test_revert_no_commit_flag(self, repo: pathlib.Path) -> None:
195 _write(repo, "base.mid", "base")
196 _commit("base")
197
198 _write(repo, "beat.mid")
199 _commit("second")
200 second_id = _head_id(repo)
201
202 _write(repo, "lead.mid")
203 _commit("third")
204
205 result = runner.invoke(cli, ["revert", "--no-commit", second_id])
206 assert result.exit_code == 0, result.output
207 assert "muse-work" in result.output or "applied" in result.output.lower()
208 # HEAD should not have advanced
209 assert _head_id(repo) != second_id # third is still HEAD
210
211 def test_revert_unknown_ref_errors(self, repo: pathlib.Path) -> None:
212 _write(repo, "beat.mid")
213 _commit("only")
214 result = runner.invoke(cli, ["revert", "deadbeef"])
215 assert result.exit_code != 0
216
217 def test_revert_root_commit_errors(self, repo: pathlib.Path) -> None:
218 _write(repo, "beat.mid")
219 _commit("root")
220 root_id = _head_id(repo)
221
222 result = runner.invoke(cli, ["revert", root_id])
223 assert result.exit_code != 0
224 assert "root" in result.output.lower() or "parent" in result.output.lower()
225
226 def test_revert_removes_added_file(self, repo: pathlib.Path) -> None:
227 _write(repo, "beat.mid", "base")
228 _commit("base")
229
230 _write(repo, "lead.mid", "added")
231 _commit("add lead")
232 lead_commit = _head_id(repo)
233
234 runner.invoke(cli, ["revert", lead_commit])
235 assert not (repo / "muse-work" / "lead.mid").exists()