gabriel / muse public

test_deps_supercharge.py file-level

at sha256:d · View file ↗ · Intel ↗

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