gabriel / muse public
test_deps_supercharge.py python
420 lines 16.6 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 138 days ago
1 """Supercharge tests for ``muse code deps`` — agent-usability gaps.
2
3 The existing TestDeps suite in test_code_commands.py covers correctness,
4 JSON schemas, all flags (--reverse, --count, --filter, --depth, --transitive),
5 file mode and symbol mode, and security (path traversal, empty file rel).
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 (all 6 JSON paths)
12 - exit_code: JSON output includes exit_code = 0 on success (all 6 paths)
13 - duration_ms: JSON output includes non-negative float duration_ms (all 6 paths)
14 - TypedDicts: _DepsFileJson and _DepsSymbolJson carry 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 Six JSON paths exercised
20 ------------------------
21 1. File mode, forward: --json billing.py → {path, imports, ...}
22 2. File mode, reverse: --reverse --json billing.py → {path, imported_by, ...}
23 3. Symbol mode, forward depth=1 --json billing.py::func → {address, depth, calls, ...}
24 4. Symbol mode, forward multi: --transitive --json → {address, by_depth, ...}
25 5. Symbol mode, reverse depth=1 --reverse --json symbol → {address, called_by, ...}
26 6. Symbol mode, reverse multi: --reverse --transitive → {address, by_depth, ...}
27 """
28
29 from __future__ import annotations
30
31 import json
32 import pathlib
33 import textwrap
34
35 import pytest
36
37 from tests.cli_test_helper import CliRunner
38
39 runner = CliRunner()
40
41 _SYMBOL = "billing.py::process_order"
42
43
44 # ---------------------------------------------------------------------------
45 # Helpers
46 # ---------------------------------------------------------------------------
47
48
49 def _env(root: pathlib.Path) -> dict[str, str]:
50 return {"MUSE_REPO_ROOT": str(root)}
51
52
53 def _run(root: pathlib.Path, *args: str):
54 return runner.invoke(None, list(args), env=_env(root))
55
56
57 # ---------------------------------------------------------------------------
58 # Fixture — repo with import graph and call graph
59 # ---------------------------------------------------------------------------
60
61
62 @pytest.fixture()
63 def deps_repo(
64 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
65 ) -> pathlib.Path:
66 """Repo with an import graph and a call graph for deps analysis.
67
68 Layout::
69
70 models.py — Invoice class with compute_total method
71 utils.py — validate() function
72 billing.py — imports models + utils; process_order calls both
73 api.py — imports billing; handle_request calls process_order
74
75 Import graph:
76 billing.py → models, utils
77 api.py → billing
78
79 Call graph (process_order):
80 process_order → validate, compute_total
81 handle_request → process_order
82 """
83 monkeypatch.chdir(tmp_path)
84 r = _run(tmp_path, "init", "--domain", "code")
85 assert r.exit_code == 0, r.output
86
87 (tmp_path / "models.py").write_text(textwrap.dedent("""\
88 class Invoice:
89 def compute_total(self, items):
90 return sum(items)
91 """))
92 (tmp_path / "utils.py").write_text(textwrap.dedent("""\
93 def validate(amount):
94 return amount > 0
95 """))
96 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
97 from models import Invoice
98 from utils import validate
99
100 def process_order(items):
101 if not validate(sum(items)):
102 raise ValueError("invalid amount")
103 inv = Invoice()
104 return inv.compute_total(items)
105 """))
106 (tmp_path / "api.py").write_text(textwrap.dedent("""\
107 from billing import process_order
108
109 def handle_request(items):
110 return process_order(items)
111 """))
112 r = _run(tmp_path, "code", "add", ".")
113 assert r.exit_code == 0, r.output
114 r = _run(tmp_path, "commit", "-m", "initial")
115 assert r.exit_code == 0, r.output
116
117 return tmp_path
118
119
120 # ---------------------------------------------------------------------------
121 # TestJsonAlias — -j works identically to --json (all major paths)
122 # ---------------------------------------------------------------------------
123
124
125 class TestJsonAlias:
126 """-j shorthand must behave identically to --json on every JSON path."""
127
128 def test_j_alias_exits_zero_file_forward(self, deps_repo: pathlib.Path) -> None:
129 r = _run(deps_repo, "code", "deps", "-j", "billing.py")
130 assert r.exit_code == 0, r.output
131
132 def test_j_alias_valid_json_file_forward(self, deps_repo: pathlib.Path) -> None:
133 r = _run(deps_repo, "code", "deps", "-j", "billing.py")
134 json.loads(r.output) # must not raise
135
136 def test_j_alias_has_imports_key(self, deps_repo: pathlib.Path) -> None:
137 r = _run(deps_repo, "code", "deps", "-j", "billing.py")
138 assert "imports" in json.loads(r.output)
139
140 def test_j_alias_exits_zero_file_reverse(self, deps_repo: pathlib.Path) -> None:
141 r = _run(deps_repo, "code", "deps", "-j", "--reverse", "billing.py")
142 assert r.exit_code == 0, r.output
143
144 def test_j_alias_has_imported_by_key(self, deps_repo: pathlib.Path) -> None:
145 r = _run(deps_repo, "code", "deps", "-j", "--reverse", "billing.py")
146 assert "imported_by" in json.loads(r.output)
147
148 def test_j_alias_exits_zero_symbol_forward(self, deps_repo: pathlib.Path) -> None:
149 r = _run(deps_repo, "code", "deps", "-j", _SYMBOL)
150 assert r.exit_code == 0, r.output
151
152 def test_j_alias_has_calls_key(self, deps_repo: pathlib.Path) -> None:
153 r = _run(deps_repo, "code", "deps", "-j", _SYMBOL)
154 assert "calls" in json.loads(r.output)
155
156 def test_j_alias_exits_zero_symbol_transitive(self, deps_repo: pathlib.Path) -> None:
157 r = _run(deps_repo, "code", "deps", "-j", "--transitive", _SYMBOL)
158 assert r.exit_code == 0, r.output
159
160 def test_j_alias_has_by_depth_key(self, deps_repo: pathlib.Path) -> None:
161 r = _run(deps_repo, "code", "deps", "-j", "--transitive", _SYMBOL)
162 assert "by_depth" in json.loads(r.output)
163
164 def test_j_alias_same_top_level_keys_file_forward(
165 self, deps_repo: pathlib.Path
166 ) -> None:
167 r1 = _run(deps_repo, "code", "deps", "--json", "billing.py")
168 r2 = _run(deps_repo, "code", "deps", "-j", "billing.py")
169 d1 = json.loads(r1.output)
170 d2 = json.loads(r2.output)
171 d1.pop("duration_ms", None)
172 d2.pop("duration_ms", None)
173 assert set(d1.keys()) == set(d2.keys())
174
175 def test_j_alias_same_imports_list_file_forward(
176 self, deps_repo: pathlib.Path
177 ) -> None:
178 r1 = _run(deps_repo, "code", "deps", "--json", "billing.py")
179 r2 = _run(deps_repo, "code", "deps", "-j", "billing.py")
180 assert json.loads(r1.output)["imports"] == json.loads(r2.output)["imports"]
181
182
183 # ---------------------------------------------------------------------------
184 # TestDurationMs — all 6 JSON paths must include duration_ms
185 # ---------------------------------------------------------------------------
186
187
188 class TestDurationMs:
189 """Every JSON output path must include a non-negative float duration_ms."""
190
191 def test_duration_ms_file_forward(self, deps_repo: pathlib.Path) -> None:
192 r = _run(deps_repo, "code", "deps", "--json", "billing.py")
193 data = json.loads(r.output)
194 assert "duration_ms" in data
195 assert isinstance(data["duration_ms"], float)
196 assert data["duration_ms"] >= 0
197
198 def test_duration_ms_file_reverse(self, deps_repo: pathlib.Path) -> None:
199 r = _run(deps_repo, "code", "deps", "--json", "--reverse", "billing.py")
200 data = json.loads(r.output)
201 assert "duration_ms" in data
202 assert isinstance(data["duration_ms"], float)
203 assert data["duration_ms"] >= 0
204
205 def test_duration_ms_symbol_forward_depth1(self, deps_repo: pathlib.Path) -> None:
206 r = _run(deps_repo, "code", "deps", "--json", _SYMBOL)
207 data = json.loads(r.output)
208 assert "duration_ms" in data
209 assert isinstance(data["duration_ms"], float)
210 assert data["duration_ms"] >= 0
211
212 def test_duration_ms_symbol_forward_transitive(
213 self, deps_repo: pathlib.Path
214 ) -> None:
215 r = _run(deps_repo, "code", "deps", "--json", "--transitive", _SYMBOL)
216 data = json.loads(r.output)
217 assert "duration_ms" in data
218 assert isinstance(data["duration_ms"], float)
219 assert data["duration_ms"] >= 0
220
221 def test_duration_ms_symbol_reverse_depth1(self, deps_repo: pathlib.Path) -> None:
222 r = _run(deps_repo, "code", "deps", "--json", "--reverse", _SYMBOL)
223 data = json.loads(r.output)
224 assert "duration_ms" in data
225 assert isinstance(data["duration_ms"], float)
226 assert data["duration_ms"] >= 0
227
228 def test_duration_ms_symbol_reverse_transitive(
229 self, deps_repo: pathlib.Path
230 ) -> None:
231 r = _run(deps_repo, "code", "deps", "--json", "--reverse", "--transitive", _SYMBOL)
232 data = json.loads(r.output)
233 assert "duration_ms" in data
234 assert isinstance(data["duration_ms"], float)
235 assert data["duration_ms"] >= 0
236
237 def test_j_alias_duration_ms_present_file_forward(
238 self, deps_repo: pathlib.Path
239 ) -> None:
240 r = _run(deps_repo, "code", "deps", "-j", "billing.py")
241 assert "duration_ms" in json.loads(r.output)
242
243 def test_j_alias_duration_ms_present_symbol(
244 self, deps_repo: pathlib.Path
245 ) -> None:
246 r = _run(deps_repo, "code", "deps", "-j", _SYMBOL)
247 assert "duration_ms" in json.loads(r.output)
248
249
250 # ---------------------------------------------------------------------------
251 # TestExitCode — all 6 JSON paths must include exit_code = 0
252 # ---------------------------------------------------------------------------
253
254
255 class TestExitCode:
256 """JSON exit_code must be 0 on success across all 6 JSON output paths."""
257
258 def test_exit_code_file_forward(self, deps_repo: pathlib.Path) -> None:
259 r = _run(deps_repo, "code", "deps", "--json", "billing.py")
260 assert r.exit_code == 0
261 data = json.loads(r.output)
262 assert "exit_code" in data
263 assert data["exit_code"] == 0
264
265 def test_exit_code_file_reverse(self, deps_repo: pathlib.Path) -> None:
266 r = _run(deps_repo, "code", "deps", "--json", "--reverse", "billing.py")
267 assert r.exit_code == 0
268 data = json.loads(r.output)
269 assert "exit_code" in data
270 assert data["exit_code"] == 0
271
272 def test_exit_code_symbol_forward_depth1(self, deps_repo: pathlib.Path) -> None:
273 r = _run(deps_repo, "code", "deps", "--json", _SYMBOL)
274 assert r.exit_code == 0
275 data = json.loads(r.output)
276 assert "exit_code" in data
277 assert data["exit_code"] == 0
278
279 def test_exit_code_symbol_forward_transitive(
280 self, deps_repo: pathlib.Path
281 ) -> None:
282 r = _run(deps_repo, "code", "deps", "--json", "--transitive", _SYMBOL)
283 assert r.exit_code == 0
284 data = json.loads(r.output)
285 assert "exit_code" in data
286 assert data["exit_code"] == 0
287
288 def test_exit_code_symbol_reverse_depth1(self, deps_repo: pathlib.Path) -> None:
289 r = _run(deps_repo, "code", "deps", "--json", "--reverse", _SYMBOL)
290 assert r.exit_code == 0
291 data = json.loads(r.output)
292 assert "exit_code" in data
293 assert data["exit_code"] == 0
294
295 def test_exit_code_symbol_reverse_transitive(
296 self, deps_repo: pathlib.Path
297 ) -> None:
298 r = _run(deps_repo, "code", "deps", "--json", "--reverse", "--transitive", _SYMBOL)
299 assert r.exit_code == 0
300 data = json.loads(r.output)
301 assert "exit_code" in data
302 assert data["exit_code"] == 0
303
304 def test_exit_code_is_int_file_forward(self, deps_repo: pathlib.Path) -> None:
305 r = _run(deps_repo, "code", "deps", "--json", "billing.py")
306 assert isinstance(json.loads(r.output)["exit_code"], int)
307
308 def test_exit_code_mirrors_process_exit(self, deps_repo: pathlib.Path) -> None:
309 r = _run(deps_repo, "code", "deps", "--json", "billing.py")
310 assert json.loads(r.output)["exit_code"] == r.exit_code
311
312 def test_j_alias_exit_code_present_file(self, deps_repo: pathlib.Path) -> None:
313 r = _run(deps_repo, "code", "deps", "-j", "billing.py")
314 assert "exit_code" in json.loads(r.output)
315
316 def test_j_alias_exit_code_present_symbol(self, deps_repo: pathlib.Path) -> None:
317 r = _run(deps_repo, "code", "deps", "-j", _SYMBOL)
318 assert "exit_code" in json.loads(r.output)
319
320
321 # ---------------------------------------------------------------------------
322 # TestTypedDicts — _DepsFileJson and _DepsSymbolJson carry new fields
323 # ---------------------------------------------------------------------------
324
325
326 class TestTypedDicts:
327 """_DepsFileJson and _DepsSymbolJson must carry exit_code and duration_ms."""
328
329 def test_deps_file_json_typeddict_exists(self) -> None:
330 from muse.cli.commands.deps import _DepsFileJson # noqa: F401
331
332 def test_deps_symbol_json_typeddict_exists(self) -> None:
333 from muse.cli.commands.deps import _DepsSymbolJson # noqa: F401
334
335 def test_file_json_has_exit_code_annotation(self) -> None:
336 from muse.cli.commands.deps import _DepsFileJson
337 assert "exit_code" in _DepsFileJson.__annotations__
338
339 def test_file_json_has_duration_ms_annotation(self) -> None:
340 from muse.cli.commands.deps import _DepsFileJson
341 assert "duration_ms" in _DepsFileJson.__annotations__
342
343 def test_symbol_json_has_exit_code_annotation(self) -> None:
344 from muse.cli.commands.deps import _DepsSymbolJson
345 assert "exit_code" in _DepsSymbolJson.__annotations__
346
347 def test_symbol_json_has_duration_ms_annotation(self) -> None:
348 from muse.cli.commands.deps import _DepsSymbolJson
349 assert "duration_ms" in _DepsSymbolJson.__annotations__
350
351 def test_file_json_retains_path_annotation(self) -> None:
352 from muse.cli.commands.deps import _DepsFileJson
353 assert "path" in _DepsFileJson.__annotations__
354
355 def test_symbol_json_retains_address_annotation(self) -> None:
356 from muse.cli.commands.deps import _DepsSymbolJson
357 assert "address" in _DepsSymbolJson.__annotations__
358
359
360 # ---------------------------------------------------------------------------
361 # TestDocstrings — run() docstring documents exit_code and duration_ms
362 # ---------------------------------------------------------------------------
363
364
365 class TestDocstrings:
366 """run() must document exit_code and duration_ms."""
367
368 def test_run_docstring_mentions_exit_code(self) -> None:
369 from muse.cli.commands.deps import run
370 assert run.__doc__ is not None
371 assert "exit_code" in run.__doc__
372
373 def test_run_docstring_mentions_duration_ms(self) -> None:
374 from muse.cli.commands.deps import run
375 assert run.__doc__ is not None
376 assert "duration_ms" in run.__doc__
377
378
379 # ---------------------------------------------------------------------------
380 # TestAnsiSanitization — no escape codes in JSON output
381 # ---------------------------------------------------------------------------
382
383
384 class TestAnsiSanitization:
385 """No ANSI escape sequences anywhere in the JSON output."""
386
387 def test_json_output_no_ansi_file_forward(self, deps_repo: pathlib.Path) -> None:
388 r = _run(deps_repo, "code", "deps", "--json", "billing.py")
389 assert "\x1b" not in r.output
390
391 def test_json_output_no_ansi_file_reverse(self, deps_repo: pathlib.Path) -> None:
392 r = _run(deps_repo, "code", "deps", "--json", "--reverse", "billing.py")
393 assert "\x1b" not in r.output
394
395 def test_json_output_no_ansi_symbol_forward(self, deps_repo: pathlib.Path) -> None:
396 r = _run(deps_repo, "code", "deps", "--json", _SYMBOL)
397 assert "\x1b" not in r.output
398
399
400 # ---------------------------------------------------------------------------
401 # TestPerformance — duration_ms under 2000 ms for a small repo
402 # ---------------------------------------------------------------------------
403
404
405 class TestPerformance:
406 """duration_ms must stay under 2000 ms for small repos."""
407
408 def test_json_duration_under_2000ms_file_forward(
409 self, deps_repo: pathlib.Path
410 ) -> None:
411 r = _run(deps_repo, "code", "deps", "--json", "billing.py")
412 assert json.loads(r.output)["duration_ms"] < 2000
413
414 def test_json_duration_under_2000ms_symbol(self, deps_repo: pathlib.Path) -> None:
415 r = _run(deps_repo, "code", "deps", "--json", _SYMBOL)
416 assert json.loads(r.output)["duration_ms"] < 2000
417
418 def test_duration_ms_is_float_not_int(self, deps_repo: pathlib.Path) -> None:
419 r = _run(deps_repo, "code", "deps", "--json", "billing.py")
420 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 138 days ago