gabriel / muse public
test_code_add_supercharge.py python
436 lines 18.1 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 add`` / ``muse code reset`` — agent-usability gaps.
2
3 Coverage matrix
4 ---------------
5 - --json / -j: alias works identically to --format json for both add and reset
6 - exit_code: every JSON output path includes it (success and error branches)
7 - duration_ms: every JSON output path includes it; non-negative float
8 - TypedDicts: _CodeAddJson, _CodeResetJson annotations exist and carry new fields
9 - Docstrings: run_add, run_reset docstrings mention exit_code and duration_ms
10 - Error JSON: error branches include exit_code and duration_ms
11 - ANSI: file paths in JSON output never contain escape sequences
12 - Performance: duration_ms stays < 1000 ms for normal operations
13 """
14
15 from __future__ import annotations
16
17 import json
18 import pathlib
19
20 import pytest
21
22 from tests.cli_test_helper import CliRunner
23
24 runner = CliRunner()
25
26
27 # ---------------------------------------------------------------------------
28 # Helpers
29 # ---------------------------------------------------------------------------
30
31
32 def _env(root: pathlib.Path) -> dict[str, str]:
33 return {"MUSE_REPO_ROOT": str(root)}
34
35
36 def _run(root: pathlib.Path, *args: str): # type: ignore[return]
37 return runner.invoke(None, list(args), env=_env(root))
38
39
40 @pytest.fixture()
41 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
42 """Fresh code-domain repo with one committed file (main.py = 'x = 1')."""
43 monkeypatch.chdir(tmp_path)
44 r = runner.invoke(None, ["init", "--domain", "code"], env=_env(tmp_path))
45 assert r.exit_code == 0, r.output
46 (tmp_path / "main.py").write_text("x = 1\n")
47 r2 = runner.invoke(None, ["commit", "--allow-empty", "-m", "init"], env=_env(tmp_path))
48 assert r2.exit_code == 0, r2.output
49 return tmp_path
50
51
52 # ---------------------------------------------------------------------------
53 # --json / -j alias
54 # ---------------------------------------------------------------------------
55
56
57 class TestJsonAlias:
58 """--json and -j are accepted on both add and reset, mirror --format json."""
59
60 def test_add_json_flag_accepted(self, repo: pathlib.Path) -> None:
61 (repo / "main.py").write_text("x = 2\n")
62 r = _run(repo, "code", "add", "--json", "main.py")
63 assert r.exit_code == 0
64 json.loads(r.output.strip()) # must be valid JSON
65
66 def test_add_j_flag_accepted(self, repo: pathlib.Path) -> None:
67 (repo / "main.py").write_text("x = 3\n")
68 r = _run(repo, "code", "add", "-j", "main.py")
69 assert r.exit_code == 0
70 json.loads(r.output.strip())
71
72 def test_reset_json_flag_accepted(self, repo: pathlib.Path) -> None:
73 (repo / "main.py").write_text("x = 4\n")
74 _run(repo, "code", "add", "main.py")
75 r = _run(repo, "code", "reset", "--json", "main.py")
76 assert r.exit_code == 0
77 json.loads(r.output.strip())
78
79 def test_reset_j_flag_accepted(self, repo: pathlib.Path) -> None:
80 (repo / "main.py").write_text("x = 5\n")
81 _run(repo, "code", "add", "main.py")
82 r = _run(repo, "code", "reset", "-j", "main.py")
83 assert r.exit_code == 0
84 json.loads(r.output.strip())
85
86 def test_add_json_and_format_json_produce_same_keys(self, repo: pathlib.Path) -> None:
87 (repo / "main.py").write_text("x = 10\n")
88 r1 = _run(repo, "code", "add", "--format", "json", "main.py")
89 (repo / "main.py").write_text("x = 11\n")
90 r2 = _run(repo, "code", "add", "--json", "main.py")
91 d1 = json.loads(r1.output.strip())
92 d2 = json.loads(r2.output.strip())
93 d1.pop("duration_ms", None)
94 d2.pop("duration_ms", None)
95 assert set(d1.keys()) == set(d2.keys())
96
97 def test_add_j_and_format_json_produce_same_keys(self, repo: pathlib.Path) -> None:
98 (repo / "main.py").write_text("x = 20\n")
99 r1 = _run(repo, "code", "add", "--format", "json", "main.py")
100 (repo / "main.py").write_text("x = 21\n")
101 r2 = _run(repo, "code", "add", "-j", "main.py")
102 d1 = json.loads(r1.output.strip())
103 d2 = json.loads(r2.output.strip())
104 d1.pop("duration_ms", None)
105 d2.pop("duration_ms", None)
106 assert set(d1.keys()) == set(d2.keys())
107
108 def test_reset_json_and_format_json_produce_same_keys(self, repo: pathlib.Path) -> None:
109 (repo / "main.py").write_text("x = 30\n")
110 _run(repo, "code", "add", "main.py")
111 r1 = _run(repo, "code", "reset", "--format", "json", "main.py")
112 (repo / "main.py").write_text("x = 31\n")
113 _run(repo, "code", "add", "main.py")
114 r2 = _run(repo, "code", "reset", "--json", "main.py")
115 d1 = json.loads(r1.output.strip())
116 d2 = json.loads(r2.output.strip())
117 d1.pop("duration_ms", None)
118 d2.pop("duration_ms", None)
119 assert set(d1.keys()) == set(d2.keys())
120
121 def test_add_dot_with_json_flag(self, repo: pathlib.Path) -> None:
122 (repo / "main.py").write_text("x = 99\n")
123 r = _run(repo, "code", "add", "--json", ".")
124 assert r.exit_code == 0
125 d = json.loads(r.output.strip())
126 assert "staged" in d
127
128 def test_add_update_flag_with_json(self, repo: pathlib.Path) -> None:
129 (repo / "main.py").write_text("x = 77\n")
130 r = _run(repo, "code", "add", "-u", "--json")
131 assert r.exit_code == 0
132 d = json.loads(r.output.strip())
133 assert "staged" in d
134
135 def test_add_all_flag_with_json(self, repo: pathlib.Path) -> None:
136 (repo / "new.py").write_text("y = 0\n")
137 r = _run(repo, "code", "add", "-A", "--json")
138 assert r.exit_code == 0
139 d = json.loads(r.output.strip())
140 assert "staged" in d
141
142
143 # ---------------------------------------------------------------------------
144 # duration_ms
145 # ---------------------------------------------------------------------------
146
147
148 class TestDurationMs:
149 """Every JSON output path from add and reset must include duration_ms."""
150
151 def test_add_success_has_duration_ms(self, repo: pathlib.Path) -> None:
152 (repo / "main.py").write_text("x = 2\n")
153 r = _run(repo, "code", "add", "--json", "main.py")
154 assert "duration_ms" in json.loads(r.output.strip())
155
156 def test_add_nothing_to_stage_has_duration_ms(self, repo: pathlib.Path) -> None:
157 r = _run(repo, "code", "add", "--json", ".")
158 assert "duration_ms" in json.loads(r.output.strip())
159
160 def test_add_dry_run_has_duration_ms(self, repo: pathlib.Path) -> None:
161 (repo / "main.py").write_text("x = 5\n")
162 r = _run(repo, "code", "add", "--dry-run", "--json", "main.py")
163 assert "duration_ms" in json.loads(r.output.strip())
164
165 def test_add_all_has_duration_ms(self, repo: pathlib.Path) -> None:
166 (repo / "new.py").write_text("y = 0\n")
167 r = _run(repo, "code", "add", "-A", "--json")
168 assert "duration_ms" in json.loads(r.output.strip())
169
170 def test_add_update_has_duration_ms(self, repo: pathlib.Path) -> None:
171 (repo / "main.py").write_text("x = 6\n")
172 r = _run(repo, "code", "add", "-u", "--json")
173 assert "duration_ms" in json.loads(r.output.strip())
174
175 def test_reset_success_has_duration_ms(self, repo: pathlib.Path) -> None:
176 (repo / "main.py").write_text("x = 7\n")
177 _run(repo, "code", "add", "main.py")
178 r = _run(repo, "code", "reset", "--json")
179 assert "duration_ms" in json.loads(r.output.strip())
180
181 def test_reset_nothing_staged_has_duration_ms(self, repo: pathlib.Path) -> None:
182 r = _run(repo, "code", "reset", "--json")
183 assert "duration_ms" in json.loads(r.output.strip())
184
185 def test_reset_dry_run_has_duration_ms(self, repo: pathlib.Path) -> None:
186 (repo / "main.py").write_text("x = 8\n")
187 _run(repo, "code", "add", "main.py")
188 r = _run(repo, "code", "reset", "--dry-run", "--json")
189 assert "duration_ms" in json.loads(r.output.strip())
190
191 def test_duration_ms_is_non_negative(self, repo: pathlib.Path) -> None:
192 (repo / "main.py").write_text("x = 9\n")
193 r = _run(repo, "code", "add", "--json", "main.py")
194 assert json.loads(r.output.strip())["duration_ms"] >= 0
195
196 def test_duration_ms_is_numeric(self, repo: pathlib.Path) -> None:
197 (repo / "main.py").write_text("x = 10\n")
198 r = _run(repo, "code", "add", "--json", "main.py")
199 val = json.loads(r.output.strip())["duration_ms"]
200 assert isinstance(val, (int, float))
201
202
203 # ---------------------------------------------------------------------------
204 # exit_code
205 # ---------------------------------------------------------------------------
206
207
208 class TestExitCode:
209 """Every JSON output path must include exit_code mirroring process exit."""
210
211 def test_add_success_has_exit_code(self, repo: pathlib.Path) -> None:
212 (repo / "main.py").write_text("x = 2\n")
213 r = _run(repo, "code", "add", "--json", "main.py")
214 assert "exit_code" in json.loads(r.output.strip())
215
216 def test_add_nothing_has_exit_code(self, repo: pathlib.Path) -> None:
217 r = _run(repo, "code", "add", "--json", ".")
218 assert "exit_code" in json.loads(r.output.strip())
219
220 def test_add_dry_run_has_exit_code(self, repo: pathlib.Path) -> None:
221 (repo / "main.py").write_text("x = 5\n")
222 r = _run(repo, "code", "add", "--dry-run", "--json", "main.py")
223 assert "exit_code" in json.loads(r.output.strip())
224
225 def test_reset_success_has_exit_code(self, repo: pathlib.Path) -> None:
226 (repo / "main.py").write_text("x = 3\n")
227 _run(repo, "code", "add", "main.py")
228 r = _run(repo, "code", "reset", "--json")
229 assert "exit_code" in json.loads(r.output.strip())
230
231 def test_reset_nothing_has_exit_code(self, repo: pathlib.Path) -> None:
232 r = _run(repo, "code", "reset", "--json")
233 assert "exit_code" in json.loads(r.output.strip())
234
235 def test_add_exit_code_zero_on_success(self, repo: pathlib.Path) -> None:
236 (repo / "main.py").write_text("x = 4\n")
237 r = _run(repo, "code", "add", "--json", "main.py")
238 assert json.loads(r.output.strip())["exit_code"] == 0
239
240 def test_reset_exit_code_zero_on_success(self, repo: pathlib.Path) -> None:
241 r = _run(repo, "code", "reset", "--json")
242 assert json.loads(r.output.strip())["exit_code"] == 0
243
244 def test_add_exit_code_mirrors_process_exit(self, repo: pathlib.Path) -> None:
245 (repo / "main.py").write_text("x = 5\n")
246 r = _run(repo, "code", "add", "--json", "main.py")
247 d = json.loads(r.output.strip())
248 assert d["exit_code"] == r.exit_code
249
250 def test_reset_exit_code_mirrors_process_exit(self, repo: pathlib.Path) -> None:
251 (repo / "main.py").write_text("x = 6\n")
252 _run(repo, "code", "add", "main.py")
253 r = _run(repo, "code", "reset", "--json", "main.py")
254 d = json.loads(r.output.strip())
255 assert d["exit_code"] == r.exit_code
256
257 def test_exit_code_is_int(self, repo: pathlib.Path) -> None:
258 r = _run(repo, "code", "reset", "--json")
259 assert isinstance(json.loads(r.output.strip())["exit_code"], int)
260
261 def test_add_all_exit_code_zero(self, repo: pathlib.Path) -> None:
262 (repo / "new.py").write_text("n = 0\n")
263 r = _run(repo, "code", "add", "-A", "--json")
264 assert json.loads(r.output.strip())["exit_code"] == 0
265
266
267 # ---------------------------------------------------------------------------
268 # TypedDicts
269 # ---------------------------------------------------------------------------
270
271
272 class TestTypedDicts:
273 """_CodeAddJson and _CodeResetJson TypedDicts must exist with required fields."""
274
275 def test_code_add_json_typeddict_exists(self) -> None:
276 from muse.cli.commands.code_stage import _CodeAddJson
277 assert "staged" in _CodeAddJson.__annotations__
278 assert "exit_code" in _CodeAddJson.__annotations__
279 assert "duration_ms" in _CodeAddJson.__annotations__
280
281 def test_code_add_json_has_files_annotation(self) -> None:
282 from muse.cli.commands.code_stage import _CodeAddJson
283 assert "files" in _CodeAddJson.__annotations__
284
285 def test_code_add_json_has_added_modified_deleted(self) -> None:
286 from muse.cli.commands.code_stage import _CodeAddJson
287 assert "added" in _CodeAddJson.__annotations__
288 assert "modified" in _CodeAddJson.__annotations__
289 assert "deleted" in _CodeAddJson.__annotations__
290
291 def test_code_add_json_has_dry_run(self) -> None:
292 from muse.cli.commands.code_stage import _CodeAddJson
293 assert "dry_run" in _CodeAddJson.__annotations__
294
295 def test_code_reset_json_typeddict_exists(self) -> None:
296 from muse.cli.commands.code_stage import _CodeResetJson
297 assert "unstaged" in _CodeResetJson.__annotations__
298 assert "exit_code" in _CodeResetJson.__annotations__
299 assert "duration_ms" in _CodeResetJson.__annotations__
300
301 def test_code_reset_json_has_files_annotation(self) -> None:
302 from muse.cli.commands.code_stage import _CodeResetJson
303 assert "files" in _CodeResetJson.__annotations__
304
305 def test_code_reset_json_has_not_staged(self) -> None:
306 from muse.cli.commands.code_stage import _CodeResetJson
307 assert "not_staged" in _CodeResetJson.__annotations__
308
309 def test_code_reset_json_has_dry_run(self) -> None:
310 from muse.cli.commands.code_stage import _CodeResetJson
311 assert "dry_run" in _CodeResetJson.__annotations__
312
313
314 # ---------------------------------------------------------------------------
315 # Docstrings
316 # ---------------------------------------------------------------------------
317
318
319 class TestDocstrings:
320 """run_add and run_reset docstrings must document exit_code and duration_ms."""
321
322 def test_add_docstring_mentions_exit_code(self) -> None:
323 from muse.cli.commands.code_stage import run_add
324 assert "exit_code" in (run_add.__doc__ or "")
325
326 def test_add_docstring_mentions_duration_ms(self) -> None:
327 from muse.cli.commands.code_stage import run_add
328 assert "duration_ms" in (run_add.__doc__ or "")
329
330 def test_reset_docstring_mentions_exit_code(self) -> None:
331 from muse.cli.commands.code_stage import run_reset
332 assert "exit_code" in (run_reset.__doc__ or "")
333
334 def test_reset_docstring_mentions_duration_ms(self) -> None:
335 from muse.cli.commands.code_stage import run_reset
336 assert "duration_ms" in (run_reset.__doc__ or "")
337
338
339 # ---------------------------------------------------------------------------
340 # Error JSON paths
341 # ---------------------------------------------------------------------------
342
343
344 class TestErrorJson:
345 """Error branches emit valid JSON with exit_code and duration_ms when --json."""
346
347 def test_add_no_matching_files_json_has_exit_code(self, repo: pathlib.Path) -> None:
348 r = _run(repo, "code", "add", "--json", "ghost.py")
349 assert r.exit_code != 0
350 d = json.loads(r.output.strip())
351 assert "exit_code" in d
352
353 def test_add_no_matching_files_json_has_duration_ms(self, repo: pathlib.Path) -> None:
354 r = _run(repo, "code", "add", "--json", "ghost.py")
355 assert r.exit_code != 0
356 d = json.loads(r.output.strip())
357 assert "duration_ms" in d
358
359 def test_add_error_exit_code_nonzero_in_json(self, repo: pathlib.Path) -> None:
360 r = _run(repo, "code", "add", "--json", "ghost.py")
361 assert r.exit_code != 0
362 d = json.loads(r.output.strip())
363 assert d["exit_code"] != 0
364
365 def test_add_error_exit_code_mirrors_process(self, repo: pathlib.Path) -> None:
366 r = _run(repo, "code", "add", "--json", "ghost.py")
367 d = json.loads(r.output.strip())
368 assert d["exit_code"] == r.exit_code
369
370 def test_add_error_json_has_staged_zero(self, repo: pathlib.Path) -> None:
371 r = _run(repo, "code", "add", "--json", "ghost.py")
372 d = json.loads(r.output.strip())
373 assert d.get("staged") == 0 or "error" in d
374
375
376 # ---------------------------------------------------------------------------
377 # ANSI sanitization in JSON output
378 # ---------------------------------------------------------------------------
379
380
381 class TestAnsiSanitizationJson:
382 """File paths emitted in JSON must never contain ANSI escape sequences."""
383
384 def test_add_json_file_path_has_no_ansi(self, repo: pathlib.Path) -> None:
385 (repo / "main.py").write_text("x = 2\n")
386 r = _run(repo, "code", "add", "--json", "main.py")
387 assert r.exit_code == 0
388 assert "\x1b" not in r.output
389
390 def test_add_json_multiple_files_no_ansi(self, repo: pathlib.Path) -> None:
391 for i in range(3):
392 (repo / f"f{i}.py").write_text(f"v = {i}\n")
393 r = _run(repo, "code", "add", "-A", "--json")
394 assert r.exit_code == 0
395 assert "\x1b" not in r.output
396
397 def test_reset_json_file_path_has_no_ansi(self, repo: pathlib.Path) -> None:
398 (repo / "main.py").write_text("x = 3\n")
399 _run(repo, "code", "add", "main.py")
400 r = _run(repo, "code", "reset", "--json", "main.py")
401 assert r.exit_code == 0
402 assert "\x1b" not in r.output
403
404 def test_add_json_dry_run_no_ansi(self, repo: pathlib.Path) -> None:
405 (repo / "main.py").write_text("x = 4\n")
406 r = _run(repo, "code", "add", "--dry-run", "--json", ".")
407 assert "\x1b" not in r.output
408
409
410 # ---------------------------------------------------------------------------
411 # Performance
412 # ---------------------------------------------------------------------------
413
414
415 class TestPerformance:
416 """duration_ms stays well within reason for normal operations."""
417
418 def test_add_single_file_duration_under_1000ms(self, repo: pathlib.Path) -> None:
419 (repo / "main.py").write_text("x = 2\n")
420 r = _run(repo, "code", "add", "--json", "main.py")
421 assert r.exit_code == 0
422 assert json.loads(r.output.strip())["duration_ms"] < 1000
423
424 def test_add_all_duration_under_1000ms(self, repo: pathlib.Path) -> None:
425 for i in range(20):
426 (repo / f"f{i}.py").write_text(f"v = {i}\n")
427 r = _run(repo, "code", "add", "-A", "--json")
428 assert r.exit_code == 0
429 assert json.loads(r.output.strip())["duration_ms"] < 1000
430
431 def test_reset_duration_under_1000ms(self, repo: pathlib.Path) -> None:
432 (repo / "main.py").write_text("x = 5\n")
433 _run(repo, "code", "add", "main.py")
434 r = _run(repo, "code", "reset", "--json")
435 assert r.exit_code == 0
436 assert json.loads(r.output.strip())["duration_ms"] < 1000
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 138 days ago