gabriel / muse public
test_reset_supercharge.py python
313 lines 13.7 KB
Raw
sha256:1d3f5470f45db58e32047678debc9438fdded1b2c7332cc743d2b8be32fdafc8 fixing more broken tests Human patch 3 days ago
1 """Supercharge tests for ``muse reset``.
2
3 Coverage tiers
4 --------------
5 Unit — TypedDict shape, alias registration, docstring completeness.
6 Integration — ``-j`` alias, ``-n`` alias, ``exit_code``/``duration_ms`` in JSON,
7 ``schema_version`` field, dry-run JSON envelope, applied JSON envelope.
8 Security — null byte / ANSI injection in ref and format args.
9 """
10
11 from __future__ import annotations
12
13 import argparse
14 import json
15 import os
16 import pathlib
17
18 import pytest
19
20 from tests.cli_test_helper import CliRunner, InvokeResult
21
22 runner = CliRunner()
23
24
25 # ──────────────────────────────────────────────────────────────────────────────
26 # Helpers
27 # ──────────────────────────────────────────────────────────────────────────────
28
29
30 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
31 saved = os.getcwd()
32 try:
33 os.chdir(repo)
34 return runner.invoke(None, args)
35 finally:
36 os.chdir(saved)
37
38
39 def _reset(repo: pathlib.Path, *extra: str) -> InvokeResult:
40 return _invoke(repo, ["reset", *extra])
41
42
43 def _commit(repo: pathlib.Path, message: str) -> str:
44 import re
45
46 _invoke(repo, ["code", "add", "."])
47 result = _invoke(repo, ["commit", "-m", message])
48 m = re.search(r"sha256:[0-9a-f]{64}", result.output + (result.stderr or ""))
49 return m.group(0) if m else ""
50
51
52 @pytest.fixture()
53 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
54 saved = os.getcwd()
55 try:
56 os.chdir(tmp_path)
57 runner.invoke(None, ["init"])
58 finally:
59 os.chdir(saved)
60 (tmp_path / "a.py").write_text("x = 1\n")
61 _commit(tmp_path, "initial")
62 (tmp_path / "b.py").write_text("y = 2\n")
63 _commit(tmp_path, "add b")
64 return tmp_path
65
66
67 @pytest.fixture()
68 def c1_id(repo: pathlib.Path) -> str:
69 """Full commit ID of the first commit (HEAD~1)."""
70 from muse.core.refs import get_head_commit_id
71 from muse.core.commits import read_commit
72
73 head_id = get_head_commit_id(repo, "main") or ""
74 head = read_commit(repo, head_id)
75 return (head.parent_commit_id or "") if head else ""
76
77
78 # ──────────────────────────────────────────────────────────────────────────────
79 # Unit — TypedDict
80 # ──────────────────────────────────────────────────────────────────────────────
81
82
83 class TestTypedDict:
84 def test_reset_json_typed_dict_exists(self) -> None:
85 from muse.cli.commands.reset import _ResetJson # noqa: F401
86
87 def test_reset_json_typed_dict_has_exit_code(self) -> None:
88 from muse.cli.commands.reset import _ResetJson
89 import typing
90
91 hints = typing.get_type_hints(_ResetJson)
92 assert "exit_code" in hints, "exit_code missing from _ResetJson"
93
94 def test_reset_json_typed_dict_has_duration_ms(self) -> None:
95 from muse.cli.commands.reset import _ResetJson
96 import typing
97
98 hints = typing.get_type_hints(_ResetJson)
99 assert "duration_ms" in hints, "duration_ms missing from _ResetJson"
100
101 def test_reset_json_typed_dict_has_schema_version(self) -> None:
102 from muse.cli.commands.reset import _ResetJson
103 import typing
104
105 hints = typing.get_type_hints(_ResetJson)
106 assert "schema" in hints, "schema_version missing from _ResetJson"
107
108 def test_reset_json_typed_dict_has_all_core_fields(self) -> None:
109 from muse.cli.commands.reset import _ResetJson
110 import typing
111
112 hints = typing.get_type_hints(_ResetJson)
113 required = {"branch", "ref", "old_commit_id", "new_commit_id", "snapshot_id", "mode", "dry_run"}
114 missing = required - set(hints)
115 assert not missing, f"Missing fields in _ResetJson: {missing}"
116
117
118 # ──────────────────────────────────────────────────────────────────────────────
119 # Unit — alias registration
120 # ──────────────────────────────────────────────────────────────────────────────
121
122
123 class TestAliasRegistration:
124 def _make_parser(self) -> "argparse.ArgumentParser":
125 import argparse
126 from muse.cli.commands.reset import register
127
128 p = argparse.ArgumentParser()
129 sub = p.add_subparsers()
130 register(sub)
131 return p
132
133 def test_j_alias_sets_json_fmt(self) -> None:
134 p = self._make_parser()
135 ns = p.parse_args(["reset", "HEAD~1", "-j"])
136 assert ns.json_out is True
137
138 def test_n_alias_sets_dry_run(self) -> None:
139 p = self._make_parser()
140 ns = p.parse_args(["reset", "HEAD~1", "-n"])
141 assert ns.dry_run is True
142
143 def test_j_and_n_together(self) -> None:
144 p = self._make_parser()
145 ns = p.parse_args(["reset", "HEAD~1", "-j", "-n"])
146 assert ns.json_out is True
147 assert ns.dry_run is True
148
149
150 # ──────────────────────────────────────────────────────────────────────────────
151 # Integration — -j alias produces identical output to --json
152 # ──────────────────────────────────────────────────────────────────────────────
153
154
155 class TestJsonAlias:
156 def test_j_alias_exit_code_zero(self, repo: pathlib.Path, c1_id: str) -> None:
157 result = _reset(repo, c1_id, "-j")
158 assert result.exit_code == 0
159
160 def test_j_alias_output_is_valid_json(self, repo: pathlib.Path, c1_id: str) -> None:
161 result = _reset(repo, c1_id, "-j")
162 data = json.loads(result.output)
163 assert isinstance(data, dict)
164
165 def test_j_alias_same_keys_as_json_flag(self, repo: pathlib.Path, c1_id: str) -> None:
166 # Use dry-run so neither call actually moves HEAD; both see same state.
167 r_json = _reset(repo, c1_id, "--json", "--dry-run")
168 r_j = _reset(repo, c1_id, "-j", "--dry-run")
169 assert set(json.loads(r_json.output)) == set(json.loads(r_j.output))
170
171
172 # ──────────────────────────────────────────────────────────────────────────────
173 # Integration — -n alias for --dry-run
174 # ──────────────────────────────────────────────────────────────────────────────
175
176
177 class TestDryRunAlias:
178 def test_n_alias_no_write(self, repo: pathlib.Path, c1_id: str) -> None:
179 from muse.core.refs import get_head_commit_id
180
181 before = get_head_commit_id(repo, "main")
182 _reset(repo, c1_id, "-n")
183 after = get_head_commit_id(repo, "main")
184 assert before == after, "-n should not advance HEAD"
185
186 def test_n_alias_json_dry_run_true(self, repo: pathlib.Path, c1_id: str) -> None:
187 result = _reset(repo, c1_id, "-n", "-j")
188 data = json.loads(result.output)
189 assert data["dry_run"] is True
190
191 def test_n_alias_exit_code_zero(self, repo: pathlib.Path, c1_id: str) -> None:
192 result = _reset(repo, c1_id, "-n")
193 assert result.exit_code == 0
194
195
196 # ──────────────────────────────────────────────────────────────────────────────
197 # Integration — JSON envelope completeness
198 # ──────────────────────────────────────────────────────────────────────────────
199
200
201 class TestJsonEnvelope:
202 def test_applied_json_has_exit_code(self, repo: pathlib.Path, c1_id: str) -> None:
203 result = _reset(repo, c1_id, "--json")
204 data = json.loads(result.output)
205 assert "exit_code" in data
206
207 def test_applied_json_exit_code_is_zero(self, repo: pathlib.Path, c1_id: str) -> None:
208 result = _reset(repo, c1_id, "--json")
209 data = json.loads(result.output)
210 assert data["exit_code"] == 0
211
212 def test_applied_json_has_duration_ms(self, repo: pathlib.Path, c1_id: str) -> None:
213 result = _reset(repo, c1_id, "--json")
214 data = json.loads(result.output)
215 assert "duration_ms" in data
216
217 def test_applied_json_duration_ms_is_float(self, repo: pathlib.Path, c1_id: str) -> None:
218 result = _reset(repo, c1_id, "--json")
219 data = json.loads(result.output)
220 assert isinstance(data["duration_ms"], float)
221 assert data["duration_ms"] >= 0.0
222
223 def test_applied_json_has_schema_version(self, repo: pathlib.Path, c1_id: str) -> None:
224 result = _reset(repo, c1_id, "--json")
225 data = json.loads(result.output)
226 assert "schema" in data
227
228 def test_applied_json_schema_version_is_string(self, repo: pathlib.Path, c1_id: str) -> None:
229 result = _reset(repo, c1_id, "--json")
230 data = json.loads(result.output)
231 assert isinstance(data["schema"], int)
232 assert data["schema"] > 0
233
234 def test_dry_run_json_has_exit_code(self, repo: pathlib.Path, c1_id: str) -> None:
235 result = _reset(repo, c1_id, "--json", "--dry-run")
236 data = json.loads(result.output)
237 assert "exit_code" in data
238
239 def test_dry_run_json_exit_code_is_zero(self, repo: pathlib.Path, c1_id: str) -> None:
240 result = _reset(repo, c1_id, "--json", "--dry-run")
241 data = json.loads(result.output)
242 assert data["exit_code"] == 0
243
244 def test_dry_run_json_has_duration_ms(self, repo: pathlib.Path, c1_id: str) -> None:
245 result = _reset(repo, c1_id, "--json", "--dry-run")
246 data = json.loads(result.output)
247 assert "duration_ms" in data
248
249 def test_dry_run_json_duration_ms_is_float(self, repo: pathlib.Path, c1_id: str) -> None:
250 result = _reset(repo, c1_id, "--json", "--dry-run")
251 data = json.loads(result.output)
252 assert isinstance(data["duration_ms"], float)
253 assert data["duration_ms"] >= 0.0
254
255 def test_dry_run_json_has_schema_version(self, repo: pathlib.Path, c1_id: str) -> None:
256 result = _reset(repo, c1_id, "--json", "--dry-run")
257 data = json.loads(result.output)
258 assert "schema" in data
259
260
261 # ──────────────────────────────────────────────────────────────────────────────
262 # Security — input sanitization
263 # ──────────────────────────────────────────────────────────────────────────────
264
265
266 class TestSecurity:
267 def test_null_byte_in_ref_does_not_crash(self, repo: pathlib.Path) -> None:
268 result = _reset(repo, "HEAD\x00malicious")
269 assert result.exit_code != 0
270
271 def test_ansi_in_ref_not_echoed_raw(self, repo: pathlib.Path) -> None:
272 result = _reset(repo, "\x1b[31mred\x1b[0m")
273 combined = result.output + (result.stderr or "")
274 assert "\x1b[31m" not in combined
275
276 def test_null_byte_in_format_does_not_crash(self, repo: pathlib.Path) -> None:
277 result = _reset(repo, "HEAD~1", "--format", "json\x00malicious")
278 assert result.exit_code != 0
279
280 def test_ansi_in_format_not_echoed_raw(self, repo: pathlib.Path) -> None:
281 result = _reset(repo, "HEAD~1", "--format", "\x1b[31mred\x1b[0m")
282 combined = result.output + (result.stderr or "")
283 assert "\x1b[31m" not in combined
284
285
286 # ──────────────────────────────────────────────────────────────────────────────
287 # Unit — docstrings
288 # ──────────────────────────────────────────────────────────────────────────────
289
290
291 class TestDocstrings:
292 def test_register_has_docstring(self) -> None:
293 from muse.cli.commands.reset import register
294
295 assert register.__doc__ and len(register.__doc__.strip()) > 20
296
297 def test_register_docstring_mentions_flags(self) -> None:
298 from muse.cli.commands.reset import register
299
300 doc = register.__doc__ or ""
301 assert "--hard" in doc or "hard" in doc.lower()
302 assert "--dry-run" in doc or "dry_run" in doc or "dry-run" in doc.lower()
303
304 def test_run_has_docstring(self) -> None:
305 from muse.cli.commands.reset import run
306
307 assert run.__doc__ and len(run.__doc__.strip()) > 20
308
309 def test_run_docstring_mentions_schema_version(self) -> None:
310 from muse.cli.commands.reset import run
311
312 doc = run.__doc__ or ""
313 assert "json" in doc.lower() or "exit_code" in doc
File History 1 commit
sha256:1d3f5470f45db58e32047678debc9438fdded1b2c7332cc743d2b8be32fdafc8 fixing more broken tests Human patch 3 days ago