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