gabriel / muse public
test_symbols_supercharge.py python
795 lines 34.2 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
1 """Seven-tier tests for ``muse/cli/commands/symbols.py``.
2
3 Tiers
4 -----
5 Unit — TypedDict fields; _c colour helper; _normalise_language;
6 _file_matches exact/suffix/separator anchoring; _resolve_file_filter
7 match/miss/ambiguous; _lang_counts; _print_human empty/non-empty;
8 _emit_json structure and field names.
9 Integration — -j alias parity; JSON envelope (schema_version, exit_code,
10 duration_ms); --kind / --language / --file filters in JSON;
11 --hashes in JSON; --count still works alongside envelope.
12 End-to-end — full CLI round-trips: basic, count, json, filters, commit ref,
13 invalid kind, ambiguous file, working-tree vs committed.
14 Stress — 1 000 _file_matches calls; 10 000 _lang_counts calls;
15 _emit_json on 500-symbol map.
16 Data integrity — total_symbols accurate; JSON result order by lineno;
17 schema_version, exit_code, duration_ms types correct;
18 all 9 entry fields present; working_tree bool invariant.
19 Security — ANSI in file path/symbol name; hostile --file value; long
20 language name; null byte in kind.
21 Performance — 10 000 _file_matches under 0.5 s; duration_ms field < 30 000ms.
22 """
23
24 from __future__ import annotations
25
26 import json
27 import os
28 import pathlib
29 import textwrap
30 import threading
31 import time
32 from typing import get_type_hints
33
34 import pytest
35
36 from tests.cli_test_helper import CliRunner, InvokeResult
37
38 runner = CliRunner()
39
40
41 # ──────────────────────────────────────────────────────────────────────────────
42 # Fixture helpers
43 # ──────────────────────────────────────────────────────────────────────────────
44
45
46 def _commit(repo: pathlib.Path, files: dict[str, str], message: str) -> None:
47 for name, content in files.items():
48 path = repo / name
49 path.parent.mkdir(parents=True, exist_ok=True)
50 path.write_text(content, encoding="utf-8")
51 saved = os.getcwd()
52 try:
53 os.chdir(repo)
54 runner.invoke(None, ["code", "add", "."])
55 runner.invoke(None, ["commit", "-m", message])
56 finally:
57 os.chdir(saved)
58
59
60 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
61 saved = os.getcwd()
62 try:
63 os.chdir(repo)
64 return runner.invoke(None, args)
65 finally:
66 os.chdir(saved)
67
68
69 def _syms(repo: pathlib.Path, *args: str) -> InvokeResult:
70 return _invoke(repo, ["code", "symbols", *args])
71
72
73 @pytest.fixture()
74 def sym_repo(tmp_path: pathlib.Path) -> pathlib.Path:
75 """Repo with two Python files and distinct symbol kinds."""
76 saved = os.getcwd()
77 try:
78 os.chdir(tmp_path)
79 runner.invoke(None, ["init"])
80 finally:
81 os.chdir(saved)
82
83 _commit(tmp_path, {
84 "billing.py": textwrap.dedent("""\
85 class Invoice:
86 def __init__(self, amount):
87 self.amount = amount
88
89 def total(self):
90 return self.amount * 1.1
91
92 def process_order(inv):
93 return inv.total()
94
95 async def send_email(to):
96 pass
97 """),
98 "utils.py": textwrap.dedent("""\
99 def helper():
100 return True
101
102 class Config:
103 debug = False
104 """),
105 }, "feat: add billing and utils")
106
107 return tmp_path
108
109
110 # ──────────────────────────────────────────────────────────────────────────────
111 # Unit — TypedDict
112 # ──────────────────────────────────────────────────────────────────────────────
113
114
115 class TestTypedDict:
116 def test_symbols_json_typeddict_exists(self) -> None:
117 from muse.cli.commands.symbols import _SymbolsJson # noqa: F401
118
119 def test_has_schema_version(self) -> None:
120 from muse.cli.commands.symbols import _SymbolsJson
121 assert "schema_version" in get_type_hints(_SymbolsJson)
122
123 def test_has_exit_code(self) -> None:
124 from muse.cli.commands.symbols import _SymbolsJson
125 assert "exit_code" in get_type_hints(_SymbolsJson)
126
127 def test_has_duration_ms(self) -> None:
128 from muse.cli.commands.symbols import _SymbolsJson
129 assert "duration_ms" in get_type_hints(_SymbolsJson)
130
131 def test_has_core_fields(self) -> None:
132 from muse.cli.commands.symbols import _SymbolsJson
133 hints = get_type_hints(_SymbolsJson)
134 for field in ("source_ref", "working_tree", "total_symbols", "results"):
135 assert field in hints, f"missing field: {field}"
136
137
138 # ──────────────────────────────────────────────────────────────────────────────
139 # Unit — _c colour helper
140 # ──────────────────────────────────────────────────────────────────────────────
141
142
143 class TestColorHelper:
144 def test_no_tty_returns_plain_text(self) -> None:
145 from muse.cli.commands.symbols import _c, _BLUE
146 assert _c("hello", _BLUE, tty=False) == "hello"
147
148 def test_tty_wraps_with_ansi(self) -> None:
149 from muse.cli.commands.symbols import _c, _BLUE, _RESET
150 result = _c("hello", _BLUE, tty=True)
151 assert _BLUE in result
152 assert _RESET in result
153 assert "hello" in result
154
155 def test_multiple_codes_all_applied(self) -> None:
156 from muse.cli.commands.symbols import _c, _BOLD, _YELLOW, _RESET
157 result = _c("x", _BOLD, _YELLOW, tty=True)
158 assert _BOLD in result
159 assert _YELLOW in result
160
161
162 # ──────────────────────────────────────────────────────────────────────────────
163 # Unit — _normalise_language
164 # ──────────────────────────────────────────────────────────────────────────────
165
166
167 class TestNormaliseLanguage:
168 def test_python_lowercase_normalised(self) -> None:
169 from muse.cli.commands.symbols import _normalise_language
170 assert _normalise_language("python") == "Python"
171
172 def test_python_uppercase_normalised(self) -> None:
173 from muse.cli.commands.symbols import _normalise_language
174 assert _normalise_language("PYTHON") == "Python"
175
176 def test_python_mixed_normalised(self) -> None:
177 from muse.cli.commands.symbols import _normalise_language
178 assert _normalise_language("Python") == "Python"
179
180 def test_unknown_language_returned_unchanged(self) -> None:
181 from muse.cli.commands.symbols import _normalise_language
182 assert _normalise_language("Brainfuck") == "Brainfuck"
183
184 def test_strips_whitespace(self) -> None:
185 from muse.cli.commands.symbols import _normalise_language
186 result = _normalise_language(" python ")
187 assert result == "Python"
188
189
190 # ──────────────────────────────────────────────────────────────────────────────
191 # Unit — _file_matches
192 # ──────────────────────────────────────────────────────────────────────────────
193
194
195 class TestFileMatches:
196 def test_exact_path_matches(self) -> None:
197 from muse.cli.commands.symbols import _file_matches
198 assert _file_matches("src/billing.py", "src/billing.py") is True
199
200 def test_suffix_with_slash_anchor_matches(self) -> None:
201 from muse.cli.commands.symbols import _file_matches
202 assert _file_matches("src/billing.py", "billing.py") is True
203
204 def test_partial_name_does_not_match(self) -> None:
205 """'y.py' must NOT match 'billy.py' — separator anchor required."""
206 from muse.cli.commands.symbols import _file_matches
207 assert _file_matches("billy.py", "y.py") is False
208
209 def test_no_match_returns_false(self) -> None:
210 from muse.cli.commands.symbols import _file_matches
211 assert _file_matches("src/utils.py", "billing.py") is False
212
213 def test_windows_backslash_normalised(self) -> None:
214 """Backslash in the suffix filter is normalised to slash before matching."""
215 from muse.cli.commands.symbols import _file_matches
216 assert _file_matches("a/b/billing.py", "b\\billing.py") is True
217
218 def test_deep_path_suffix_matches(self) -> None:
219 from muse.cli.commands.symbols import _file_matches
220 assert _file_matches("a/b/c/billing.py", "billing.py") is True
221
222
223 # ──────────────────────────────────────────────────────────────────────────────
224 # Unit — _resolve_file_filter
225 # ──────────────────────────────────────────────────────────────────────────────
226
227
228 class TestResolveFileFilter:
229 def _manifest(self, *paths: str) -> dict:
230 return {p: f"sha256:{'aa' * 32}" for p in paths}
231
232 def test_exact_match_returns_path(self) -> None:
233 from muse.cli.commands.symbols import _resolve_file_filter
234 result = _resolve_file_filter("billing.py", self._manifest("billing.py"))
235 assert result == "billing.py"
236
237 def test_suffix_match_returns_full_path(self) -> None:
238 from muse.cli.commands.symbols import _resolve_file_filter
239 result = _resolve_file_filter("billing.py", self._manifest("src/billing.py"))
240 assert result == "src/billing.py"
241
242 def test_no_match_returns_none(self) -> None:
243 from muse.cli.commands.symbols import _resolve_file_filter
244 result = _resolve_file_filter("nope.py", self._manifest("billing.py"))
245 assert result is None
246
247 def test_ambiguous_raises_system_exit(self) -> None:
248 from muse.cli.commands.symbols import _resolve_file_filter
249 with pytest.raises(SystemExit):
250 _resolve_file_filter(
251 "billing.py",
252 self._manifest("a/billing.py", "b/billing.py"),
253 )
254
255
256 # ──────────────────────────────────────────────────────────────────────────────
257 # Unit — _lang_counts
258 # ──────────────────────────────────────────────────────────────────────────────
259
260
261 class TestLangCounts:
262 def _tree(self, n: int) -> dict:
263 """Fake SymbolTree with n symbols."""
264 return {f"sym_{i}": {"lineno": i} for i in range(n)}
265
266 def test_single_python_file(self) -> None:
267 from muse.cli.commands.symbols import _lang_counts
268 counts = _lang_counts({"billing.py": self._tree(3)})
269 assert counts.get("Python") == 3
270
271 def test_multiple_files_summed_per_language(self) -> None:
272 from muse.cli.commands.symbols import _lang_counts
273 counts = _lang_counts({
274 "a.py": self._tree(2),
275 "b.py": self._tree(4),
276 })
277 assert counts.get("Python") == 6
278
279 def test_empty_map_returns_empty(self) -> None:
280 from muse.cli.commands.symbols import _lang_counts
281 assert _lang_counts({}) == {}
282
283 def test_unknown_extension_grouped_correctly(self) -> None:
284 from muse.cli.commands.symbols import _lang_counts
285 counts = _lang_counts({"thing.xyz": self._tree(1)})
286 assert sum(counts.values()) == 1
287
288
289 # ──────────────────────────────────────────────────────────────────────────────
290 # Unit — _print_human
291 # ──────────────────────────────────────────────────────────────────────────────
292
293
294 class TestPrintHuman:
295 def test_empty_map_prints_no_symbols(self, capsys) -> None:
296 from muse.cli.commands.symbols import _print_human
297 _print_human({}, show_hashes=False, tty=False)
298 captured = capsys.readouterr()
299 assert "no semantic symbols found" in captured.out
300
301 def test_non_empty_map_shows_file_and_symbol(self, capsys) -> None:
302 from muse.cli.commands.symbols import _print_human
303 tree = {
304 "billing.py::Invoice": {
305 "kind": "class",
306 "name": "Invoice",
307 "qualified_name": "Invoice",
308 "lineno": 1,
309 "content_id": "sha256:" + "ab" * 32,
310 }
311 }
312 _print_human({"billing.py": tree}, show_hashes=False, tty=False)
313 out = capsys.readouterr().out
314 assert "billing.py" in out
315 assert "Invoice" in out
316
317 def test_show_hashes_appends_hash_suffix(self, capsys) -> None:
318 from muse.cli.commands.symbols import _print_human
319 tree = {
320 "f.py::fn": {
321 "kind": "function",
322 "name": "fn",
323 "qualified_name": "fn",
324 "lineno": 1,
325 "content_id": "sha256:" + "cd" * 32,
326 }
327 }
328 _print_human({"f.py": tree}, show_hashes=True, tty=False)
329 out = capsys.readouterr().out
330 assert ".." in out
331
332 def test_summary_line_shows_count(self, capsys) -> None:
333 from muse.cli.commands.symbols import _print_human
334 tree = {
335 "f.py::fn": {
336 "kind": "function", "name": "fn", "qualified_name": "fn",
337 "lineno": 1, "content_id": "sha256:" + "aa" * 32,
338 }
339 }
340 _print_human({"f.py": tree}, show_hashes=False, tty=False)
341 out = capsys.readouterr().out
342 assert "1 symbol" in out or "symbol" in out
343
344
345 # ──────────────────────────────────────────────────────────────────────────────
346 # Unit — _emit_json
347 # ──────────────────────────────────────────────────────────────────────────────
348
349
350 class TestEmitJson:
351 def _tree(self) -> dict:
352 return {
353 "billing.py::Invoice": {
354 "kind": "class",
355 "name": "Invoice",
356 "qualified_name": "Invoice",
357 "lineno": 1,
358 "end_lineno": 10,
359 "content_id": "sha256:" + "aa" * 32,
360 "body_hash": "sha256:" + "bb" * 32,
361 "signature_id": "sha256:" + "cc" * 32,
362 }
363 }
364
365 def test_emits_json_with_schema_version(self, capsys) -> None:
366 from muse.cli.commands.symbols import _emit_json
367 _emit_json({"billing.py": self._tree()}, source_ref="abc123", working_tree=True)
368 d = json.loads(capsys.readouterr().out)
369 assert "schema_version" in d
370
371 def test_emits_json_with_exit_code(self, capsys) -> None:
372 from muse.cli.commands.symbols import _emit_json
373 _emit_json({"billing.py": self._tree()}, source_ref="abc123", working_tree=True)
374 d = json.loads(capsys.readouterr().out)
375 assert d["exit_code"] == 0
376
377 def test_emits_json_with_duration_ms(self, capsys) -> None:
378 from muse.cli.commands.symbols import _emit_json
379 _emit_json({"billing.py": self._tree()}, source_ref="abc123", working_tree=True)
380 d = json.loads(capsys.readouterr().out)
381 assert "duration_ms" in d
382 assert isinstance(d["duration_ms"], float)
383
384 def test_total_symbols_correct(self, capsys) -> None:
385 from muse.cli.commands.symbols import _emit_json
386 _emit_json({"billing.py": self._tree()}, source_ref="abc123", working_tree=True)
387 d = json.loads(capsys.readouterr().out)
388 assert d["total_symbols"] == 1
389
390 def test_result_entry_has_path_field(self, capsys) -> None:
391 from muse.cli.commands.symbols import _emit_json
392 _emit_json({"billing.py": self._tree()}, source_ref="abc123", working_tree=True)
393 d = json.loads(capsys.readouterr().out)
394 assert d["results"][0]["path"] == "billing.py"
395
396 def test_result_entry_fields_complete(self, capsys) -> None:
397 from muse.cli.commands.symbols import _emit_json
398 _emit_json({"billing.py": self._tree()}, source_ref="abc123", working_tree=True)
399 d = json.loads(capsys.readouterr().out)
400 entry = d["results"][0]
401 for field in ("address", "kind", "name", "qualified_name", "path",
402 "lineno", "end_lineno", "content_id", "body_hash", "signature_id"):
403 assert field in entry, f"missing field: {field}"
404
405 def test_working_tree_false_propagated(self, capsys) -> None:
406 from muse.cli.commands.symbols import _emit_json
407 _emit_json({"billing.py": self._tree()}, source_ref="a1b2c3d4", working_tree=False)
408 d = json.loads(capsys.readouterr().out)
409 assert d["working_tree"] is False
410
411 def test_empty_map_emits_zero_results(self, capsys) -> None:
412 from muse.cli.commands.symbols import _emit_json
413 _emit_json({}, source_ref="abc123", working_tree=True)
414 d = json.loads(capsys.readouterr().out)
415 assert d["total_symbols"] == 0
416 assert d["results"] == []
417
418
419 # ──────────────────────────────────────────────────────────────────────────────
420 # Integration — alias, docstrings, envelope
421 # ──────────────────────────────────────────────────────────────────────────────
422
423
424 class TestAliasRegistration:
425 def test_j_alias_registered(self) -> None:
426 from muse.cli.commands.symbols import register
427 import argparse
428 p = argparse.ArgumentParser()
429 sub = p.add_subparsers()
430 register(sub)
431 ns = p.parse_args(["symbols", "-j"])
432 assert ns.as_json is True
433
434 def test_json_long_form_works(self) -> None:
435 from muse.cli.commands.symbols import register
436 import argparse
437 p = argparse.ArgumentParser()
438 sub = p.add_subparsers()
439 register(sub)
440 ns = p.parse_args(["symbols", "--json"])
441 assert ns.as_json is True
442
443
444 class TestDocstrings:
445 def test_register_mentions_json_alias(self) -> None:
446 from muse.cli.commands.symbols import register
447 doc = register.__doc__ or ""
448 assert "--json" in doc or "-j" in doc
449
450 def test_run_mentions_exit_code(self) -> None:
451 from muse.cli.commands.symbols import run
452 assert "exit_code" in (run.__doc__ or "")
453
454 def test_run_mentions_duration_ms(self) -> None:
455 from muse.cli.commands.symbols import run
456 assert "duration_ms" in (run.__doc__ or "")
457
458 def test_run_mentions_schema_version(self) -> None:
459 from muse.cli.commands.symbols import run
460 assert "schema_version" in (run.__doc__ or "")
461
462
463 class TestJsonEnvelope:
464 def test_schema_version_present(self, sym_repo) -> None:
465 r = _syms(sym_repo, "--json")
466 assert r.exit_code == 0
467 assert "schema_version" in json.loads(r.output)
468
469 def test_exit_code_zero(self, sym_repo) -> None:
470 r = _syms(sym_repo, "--json")
471 assert r.exit_code == 0
472 assert json.loads(r.output)["exit_code"] == 0
473
474 def test_duration_ms_is_float(self, sym_repo) -> None:
475 r = _syms(sym_repo, "--json")
476 assert r.exit_code == 0
477 d = json.loads(r.output)
478 assert isinstance(d["duration_ms"], float)
479
480 def test_schema_version_nonempty_string(self, sym_repo) -> None:
481 r = _syms(sym_repo, "--json")
482 assert r.exit_code == 0
483 d = json.loads(r.output)
484 assert isinstance(d["schema_version"], str) and len(d["schema_version"]) > 0
485
486
487 class TestJsonAlias:
488 def test_j_parity_with_json(self, sym_repo) -> None:
489 r1 = _syms(sym_repo, "--json")
490 r2 = _syms(sym_repo, "-j")
491 assert r1.exit_code == 0
492 assert r2.exit_code == 0
493 d1, d2 = json.loads(r1.output), json.loads(r2.output)
494 assert d1["total_symbols"] == d2["total_symbols"]
495 assert d1["results"] == d2["results"]
496 assert d1["schema_version"] == d2["schema_version"]
497 assert d1["exit_code"] == d2["exit_code"]
498
499
500 # ──────────────────────────────────────────────────────────────────────────────
501 # End-to-end
502 # ──────────────────────────────────────────────────────────────────────────────
503
504
505 class TestEndToEnd:
506 def test_basic_exits_zero(self, sym_repo) -> None:
507 assert _syms(sym_repo).exit_code == 0
508
509 def test_basic_shows_symbols(self, sym_repo) -> None:
510 r = _syms(sym_repo)
511 assert "Invoice" in r.output
512 assert "symbols across" in r.output
513
514 def test_count_flag(self, sym_repo) -> None:
515 r = _syms(sym_repo, "--count")
516 assert r.exit_code == 0
517 assert "symbols" in r.output
518 assert "Python" in r.output
519 assert "Invoice" not in r.output
520
521 def test_kind_class_filter(self, sym_repo) -> None:
522 r = _syms(sym_repo, "--kind", "class")
523 assert r.exit_code == 0
524 assert "Invoice" in r.output
525 assert "process_order" not in r.output
526
527 def test_kind_function_filter(self, sym_repo) -> None:
528 r = _syms(sym_repo, "--kind", "function")
529 assert r.exit_code == 0
530 assert "process_order" in r.output
531 assert "Invoice" not in r.output
532
533 def test_invalid_kind_exits_nonzero(self, sym_repo) -> None:
534 r = _syms(sym_repo, "--kind", "potato")
535 assert r.exit_code != 0
536
537 def test_file_filter(self, sym_repo) -> None:
538 r = _syms(sym_repo, "--file", "billing.py")
539 assert r.exit_code == 0
540 assert "Invoice" in r.output
541
542 def test_file_filter_no_match_shows_no_symbols(self, sym_repo) -> None:
543 r = _syms(sym_repo, "--file", "nonexistent.py")
544 assert r.exit_code == 0
545 assert "no semantic symbols found" in r.output
546
547 def test_language_filter_python(self, sym_repo) -> None:
548 r = _syms(sym_repo, "--language", "python")
549 assert r.exit_code == 0
550 assert "Invoice" in r.output
551
552 def test_language_filter_case_insensitive(self, sym_repo) -> None:
553 for variant in ("python", "Python", "PYTHON"):
554 r = _syms(sym_repo, "--language", variant)
555 assert r.exit_code == 0, f"failed for {variant!r}"
556 assert "Invoice" in r.output
557
558 def test_language_no_match_shows_no_symbols(self, sym_repo) -> None:
559 r = _syms(sym_repo, "--language", "Go")
560 assert r.exit_code == 0
561 assert "no semantic symbols found" in r.output
562
563 def test_hashes_flag(self, sym_repo) -> None:
564 r = _syms(sym_repo, "--hashes")
565 assert r.exit_code == 0
566 assert ".." in r.output
567
568 def test_count_and_json_mutually_exclusive(self, sym_repo) -> None:
569 r = _syms(sym_repo, "--count", "--json")
570 assert r.exit_code != 0
571
572 def test_commit_head_exits_zero(self, sym_repo) -> None:
573 r = _syms(sym_repo, "--commit", "HEAD")
574 assert r.exit_code == 0
575 assert "Invoice" in r.output
576
577 def test_json_working_tree_true_when_no_commit(self, sym_repo) -> None:
578 r = _syms(sym_repo, "--json")
579 assert r.exit_code == 0
580 d = json.loads(r.output)
581 assert d["working_tree"] is True
582 assert d["source_ref"] == "working-tree"
583
584 def test_json_working_tree_false_with_commit(self, sym_repo) -> None:
585 r = _syms(sym_repo, "--json", "--commit", "HEAD")
586 assert r.exit_code == 0
587 d = json.loads(r.output)
588 assert d["working_tree"] is False
589 assert d["source_ref"] != "working-tree"
590
591 def test_json_result_path_field(self, sym_repo) -> None:
592 r = _syms(sym_repo, "--json", "--file", "billing.py")
593 assert r.exit_code == 0
594 d = json.loads(r.output)
595 assert all(e["path"] == "billing.py" for e in d["results"])
596
597 def test_j_alias_works_in_cli(self, sym_repo) -> None:
598 r = _syms(sym_repo, "-j")
599 assert r.exit_code == 0
600 d = json.loads(r.output)
601 assert "total_symbols" in d
602
603 def test_invalid_commit_ref_exits_nonzero(self, sym_repo) -> None:
604 r = _syms(sym_repo, "--commit", "deadbeefdeadbeef")
605 assert r.exit_code != 0
606
607
608 # ──────────────────────────────────────────────────────────────────────────────
609 # Stress
610 # ──────────────────────────────────────────────────────────────────────────────
611
612
613 class TestStress:
614 def test_1000_file_matches_calls(self) -> None:
615 from muse.cli.commands.symbols import _file_matches
616 for i in range(1_000):
617 _file_matches(f"src/file{i}.py", "billing.py")
618
619 def test_10000_normalise_language_calls(self) -> None:
620 from muse.cli.commands.symbols import _normalise_language
621 for _ in range(10_000):
622 result = _normalise_language("python")
623 assert result == "Python"
624
625 def test_emit_json_500_symbol_map(self, capsys) -> None:
626 from muse.cli.commands.symbols import _emit_json
627 tree = {
628 f"f.py::sym_{i}": {
629 "kind": "function",
630 "name": f"sym_{i}",
631 "qualified_name": f"sym_{i}",
632 "lineno": i + 1,
633 "end_lineno": i + 5,
634 "content_id": "sha256:" + "aa" * 32,
635 "body_hash": "sha256:" + "bb" * 32,
636 "signature_id": "sha256:" + "cc" * 32,
637 }
638 for i in range(500)
639 }
640 _emit_json({"f.py": tree}, source_ref="abc123", working_tree=True)
641 d = json.loads(capsys.readouterr().out)
642 assert d["total_symbols"] == 500
643
644 def test_concurrent_file_matches(self) -> None:
645 from muse.cli.commands.symbols import _file_matches
646 results: list[bool] = []
647 lock = threading.Lock()
648
649 def _run() -> None:
650 v = _file_matches("src/billing.py", "billing.py")
651 with lock:
652 results.append(v)
653
654 threads = [threading.Thread(target=_run) for _ in range(50)]
655 for t in threads: t.start()
656 for t in threads: t.join()
657 assert all(results)
658 assert len(results) == 50
659
660
661 # ──────────────────────────────────────────────────────────────────────────────
662 # Data integrity
663 # ──────────────────────────────────────────────────────────────────────────────
664
665
666 class TestDataIntegrity:
667 def test_total_symbols_matches_results_length(self, sym_repo) -> None:
668 r = _syms(sym_repo, "--json")
669 assert r.exit_code == 0
670 d = json.loads(r.output)
671 assert d["total_symbols"] == len(d["results"])
672
673 def test_json_results_ordered_by_lineno(self, sym_repo) -> None:
674 r = _syms(sym_repo, "--json", "--file", "billing.py")
675 assert r.exit_code == 0
676 linenos = [e["lineno"] for e in json.loads(r.output)["results"]]
677 assert linenos == sorted(linenos)
678
679 def test_all_result_fields_present(self, sym_repo) -> None:
680 r = _syms(sym_repo, "--json")
681 assert r.exit_code == 0
682 for entry in json.loads(r.output)["results"]:
683 for field in ("address", "kind", "name", "qualified_name", "path",
684 "lineno", "end_lineno", "content_id", "body_hash", "signature_id"):
685 assert field in entry, f"missing field: {field}"
686
687 def test_schema_version_is_string(self, sym_repo) -> None:
688 r = _syms(sym_repo, "--json")
689 assert r.exit_code == 0
690 assert isinstance(json.loads(r.output)["schema_version"], str)
691
692 def test_exit_code_is_int(self, sym_repo) -> None:
693 r = _syms(sym_repo, "--json")
694 assert r.exit_code == 0
695 assert isinstance(json.loads(r.output)["exit_code"], int)
696
697 def test_duration_ms_nonnegative(self, sym_repo) -> None:
698 r = _syms(sym_repo, "--json")
699 assert r.exit_code == 0
700 assert json.loads(r.output)["duration_ms"] >= 0
701
702 def test_working_tree_is_bool(self, sym_repo) -> None:
703 r = _syms(sym_repo, "--json")
704 assert r.exit_code == 0
705 assert isinstance(json.loads(r.output)["working_tree"], bool)
706
707 def test_kind_filter_propagated_to_results(self, sym_repo) -> None:
708 r = _syms(sym_repo, "--json", "--kind", "class")
709 assert r.exit_code == 0
710 d = json.loads(r.output)
711 assert all(e["kind"] == "class" for e in d["results"])
712
713 def test_file_filter_propagated_to_results(self, sym_repo) -> None:
714 r = _syms(sym_repo, "--json", "--file", "billing.py")
715 assert r.exit_code == 0
716 d = json.loads(r.output)
717 assert all(e["path"] == "billing.py" for e in d["results"])
718
719 def test_lang_counts_total_matches_total_symbols(self, sym_repo) -> None:
720 from muse.cli.commands.symbols import _lang_counts
721 tree = {
722 "a.py::f1": {"lineno": 1},
723 "a.py::f2": {"lineno": 2},
724 }
725 counts = _lang_counts({"a.py": tree})
726 assert sum(counts.values()) == 2
727
728
729 # ──────────────────────────────────────────────────────────────────────────────
730 # Security
731 # ──────────────────────────────────────────────────────────────────────────────
732
733
734 class TestSecurity:
735 def test_ansi_in_file_filter_does_not_crash(self, sym_repo) -> None:
736 r = _syms(sym_repo, "--file", "\x1b[31mbad\x1b[0m.py")
737 assert r.exit_code in (0, 1, 2)
738
739 def test_very_long_language_does_not_crash(self, sym_repo) -> None:
740 r = _syms(sym_repo, "--language", "x" * 10_000)
741 assert r.exit_code in (0, 1, 2)
742
743 def test_sql_injection_in_file_filter_does_not_crash(self, sym_repo) -> None:
744 r = _syms(sym_repo, "--file", "'; DROP TABLE symbols; --")
745 assert r.exit_code in (0, 1, 2)
746
747 def test_file_matches_with_ansi_in_path(self) -> None:
748 from muse.cli.commands.symbols import _file_matches
749 evil = "\x1b[31mbilling\x1b[0m.py"
750 # Should return False without raising
751 result = _file_matches("billing.py", evil)
752 assert isinstance(result, bool)
753
754 def test_hostile_detail_survives_json_serialisation(self, capsys) -> None:
755 from muse.cli.commands.symbols import _emit_json
756 tree = {
757 "f.py::fn": {
758 "kind": "function",
759 "name": '"; DROP TABLE --',
760 "qualified_name": '"; DROP TABLE --',
761 "lineno": 1,
762 "end_lineno": 5,
763 "content_id": "sha256:" + "aa" * 32,
764 "body_hash": "sha256:" + "bb" * 32,
765 "signature_id": "sha256:" + "cc" * 32,
766 }
767 }
768 _emit_json({"f.py": tree}, source_ref="abc", working_tree=True)
769 d = json.loads(capsys.readouterr().out)
770 assert d["results"][0]["name"] == '"; DROP TABLE --'
771
772 def test_unicode_in_file_filter_does_not_crash(self, sym_repo) -> None:
773 r = _syms(sym_repo, "--file", "音符.py")
774 assert r.exit_code in (0, 1, 2)
775
776
777 # ──────────────────────────────────────────────────────────────────────────────
778 # Performance
779 # ──────────────────────────────────────────────────────────────────────────────
780
781
782 class TestPerformance:
783 def test_10000_file_matches_under_500ms(self) -> None:
784 from muse.cli.commands.symbols import _file_matches
785 start = time.perf_counter()
786 for i in range(10_000):
787 _file_matches(f"src/billing_{i}.py", "billing.py")
788 elapsed = time.perf_counter() - start
789 assert elapsed < 0.5, f"10 000 _file_matches took {elapsed:.2f}s"
790
791 def test_duration_ms_under_30000ms(self, sym_repo) -> None:
792 r = _syms(sym_repo, "--json")
793 assert r.exit_code == 0
794 d = json.loads(r.output)
795 assert d["duration_ms"] < 30_000
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago