gabriel / muse public
test_dead_supercharge.py python
358 lines 13.5 KB
Raw
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 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 from collections.abc import Mapping
25
26 import argparse
27
28 import json
29 import pathlib
30 import textwrap
31
32 import pytest
33
34 from tests.cli_test_helper import CliRunner, InvokeResult
35
36 runner = CliRunner()
37
38
39 # ---------------------------------------------------------------------------
40 # Helpers
41 # ---------------------------------------------------------------------------
42
43
44 def _env(root: pathlib.Path) -> Mapping[str, str]:
45 return {"MUSE_REPO_ROOT": str(root)}
46
47
48 def _run(root: pathlib.Path, *args: str) -> InvokeResult:
49 return runner.invoke(None, list(args), env=_env(root))
50
51
52 # ---------------------------------------------------------------------------
53 # Fixture — repo with an obvious dead-code candidate
54 # ---------------------------------------------------------------------------
55
56
57 @pytest.fixture()
58 def dead_repo(
59 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
60 ) -> pathlib.Path:
61 """Repo with one referenced function and one dead (orphaned) function.
62
63 Layout::
64
65 billing.py — Invoice class + process_order (referenced)
66 utils.py — validate_amount (referenced by billing.py)
67 orphaned_helper (never referenced, module not imported
68 by anyone → HIGH confidence dead candidate)
69 """
70 monkeypatch.chdir(tmp_path)
71 r = _run(tmp_path, "init", "--domain", "code")
72 assert r.exit_code == 0, r.output
73
74 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
75 from utils import validate_amount
76
77 class Invoice:
78 def compute_total(self, items):
79 return sum(items)
80
81 def process_order(invoice, items):
82 if not validate_amount(sum(items)):
83 raise ValueError("bad amount")
84 return invoice.compute_total(items)
85 """))
86 (tmp_path / "utils.py").write_text(textwrap.dedent("""\
87 def validate_amount(amount):
88 return amount > 0
89
90 def orphaned_helper(x):
91 return x * 2
92 """))
93 r = _run(tmp_path, "code", "add", ".")
94 assert r.exit_code == 0, r.output
95 r = _run(tmp_path, "commit", "-m", "initial")
96 assert r.exit_code == 0, r.output
97
98 return tmp_path
99
100
101 # ---------------------------------------------------------------------------
102 # TestJsonAlias — -j works identically to --json
103 # ---------------------------------------------------------------------------
104
105
106 class TestJsonAlias:
107 """-j shorthand must behave identically to --json."""
108
109 def test_j_alias_exits_zero(self, dead_repo: pathlib.Path) -> None:
110 r = _run(dead_repo, "code", "dead", "-j")
111 assert r.exit_code == 0, r.output
112
113 def test_j_alias_valid_json(self, dead_repo: pathlib.Path) -> None:
114 r = _run(dead_repo, "code", "dead", "-j")
115 json.loads(r.output) # must not raise
116
117 def test_j_alias_has_results_key(self, dead_repo: pathlib.Path) -> None:
118 r = _run(dead_repo, "code", "dead", "-j")
119 assert "results" in json.loads(r.output)
120
121 def test_j_alias_has_duration_ms_key(self, dead_repo: pathlib.Path) -> None:
122 r = _run(dead_repo, "code", "dead", "-j")
123 assert "duration_ms" in json.loads(r.output)
124
125 def test_j_alias_same_top_level_keys_as_json_flag(
126 self, dead_repo: pathlib.Path
127 ) -> None:
128 r1 = _run(dead_repo, "code", "dead", "--json")
129 r2 = _run(dead_repo, "code", "dead", "-j")
130 d1 = json.loads(r1.output)
131 d2 = json.loads(r2.output)
132 d1.pop("duration_ms", None)
133 d2.pop("duration_ms", None)
134 assert set(d1.keys()) == set(d2.keys())
135
136 def test_j_alias_result_count_matches_json_flag(
137 self, dead_repo: pathlib.Path
138 ) -> None:
139 r1 = _run(dead_repo, "code", "dead", "--json")
140 r2 = _run(dead_repo, "code", "dead", "-j")
141 assert len(json.loads(r1.output)["results"]) == len(json.loads(r2.output)["results"])
142
143 def test_j_alias_with_high_confidence_only(self, dead_repo: pathlib.Path) -> None:
144 r = _run(dead_repo, "code", "dead", "-j", "--high-confidence-only")
145 assert r.exit_code == 0, r.output
146 data = json.loads(r.output)
147 for c in data["results"]:
148 assert c["confidence"] == "high"
149
150 def test_j_alias_with_kind_filter(self, dead_repo: pathlib.Path) -> None:
151 r = _run(dead_repo, "code", "dead", "-j", "--kind", "function")
152 assert r.exit_code == 0, r.output
153 data = json.loads(r.output)
154 for c in data["results"]:
155 assert c["kind"] == "function"
156
157
158 # ---------------------------------------------------------------------------
159 # TestDurationMs — JSON output must include duration_ms (regression guard)
160 # ---------------------------------------------------------------------------
161
162
163 class TestDurationMs:
164 """duration_ms already exists — this class guards against regression."""
165
166 def test_json_has_duration_ms(self, dead_repo: pathlib.Path) -> None:
167 r = _run(dead_repo, "code", "dead", "--json")
168 assert "duration_ms" in json.loads(r.output)
169
170 def test_json_duration_ms_nonnegative(self, dead_repo: pathlib.Path) -> None:
171 r = _run(dead_repo, "code", "dead", "--json")
172 assert json.loads(r.output)["duration_ms"] >= 0
173
174 def test_json_duration_ms_is_float(self, dead_repo: pathlib.Path) -> None:
175 r = _run(dead_repo, "code", "dead", "--json")
176 assert isinstance(json.loads(r.output)["duration_ms"], float)
177
178 def test_j_alias_duration_ms_present(self, dead_repo: pathlib.Path) -> None:
179 r = _run(dead_repo, "code", "dead", "-j")
180 assert "duration_ms" in json.loads(r.output)
181
182 def test_duration_ms_with_high_confidence_filter(
183 self, dead_repo: pathlib.Path
184 ) -> None:
185 r = _run(dead_repo, "code", "dead", "--json", "--high-confidence-only")
186 data = json.loads(r.output)
187 assert "duration_ms" in data
188 assert data["duration_ms"] >= 0
189
190 def test_duration_ms_with_compare(self, dead_repo: pathlib.Path) -> None:
191 r = _run(dead_repo, "code", "dead", "--json", "--compare", "HEAD")
192 data = json.loads(r.output)
193 assert "duration_ms" in data
194 assert isinstance(data["duration_ms"], float)
195
196
197 # ---------------------------------------------------------------------------
198 # TestExitCode — JSON includes exit_code = 0 on success
199 # ---------------------------------------------------------------------------
200
201
202 class TestExitCode:
203 """JSON exit_code must be 0 on success."""
204
205 def test_json_has_exit_code(self, dead_repo: pathlib.Path) -> None:
206 r = _run(dead_repo, "code", "dead", "--json")
207 assert "exit_code" in json.loads(r.output)
208
209 def test_json_exit_code_zero(self, dead_repo: pathlib.Path) -> None:
210 r = _run(dead_repo, "code", "dead", "--json")
211 assert r.exit_code == 0
212 assert json.loads(r.output)["exit_code"] == 0
213
214 def test_json_exit_code_is_int(self, dead_repo: pathlib.Path) -> None:
215 r = _run(dead_repo, "code", "dead", "--json")
216 assert isinstance(json.loads(r.output)["exit_code"], int)
217
218 def test_j_alias_exit_code_present(self, dead_repo: pathlib.Path) -> None:
219 r = _run(dead_repo, "code", "dead", "-j")
220 assert "exit_code" in json.loads(r.output)
221
222 def test_exit_code_mirrors_process_exit(self, dead_repo: pathlib.Path) -> None:
223 r = _run(dead_repo, "code", "dead", "--json")
224 assert json.loads(r.output)["exit_code"] == r.exit_code
225
226 def test_exit_code_zero_with_high_confidence_filter(
227 self, dead_repo: pathlib.Path
228 ) -> None:
229 r = _run(dead_repo, "code", "dead", "--json", "--high-confidence-only")
230 assert r.exit_code == 0
231 assert json.loads(r.output)["exit_code"] == 0
232
233 def test_exit_code_zero_with_compare(self, dead_repo: pathlib.Path) -> None:
234 r = _run(dead_repo, "code", "dead", "--json", "--compare", "HEAD")
235 assert r.exit_code == 0
236 assert json.loads(r.output)["exit_code"] == 0
237
238 def test_exit_code_zero_with_kind_filter(self, dead_repo: pathlib.Path) -> None:
239 r = _run(dead_repo, "code", "dead", "--json", "--kind", "function")
240 assert r.exit_code == 0
241 assert json.loads(r.output)["exit_code"] == 0
242
243
244 # ---------------------------------------------------------------------------
245 # TestTypedDicts — _DeadPayload carries exit_code annotation
246 # ---------------------------------------------------------------------------
247
248
249 class TestTypedDicts:
250 """_DeadPayload must carry exit_code and duration_ms annotations."""
251
252 def test_dead_payload_typeddict_exists(self) -> None:
253 from muse.cli.commands.dead import _DeadPayload # noqa: F401
254
255 def test_has_exit_code_annotation(self) -> None:
256 from muse.cli.commands.dead import _DeadPayload
257 assert "exit_code" in _DeadPayload.__annotations__
258
259 def test_has_duration_ms_annotation(self) -> None:
260 from muse.cli.commands.dead import _DeadPayload
261 assert "duration_ms" in _DeadPayload.__annotations__
262
263 def test_retains_results_annotation(self) -> None:
264 from muse.cli.commands.dead import _DeadPayload
265 assert "results" in _DeadPayload.__annotations__
266
267 def test_retains_high_confidence_count_annotation(self) -> None:
268 from muse.cli.commands.dead import _DeadPayload
269 assert "high_confidence_count" in _DeadPayload.__annotations__
270
271 def test_retains_source_annotation(self) -> None:
272 from muse.cli.commands.dead import _DeadPayload
273 assert "source" in _DeadPayload.__annotations__
274
275 def test_retains_total_files_scanned_annotation(self) -> None:
276 from muse.cli.commands.dead import _DeadPayload
277 assert "total_files_scanned" in _DeadPayload.__annotations__
278
279
280 # ---------------------------------------------------------------------------
281 # TestAnsiSanitization — no escape codes in JSON output
282 # ---------------------------------------------------------------------------
283
284
285 class TestAnsiSanitization:
286 """No ANSI escape sequences anywhere in the JSON output."""
287
288 def test_json_output_no_ansi(self, dead_repo: pathlib.Path) -> None:
289 r = _run(dead_repo, "code", "dead", "--json")
290 assert "\x1b" not in r.output
291
292 def test_j_alias_output_no_ansi(self, dead_repo: pathlib.Path) -> None:
293 r = _run(dead_repo, "code", "dead", "-j")
294 assert "\x1b" not in r.output
295
296 def test_json_output_no_ansi_with_high_confidence(
297 self, dead_repo: pathlib.Path
298 ) -> None:
299 r = _run(dead_repo, "code", "dead", "--json", "--high-confidence-only")
300 assert "\x1b" not in r.output
301
302
303 # ---------------------------------------------------------------------------
304 # TestPerformance — duration_ms under 5000 ms for a small repo
305 # ---------------------------------------------------------------------------
306
307
308 class TestPerformance:
309 """duration_ms must stay under 5000 ms for small repos.
310
311 dead uses parallel AST workers so the budget is slightly higher than
312 other commands (2000 ms is too tight for cold-start thread-pool overhead
313 on CI runners).
314 """
315
316 def test_json_duration_under_5000ms(self, dead_repo: pathlib.Path) -> None:
317 r = _run(dead_repo, "code", "dead", "--json")
318 assert json.loads(r.output)["duration_ms"] < 5000
319
320 def test_j_alias_duration_under_5000ms(self, dead_repo: pathlib.Path) -> None:
321 r = _run(dead_repo, "code", "dead", "-j")
322 assert json.loads(r.output)["duration_ms"] < 5000
323
324 def test_duration_ms_is_float_not_int(self, dead_repo: pathlib.Path) -> None:
325 r = _run(dead_repo, "code", "dead", "--json")
326 assert isinstance(json.loads(r.output)["duration_ms"], float)
327
328
329 # ---------------------------------------------------------------------------
330 # TestRegisterFlags — --json / -j normalized at argparse level
331 # ---------------------------------------------------------------------------
332
333
334 class TestRegisterFlags:
335 """register() must expose --json with -j shorthand and dest=json_out."""
336
337 def _make_parser(self) -> argparse.ArgumentParser:
338 import argparse as ap
339 from muse.cli.commands.dead import register
340 root = ap.ArgumentParser()
341 subs = root.add_subparsers()
342 register(subs)
343 return root
344
345 def test_json_out_default_false(self) -> None:
346 p = self._make_parser()
347 ns = p.parse_args(['code', 'dead'])
348 assert ns.json_out is False
349
350 def test_json_out_true_with_json_flag(self) -> None:
351 p = self._make_parser()
352 ns = p.parse_args(['code', 'dead', '--json'])
353 assert ns.json_out is True
354
355 def test_json_out_true_with_j_flag(self) -> None:
356 p = self._make_parser()
357 ns = p.parse_args(['code', 'dead', '-j'])
358 assert ns.json_out is True
File History 2 commits
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago