gabriel / muse public
test_impact_supercharge.py python
336 lines 13.3 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 135 days ago
1 """Supercharge tests for ``muse code impact`` — agent-usability gaps.
2
3 No prior tests existed for ``muse code impact``. This file covers:
4
5 Coverage matrix
6 ---------------
7 - --json / -j: -j alias works identically to --json
8 - exit_code: JSON output includes exit_code = 0 on success
9 - duration_ms: JSON output includes non-negative float duration_ms
10 - TypedDicts: _ImpactJson carries exit_code and duration_ms
11 - ForwardJson: forward mode JSON carries exit_code and duration_ms
12 - Docstrings: run() docstring mentions exit_code and duration_ms
13 - ANSI: JSON output never contains terminal escape sequences
14 - Performance: duration_ms stays under 2000 ms for a small repo
15 - Shapes: reverse mode vs forward mode JSON shapes are distinct
16 """
17
18 from __future__ import annotations
19 from collections.abc import Mapping
20
21 import json
22 import pathlib
23 import textwrap
24
25 import pytest
26
27 from tests.cli_test_helper import CliRunner
28
29 runner = CliRunner()
30
31
32 # ---------------------------------------------------------------------------
33 # Helpers
34 # ---------------------------------------------------------------------------
35
36
37 def _env(root: pathlib.Path) -> Mapping[str, str]:
38 return {"MUSE_REPO_ROOT": str(root)}
39
40
41 def _run(root: pathlib.Path, *args: str):
42 return runner.invoke(None, list(args), env=_env(root))
43
44
45 # ---------------------------------------------------------------------------
46 # Fixture — minimal Python repo with call relationships
47 # ---------------------------------------------------------------------------
48
49
50 @pytest.fixture()
51 def impact_repo(
52 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
53 ) -> pathlib.Path:
54 """Repo with a simple call graph.
55
56 core.py — compute(x) + validate(x)
57 service.py — process(x) calls compute(x)
58 api.py — handle(req) calls process(x)
59 """
60 monkeypatch.chdir(tmp_path)
61 r = _run(tmp_path, "init", "--domain", "code")
62 assert r.exit_code == 0, r.output
63
64 (tmp_path / "core.py").write_text(textwrap.dedent("""\
65 def compute(x):
66 return x * 2
67
68 def validate(x):
69 return x > 0
70 """))
71 (tmp_path / "service.py").write_text(textwrap.dedent("""\
72 from core import compute
73
74 def process(x):
75 return compute(x)
76 """))
77 (tmp_path / "api.py").write_text(textwrap.dedent("""\
78 from service import process
79
80 def handle(req):
81 return process(req)
82 """))
83 r = _run(tmp_path, "code", "add", ".")
84 assert r.exit_code == 0, r.output
85 r = _run(tmp_path, "commit", "-m", "seed impact repo")
86 assert r.exit_code == 0, r.output
87
88 return tmp_path
89
90
91 # ---------------------------------------------------------------------------
92 # TestJsonAlias — -j works identically to --json
93 # ---------------------------------------------------------------------------
94
95
96 class TestJsonAlias:
97 """-j shorthand must behave identically to --json."""
98
99 def test_j_alias_exits_zero(self, impact_repo: pathlib.Path) -> None:
100 r = _run(impact_repo, "code", "impact", "core.py::compute", "-j")
101 assert r.exit_code == 0, r.output
102
103 def test_j_alias_valid_json(self, impact_repo: pathlib.Path) -> None:
104 r = _run(impact_repo, "code", "impact", "core.py::compute", "-j")
105 json.loads(r.output) # must not raise
106
107 def test_j_alias_has_blast_radius_key(self, impact_repo: pathlib.Path) -> None:
108 r = _run(impact_repo, "code", "impact", "core.py::compute", "-j")
109 assert "blast_radius" in json.loads(r.output)
110
111 def test_j_alias_has_mode_key(self, impact_repo: pathlib.Path) -> None:
112 r = _run(impact_repo, "code", "impact", "core.py::compute", "-j")
113 assert "mode" in json.loads(r.output)
114
115 def test_j_alias_same_keys_as_json_flag(self, impact_repo: pathlib.Path) -> None:
116 r1 = _run(impact_repo, "code", "impact", "core.py::compute", "--json")
117 r2 = _run(impact_repo, "code", "impact", "core.py::compute", "-j")
118 d1 = json.loads(r1.output)
119 d2 = json.loads(r2.output)
120 d1.pop("duration_ms", None)
121 d2.pop("duration_ms", None)
122 assert set(d1.keys()) == set(d2.keys())
123
124 def test_j_alias_mode_is_reverse(self, impact_repo: pathlib.Path) -> None:
125 r = _run(impact_repo, "code", "impact", "core.py::compute", "-j")
126 assert json.loads(r.output)["mode"] == "reverse"
127
128 def test_j_alias_address_echoed(self, impact_repo: pathlib.Path) -> None:
129 r = _run(impact_repo, "code", "impact", "core.py::compute", "-j")
130 assert json.loads(r.output)["address"] == "core.py::compute"
131
132 def test_j_alias_forward_mode(self, impact_repo: pathlib.Path) -> None:
133 r = _run(impact_repo, "code", "impact", "core.py::compute", "-j", "--forward")
134 assert r.exit_code == 0, r.output
135 data = json.loads(r.output)
136 assert data["mode"] == "forward"
137
138 def test_j_alias_total_is_int(self, impact_repo: pathlib.Path) -> None:
139 r = _run(impact_repo, "code", "impact", "core.py::compute", "-j")
140 assert isinstance(json.loads(r.output)["total"], int)
141
142
143 # ---------------------------------------------------------------------------
144 # TestDurationMs — JSON output must include duration_ms
145 # ---------------------------------------------------------------------------
146
147
148 class TestDurationMs:
149 """JSON output must include a non-negative float duration_ms."""
150
151 def test_json_has_duration_ms(self, impact_repo: pathlib.Path) -> None:
152 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json")
153 assert "duration_ms" in json.loads(r.output)
154
155 def test_json_duration_ms_nonnegative(self, impact_repo: pathlib.Path) -> None:
156 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json")
157 assert json.loads(r.output)["duration_ms"] >= 0
158
159 def test_json_duration_ms_is_float(self, impact_repo: pathlib.Path) -> None:
160 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json")
161 assert isinstance(json.loads(r.output)["duration_ms"], float)
162
163 def test_j_alias_duration_ms_present(self, impact_repo: pathlib.Path) -> None:
164 r = _run(impact_repo, "code", "impact", "core.py::compute", "-j")
165 assert "duration_ms" in json.loads(r.output)
166
167 def test_forward_mode_duration_ms_present(self, impact_repo: pathlib.Path) -> None:
168 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json", "--forward")
169 data = json.loads(r.output)
170 assert "duration_ms" in data
171 assert isinstance(data["duration_ms"], float)
172
173 def test_duration_ms_under_2000ms(self, impact_repo: pathlib.Path) -> None:
174 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json")
175 assert json.loads(r.output)["duration_ms"] < 2000
176
177
178 # ---------------------------------------------------------------------------
179 # TestExitCode — JSON includes exit_code = 0 on success
180 # ---------------------------------------------------------------------------
181
182
183 class TestExitCode:
184 """JSON exit_code must be 0 on success."""
185
186 def test_json_has_exit_code(self, impact_repo: pathlib.Path) -> None:
187 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json")
188 assert "exit_code" in json.loads(r.output)
189
190 def test_json_exit_code_zero(self, impact_repo: pathlib.Path) -> None:
191 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json")
192 assert r.exit_code == 0
193 assert json.loads(r.output)["exit_code"] == 0
194
195 def test_json_exit_code_is_int(self, impact_repo: pathlib.Path) -> None:
196 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json")
197 assert isinstance(json.loads(r.output)["exit_code"], int)
198
199 def test_j_alias_exit_code_present(self, impact_repo: pathlib.Path) -> None:
200 r = _run(impact_repo, "code", "impact", "core.py::compute", "-j")
201 assert "exit_code" in json.loads(r.output)
202
203 def test_exit_code_mirrors_process_exit(self, impact_repo: pathlib.Path) -> None:
204 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json")
205 assert json.loads(r.output)["exit_code"] == r.exit_code
206
207 def test_forward_mode_exit_code_zero(self, impact_repo: pathlib.Path) -> None:
208 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json", "--forward")
209 assert r.exit_code == 0
210 assert json.loads(r.output)["exit_code"] == 0
211
212 def test_exit_code_leaf_symbol(self, impact_repo: pathlib.Path) -> None:
213 """exit_code is 0 even for a symbol with no callers."""
214 r = _run(impact_repo, "code", "impact", "core.py::validate", "--json")
215 assert r.exit_code == 0
216 assert json.loads(r.output)["exit_code"] == 0
217
218
219 # ---------------------------------------------------------------------------
220 # TestTypedDicts — _ImpactJson carries exit_code and duration_ms
221 # ---------------------------------------------------------------------------
222
223
224 class TestTypedDicts:
225 """_ImpactJson must carry exit_code and duration_ms annotations."""
226
227 def test_impact_json_typeddict_exists(self) -> None:
228 from muse.cli.commands.impact import _ImpactJson # noqa: F401
229
230 def test_has_exit_code_annotation(self) -> None:
231 from muse.cli.commands.impact import _ImpactJson
232 assert "exit_code" in _ImpactJson.__annotations__
233
234 def test_has_duration_ms_annotation(self) -> None:
235 from muse.cli.commands.impact import _ImpactJson
236 assert "duration_ms" in _ImpactJson.__annotations__
237
238 def test_retains_blast_radius_annotation(self) -> None:
239 from muse.cli.commands.impact import _ImpactJson
240 assert "blast_radius" in _ImpactJson.__annotations__
241
242 def test_retains_mode_annotation(self) -> None:
243 from muse.cli.commands.impact import _ImpactJson
244 assert "mode" in _ImpactJson.__annotations__
245
246 def test_retains_address_annotation(self) -> None:
247 from muse.cli.commands.impact import _ImpactJson
248 assert "address" in _ImpactJson.__annotations__
249
250 def test_retains_total_annotation(self) -> None:
251 from muse.cli.commands.impact import _ImpactJson
252 assert "total" in _ImpactJson.__annotations__
253
254
255 # ---------------------------------------------------------------------------
256 # TestAnsiSanitization — no escape codes in JSON output
257 # ---------------------------------------------------------------------------
258
259
260 class TestAnsiSanitization:
261 """No ANSI escape sequences anywhere in the JSON output."""
262
263 def test_json_output_no_ansi(self, impact_repo: pathlib.Path) -> None:
264 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json")
265 assert "\x1b" not in r.output
266
267 def test_j_alias_output_no_ansi(self, impact_repo: pathlib.Path) -> None:
268 r = _run(impact_repo, "code", "impact", "core.py::compute", "-j")
269 assert "\x1b" not in r.output
270
271 def test_forward_mode_no_ansi(self, impact_repo: pathlib.Path) -> None:
272 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json", "--forward")
273 assert "\x1b" not in r.output
274
275
276 # ---------------------------------------------------------------------------
277 # TestForwardMode — forward mode shape
278 # ---------------------------------------------------------------------------
279
280
281 class TestForwardMode:
282 """--forward mode must emit a valid, distinct JSON shape."""
283
284 def test_forward_has_callees_key(self, impact_repo: pathlib.Path) -> None:
285 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json", "--forward")
286 assert r.exit_code == 0, r.output
287 assert "callees" in json.loads(r.output)
288
289 def test_forward_mode_field_is_forward(self, impact_repo: pathlib.Path) -> None:
290 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json", "--forward")
291 assert json.loads(r.output)["mode"] == "forward"
292
293 def test_forward_has_total_key(self, impact_repo: pathlib.Path) -> None:
294 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json", "--forward")
295 data = json.loads(r.output)
296 assert "total" in data
297 assert isinstance(data["total"], int)
298
299 def test_forward_and_json_not_reverse(self, impact_repo: pathlib.Path) -> None:
300 r = _run(impact_repo, "code", "impact", "core.py::compute", "--json", "--forward")
301 data = json.loads(r.output)
302 assert "blast_radius" not in data
303
304
305 # ---------------------------------------------------------------------------
306 # TestRegisterFlags — argparse-level verification
307 # ---------------------------------------------------------------------------
308
309
310 class TestRegisterFlags:
311 """Verify that register() wires --json / -j correctly."""
312
313 def _make_parser(self):
314 import argparse
315 from muse.cli.commands.impact import register
316 ap = argparse.ArgumentParser()
317 subs = ap.add_subparsers()
318 register(subs)
319 return ap
320
321 def test_json_flag_long(self):
322 ns = self._make_parser().parse_args(["impact", "core.py::Fn", "--json"])
323 assert ns.json_out is True
324
325 def test_j_alias(self):
326 ns = self._make_parser().parse_args(["impact", "core.py::Fn", "-j"])
327 assert ns.json_out is True
328
329 def test_default_is_text(self):
330 ns = self._make_parser().parse_args(["impact", "core.py::Fn"])
331 assert ns.json_out is False
332
333 def test_dest_is_json_out(self):
334 ns = self._make_parser().parse_args(["impact", "core.py::Fn", "-j"])
335 assert hasattr(ns, "json_out")
336 assert not hasattr(ns, "fmt")
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 135 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 141 days ago