gabriel / muse public
test_compare_supercharge.py python
406 lines 14.2 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 compare`` — agent-usability gaps.
2
3 The existing TestCompare suite in test_code_commands.py covers correctness,
4 JSON schema, all filters (--kind, --file, --language), --stat, --semver, and
5 invalid-ref error paths. This file targets only the gaps those tests leave open:
6
7 Coverage matrix
8 ---------------
9 - --json / -j: -j alias works identically to --json
10 - exit_code: JSON output includes exit_code = 0 on success
11 - duration_ms: JSON output includes non-negative float duration_ms
12 - TypedDicts: _CompareJson gains exit_code/duration_ms annotations
13 - Docstrings: run() docstring mentions exit_code and duration_ms
14 - ANSI: JSON output never contains terminal escape sequences
15 - Performance: duration_ms stays under 2000 ms for a small repo
16 """
17
18 from __future__ import annotations
19
20 import json
21 import pathlib
22 import textwrap
23
24 import pytest
25
26 from tests.cli_test_helper import CliRunner
27
28 runner = CliRunner()
29
30
31 # ---------------------------------------------------------------------------
32 # Helpers
33 # ---------------------------------------------------------------------------
34
35
36 def _env(root: pathlib.Path) -> dict[str, str]:
37 return {"MUSE_REPO_ROOT": str(root)}
38
39
40 def _run(root: pathlib.Path, *args: str):
41 return runner.invoke(None, list(args), env=_env(root))
42
43
44 # ---------------------------------------------------------------------------
45 # Fixture — two-commit repo with a semantic change between them
46 # ---------------------------------------------------------------------------
47
48
49 @pytest.fixture()
50 def compare_repo(
51 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
52 ) -> tuple[pathlib.Path, str, str]:
53 """Repo with two commits.
54
55 Commit A — alpha.py defines alpha_fn().
56 Commit B — alpha.py also defines beta_fn() (added symbol).
57
58 Returns (path, commit_id_a, commit_id_b).
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 / "alpha.py").write_text(textwrap.dedent("""\
65 def alpha_fn():
66 return 1
67 """))
68 r = _run(tmp_path, "code", "add", ".")
69 assert r.exit_code == 0, r.output
70 r = _run(tmp_path, "commit", "-m", "add alpha_fn")
71 assert r.exit_code == 0, r.output
72
73 from muse.core.store import get_head_commit_id, read_current_branch
74 branch = read_current_branch(tmp_path)
75 commit_a = get_head_commit_id(tmp_path, branch)
76
77 (tmp_path / "alpha.py").write_text(textwrap.dedent("""\
78 def alpha_fn():
79 return 1
80
81 def beta_fn():
82 return 2
83 """))
84 r = _run(tmp_path, "code", "add", ".")
85 assert r.exit_code == 0, r.output
86 r = _run(tmp_path, "commit", "-m", "add beta_fn")
87 assert r.exit_code == 0, r.output
88
89 commit_b = get_head_commit_id(tmp_path, branch)
90 assert commit_a is not None
91 assert commit_b is not None
92 return tmp_path, commit_a, commit_b
93
94
95 # ---------------------------------------------------------------------------
96 # TestJsonAlias — -j works identically to --json
97 # ---------------------------------------------------------------------------
98
99
100 class TestJsonAlias:
101 """-j shorthand must behave identically to --json."""
102
103 def test_j_alias_exits_zero(
104 self, compare_repo: tuple[pathlib.Path, str, str]
105 ) -> None:
106 root, a, b = compare_repo
107 r = _run(root, "code", "compare", a, b, "-j")
108 assert r.exit_code == 0, r.output
109
110 def test_j_alias_valid_json(
111 self, compare_repo: tuple[pathlib.Path, str, str]
112 ) -> None:
113 root, a, b = compare_repo
114 r = _run(root, "code", "compare", a, b, "-j")
115 json.loads(r.output) # must not raise
116
117 def test_j_alias_has_from_key(
118 self, compare_repo: tuple[pathlib.Path, str, str]
119 ) -> None:
120 root, a, b = compare_repo
121 r = _run(root, "code", "compare", a, b, "-j")
122 data = json.loads(r.output)
123 assert "from" in data
124
125 def test_j_alias_has_ops_key(
126 self, compare_repo: tuple[pathlib.Path, str, str]
127 ) -> None:
128 root, a, b = compare_repo
129 r = _run(root, "code", "compare", a, b, "-j")
130 data = json.loads(r.output)
131 assert "ops" in data
132
133 def test_j_alias_same_top_level_keys_as_json_flag(
134 self, compare_repo: tuple[pathlib.Path, str, str]
135 ) -> None:
136 root, a, b = compare_repo
137 r1 = _run(root, "code", "compare", a, b, "--json")
138 r2 = _run(root, "code", "compare", a, b, "-j")
139 d1 = json.loads(r1.output)
140 d2 = json.loads(r2.output)
141 d1.pop("duration_ms", None)
142 d2.pop("duration_ms", None)
143 assert set(d1.keys()) == set(d2.keys())
144
145 def test_j_alias_op_count_matches_json_flag(
146 self, compare_repo: tuple[pathlib.Path, str, str]
147 ) -> None:
148 root, a, b = compare_repo
149 r1 = _run(root, "code", "compare", a, b, "--json")
150 r2 = _run(root, "code", "compare", a, b, "-j")
151 assert len(json.loads(r1.output)["ops"]) == len(json.loads(r2.output)["ops"])
152
153 def test_j_alias_same_ref_empty_ops(
154 self, compare_repo: tuple[pathlib.Path, str, str]
155 ) -> None:
156 root, a, _ = compare_repo
157 r = _run(root, "code", "compare", a, a, "-j")
158 assert r.exit_code == 0, r.output
159 assert json.loads(r.output)["ops"] == []
160
161 def test_j_alias_with_language_filter(
162 self, compare_repo: tuple[pathlib.Path, str, str]
163 ) -> None:
164 root, a, b = compare_repo
165 r = _run(root, "code", "compare", a, b, "-j", "--language", "Python")
166 assert r.exit_code == 0, r.output
167 data = json.loads(r.output)
168 assert data["filters"]["language"] == "Python"
169
170
171 # ---------------------------------------------------------------------------
172 # TestDurationMs — JSON output must include duration_ms
173 # ---------------------------------------------------------------------------
174
175
176 class TestDurationMs:
177 """JSON output must include a non-negative float duration_ms."""
178
179 def test_json_has_duration_ms(
180 self, compare_repo: tuple[pathlib.Path, str, str]
181 ) -> None:
182 root, a, b = compare_repo
183 r = _run(root, "code", "compare", a, b, "--json")
184 data = json.loads(r.output)
185 assert "duration_ms" in data
186
187 def test_json_duration_ms_nonnegative(
188 self, compare_repo: tuple[pathlib.Path, str, str]
189 ) -> None:
190 root, a, b = compare_repo
191 r = _run(root, "code", "compare", a, b, "--json")
192 assert json.loads(r.output)["duration_ms"] >= 0
193
194 def test_json_duration_ms_is_float(
195 self, compare_repo: tuple[pathlib.Path, str, str]
196 ) -> None:
197 root, a, b = compare_repo
198 r = _run(root, "code", "compare", a, b, "--json")
199 assert isinstance(json.loads(r.output)["duration_ms"], float)
200
201 def test_j_alias_duration_ms_present(
202 self, compare_repo: tuple[pathlib.Path, str, str]
203 ) -> None:
204 root, a, b = compare_repo
205 r = _run(root, "code", "compare", a, b, "-j")
206 assert "duration_ms" in json.loads(r.output)
207
208 def test_duration_ms_same_ref(
209 self, compare_repo: tuple[pathlib.Path, str, str]
210 ) -> None:
211 """duration_ms is present even when there are no changes."""
212 root, a, _ = compare_repo
213 r = _run(root, "code", "compare", a, a, "--json")
214 data = json.loads(r.output)
215 assert "duration_ms" in data
216 assert data["duration_ms"] >= 0
217
218 def test_duration_ms_with_kind_filter(
219 self, compare_repo: tuple[pathlib.Path, str, str]
220 ) -> None:
221 root, a, b = compare_repo
222 r = _run(root, "code", "compare", a, b, "--json", "--kind", "function")
223 data = json.loads(r.output)
224 assert "duration_ms" in data
225 assert data["duration_ms"] >= 0
226
227
228 # ---------------------------------------------------------------------------
229 # TestExitCode — JSON includes exit_code = 0 on success
230 # ---------------------------------------------------------------------------
231
232
233 class TestExitCode:
234 """JSON exit_code must be 0 on success."""
235
236 def test_json_has_exit_code(
237 self, compare_repo: tuple[pathlib.Path, str, str]
238 ) -> None:
239 root, a, b = compare_repo
240 r = _run(root, "code", "compare", a, b, "--json")
241 assert "exit_code" in json.loads(r.output)
242
243 def test_json_exit_code_zero_with_changes(
244 self, compare_repo: tuple[pathlib.Path, str, str]
245 ) -> None:
246 root, a, b = compare_repo
247 r = _run(root, "code", "compare", a, b, "--json")
248 assert r.exit_code == 0
249 assert json.loads(r.output)["exit_code"] == 0
250
251 def test_json_exit_code_zero_no_changes(
252 self, compare_repo: tuple[pathlib.Path, str, str]
253 ) -> None:
254 root, a, _ = compare_repo
255 r = _run(root, "code", "compare", a, a, "--json")
256 assert r.exit_code == 0
257 assert json.loads(r.output)["exit_code"] == 0
258
259 def test_json_exit_code_is_int(
260 self, compare_repo: tuple[pathlib.Path, str, str]
261 ) -> None:
262 root, a, b = compare_repo
263 r = _run(root, "code", "compare", a, b, "--json")
264 assert isinstance(json.loads(r.output)["exit_code"], int)
265
266 def test_j_alias_exit_code_present(
267 self, compare_repo: tuple[pathlib.Path, str, str]
268 ) -> None:
269 root, a, b = compare_repo
270 r = _run(root, "code", "compare", a, b, "-j")
271 assert "exit_code" in json.loads(r.output)
272
273 def test_exit_code_mirrors_process_exit(
274 self, compare_repo: tuple[pathlib.Path, str, str]
275 ) -> None:
276 root, a, b = compare_repo
277 r = _run(root, "code", "compare", a, b, "--json")
278 data = json.loads(r.output)
279 assert data["exit_code"] == r.exit_code
280
281 def test_exit_code_zero_with_filters(
282 self, compare_repo: tuple[pathlib.Path, str, str]
283 ) -> None:
284 root, a, b = compare_repo
285 r = _run(root, "code", "compare", a, b, "--json", "--kind", "function")
286 assert r.exit_code == 0
287 assert json.loads(r.output)["exit_code"] == 0
288
289
290 # ---------------------------------------------------------------------------
291 # TestTypedDicts — _CompareJson carries the new fields
292 # ---------------------------------------------------------------------------
293
294
295 class TestTypedDicts:
296 """_CompareJson must carry exit_code and duration_ms annotations."""
297
298 def test_compare_json_typeddict_exists(self) -> None:
299 from muse.cli.commands.compare import _CompareJson # noqa: F401
300
301 def test_has_exit_code_annotation(self) -> None:
302 from muse.cli.commands.compare import _CompareJson
303 assert "exit_code" in _CompareJson.__annotations__
304
305 def test_has_duration_ms_annotation(self) -> None:
306 from muse.cli.commands.compare import _CompareJson
307 assert "duration_ms" in _CompareJson.__annotations__
308
309 def test_retains_from_annotation(self) -> None:
310 from muse.cli.commands.compare import _CompareJson
311 assert "from" in _CompareJson.__annotations__
312
313 def test_retains_to_annotation(self) -> None:
314 from muse.cli.commands.compare import _CompareJson
315 assert "to" in _CompareJson.__annotations__
316
317 def test_retains_stat_annotation(self) -> None:
318 from muse.cli.commands.compare import _CompareJson
319 assert "stat" in _CompareJson.__annotations__
320
321 def test_retains_ops_annotation(self) -> None:
322 from muse.cli.commands.compare import _CompareJson
323 assert "ops" in _CompareJson.__annotations__
324
325 def test_retains_filters_annotation(self) -> None:
326 from muse.cli.commands.compare import _CompareJson
327 assert "filters" in _CompareJson.__annotations__
328
329
330 # ---------------------------------------------------------------------------
331 # TestDocstrings — run() docstring documents new fields
332 # ---------------------------------------------------------------------------
333
334
335 class TestDocstrings:
336 """run() must document exit_code and duration_ms."""
337
338 def test_run_docstring_mentions_exit_code(self) -> None:
339 from muse.cli.commands.compare import run
340 assert run.__doc__ is not None
341 assert "exit_code" in run.__doc__
342
343 def test_run_docstring_mentions_duration_ms(self) -> None:
344 from muse.cli.commands.compare import run
345 assert run.__doc__ is not None
346 assert "duration_ms" in run.__doc__
347
348
349 # ---------------------------------------------------------------------------
350 # TestAnsiSanitization — no escape codes in JSON output
351 # ---------------------------------------------------------------------------
352
353
354 class TestAnsiSanitization:
355 """No ANSI escape sequences anywhere in the JSON output."""
356
357 def test_json_output_no_ansi_with_changes(
358 self, compare_repo: tuple[pathlib.Path, str, str]
359 ) -> None:
360 root, a, b = compare_repo
361 r = _run(root, "code", "compare", a, b, "--json")
362 assert "\x1b" not in r.output
363
364 def test_j_alias_output_no_ansi(
365 self, compare_repo: tuple[pathlib.Path, str, str]
366 ) -> None:
367 root, a, b = compare_repo
368 r = _run(root, "code", "compare", a, b, "-j")
369 assert "\x1b" not in r.output
370
371 def test_json_output_no_ansi_no_changes(
372 self, compare_repo: tuple[pathlib.Path, str, str]
373 ) -> None:
374 root, a, _ = compare_repo
375 r = _run(root, "code", "compare", a, a, "--json")
376 assert "\x1b" not in r.output
377
378
379 # ---------------------------------------------------------------------------
380 # TestPerformance — duration_ms under 2000 ms for a small repo
381 # ---------------------------------------------------------------------------
382
383
384 class TestPerformance:
385 """duration_ms must stay under 2000 ms for small repos."""
386
387 def test_json_duration_under_2000ms(
388 self, compare_repo: tuple[pathlib.Path, str, str]
389 ) -> None:
390 root, a, b = compare_repo
391 r = _run(root, "code", "compare", a, b, "--json")
392 assert json.loads(r.output)["duration_ms"] < 2000
393
394 def test_j_alias_duration_under_2000ms(
395 self, compare_repo: tuple[pathlib.Path, str, str]
396 ) -> None:
397 root, a, b = compare_repo
398 r = _run(root, "code", "compare", a, b, "-j")
399 assert json.loads(r.output)["duration_ms"] < 2000
400
401 def test_duration_ms_is_float_not_int(
402 self, compare_repo: tuple[pathlib.Path, str, str]
403 ) -> None:
404 root, a, b = compare_repo
405 r = _run(root, "code", "compare", a, b, "--json")
406 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