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