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