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