gabriel / muse public
test_semantic_cherry_pick_supercharge.py python
427 lines 18.6 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """TDD supercharge tests for ``muse code semantic-cherry-pick``.
2
3 Gaps being closed
4 -----------------
5 - ``-j`` alias for ``--json``
6 - ``-n`` alias for ``--dry-run``
7 - ``exit_code`` and ``duration_ms`` in JSON envelope
8 - ``_CherryPickResultJson`` top-level TypedDict
9 - Security: null byte / ANSI in address in JSON output
10 - ``schema_version`` is a non-empty string
11 - ``branch`` field present and correct in JSON
12 - Docstring completeness for ``register()`` and ``run()``
13
14 Test classes
15 ------------
16 TestJsonAlias -j alias works identically to --json
17 TestJsonEnvelope exit_code, duration_ms, schema_version in all JSON
18 TestTypedDict _CherryPickResultJson importable and typed
19 TestCLIAliases -n alias for dry-run
20 TestCLISecurity null byte / ANSI in address in JSON
21 TestDocstrings register() and run() docstring completeness
22 """
23
24 from __future__ import annotations
25
26 import json
27 import pathlib
28 import textwrap
29 import typing
30
31 import pytest
32
33 from tests.cli_test_helper import CliRunner
34
35 cli = None
36 runner = CliRunner()
37
38
39 # ---------------------------------------------------------------------------
40 # Helpers
41 # ---------------------------------------------------------------------------
42
43
44 def _run(root: pathlib.Path, *args: str):
45 return runner.invoke(cli, list(args), env={"MUSE_REPO_ROOT": str(root)})
46
47
48 def _commit(root: pathlib.Path, msg: str = "commit") -> None:
49 r = _run(root, "code", "add", ".")
50 assert r.exit_code == 0, r.output
51 r2 = _run(root, "commit", "-m", msg)
52 assert r2.exit_code == 0, r2.output
53
54
55 # ---------------------------------------------------------------------------
56 # Fixture — two-commit repo with an evolving function
57 # ---------------------------------------------------------------------------
58
59
60 @pytest.fixture
61 def two_commit_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
62 """Code repo: commit 1 has v1 of compute_total; commit 2 has v2."""
63 monkeypatch.chdir(tmp_path)
64 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
65 r = _run(tmp_path, "init", "--domain", "code")
66 assert r.exit_code == 0, r.output
67
68 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
69 def compute_total(items: list[int]) -> int:
70 return sum(items)
71
72 def helper() -> None:
73 pass
74 """))
75 _commit(tmp_path, "v1")
76
77 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
78 def compute_total(items: list[int]) -> int:
79 return sum(items) * 2 # v2 — different body
80
81 def helper() -> None:
82 pass
83 """))
84 _commit(tmp_path, "v2")
85 return tmp_path
86
87
88 def _first_commit_id(root: pathlib.Path, runner_obj, cli_obj) -> str:
89 """Return the commit_id of the first (oldest) commit."""
90 r = runner_obj.invoke(cli_obj, ["log", "--json"], env={"MUSE_REPO_ROOT": str(root)})
91 commits = json.loads(r.output)["commits"]
92 return commits[-1]["commit_id"] # oldest
93
94
95 # ---------------------------------------------------------------------------
96 # 1. -j alias
97 # ---------------------------------------------------------------------------
98
99
100 class TestJsonAlias:
101 def test_j_alias_exits_zero(self, two_commit_repo: pathlib.Path) -> None:
102 first = _first_commit_id(two_commit_repo, runner, cli)
103 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
104 "billing.py::compute_total", "--from", first, "-j")
105 assert r.exit_code == 0, r.output
106
107 def test_j_alias_emits_valid_json(self, two_commit_repo: pathlib.Path) -> None:
108 first = _first_commit_id(two_commit_repo, runner, cli)
109 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
110 "billing.py::compute_total", "--from", first, "-j")
111 data = json.loads(r.output.strip())
112 assert isinstance(data, dict)
113
114 def test_j_alias_has_results(self, two_commit_repo: pathlib.Path) -> None:
115 first = _first_commit_id(two_commit_repo, runner, cli)
116 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
117 "billing.py::compute_total", "--from", first, "-j")
118 data = json.loads(r.output)
119 assert "results" in data
120
121 def test_j_alias_same_keys_as_json_flag(self, two_commit_repo: pathlib.Path) -> None:
122 first = _first_commit_id(two_commit_repo, runner, cli)
123 # Use dry-run so both calls see the same state
124 r1 = _run(two_commit_repo, "code", "semantic-cherry-pick",
125 "billing.py::compute_total", "--from", first, "--json", "--dry-run")
126 r2 = _run(two_commit_repo, "code", "semantic-cherry-pick",
127 "billing.py::compute_total", "--from", first, "-j", "--dry-run")
128 d1, d2 = json.loads(r1.output), json.loads(r2.output)
129 d1.pop("duration_ms", None)
130 d2.pop("duration_ms", None)
131 assert set(d1.keys()) == set(d2.keys())
132
133 def test_j_alias_same_applied_count(self, two_commit_repo: pathlib.Path) -> None:
134 first = _first_commit_id(two_commit_repo, runner, cli)
135 r1 = _run(two_commit_repo, "code", "semantic-cherry-pick",
136 "billing.py::compute_total", "--from", first, "--json", "--dry-run")
137 r2 = _run(two_commit_repo, "code", "semantic-cherry-pick",
138 "billing.py::compute_total", "--from", first, "-j", "--dry-run")
139 assert json.loads(r1.output)["applied"] == json.loads(r2.output)["applied"]
140
141 def test_j_with_dry_run(self, two_commit_repo: pathlib.Path) -> None:
142 first = _first_commit_id(two_commit_repo, runner, cli)
143 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
144 "billing.py::compute_total", "--from", first, "-j", "--dry-run")
145 data = json.loads(r.output)
146 assert data["dry_run"] is True
147
148 def test_j_alias_dry_run_does_not_write(self, two_commit_repo: pathlib.Path) -> None:
149 first = _first_commit_id(two_commit_repo, runner, cli)
150 original = (two_commit_repo / "billing.py").read_text()
151 _run(two_commit_repo, "code", "semantic-cherry-pick",
152 "billing.py::compute_total", "--from", first, "-j", "--dry-run")
153 assert (two_commit_repo / "billing.py").read_text() == original
154
155
156 # ---------------------------------------------------------------------------
157 # 2. JSON envelope: exit_code + duration_ms
158 # ---------------------------------------------------------------------------
159
160
161 class TestJsonEnvelope:
162 def test_has_exit_code(self, two_commit_repo: pathlib.Path) -> None:
163 first = _first_commit_id(two_commit_repo, runner, cli)
164 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
165 "billing.py::compute_total", "--from", first, "--json", "--dry-run")
166 data = json.loads(r.output)
167 assert "exit_code" in data
168
169 def test_exit_code_is_zero(self, two_commit_repo: pathlib.Path) -> None:
170 first = _first_commit_id(two_commit_repo, runner, cli)
171 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
172 "billing.py::compute_total", "--from", first, "--json", "--dry-run")
173 data = json.loads(r.output)
174 assert data["exit_code"] == 0
175
176 def test_has_duration_ms(self, two_commit_repo: pathlib.Path) -> None:
177 first = _first_commit_id(two_commit_repo, runner, cli)
178 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
179 "billing.py::compute_total", "--from", first, "--json", "--dry-run")
180 data = json.loads(r.output)
181 assert "duration_ms" in data
182
183 def test_duration_ms_is_positive_float(self, two_commit_repo: pathlib.Path) -> None:
184 first = _first_commit_id(two_commit_repo, runner, cli)
185 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
186 "billing.py::compute_total", "--from", first, "--json", "--dry-run")
187 data = json.loads(r.output)
188 assert isinstance(data["duration_ms"], float)
189 assert data["duration_ms"] > 0
190
191 def test_schema_version_is_nonempty_string(self, two_commit_repo: pathlib.Path) -> None:
192 first = _first_commit_id(two_commit_repo, runner, cli)
193 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
194 "billing.py::compute_total", "--from", first, "--json", "--dry-run")
195 data = json.loads(r.output)
196 assert isinstance(data["schema"], int)
197 assert data["schema"] > 0
198
199 def test_branch_field_present(self, two_commit_repo: pathlib.Path) -> None:
200 first = _first_commit_id(two_commit_repo, runner, cli)
201 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
202 "billing.py::compute_total", "--from", first, "--json", "--dry-run")
203 data = json.loads(r.output)
204 assert "branch" in data
205 assert isinstance(data["branch"], str)
206
207 def test_exit_code_present_after_apply(self, two_commit_repo: pathlib.Path) -> None:
208 first = _first_commit_id(two_commit_repo, runner, cli)
209 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
210 "billing.py::compute_total", "--from", first, "--json")
211 data = json.loads(r.output)
212 assert data["exit_code"] == 0
213
214 def test_duration_ms_present_after_apply(self, two_commit_repo: pathlib.Path) -> None:
215 first = _first_commit_id(two_commit_repo, runner, cli)
216 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
217 "billing.py::compute_total", "--from", first, "--json")
218 data = json.loads(r.output)
219 assert "duration_ms" in data
220
221 def test_not_found_still_has_exit_code(self, two_commit_repo: pathlib.Path) -> None:
222 first = _first_commit_id(two_commit_repo, runner, cli)
223 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
224 "billing.py::nonexistent_fn", "--from", first, "--json", "--dry-run")
225 data = json.loads(r.output)
226 assert "exit_code" in data
227 assert data["exit_code"] == 0
228
229 def test_not_found_still_has_duration_ms(self, two_commit_repo: pathlib.Path) -> None:
230 first = _first_commit_id(two_commit_repo, runner, cli)
231 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
232 "billing.py::nonexistent_fn", "--from", first, "--json", "--dry-run")
233 data = json.loads(r.output)
234 assert "duration_ms" in data
235
236
237 # ---------------------------------------------------------------------------
238 # 3. _CherryPickResultJson TypedDict
239 # ---------------------------------------------------------------------------
240
241
242 class TestTypedDict:
243 def test_cherry_pick_result_json_importable(self) -> None:
244 from muse.cli.commands.semantic_cherry_pick import _CherryPickResultJson
245 assert _CherryPickResultJson is not None
246
247 def test_has_exit_code_field(self) -> None:
248 from muse.cli.commands.semantic_cherry_pick import _CherryPickResultJson
249 hints = typing.get_type_hints(_CherryPickResultJson)
250 assert "exit_code" in hints
251
252 def test_has_duration_ms_field(self) -> None:
253 from muse.cli.commands.semantic_cherry_pick import _CherryPickResultJson
254 hints = typing.get_type_hints(_CherryPickResultJson)
255 assert "duration_ms" in hints
256
257 def test_has_schema_version_field(self) -> None:
258 from muse.cli.commands.semantic_cherry_pick import _CherryPickResultJson
259 hints = typing.get_type_hints(_CherryPickResultJson)
260 assert "schema" in hints
261
262 def test_has_results_field(self) -> None:
263 from muse.cli.commands.semantic_cherry_pick import _CherryPickResultJson
264 hints = typing.get_type_hints(_CherryPickResultJson)
265 assert "results" in hints
266
267 def test_has_branch_field(self) -> None:
268 from muse.cli.commands.semantic_cherry_pick import _CherryPickResultJson
269 hints = typing.get_type_hints(_CherryPickResultJson)
270 assert "branch" in hints
271
272 def test_has_dry_run_field(self) -> None:
273 from muse.cli.commands.semantic_cherry_pick import _CherryPickResultJson
274 hints = typing.get_type_hints(_CherryPickResultJson)
275 assert "dry_run" in hints
276
277 def test_has_applied_field(self) -> None:
278 from muse.cli.commands.semantic_cherry_pick import _CherryPickResultJson
279 hints = typing.get_type_hints(_CherryPickResultJson)
280 assert "applied" in hints
281
282 def test_has_failed_field(self) -> None:
283 from muse.cli.commands.semantic_cherry_pick import _CherryPickResultJson
284 hints = typing.get_type_hints(_CherryPickResultJson)
285 assert "failed" in hints
286
287 def test_has_unverified_field(self) -> None:
288 from muse.cli.commands.semantic_cherry_pick import _CherryPickResultJson
289 hints = typing.get_type_hints(_CherryPickResultJson)
290 assert "unverified" in hints
291
292
293 # ---------------------------------------------------------------------------
294 # 4. -n alias for --dry-run
295 # ---------------------------------------------------------------------------
296
297
298 class TestCLIAliases:
299 def test_n_alias_exits_zero(self, two_commit_repo: pathlib.Path) -> None:
300 first = _first_commit_id(two_commit_repo, runner, cli)
301 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
302 "billing.py::compute_total", "--from", first, "-n", "--json")
303 assert r.exit_code == 0, r.output
304
305 def test_n_alias_dry_run_flag_true(self, two_commit_repo: pathlib.Path) -> None:
306 first = _first_commit_id(two_commit_repo, runner, cli)
307 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
308 "billing.py::compute_total", "--from", first, "-n", "--json")
309 data = json.loads(r.output)
310 assert data["dry_run"] is True
311
312 def test_n_alias_does_not_write(self, two_commit_repo: pathlib.Path) -> None:
313 first = _first_commit_id(two_commit_repo, runner, cli)
314 original = (two_commit_repo / "billing.py").read_text()
315 _run(two_commit_repo, "code", "semantic-cherry-pick",
316 "billing.py::compute_total", "--from", first, "-n")
317 assert (two_commit_repo / "billing.py").read_text() == original
318
319 def test_n_alias_same_output_as_dry_run(self, two_commit_repo: pathlib.Path) -> None:
320 first = _first_commit_id(two_commit_repo, runner, cli)
321 r1 = _run(two_commit_repo, "code", "semantic-cherry-pick",
322 "billing.py::compute_total", "--from", first, "--dry-run", "--json")
323 r2 = _run(two_commit_repo, "code", "semantic-cherry-pick",
324 "billing.py::compute_total", "--from", first, "-n", "--json")
325 d1, d2 = json.loads(r1.output), json.loads(r2.output)
326 for k in ("duration_ms", "timestamp"):
327 d1.pop(k, None)
328 d2.pop(k, None)
329 assert d1 == d2
330
331
332 # ---------------------------------------------------------------------------
333 # 5. Security
334 # ---------------------------------------------------------------------------
335
336
337 class TestCLISecurity:
338 def test_null_byte_in_address_not_in_raw_stdout(self, two_commit_repo: pathlib.Path) -> None:
339 first = _first_commit_id(two_commit_repo, runner, cli)
340 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
341 "billing.py::compute\x00total", "--from", first, "--json")
342 # json.dumps encodes null as \u0000 — raw \x00 must not appear
343 assert "\x00" not in r.output
344
345 def test_ansi_escape_not_in_json_stdout(self, two_commit_repo: pathlib.Path) -> None:
346 first = _first_commit_id(two_commit_repo, runner, cli)
347 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
348 "billing.py::compute_total", "--from", first, "--json", "--dry-run")
349 assert "\x1b" not in r.output
350
351 def test_path_traversal_in_address_is_not_found(self, two_commit_repo: pathlib.Path) -> None:
352 first = _first_commit_id(two_commit_repo, runner, cli)
353 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
354 "../../etc/passwd::compute_total", "--from", first, "--json", "--dry-run")
355 data = json.loads(r.output)
356 assert data["results"][0]["status"] == "not_found"
357
358 def test_path_traversal_exit_code_zero(self, two_commit_repo: pathlib.Path) -> None:
359 # not_found is a graceful result — command still exits 0
360 first = _first_commit_id(two_commit_repo, runner, cli)
361 r = _run(two_commit_repo, "code", "semantic-cherry-pick",
362 "../../etc/passwd::compute_total", "--from", first, "--json", "--dry-run")
363 assert r.exit_code == 0
364
365
366 # ---------------------------------------------------------------------------
367 # 6. Docstrings
368 # ---------------------------------------------------------------------------
369
370
371 class TestDocstrings:
372 def test_run_docstring_exists(self) -> None:
373 from muse.cli.commands.semantic_cherry_pick import run
374 assert run.__doc__ is not None
375 assert len(run.__doc__) > 80
376
377 def test_run_docstring_mentions_json(self) -> None:
378 from muse.cli.commands.semantic_cherry_pick import run
379 assert "json" in (run.__doc__ or "").lower()
380
381
382
383 def test_register_docstring_exists(self) -> None:
384 from muse.cli.commands.semantic_cherry_pick import register
385 assert register.__doc__ is not None
386 assert len(register.__doc__) > 80
387
388 def test_register_docstring_mentions_from(self) -> None:
389 from muse.cli.commands.semantic_cherry_pick import register
390 assert "--from" in (register.__doc__ or "")
391
392 def test_register_docstring_mentions_dry_run(self) -> None:
393 from muse.cli.commands.semantic_cherry_pick import register
394 assert "dry-run" in (register.__doc__ or "") or "dry_run" in (register.__doc__ or "")
395
396 def test_register_docstring_mentions_json(self) -> None:
397 from muse.cli.commands.semantic_cherry_pick import register
398 assert "json" in (register.__doc__ or "").lower()
399
400
401 class TestRegisterFlags:
402 def test_default_json_out_is_false(self):
403 import argparse
404 from muse.cli.commands.semantic_cherry_pick import register
405 p = argparse.ArgumentParser()
406 subs = p.add_subparsers()
407 register(subs)
408 args = p.parse_args(["semantic-cherry-pick", "src/billing.py::compute_total", "--from", "HEAD~1"])
409 assert args.json_out is False
410
411 def test_json_flag_sets_json_out(self):
412 import argparse
413 from muse.cli.commands.semantic_cherry_pick import register
414 p = argparse.ArgumentParser()
415 subs = p.add_subparsers()
416 register(subs)
417 args = p.parse_args(["semantic-cherry-pick", "src/billing.py::compute_total", "--from", "HEAD~1", "--json"])
418 assert args.json_out is True
419
420 def test_j_shorthand_sets_json_out(self):
421 import argparse
422 from muse.cli.commands.semantic_cherry_pick import register
423 p = argparse.ArgumentParser()
424 subs = p.add_subparsers()
425 register(subs)
426 args = p.parse_args(["semantic-cherry-pick", "src/billing.py::compute_total", "--from", "HEAD~1", "-j"])
427 assert args.json_out is True
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago