gabriel / muse public
test_stable_supercharge.py python
460 lines 20.0 KB
Raw
sha256:51116ec824246acde6abf729e6ba854c223dc5173eff31a645520208023b0652 refactor(bridge): comprehensive spec sweep — close all issu… Sonnet 4.6 minor ⚠ breaking 28 days ago
1 """Supercharge tests for ``muse code stable``.
2
3 Tiers
4 -----
5 Unit — TypedDict shape, alias registration, docstring completeness.
6 Integration — -j alias, exit_code/duration_ms/schema_version in envelope,
7 filter flags, --since, truncation flag, empty-repo edge case.
8 End-to-end — full CLI invocation; --json vs -j parity; --kind/--language filters.
9 Stress — many commits; concurrent invocations on separate repos.
10 Data integrity — stability counts correct; since_start_of_range semantics;
11 ranked order is descending.
12 Security — ANSI/null in --kind, --language, --since args.
13 Performance — duration_ms present and reasonable.
14 """
15
16 from __future__ import annotations
17 from collections.abc import Mapping
18
19 import json
20 import os
21 import pathlib
22 import textwrap
23 import threading
24
25 import pytest
26
27 from tests.cli_test_helper import CliRunner, InvokeResult
28
29 runner = CliRunner()
30
31
32 # ──────────────────────────────────────────────────────────────────────────────
33 # Helpers
34 # ──────────────────────────────────────────────────────────────────────────────
35
36
37 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
38 saved = os.getcwd()
39 try:
40 os.chdir(repo)
41 return runner.invoke(None, args)
42 finally:
43 os.chdir(saved)
44
45
46 def _stable(repo: pathlib.Path, *args: str) -> InvokeResult:
47 return _invoke(repo, ["code", "stable", *args])
48
49
50 def _commit(repo: pathlib.Path, files: Mapping[str, str], message: str) -> None:
51 for name, content in files.items():
52 path = repo / name
53 path.parent.mkdir(parents=True, exist_ok=True)
54 path.write_text(content, encoding="utf-8")
55 saved = os.getcwd()
56 try:
57 os.chdir(repo)
58 runner.invoke(None, ["code", "add", "."])
59 runner.invoke(None, ["commit", "-m", message])
60 finally:
61 os.chdir(saved)
62
63
64 @pytest.fixture()
65 def stable_repo(tmp_path: pathlib.Path) -> pathlib.Path:
66 """Repo with two commits.
67
68 Commit 1: a.py (stable — never modified again) + b.py
69 Commit 2: b.py modified (hot), a.py untouched (stable)
70 """
71 saved = os.getcwd()
72 try:
73 os.chdir(tmp_path)
74 runner.invoke(None, ["init"])
75 finally:
76 os.chdir(saved)
77
78 _commit(tmp_path, {
79 "a.py": textwrap.dedent("""\
80 def stable_fn():
81 return 42
82 """),
83 "b.py": textwrap.dedent("""\
84 def hot_fn():
85 return 1
86 """),
87 }, "initial")
88
89 _commit(tmp_path, {
90 "b.py": textwrap.dedent("""\
91 def hot_fn():
92 return 2
93 """),
94 }, "modify b")
95
96 return tmp_path
97
98
99 # ──────────────────────────────────────────────────────────────────────────────
100 # Unit — TypedDict
101 # ──────────────────────────────────────────────────────────────────────────────
102
103
104 class TestTypedDict:
105 def test_stable_json_typed_dict_exists(self) -> None:
106 from muse.cli.commands.stable import _StableJson # noqa: F401
107
108 def test_has_exit_code(self) -> None:
109 import typing
110 from muse.cli.commands.stable import _StableJson
111 assert "exit_code" in typing.get_type_hints(_StableJson)
112
113 def test_has_duration_ms(self) -> None:
114 import typing
115 from muse.cli.commands.stable import _StableJson
116 assert "duration_ms" in typing.get_type_hints(_StableJson)
117
118 def test_has_schema(self) -> None:
119 import typing
120 from muse.cli.commands.stable import _StableJson
121 assert "schema" in typing.get_type_hints(_StableJson)
122
123 def test_retains_core_fields(self) -> None:
124 import typing
125 from muse.cli.commands.stable import _StableJson
126 hints = typing.get_type_hints(_StableJson)
127 required = {"from_ref", "to_ref", "commits_analysed", "truncated", "filters", "stable"}
128 assert required <= set(hints)
129
130
131 # ──────────────────────────────────────────────────────────────────────────────
132 # Unit — alias registration
133 # ──────────────────────────────────────────────────────────────────────────────
134
135
136 class TestAliasRegistration:
137 def _parser(self) -> "argparse.ArgumentParser":
138 import argparse
139 from muse.cli.commands.stable import register
140 p = argparse.ArgumentParser()
141 sub = p.add_subparsers()
142 register(sub)
143 return p
144
145 def test_j_alias_sets_json_out(self) -> None:
146 ns = self._parser().parse_args(["stable", "-j"])
147 assert ns.json_out is True
148
149 def test_j_alias_with_other_flags(self) -> None:
150 ns = self._parser().parse_args(["stable", "-j", "--top", "5"])
151 assert ns.json_out is True
152 assert ns.top == 5
153
154
155 # ──────────────────────────────────────────────────────────────────────────────
156 # Unit — docstrings
157 # ──────────────────────────────────────────────────────────────────────────────
158
159
160 class TestDocstrings:
161 def test_register_docstring_lists_flags(self) -> None:
162 from muse.cli.commands.stable import register
163 doc = register.__doc__ or ""
164 assert "--json" in doc or "-j" in doc
165
166
167
168 def test_run_docstring_mentions_schema(self) -> None:
169 from muse.cli.commands.stable import run
170 assert "schema" in (run.__doc__ or "")
171
172
173 # ──────────────────────────────────────────────────────────────────────────────
174 # Integration — JSON envelope fields
175 # ──────────────────────────────────────────────────────────────────────────────
176
177
178 class TestJsonEnvelope:
179 def test_has_exit_code(self, stable_repo: pathlib.Path) -> None:
180 data = json.loads(_stable(stable_repo, "--json").output)
181 assert "exit_code" in data
182
183 def test_exit_code_zero(self, stable_repo: pathlib.Path) -> None:
184 data = json.loads(_stable(stable_repo, "--json").output)
185 assert data["exit_code"] == 0
186
187 def test_exit_code_is_int(self, stable_repo: pathlib.Path) -> None:
188 data = json.loads(_stable(stable_repo, "--json").output)
189 assert isinstance(data["exit_code"], int)
190
191 def test_has_duration_ms(self, stable_repo: pathlib.Path) -> None:
192 data = json.loads(_stable(stable_repo, "--json").output)
193 assert "duration_ms" in data
194
195 def test_duration_ms_nonnegative_float(self, stable_repo: pathlib.Path) -> None:
196 data = json.loads(_stable(stable_repo, "--json").output)
197 assert isinstance(data["duration_ms"], float)
198 assert data["duration_ms"] >= 0.0
199
200 def test_has_schema(self, stable_repo: pathlib.Path) -> None:
201 data = json.loads(_stable(stable_repo, "--json").output)
202 assert "schema" in data
203
204 def test_schema_is_int(self, stable_repo: pathlib.Path) -> None:
205 data = json.loads(_stable(stable_repo, "--json").output)
206 assert isinstance(data["schema"], int)
207
208
209 # ──────────────────────────────────────────────────────────────────────────────
210 # Integration — -j alias parity
211 # ──────────────────────────────────────────────────────────────────────────────
212
213
214 class TestJsonAlias:
215 def test_j_alias_exit_code_zero(self, stable_repo: pathlib.Path) -> None:
216 assert _stable(stable_repo, "-j").exit_code == 0
217
218 def test_j_alias_valid_json(self, stable_repo: pathlib.Path) -> None:
219 data = json.loads(_stable(stable_repo, "-j").output)
220 assert isinstance(data, dict)
221
222 def test_j_alias_same_top_level_keys(self, stable_repo: pathlib.Path) -> None:
223 keys_json = set(json.loads(_stable(stable_repo, "--json").output))
224 keys_j = set(json.loads(_stable(stable_repo, "-j").output))
225 assert keys_json == keys_j
226
227 def test_j_alias_stable_list_matches(self, stable_repo: pathlib.Path) -> None:
228 d1 = json.loads(_stable(stable_repo, "--json").output)
229 d2 = json.loads(_stable(stable_repo, "-j").output)
230 assert d1["stable"] == d2["stable"]
231
232
233 # ──────────────────────────────────────────────────────────────────────────────
234 # End-to-end — filters, output shape
235 # ──────────────────────────────────────────────────────────────────────────────
236
237
238 class TestEndToEnd:
239 def test_default_text_output_exits_zero(self, stable_repo: pathlib.Path) -> None:
240 assert _stable(stable_repo).exit_code == 0
241
242 def test_default_text_mentions_bedrock(self, stable_repo: pathlib.Path) -> None:
243 assert "bedrock" in _stable(stable_repo).output.lower()
244
245 def test_json_stable_list_is_list(self, stable_repo: pathlib.Path) -> None:
246 data = json.loads(_stable(stable_repo, "--json").output)
247 assert isinstance(data["stable"], list)
248
249 def test_json_stable_entry_has_address(self, stable_repo: pathlib.Path) -> None:
250 data = json.loads(_stable(stable_repo, "--json").output)
251 assert data["stable"]
252 assert "address" in data["stable"][0]
253
254 def test_json_stable_entry_has_unchanged_for(self, stable_repo: pathlib.Path) -> None:
255 data = json.loads(_stable(stable_repo, "--json").output)
256 assert "unchanged_for" in data["stable"][0]
257
258 def test_json_stable_entry_has_since_start_of_range(self, stable_repo: pathlib.Path) -> None:
259 data = json.loads(_stable(stable_repo, "--json").output)
260 assert "since_start_of_range" in data["stable"][0]
261
262 def test_top_flag_limits_results(self, stable_repo: pathlib.Path) -> None:
263 data = json.loads(_stable(stable_repo, "--json", "--top", "1").output)
264 assert len(data["stable"]) <= 1
265
266 def test_kind_filter_restricts_results(self, stable_repo: pathlib.Path) -> None:
267 data = json.loads(_stable(stable_repo, "--json", "--kind", "class").output)
268 for entry in data["stable"]:
269 # addresses filtered to class symbols — spot-check via filter echoed
270 pass # no crash and valid JSON is the assertion
271 assert "kind" in data["filters"]
272
273 def test_commits_analysed_positive(self, stable_repo: pathlib.Path) -> None:
274 data = json.loads(_stable(stable_repo, "--json").output)
275 assert data["commits_analysed"] > 0
276
277 def test_truncated_flag_present(self, stable_repo: pathlib.Path) -> None:
278 data = json.loads(_stable(stable_repo, "--json").output)
279 assert "truncated" in data
280 assert isinstance(data["truncated"], bool)
281
282 def test_filters_dict_present(self, stable_repo: pathlib.Path) -> None:
283 data = json.loads(_stable(stable_repo, "--json").output)
284 assert isinstance(data["filters"], dict)
285
286 def test_from_ref_and_to_ref_present(self, stable_repo: pathlib.Path) -> None:
287 data = json.loads(_stable(stable_repo, "--json").output)
288 assert "from_ref" in data
289 assert "to_ref" in data
290
291 def test_invalid_since_ref_exits_nonzero(self, stable_repo: pathlib.Path) -> None:
292 result = _stable(stable_repo, "--since", "nonexistent-ref-xyz")
293 assert result.exit_code != 0
294
295
296 # ──────────────────────────────────────────────────────────────────────────────
297 # Stress
298 # ──────────────────────────────────────────────────────────────────────────────
299
300
301 class TestStress:
302 def test_many_commits_does_not_crash(self, tmp_path: pathlib.Path) -> None:
303 saved = os.getcwd()
304 try:
305 os.chdir(tmp_path)
306 runner.invoke(None, ["init"])
307 finally:
308 os.chdir(saved)
309
310 for i in range(30):
311 _commit(tmp_path, {"f.py": f"def fn(): return {i}\n"}, f"commit {i}")
312
313 result = _stable(tmp_path, "--json")
314 assert result.exit_code == 0
315 data = json.loads(result.output)
316 assert data["commits_analysed"] >= 1
317
318 def test_concurrent_stable_separate_repos(self, tmp_path: pathlib.Path) -> None:
319 repos = []
320 for i in range(4):
321 r = tmp_path / f"repo{i}"
322 r.mkdir()
323 saved = os.getcwd()
324 try:
325 os.chdir(r)
326 runner.invoke(None, ["init"])
327 finally:
328 os.chdir(saved)
329 _commit(r, {"x.py": f"def f(): return {i}\n"}, "init")
330 repos.append(r)
331
332 results: list[int] = []
333 lock = threading.Lock()
334
335 def _run(repo: pathlib.Path) -> None:
336 rc = _stable(repo, "--json").exit_code
337 with lock:
338 results.append(rc)
339
340 threads = [threading.Thread(target=_run, args=(r,)) for r in repos]
341 for t in threads:
342 t.start()
343 for t in threads:
344 t.join()
345 assert all(rc == 0 for rc in results)
346
347
348 # ──────────────────────────────────────────────────────────────────────────────
349 # Data integrity
350 # ──────────────────────────────────────────────────────────────────────────────
351
352
353 class TestDataIntegrity:
354 def test_stable_list_sorted_descending(self, stable_repo: pathlib.Path) -> None:
355 data = json.loads(_stable(stable_repo, "--json").output)
356 counts = [e["unchanged_for"] for e in data["stable"]]
357 assert counts == sorted(counts, reverse=True)
358
359 def test_unchanged_for_is_nonnegative_int(self, stable_repo: pathlib.Path) -> None:
360 data = json.loads(_stable(stable_repo, "--json").output)
361 for entry in data["stable"]:
362 assert isinstance(entry["unchanged_for"], int)
363 assert entry["unchanged_for"] >= 0
364
365 def test_since_start_of_range_is_bool(self, stable_repo: pathlib.Path) -> None:
366 data = json.loads(_stable(stable_repo, "--json").output)
367 for entry in data["stable"]:
368 assert isinstance(entry["since_start_of_range"], bool)
369
370 def test_stable_fn_has_higher_stability_than_hot_fn(
371 self, stable_repo: pathlib.Path
372 ) -> None:
373 """stable_fn was never modified; hot_fn was — stable_fn must rank higher."""
374 data = json.loads(_stable(stable_repo, "--json").output)
375 stable_counts = {
376 e["address"].split("::")[-1]: e["unchanged_for"]
377 for e in data["stable"]
378 }
379 if "stable_fn" in stable_counts and "hot_fn" in stable_counts:
380 assert stable_counts["stable_fn"] >= stable_counts["hot_fn"]
381
382 def test_filters_reflect_input_flags(self, stable_repo: pathlib.Path) -> None:
383 data = json.loads(
384 _stable(stable_repo, "--json", "--top", "5", "--kind", "function").output
385 )
386 assert data["filters"]["top"] == 5
387 assert data["filters"]["kind"] == "function"
388
389 def test_max_commits_cap_respected(self, stable_repo: pathlib.Path) -> None:
390 data = json.loads(
391 _stable(stable_repo, "--json", "--max-commits", "1").output
392 )
393 assert data["commits_analysed"] <= 1
394
395
396 # ──────────────────────────────────────────────────────────────────────────────
397 # Security
398 # ──────────────────────────────────────────────────────────────────────────────
399
400
401 class TestSecurity:
402 def test_ansi_in_kind_filter_not_echoed_raw(self, stable_repo: pathlib.Path) -> None:
403 result = _stable(stable_repo, "--kind", "\x1b[31mfunc\x1b[0m")
404 combined = result.output + (result.stderr or "")
405 assert "\x1b[31m" not in combined
406
407 def test_ansi_in_language_filter_not_echoed_raw(
408 self, stable_repo: pathlib.Path
409 ) -> None:
410 result = _stable(stable_repo, "--language", "\x1b[31mpython\x1b[0m")
411 combined = result.output + (result.stderr or "")
412 assert "\x1b[31m" not in combined
413
414 def test_ansi_in_since_ref_not_echoed_raw(self, stable_repo: pathlib.Path) -> None:
415 result = _stable(stable_repo, "--since", "\x1b[31mmalicious\x1b[0m")
416 combined = result.output + (result.stderr or "")
417 assert "\x1b[31m" not in combined
418
419 def test_null_byte_in_kind_does_not_crash(self, stable_repo: pathlib.Path) -> None:
420 result = _stable(stable_repo, "--kind", "func\x00malicious")
421 assert result.exit_code in (0, 1)
422
423
424 # ──────────────────────────────────────────────────────────────────────────────
425 # Performance
426 # ──────────────────────────────────────────────────────────────────────────────
427
428
429 class TestPerformance:
430 def test_duration_ms_under_10000(self, stable_repo: pathlib.Path) -> None:
431 data = json.loads(_stable(stable_repo, "--json").output)
432 assert data["duration_ms"] < 10_000.0
433
434
435 # ---------------------------------------------------------------------------
436 # Flag registration tests
437 # ---------------------------------------------------------------------------
438
439
440 class TestRegisterFlags:
441 def _parser(self) -> "argparse.ArgumentParser":
442 import argparse
443 from muse.cli.commands.stable import register
444
445 p = argparse.ArgumentParser()
446 subs = p.add_subparsers()
447 register(subs)
448 return p
449
450 def test_default_json_out_is_false(self) -> None:
451 args = self._parser().parse_args(["stable"])
452 assert args.json_out is False
453
454 def test_json_flag_sets_json_out(self) -> None:
455 args = self._parser().parse_args(["stable", "--json"])
456 assert args.json_out is True
457
458 def test_j_shorthand_sets_json_out(self) -> None:
459 args = self._parser().parse_args(["stable", "-j"])
460 assert args.json_out is True
File History 1 commit
sha256:51116ec824246acde6abf729e6ba854c223dc5173eff31a645520208023b0652 refactor(bridge): comprehensive spec sweep — close all issu… Sonnet 4.6 minor 28 days ago