gabriel / muse public
test_dead_supercharge.py python
342 lines 13.1 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 142 days ago
1 """Supercharge tests for ``muse code dead`` — agent-usability gaps.
2
3 The existing test_cmd_dead.py covers unit helpers (_module_is_imported,
4 _matches_path_filter, _find_symbol_span, _delete_symbol_lines, _analyse_file,
5 _is_test_file, _DeadCandidate), integration schema (--json, --kind, --count,
6 --compare, --workers, --save-allowlist, --allowlist), security (--delete
7 prompts), and stress (200-function file, 50-file codebase).
8
9 This file targets only the gaps those tests leave open:
10
11 Coverage matrix
12 ---------------
13 - --json / -j: -j alias works identically to --json
14 - exit_code: JSON output includes exit_code = 0 on success
15 - duration_ms: JSON output carries non-negative float (already present —
16 verified here for completeness and regression guard)
17 - TypedDicts: _DeadPayload gains exit_code annotation
18 - Docstrings: run() docstring mentions exit_code and duration_ms
19 - ANSI: JSON output never contains terminal escape sequences
20 - Performance: duration_ms stays under 5000 ms for a small repo
21 """
22
23 from __future__ import annotations
24
25 import json
26 import pathlib
27 import textwrap
28
29 import pytest
30
31 from tests.cli_test_helper import CliRunner
32
33 runner = CliRunner()
34
35
36 # ---------------------------------------------------------------------------
37 # Helpers
38 # ---------------------------------------------------------------------------
39
40
41 def _env(root: pathlib.Path) -> dict[str, str]:
42 return {"MUSE_REPO_ROOT": str(root)}
43
44
45 def _run(root: pathlib.Path, *args: str):
46 return runner.invoke(None, list(args), env=_env(root))
47
48
49 # ---------------------------------------------------------------------------
50 # Fixture — repo with an obvious dead-code candidate
51 # ---------------------------------------------------------------------------
52
53
54 @pytest.fixture()
55 def dead_repo(
56 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
57 ) -> pathlib.Path:
58 """Repo with one referenced function and one dead (orphaned) function.
59
60 Layout::
61
62 billing.py — Invoice class + process_order (referenced)
63 utils.py — validate_amount (referenced by billing.py)
64 orphaned_helper (never referenced, module not imported
65 by anyone → HIGH confidence dead candidate)
66 """
67 monkeypatch.chdir(tmp_path)
68 r = _run(tmp_path, "init", "--domain", "code")
69 assert r.exit_code == 0, r.output
70
71 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
72 from utils import validate_amount
73
74 class Invoice:
75 def compute_total(self, items):
76 return sum(items)
77
78 def process_order(invoice, items):
79 if not validate_amount(sum(items)):
80 raise ValueError("bad amount")
81 return invoice.compute_total(items)
82 """))
83 (tmp_path / "utils.py").write_text(textwrap.dedent("""\
84 def validate_amount(amount):
85 return amount > 0
86
87 def orphaned_helper(x):
88 return x * 2
89 """))
90 r = _run(tmp_path, "code", "add", ".")
91 assert r.exit_code == 0, r.output
92 r = _run(tmp_path, "commit", "-m", "initial")
93 assert r.exit_code == 0, r.output
94
95 return tmp_path
96
97
98 # ---------------------------------------------------------------------------
99 # TestJsonAlias — -j works identically to --json
100 # ---------------------------------------------------------------------------
101
102
103 class TestJsonAlias:
104 """-j shorthand must behave identically to --json."""
105
106 def test_j_alias_exits_zero(self, dead_repo: pathlib.Path) -> None:
107 r = _run(dead_repo, "code", "dead", "-j")
108 assert r.exit_code == 0, r.output
109
110 def test_j_alias_valid_json(self, dead_repo: pathlib.Path) -> None:
111 r = _run(dead_repo, "code", "dead", "-j")
112 json.loads(r.output) # must not raise
113
114 def test_j_alias_has_results_key(self, dead_repo: pathlib.Path) -> None:
115 r = _run(dead_repo, "code", "dead", "-j")
116 assert "results" in json.loads(r.output)
117
118 def test_j_alias_has_duration_ms_key(self, dead_repo: pathlib.Path) -> None:
119 r = _run(dead_repo, "code", "dead", "-j")
120 assert "duration_ms" in json.loads(r.output)
121
122 def test_j_alias_same_top_level_keys_as_json_flag(
123 self, dead_repo: pathlib.Path
124 ) -> None:
125 r1 = _run(dead_repo, "code", "dead", "--json")
126 r2 = _run(dead_repo, "code", "dead", "-j")
127 d1 = json.loads(r1.output)
128 d2 = 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_result_count_matches_json_flag(
134 self, dead_repo: pathlib.Path
135 ) -> None:
136 r1 = _run(dead_repo, "code", "dead", "--json")
137 r2 = _run(dead_repo, "code", "dead", "-j")
138 assert len(json.loads(r1.output)["results"]) == len(json.loads(r2.output)["results"])
139
140 def test_j_alias_with_high_confidence_only(self, dead_repo: pathlib.Path) -> None:
141 r = _run(dead_repo, "code", "dead", "-j", "--high-confidence-only")
142 assert r.exit_code == 0, r.output
143 data = json.loads(r.output)
144 for c in data["results"]:
145 assert c["confidence"] == "high"
146
147 def test_j_alias_with_kind_filter(self, dead_repo: pathlib.Path) -> None:
148 r = _run(dead_repo, "code", "dead", "-j", "--kind", "function")
149 assert r.exit_code == 0, r.output
150 data = json.loads(r.output)
151 for c in data["results"]:
152 assert c["kind"] == "function"
153
154
155 # ---------------------------------------------------------------------------
156 # TestDurationMs — JSON output must include duration_ms (regression guard)
157 # ---------------------------------------------------------------------------
158
159
160 class TestDurationMs:
161 """duration_ms already exists — this class guards against regression."""
162
163 def test_json_has_duration_ms(self, dead_repo: pathlib.Path) -> None:
164 r = _run(dead_repo, "code", "dead", "--json")
165 assert "duration_ms" in json.loads(r.output)
166
167 def test_json_duration_ms_nonnegative(self, dead_repo: pathlib.Path) -> None:
168 r = _run(dead_repo, "code", "dead", "--json")
169 assert json.loads(r.output)["duration_ms"] >= 0
170
171 def test_json_duration_ms_is_float(self, dead_repo: pathlib.Path) -> None:
172 r = _run(dead_repo, "code", "dead", "--json")
173 assert isinstance(json.loads(r.output)["duration_ms"], float)
174
175 def test_j_alias_duration_ms_present(self, dead_repo: pathlib.Path) -> None:
176 r = _run(dead_repo, "code", "dead", "-j")
177 assert "duration_ms" in json.loads(r.output)
178
179 def test_duration_ms_with_high_confidence_filter(
180 self, dead_repo: pathlib.Path
181 ) -> None:
182 r = _run(dead_repo, "code", "dead", "--json", "--high-confidence-only")
183 data = json.loads(r.output)
184 assert "duration_ms" in data
185 assert data["duration_ms"] >= 0
186
187 def test_duration_ms_with_compare(self, dead_repo: pathlib.Path) -> None:
188 r = _run(dead_repo, "code", "dead", "--json", "--compare", "HEAD")
189 data = json.loads(r.output)
190 assert "duration_ms" in data
191 assert isinstance(data["duration_ms"], float)
192
193
194 # ---------------------------------------------------------------------------
195 # TestExitCode — JSON includes exit_code = 0 on success
196 # ---------------------------------------------------------------------------
197
198
199 class TestExitCode:
200 """JSON exit_code must be 0 on success."""
201
202 def test_json_has_exit_code(self, dead_repo: pathlib.Path) -> None:
203 r = _run(dead_repo, "code", "dead", "--json")
204 assert "exit_code" in json.loads(r.output)
205
206 def test_json_exit_code_zero(self, dead_repo: pathlib.Path) -> None:
207 r = _run(dead_repo, "code", "dead", "--json")
208 assert r.exit_code == 0
209 assert json.loads(r.output)["exit_code"] == 0
210
211 def test_json_exit_code_is_int(self, dead_repo: pathlib.Path) -> None:
212 r = _run(dead_repo, "code", "dead", "--json")
213 assert isinstance(json.loads(r.output)["exit_code"], int)
214
215 def test_j_alias_exit_code_present(self, dead_repo: pathlib.Path) -> None:
216 r = _run(dead_repo, "code", "dead", "-j")
217 assert "exit_code" in json.loads(r.output)
218
219 def test_exit_code_mirrors_process_exit(self, dead_repo: pathlib.Path) -> None:
220 r = _run(dead_repo, "code", "dead", "--json")
221 assert json.loads(r.output)["exit_code"] == r.exit_code
222
223 def test_exit_code_zero_with_high_confidence_filter(
224 self, dead_repo: pathlib.Path
225 ) -> None:
226 r = _run(dead_repo, "code", "dead", "--json", "--high-confidence-only")
227 assert r.exit_code == 0
228 assert json.loads(r.output)["exit_code"] == 0
229
230 def test_exit_code_zero_with_compare(self, dead_repo: pathlib.Path) -> None:
231 r = _run(dead_repo, "code", "dead", "--json", "--compare", "HEAD")
232 assert r.exit_code == 0
233 assert json.loads(r.output)["exit_code"] == 0
234
235 def test_exit_code_zero_with_kind_filter(self, dead_repo: pathlib.Path) -> None:
236 r = _run(dead_repo, "code", "dead", "--json", "--kind", "function")
237 assert r.exit_code == 0
238 assert json.loads(r.output)["exit_code"] == 0
239
240
241 # ---------------------------------------------------------------------------
242 # TestTypedDicts — _DeadPayload carries exit_code annotation
243 # ---------------------------------------------------------------------------
244
245
246 class TestTypedDicts:
247 """_DeadPayload must carry exit_code and duration_ms annotations."""
248
249 def test_dead_payload_typeddict_exists(self) -> None:
250 from muse.cli.commands.dead import _DeadPayload # noqa: F401
251
252 def test_has_exit_code_annotation(self) -> None:
253 from muse.cli.commands.dead import _DeadPayload
254 assert "exit_code" in _DeadPayload.__annotations__
255
256 def test_has_duration_ms_annotation(self) -> None:
257 from muse.cli.commands.dead import _DeadPayload
258 assert "duration_ms" in _DeadPayload.__annotations__
259
260 def test_retains_results_annotation(self) -> None:
261 from muse.cli.commands.dead import _DeadPayload
262 assert "results" in _DeadPayload.__annotations__
263
264 def test_retains_high_confidence_count_annotation(self) -> None:
265 from muse.cli.commands.dead import _DeadPayload
266 assert "high_confidence_count" in _DeadPayload.__annotations__
267
268 def test_retains_source_annotation(self) -> None:
269 from muse.cli.commands.dead import _DeadPayload
270 assert "source" in _DeadPayload.__annotations__
271
272 def test_retains_total_files_scanned_annotation(self) -> None:
273 from muse.cli.commands.dead import _DeadPayload
274 assert "total_files_scanned" in _DeadPayload.__annotations__
275
276
277 # ---------------------------------------------------------------------------
278 # TestDocstrings — run() docstring documents new fields
279 # ---------------------------------------------------------------------------
280
281
282 class TestDocstrings:
283 """run() must document exit_code and duration_ms."""
284
285 def test_run_docstring_mentions_exit_code(self) -> None:
286 from muse.cli.commands.dead import run
287 assert run.__doc__ is not None
288 assert "exit_code" in run.__doc__
289
290 def test_run_docstring_mentions_duration_ms(self) -> None:
291 from muse.cli.commands.dead import run
292 assert run.__doc__ is not None
293 assert "duration_ms" in run.__doc__
294
295
296 # ---------------------------------------------------------------------------
297 # TestAnsiSanitization — no escape codes in JSON output
298 # ---------------------------------------------------------------------------
299
300
301 class TestAnsiSanitization:
302 """No ANSI escape sequences anywhere in the JSON output."""
303
304 def test_json_output_no_ansi(self, dead_repo: pathlib.Path) -> None:
305 r = _run(dead_repo, "code", "dead", "--json")
306 assert "\x1b" not in r.output
307
308 def test_j_alias_output_no_ansi(self, dead_repo: pathlib.Path) -> None:
309 r = _run(dead_repo, "code", "dead", "-j")
310 assert "\x1b" not in r.output
311
312 def test_json_output_no_ansi_with_high_confidence(
313 self, dead_repo: pathlib.Path
314 ) -> None:
315 r = _run(dead_repo, "code", "dead", "--json", "--high-confidence-only")
316 assert "\x1b" not in r.output
317
318
319 # ---------------------------------------------------------------------------
320 # TestPerformance — duration_ms under 5000 ms for a small repo
321 # ---------------------------------------------------------------------------
322
323
324 class TestPerformance:
325 """duration_ms must stay under 5000 ms for small repos.
326
327 dead uses parallel AST workers so the budget is slightly higher than
328 other commands (2000 ms is too tight for cold-start thread-pool overhead
329 on CI runners).
330 """
331
332 def test_json_duration_under_5000ms(self, dead_repo: pathlib.Path) -> None:
333 r = _run(dead_repo, "code", "dead", "--json")
334 assert json.loads(r.output)["duration_ms"] < 5000
335
336 def test_j_alias_duration_under_5000ms(self, dead_repo: pathlib.Path) -> None:
337 r = _run(dead_repo, "code", "dead", "-j")
338 assert json.loads(r.output)["duration_ms"] < 5000
339
340 def test_duration_ms_is_float_not_int(self, dead_repo: pathlib.Path) -> None:
341 r = _run(dead_repo, "code", "dead", "--json")
342 assert isinstance(json.loads(r.output)["duration_ms"], float)
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 142 days ago