gabriel / muse public
test_grep_supercharge.py python
363 lines 14.1 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 140 days ago
1 """Supercharge tests for ``muse code grep`` — agent-usability gaps.
2
3 The existing test_cmd_grep.py covers correctness, --regex, --kind, --language,
4 --file, --count, --hashes, --commit, --json schema (source_ref, working_tree,
5 pattern, total_matches, results), qualified-name search, ReDoS guards, and
6 a 1000-symbol stress test.
7
8 This file targets only the gaps those tests leave open:
9
10 Coverage matrix
11 ---------------
12 - --json / -j: -j alias works identically to --json
13 - exit_code: JSON output includes exit_code = 0 on success
14 - duration_ms: JSON output includes non-negative float duration_ms
15 - TypedDicts: _GrepOutputJson carries all fields including exit_code/duration_ms
16 - Docstrings: run() docstring mentions exit_code and duration_ms
17 - ANSI: JSON output never contains terminal escape sequences
18 - Performance: duration_ms stays under 2000 ms for a small repo
19 """
20
21 from __future__ import annotations
22 from collections.abc import Mapping
23
24 import json
25 import pathlib
26 import textwrap
27
28 import pytest
29
30 from tests.cli_test_helper import CliRunner
31
32 runner = CliRunner()
33
34
35 # ---------------------------------------------------------------------------
36 # Helpers
37 # ---------------------------------------------------------------------------
38
39
40 def _env(root: pathlib.Path) -> Mapping[str, str]:
41 return {"MUSE_REPO_ROOT": str(root)}
42
43
44 def _run(root: pathlib.Path, *args: str):
45 return runner.invoke(None, list(args), env=_env(root))
46
47
48 # ---------------------------------------------------------------------------
49 # Fixture — minimal repo with named symbols
50 # ---------------------------------------------------------------------------
51
52
53 @pytest.fixture()
54 def grep_repo(
55 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
56 ) -> pathlib.Path:
57 """Repo with two Python files and several named symbols.
58
59 billing.py — Invoice class + validate_amount function
60 auth.py — verify_token function + AuthError class
61 """
62 monkeypatch.chdir(tmp_path)
63 r = _run(tmp_path, "init", "--domain", "code")
64 assert r.exit_code == 0, r.output
65
66 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
67 class Invoice:
68 def compute_total(self, items):
69 return sum(items)
70
71 def validate_amount(amount):
72 if amount < 0:
73 raise ValueError("negative amount")
74 return amount
75 """))
76 (tmp_path / "auth.py").write_text(textwrap.dedent("""\
77 class AuthError(Exception):
78 pass
79
80 def verify_token(token):
81 if not token:
82 raise AuthError("missing token")
83 return True
84 """))
85 r = _run(tmp_path, "code", "add", ".")
86 assert r.exit_code == 0, r.output
87 r = _run(tmp_path, "commit", "-m", "seed grep repo")
88 assert r.exit_code == 0, r.output
89
90 return tmp_path
91
92
93 # ---------------------------------------------------------------------------
94 # TestJsonAlias — -j works identically to --json
95 # ---------------------------------------------------------------------------
96
97
98 class TestJsonAlias:
99 """-j shorthand must behave identically to --json."""
100
101 def test_j_alias_exits_zero(self, grep_repo: pathlib.Path) -> None:
102 r = _run(grep_repo, "code", "grep", "validate", "-j")
103 assert r.exit_code == 0, r.output
104
105 def test_j_alias_valid_json(self, grep_repo: pathlib.Path) -> None:
106 r = _run(grep_repo, "code", "grep", "validate", "-j")
107 json.loads(r.output) # must not raise
108
109 def test_j_alias_has_results_key(self, grep_repo: pathlib.Path) -> None:
110 r = _run(grep_repo, "code", "grep", "validate", "-j")
111 assert "results" in json.loads(r.output)
112
113 def test_j_alias_has_total_matches_key(self, grep_repo: pathlib.Path) -> None:
114 r = _run(grep_repo, "code", "grep", "validate", "-j")
115 assert "total_matches" in json.loads(r.output)
116
117 def test_j_alias_has_pattern_key(self, grep_repo: pathlib.Path) -> None:
118 r = _run(grep_repo, "code", "grep", "validate", "-j")
119 assert "pattern" in json.loads(r.output)
120
121 def test_j_alias_same_top_level_keys_as_json_flag(
122 self, grep_repo: pathlib.Path
123 ) -> None:
124 r1 = _run(grep_repo, "code", "grep", "validate", "--json")
125 r2 = _run(grep_repo, "code", "grep", "validate", "-j")
126 d1 = json.loads(r1.output)
127 d2 = json.loads(r2.output)
128 d1.pop("duration_ms", None)
129 d2.pop("duration_ms", None)
130 assert set(d1.keys()) == set(d2.keys())
131
132 def test_j_alias_match_count_matches_json_flag(
133 self, grep_repo: pathlib.Path
134 ) -> None:
135 r1 = _run(grep_repo, "code", "grep", "validate", "--json")
136 r2 = _run(grep_repo, "code", "grep", "validate", "-j")
137 assert json.loads(r1.output)["total_matches"] == json.loads(r2.output)["total_matches"]
138
139 def test_j_alias_pattern_echoed(self, grep_repo: pathlib.Path) -> None:
140 r = _run(grep_repo, "code", "grep", "Invoice", "-j")
141 assert json.loads(r.output)["pattern"] == "Invoice"
142
143 def test_j_alias_no_match_empty_results(self, grep_repo: pathlib.Path) -> None:
144 r = _run(grep_repo, "code", "grep", "zzz_never", "-j")
145 assert r.exit_code == 0, r.output
146 data = json.loads(r.output)
147 assert data["results"] == []
148 assert data["total_matches"] == 0
149
150 def test_j_alias_with_kind_filter(self, grep_repo: pathlib.Path) -> None:
151 r = _run(grep_repo, "code", "grep", "Invoice", "-j", "--kind", "class")
152 assert r.exit_code == 0, r.output
153 data = json.loads(r.output)
154 for res in data["results"]:
155 assert res["kind"] == "class"
156
157
158 # ---------------------------------------------------------------------------
159 # TestDurationMs — JSON output must include duration_ms
160 # ---------------------------------------------------------------------------
161
162
163 class TestDurationMs:
164 """JSON output must include a non-negative float duration_ms."""
165
166 def test_json_has_duration_ms(self, grep_repo: pathlib.Path) -> None:
167 r = _run(grep_repo, "code", "grep", "validate", "--json")
168 assert "duration_ms" in json.loads(r.output)
169
170 def test_json_duration_ms_nonnegative(self, grep_repo: pathlib.Path) -> None:
171 r = _run(grep_repo, "code", "grep", "validate", "--json")
172 assert json.loads(r.output)["duration_ms"] >= 0
173
174 def test_json_duration_ms_is_float(self, grep_repo: pathlib.Path) -> None:
175 r = _run(grep_repo, "code", "grep", "validate", "--json")
176 assert isinstance(json.loads(r.output)["duration_ms"], float)
177
178 def test_j_alias_duration_ms_present(self, grep_repo: pathlib.Path) -> None:
179 r = _run(grep_repo, "code", "grep", "validate", "-j")
180 assert "duration_ms" in json.loads(r.output)
181
182 def test_duration_ms_no_results(self, grep_repo: pathlib.Path) -> None:
183 """duration_ms present even when no symbols match."""
184 r = _run(grep_repo, "code", "grep", "zzz_never", "--json")
185 data = json.loads(r.output)
186 assert "duration_ms" in data
187 assert data["duration_ms"] >= 0
188
189 def test_duration_ms_with_kind_filter(self, grep_repo: pathlib.Path) -> None:
190 r = _run(grep_repo, "code", "grep", "Invoice", "--json", "--kind", "class")
191 data = json.loads(r.output)
192 assert "duration_ms" in data
193 assert isinstance(data["duration_ms"], float)
194
195 def test_duration_ms_with_regex(self, grep_repo: pathlib.Path) -> None:
196 r = _run(grep_repo, "code", "grep", "^validate", "--json", "--regex")
197 data = json.loads(r.output)
198 assert "duration_ms" in data
199 assert data["duration_ms"] >= 0
200
201
202 # ---------------------------------------------------------------------------
203 # TestExitCode — JSON includes exit_code = 0 on success
204 # ---------------------------------------------------------------------------
205
206
207 class TestExitCode:
208 """JSON exit_code must be 0 on success."""
209
210 def test_json_has_exit_code(self, grep_repo: pathlib.Path) -> None:
211 r = _run(grep_repo, "code", "grep", "validate", "--json")
212 assert "exit_code" in json.loads(r.output)
213
214 def test_json_exit_code_zero(self, grep_repo: pathlib.Path) -> None:
215 r = _run(grep_repo, "code", "grep", "validate", "--json")
216 assert r.exit_code == 0
217 assert json.loads(r.output)["exit_code"] == 0
218
219 def test_json_exit_code_is_int(self, grep_repo: pathlib.Path) -> None:
220 r = _run(grep_repo, "code", "grep", "validate", "--json")
221 assert isinstance(json.loads(r.output)["exit_code"], int)
222
223 def test_j_alias_exit_code_present(self, grep_repo: pathlib.Path) -> None:
224 r = _run(grep_repo, "code", "grep", "validate", "-j")
225 assert "exit_code" in json.loads(r.output)
226
227 def test_exit_code_mirrors_process_exit(self, grep_repo: pathlib.Path) -> None:
228 r = _run(grep_repo, "code", "grep", "validate", "--json")
229 assert json.loads(r.output)["exit_code"] == r.exit_code
230
231 def test_exit_code_zero_empty_results(self, grep_repo: pathlib.Path) -> None:
232 """exit_code is 0 even when no symbols match."""
233 r = _run(grep_repo, "code", "grep", "zzz_never", "--json")
234 assert r.exit_code == 0
235 data = json.loads(r.output)
236 assert data["exit_code"] == 0
237 assert data["results"] == []
238
239 def test_exit_code_zero_with_kind_filter(self, grep_repo: pathlib.Path) -> None:
240 r = _run(grep_repo, "code", "grep", "Invoice", "--json", "--kind", "class")
241 assert r.exit_code == 0
242 assert json.loads(r.output)["exit_code"] == 0
243
244 def test_exit_code_zero_with_regex(self, grep_repo: pathlib.Path) -> None:
245 r = _run(grep_repo, "code", "grep", "validate.*", "--json", "--regex")
246 assert r.exit_code == 0
247 assert json.loads(r.output)["exit_code"] == 0
248
249
250 # ---------------------------------------------------------------------------
251 # TestTypedDicts — _GrepOutputJson carries all fields
252 # ---------------------------------------------------------------------------
253
254
255 class TestTypedDicts:
256 """_GrepOutputJson must carry exit_code and duration_ms annotations."""
257
258 def test_grep_output_json_typeddict_exists(self) -> None:
259 from muse.cli.commands.grep import _GrepOutputJson # noqa: F401
260
261 def test_has_exit_code_annotation(self) -> None:
262 from muse.cli.commands.grep import _GrepOutputJson
263 assert "exit_code" in _GrepOutputJson.__annotations__
264
265 def test_has_duration_ms_annotation(self) -> None:
266 from muse.cli.commands.grep import _GrepOutputJson
267 assert "duration_ms" in _GrepOutputJson.__annotations__
268
269 def test_retains_results_annotation(self) -> None:
270 from muse.cli.commands.grep import _GrepOutputJson
271 assert "results" in _GrepOutputJson.__annotations__
272
273 def test_retains_total_matches_annotation(self) -> None:
274 from muse.cli.commands.grep import _GrepOutputJson
275 assert "total_matches" in _GrepOutputJson.__annotations__
276
277 def test_retains_pattern_annotation(self) -> None:
278 from muse.cli.commands.grep import _GrepOutputJson
279 assert "pattern" in _GrepOutputJson.__annotations__
280
281 def test_retains_source_ref_annotation(self) -> None:
282 from muse.cli.commands.grep import _GrepOutputJson
283 assert "source_ref" in _GrepOutputJson.__annotations__
284
285 def test_retains_working_tree_annotation(self) -> None:
286 from muse.cli.commands.grep import _GrepOutputJson
287 assert "working_tree" in _GrepOutputJson.__annotations__
288
289
290 # ---------------------------------------------------------------------------
291 # TestAnsiSanitization — no escape codes in JSON output
292 # ---------------------------------------------------------------------------
293
294
295 class TestAnsiSanitization:
296 """No ANSI escape sequences anywhere in the JSON output."""
297
298 def test_json_output_no_ansi(self, grep_repo: pathlib.Path) -> None:
299 r = _run(grep_repo, "code", "grep", "validate", "--json")
300 assert "\x1b" not in r.output
301
302 def test_j_alias_output_no_ansi(self, grep_repo: pathlib.Path) -> None:
303 r = _run(grep_repo, "code", "grep", "validate", "-j")
304 assert "\x1b" not in r.output
305
306 def test_json_no_ansi_with_results(self, grep_repo: pathlib.Path) -> None:
307 r = _run(grep_repo, "code", "grep", "Invoice", "--json")
308 assert "\x1b" not in r.output
309
310
311 # ---------------------------------------------------------------------------
312 # TestPerformance — duration_ms under 2000 ms for a small repo
313 # ---------------------------------------------------------------------------
314
315
316 class TestPerformance:
317 """duration_ms must stay under 2000 ms for small repos."""
318
319 def test_json_duration_under_2000ms(self, grep_repo: pathlib.Path) -> None:
320 r = _run(grep_repo, "code", "grep", "validate", "--json")
321 assert json.loads(r.output)["duration_ms"] < 2000
322
323 def test_j_alias_duration_under_2000ms(self, grep_repo: pathlib.Path) -> None:
324 r = _run(grep_repo, "code", "grep", "Invoice", "-j")
325 assert json.loads(r.output)["duration_ms"] < 2000
326
327 def test_duration_ms_is_float_not_int(self, grep_repo: pathlib.Path) -> None:
328 r = _run(grep_repo, "code", "grep", "validate", "--json")
329 assert isinstance(json.loads(r.output)["duration_ms"], float)
330
331
332 # ---------------------------------------------------------------------------
333 # TestRegisterFlags — argparse-level verification
334 # ---------------------------------------------------------------------------
335
336
337 class TestRegisterFlags:
338 """Verify that register() wires --json / -j correctly."""
339
340 def _make_parser(self):
341 import argparse
342 from muse.cli.commands.grep import register
343 ap = argparse.ArgumentParser()
344 subs = ap.add_subparsers()
345 register(subs)
346 return ap
347
348 def test_json_flag_long(self):
349 ns = self._make_parser().parse_args(["grep", "X", "--json"])
350 assert ns.json_out is True
351
352 def test_j_alias(self):
353 ns = self._make_parser().parse_args(["grep", "X", "-j"])
354 assert ns.json_out is True
355
356 def test_default_is_text(self):
357 ns = self._make_parser().parse_args(["grep", "X"])
358 assert ns.json_out is False
359
360 def test_dest_is_json_out(self):
361 ns = self._make_parser().parse_args(["grep", "X", "-j"])
362 assert hasattr(ns, "json_out")
363 assert not hasattr(ns, "fmt")
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 140 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 146 days ago