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