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