gabriel / muse public
test_clones_supercharge.py python
353 lines 13.8 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago
1 """Supercharge tests for ``muse code clones`` — agent-usability gaps.
2
3 The existing test_cmd_clones.py covers correctness, JSON schema, flags, E2E,
4 and stress. This file targets only the gaps those tests leave open:
5
6 Coverage matrix
7 ---------------
8 - --json / -j: -j alias works identically to --json
9 - exit_code: JSON output includes exit_code = 0 on success
10 - duration_ms: JSON output includes non-negative float duration_ms
11 - TypedDicts: _ClonesOutputJson gains exit_code/duration_ms annotations
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 """
16
17 from __future__ import annotations
18
19 import json
20 import pathlib
21 import textwrap
22
23 import pytest
24
25 from tests.cli_test_helper import CliRunner
26
27 runner = CliRunner()
28
29
30 # ---------------------------------------------------------------------------
31 # Helpers
32 # ---------------------------------------------------------------------------
33
34
35 def _env(root: pathlib.Path) -> dict[str, str]:
36 return {"MUSE_REPO_ROOT": str(root)}
37
38
39 def _run(root: pathlib.Path, *args: str):
40 return runner.invoke(None, list(args), env=_env(root))
41
42
43 # ---------------------------------------------------------------------------
44 # Fixture — repo with exact and near clones committed
45 # ---------------------------------------------------------------------------
46
47
48 @pytest.fixture()
49 def clones_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
50 """Code-domain repo with committed duplicate symbols.
51
52 billing.py and payments.py both define compute_total with identical bodies
53 (exact clone). validate() in both files shares the same signature but has
54 different bodies (near-clone).
55 """
56 monkeypatch.chdir(tmp_path)
57
58 r = _run(tmp_path, "init", "--domain", "code")
59 assert r.exit_code == 0, r.output
60
61 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
62 def compute_total(items):
63 return sum(items)
64
65 def validate(value):
66 if value is None:
67 raise ValueError("billing: value required")
68 return True
69 """))
70 (tmp_path / "payments.py").write_text(textwrap.dedent("""\
71 def compute_total(items):
72 return sum(items)
73
74 def validate(value):
75 if not isinstance(value, (int, float)):
76 raise TypeError("payments: numeric value required")
77 return True
78 """))
79
80 r1 = _run(tmp_path, "code", "add", "billing.py")
81 assert r1.exit_code == 0, r1.output
82 r2 = _run(tmp_path, "code", "add", "payments.py")
83 assert r2.exit_code == 0, r2.output
84 r3 = _run(tmp_path, "commit", "-m", "add billing and payments with clones")
85 assert r3.exit_code == 0, r3.output
86
87 return tmp_path
88
89
90 # ---------------------------------------------------------------------------
91 # TestJsonAlias — -j works identically to --json
92 # ---------------------------------------------------------------------------
93
94
95 class TestJsonAlias:
96 """-j shorthand must behave identically to --json."""
97
98 def test_j_alias_exits_zero(self, clones_repo: pathlib.Path) -> None:
99 r = _run(clones_repo, "code", "clones", "-j")
100 assert r.exit_code == 0, r.output
101
102 def test_j_alias_valid_json(self, clones_repo: pathlib.Path) -> None:
103 r = _run(clones_repo, "code", "clones", "-j")
104 json.loads(r.output) # must not raise
105
106 def test_j_alias_has_clusters_key(self, clones_repo: pathlib.Path) -> None:
107 r = _run(clones_repo, "code", "clones", "-j")
108 data = json.loads(r.output)
109 assert "clusters" in data
110
111 def test_j_alias_has_commit_key(self, clones_repo: pathlib.Path) -> None:
112 r = _run(clones_repo, "code", "clones", "-j")
113 data = json.loads(r.output)
114 assert "commit" in data
115
116 def test_j_alias_same_top_level_keys_as_json_flag(self, clones_repo: pathlib.Path) -> None:
117 r1 = _run(clones_repo, "code", "clones", "--json")
118 r2 = _run(clones_repo, "code", "clones", "-j")
119 d1 = json.loads(r1.output)
120 d2 = json.loads(r2.output)
121 d1.pop("duration_ms", None)
122 d2.pop("duration_ms", None)
123 assert set(d1.keys()) == set(d2.keys())
124
125 def test_j_alias_same_cluster_count(self, clones_repo: pathlib.Path) -> None:
126 r1 = _run(clones_repo, "code", "clones", "--json")
127 r2 = _run(clones_repo, "code", "clones", "-j")
128 assert json.loads(r1.output)["exact_clone_clusters"] == \
129 json.loads(r2.output)["exact_clone_clusters"]
130
131 def test_j_alias_with_tier_exact(self, clones_repo: pathlib.Path) -> None:
132 r = _run(clones_repo, "code", "clones", "--tier", "exact", "-j")
133 assert r.exit_code == 0, r.output
134 data = json.loads(r.output)
135 assert data["tier"] == "exact"
136
137 def test_j_alias_with_tier_near(self, clones_repo: pathlib.Path) -> None:
138 r = _run(clones_repo, "code", "clones", "--tier", "near", "-j")
139 assert r.exit_code == 0, r.output
140 data = json.loads(r.output)
141 assert data["tier"] == "near"
142
143
144 # ---------------------------------------------------------------------------
145 # TestDurationMs — JSON output must include duration_ms
146 # ---------------------------------------------------------------------------
147
148
149 class TestDurationMs:
150 """JSON output must include a non-negative float duration_ms."""
151
152 def test_json_has_duration_ms(self, clones_repo: pathlib.Path) -> None:
153 r = _run(clones_repo, "code", "clones", "--json")
154 data = json.loads(r.output)
155 assert "duration_ms" in data
156
157 def test_json_duration_ms_nonnegative(self, clones_repo: pathlib.Path) -> None:
158 r = _run(clones_repo, "code", "clones", "--json")
159 data = json.loads(r.output)
160 assert data["duration_ms"] >= 0
161
162 def test_json_duration_ms_is_float(self, clones_repo: pathlib.Path) -> None:
163 r = _run(clones_repo, "code", "clones", "--json")
164 data = json.loads(r.output)
165 assert isinstance(data["duration_ms"], float)
166
167 def test_j_alias_duration_ms_present(self, clones_repo: pathlib.Path) -> None:
168 r = _run(clones_repo, "code", "clones", "-j")
169 data = json.loads(r.output)
170 assert "duration_ms" in data
171
172 def test_duration_ms_with_tier_exact(self, clones_repo: pathlib.Path) -> None:
173 r = _run(clones_repo, "code", "clones", "--json", "--tier", "exact")
174 data = json.loads(r.output)
175 assert "duration_ms" in data
176 assert data["duration_ms"] >= 0
177
178 def test_duration_ms_with_no_clones(self, tmp_path: pathlib.Path,
179 monkeypatch: pytest.MonkeyPatch) -> None:
180 """duration_ms is present even when no clones are found."""
181 monkeypatch.chdir(tmp_path)
182 r = _run(tmp_path, "init", "--domain", "code")
183 assert r.exit_code == 0
184 (tmp_path / "solo.py").write_text("def unique_fn():\n return 42\n")
185 _run(tmp_path, "code", "add", "solo.py")
186 _run(tmp_path, "commit", "-m", "solo")
187 r2 = _run(tmp_path, "code", "clones", "--json")
188 data = json.loads(r2.output)
189 assert "duration_ms" in data
190 assert data["duration_ms"] >= 0
191
192
193 # ---------------------------------------------------------------------------
194 # TestExitCode — JSON output must include exit_code = 0
195 # ---------------------------------------------------------------------------
196
197
198 class TestExitCode:
199 """JSON output must include exit_code = 0 on success."""
200
201 def test_json_has_exit_code(self, clones_repo: pathlib.Path) -> None:
202 r = _run(clones_repo, "code", "clones", "--json")
203 data = json.loads(r.output)
204 assert "exit_code" in data
205
206 def test_json_exit_code_zero_on_success(self, clones_repo: pathlib.Path) -> None:
207 r = _run(clones_repo, "code", "clones", "--json")
208 assert r.exit_code == 0
209 data = json.loads(r.output)
210 assert data["exit_code"] == 0
211
212 def test_json_exit_code_is_int(self, clones_repo: pathlib.Path) -> None:
213 r = _run(clones_repo, "code", "clones", "--json")
214 data = json.loads(r.output)
215 assert isinstance(data["exit_code"], int)
216
217 def test_j_alias_exit_code_present(self, clones_repo: pathlib.Path) -> None:
218 r = _run(clones_repo, "code", "clones", "-j")
219 data = json.loads(r.output)
220 assert "exit_code" in data
221
222 def test_exit_code_mirrors_process_exit(self, clones_repo: pathlib.Path) -> None:
223 r = _run(clones_repo, "code", "clones", "--json")
224 data = json.loads(r.output)
225 assert data["exit_code"] == r.exit_code
226
227 def test_exit_code_zero_with_no_clones(self, tmp_path: pathlib.Path,
228 monkeypatch: pytest.MonkeyPatch) -> None:
229 monkeypatch.chdir(tmp_path)
230 _run(tmp_path, "init", "--domain", "code")
231 (tmp_path / "solo.py").write_text("def unique_fn():\n return 42\n")
232 _run(tmp_path, "code", "add", "solo.py")
233 _run(tmp_path, "commit", "-m", "solo")
234 r = _run(tmp_path, "code", "clones", "--json")
235 assert r.exit_code == 0
236 data = json.loads(r.output)
237 assert data["exit_code"] == 0
238
239 def test_exit_code_zero_with_tier_near(self, clones_repo: pathlib.Path) -> None:
240 r = _run(clones_repo, "code", "clones", "--json", "--tier", "near")
241 assert r.exit_code == 0
242 data = json.loads(r.output)
243 assert data["exit_code"] == 0
244
245
246 # ---------------------------------------------------------------------------
247 # TestTypedDicts — _ClonesOutputJson carries the new fields
248 # ---------------------------------------------------------------------------
249
250
251 class TestTypedDicts:
252 """_ClonesOutputJson must carry exit_code and duration_ms annotations."""
253
254 def test_clones_output_json_exists(self) -> None:
255 from muse.cli.commands.clones import _ClonesOutputJson # noqa: F401
256
257 def test_has_exit_code_annotation(self) -> None:
258 from muse.cli.commands.clones import _ClonesOutputJson
259 assert "exit_code" in _ClonesOutputJson.__annotations__
260
261 def test_has_duration_ms_annotation(self) -> None:
262 from muse.cli.commands.clones import _ClonesOutputJson
263 assert "duration_ms" in _ClonesOutputJson.__annotations__
264
265 def test_retains_clusters_annotation(self) -> None:
266 from muse.cli.commands.clones import _ClonesOutputJson
267 assert "clusters" in _ClonesOutputJson.__annotations__
268
269 def test_retains_commit_annotation(self) -> None:
270 from muse.cli.commands.clones import _ClonesOutputJson
271 assert "commit" in _ClonesOutputJson.__annotations__
272
273 def test_retains_exact_clone_clusters_annotation(self) -> None:
274 from muse.cli.commands.clones import _ClonesOutputJson
275 assert "exact_clone_clusters" in _ClonesOutputJson.__annotations__
276
277 def test_retains_near_clone_clusters_annotation(self) -> None:
278 from muse.cli.commands.clones import _ClonesOutputJson
279 assert "near_clone_clusters" in _ClonesOutputJson.__annotations__
280
281 def test_retains_file_hotspots_annotation(self) -> None:
282 from muse.cli.commands.clones import _ClonesOutputJson
283 assert "file_hotspots" in _ClonesOutputJson.__annotations__
284
285 def test_member_dict_exists(self) -> None:
286 from muse.cli.commands.clones import _MemberDict # noqa: F401
287
288 def test_cluster_dict_exists(self) -> None:
289 from muse.cli.commands.clones import _ClusterDict # noqa: F401
290
291
292 # ---------------------------------------------------------------------------
293 # TestDocstrings — run() docstring documents new fields
294 # ---------------------------------------------------------------------------
295
296
297 class TestDocstrings:
298 """run() must document exit_code and duration_ms."""
299
300 def test_run_docstring_mentions_exit_code(self) -> None:
301 from muse.cli.commands.clones import run
302 assert run.__doc__ is not None
303 assert "exit_code" in run.__doc__
304
305 def test_run_docstring_mentions_duration_ms(self) -> None:
306 from muse.cli.commands.clones import run
307 assert run.__doc__ is not None
308 assert "duration_ms" in run.__doc__
309
310
311 # ---------------------------------------------------------------------------
312 # TestAnsiSanitization — no escape codes in JSON output
313 # ---------------------------------------------------------------------------
314
315
316 class TestAnsiSanitization:
317 """No ANSI escape sequences anywhere in the JSON output."""
318
319 def test_json_output_no_ansi(self, clones_repo: pathlib.Path) -> None:
320 r = _run(clones_repo, "code", "clones", "--json")
321 assert "\x1b" not in r.output
322
323 def test_j_alias_output_no_ansi(self, clones_repo: pathlib.Path) -> None:
324 r = _run(clones_repo, "code", "clones", "-j")
325 assert "\x1b" not in r.output
326
327 def test_tier_near_json_no_ansi(self, clones_repo: pathlib.Path) -> None:
328 r = _run(clones_repo, "code", "clones", "--json", "--tier", "near")
329 assert "\x1b" not in r.output
330
331
332 # ---------------------------------------------------------------------------
333 # TestPerformance — duration_ms under 2000 ms for a small repo
334 # ---------------------------------------------------------------------------
335
336
337 class TestPerformance:
338 """duration_ms must stay under 2000 ms for small repos."""
339
340 def test_json_duration_under_2000ms(self, clones_repo: pathlib.Path) -> None:
341 r = _run(clones_repo, "code", "clones", "--json")
342 data = json.loads(r.output)
343 assert data["duration_ms"] < 2000
344
345 def test_j_alias_duration_under_2000ms(self, clones_repo: pathlib.Path) -> None:
346 r = _run(clones_repo, "code", "clones", "-j")
347 data = json.loads(r.output)
348 assert data["duration_ms"] < 2000
349
350 def test_duration_ms_is_float_not_int(self, clones_repo: pathlib.Path) -> None:
351 r = _run(clones_repo, "code", "clones", "--json")
352 data = json.loads(r.output)
353 assert isinstance(data["duration_ms"], float)
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago