gabriel / muse public
test_checkout_symbol_supercharge.py python
381 lines 15.9 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 checkout-symbol`` — agent-usability gaps.
2
3 The existing test_cmd_checkout_symbol.py already covers correctness, JSON
4 schema, E2E round-trips, stress, and post-write verification. This file
5 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: all three JSON paths (no-op, dry-run, write) include exit_code = 0
11 - duration_ms: all three JSON paths include non-negative float duration_ms
12 - TypedDicts: _CheckoutSymbolOutputJson 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 — repo with two commits so checkout-symbol has history to restore
46 # ---------------------------------------------------------------------------
47
48
49 @pytest.fixture()
50 def cs_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
51 """Code-domain repo with two commits on billing.py.
52
53 Commit 1 (v1): compute_total returns sum(items)
54 Commit 2 (v2): compute_total returns round(sum(items), 2)
55
56 checkout-symbol --commit <v1> restores the original body.
57 The current working tree matches commit 2.
58 """
59 monkeypatch.chdir(tmp_path)
60
61 r = _run(tmp_path, "init", "--domain", "code")
62 assert r.exit_code == 0, r.output
63
64 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
65 class Invoice:
66 def compute_total(self, items):
67 return sum(items)
68
69 def add_tax(self, rate):
70 return rate
71 """))
72 r1 = _run(tmp_path, "code", "add", "billing.py")
73 assert r1.exit_code == 0, r1.output
74 r2 = _run(tmp_path, "commit", "-m", "v1 billing")
75 assert r2.exit_code == 0, r2.output
76
77 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
78 class Invoice:
79 def compute_total(self, items):
80 return round(sum(items), 2)
81
82 def add_tax(self, rate):
83 return rate
84 """))
85 r3 = _run(tmp_path, "code", "add", "billing.py")
86 assert r3.exit_code == 0, r3.output
87 r4 = _run(tmp_path, "commit", "-m", "v2 billing")
88 assert r4.exit_code == 0, r4.output
89
90 return tmp_path
91
92
93 def _v1_commit_id(root: pathlib.Path) -> str:
94 """Return the full commit_id of the first (v1) commit."""
95 r = runner.invoke(None, ["log", "--json"], env=_env(root))
96 commits = json.loads(r.output)["commits"]
97 # commits are newest-first; v1 is last
98 return commits[-1]["commit_id"]
99
100
101 ADDRESS = "billing.py::Invoice.compute_total"
102
103
104 # ---------------------------------------------------------------------------
105 # TestJsonAlias — -j works identically to --json
106 # ---------------------------------------------------------------------------
107
108
109 class TestJsonAlias:
110 """-j shorthand must behave identically to --json."""
111
112 def test_j_alias_dry_run_exits_zero(self, cs_repo: pathlib.Path) -> None:
113 v1 = _v1_commit_id(cs_repo)
114 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-j")
115 assert r.exit_code == 0, r.output
116
117 def test_j_alias_dry_run_valid_json(self, cs_repo: pathlib.Path) -> None:
118 v1 = _v1_commit_id(cs_repo)
119 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-j")
120 json.loads(r.output) # must not raise
121
122 def test_j_alias_has_address_key(self, cs_repo: pathlib.Path) -> None:
123 v1 = _v1_commit_id(cs_repo)
124 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-j")
125 data = json.loads(r.output)
126 assert "address" in data
127
128 def test_j_alias_has_changed_key(self, cs_repo: pathlib.Path) -> None:
129 v1 = _v1_commit_id(cs_repo)
130 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-j")
131 data = json.loads(r.output)
132 assert "changed" in data
133
134 def test_j_alias_same_top_level_keys_as_json_flag(self, cs_repo: pathlib.Path) -> None:
135 v1 = _v1_commit_id(cs_repo)
136 r1 = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json")
137 r2 = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-j")
138 d1 = json.loads(r1.output)
139 d2 = json.loads(r2.output)
140 d1.pop("duration_ms", None)
141 d2.pop("duration_ms", None)
142 assert set(d1.keys()) == set(d2.keys())
143
144 def test_j_alias_write_path_exits_zero(self, cs_repo: pathlib.Path) -> None:
145 v1 = _v1_commit_id(cs_repo)
146 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "-j")
147 assert r.exit_code == 0, r.output
148
149 def test_j_alias_write_path_valid_json(self, cs_repo: pathlib.Path) -> None:
150 v1 = _v1_commit_id(cs_repo)
151 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "-j")
152 json.loads(r.output) # must not raise
153
154
155 # ---------------------------------------------------------------------------
156 # TestDurationMs — all three JSON paths include duration_ms
157 # ---------------------------------------------------------------------------
158
159
160 class TestDurationMs:
161 """Every JSON code path must emit a non-negative float duration_ms."""
162
163 def test_dry_run_has_duration_ms(self, cs_repo: pathlib.Path) -> None:
164 v1 = _v1_commit_id(cs_repo)
165 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json")
166 data = json.loads(r.output)
167 assert "duration_ms" in data
168
169 def test_dry_run_duration_ms_nonnegative(self, cs_repo: pathlib.Path) -> None:
170 v1 = _v1_commit_id(cs_repo)
171 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json")
172 data = json.loads(r.output)
173 assert data["duration_ms"] >= 0
174
175 def test_dry_run_duration_ms_is_float(self, cs_repo: pathlib.Path) -> None:
176 v1 = _v1_commit_id(cs_repo)
177 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json")
178 data = json.loads(r.output)
179 assert isinstance(data["duration_ms"], float)
180
181 def test_write_path_has_duration_ms(self, cs_repo: pathlib.Path) -> None:
182 v1 = _v1_commit_id(cs_repo)
183 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json")
184 data = json.loads(r.output)
185 assert "duration_ms" in data
186
187 def test_write_path_duration_ms_nonnegative(self, cs_repo: pathlib.Path) -> None:
188 v1 = _v1_commit_id(cs_repo)
189 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json")
190 data = json.loads(r.output)
191 assert data["duration_ms"] >= 0
192
193 def test_noop_path_has_duration_ms(self, cs_repo: pathlib.Path) -> None:
194 """No-op: restore HEAD commit → symbol already matches."""
195 r_log = runner.invoke(None, ["log", "--json"], env=_env(cs_repo))
196 head_id = json.loads(r_log.output)["commits"][0]["commit_id"]
197 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", head_id, "--json")
198 data = json.loads(r.output)
199 assert "duration_ms" in data
200 assert data["duration_ms"] >= 0
201
202 def test_j_alias_dry_run_duration_ms_present(self, cs_repo: pathlib.Path) -> None:
203 v1 = _v1_commit_id(cs_repo)
204 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-j")
205 data = json.loads(r.output)
206 assert "duration_ms" in data
207
208
209 # ---------------------------------------------------------------------------
210 # TestExitCode — all three JSON paths include exit_code = 0
211 # ---------------------------------------------------------------------------
212
213
214 class TestExitCode:
215 """All JSON paths must carry exit_code = 0 (errors exit before JSON is emitted)."""
216
217 def test_dry_run_has_exit_code(self, cs_repo: pathlib.Path) -> None:
218 v1 = _v1_commit_id(cs_repo)
219 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json")
220 data = json.loads(r.output)
221 assert "exit_code" in data
222
223 def test_dry_run_exit_code_zero(self, cs_repo: pathlib.Path) -> None:
224 v1 = _v1_commit_id(cs_repo)
225 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json")
226 assert r.exit_code == 0
227 data = json.loads(r.output)
228 assert data["exit_code"] == 0
229
230 def test_dry_run_exit_code_is_int(self, cs_repo: pathlib.Path) -> None:
231 v1 = _v1_commit_id(cs_repo)
232 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json")
233 data = json.loads(r.output)
234 assert isinstance(data["exit_code"], int)
235
236 def test_write_path_has_exit_code(self, cs_repo: pathlib.Path) -> None:
237 v1 = _v1_commit_id(cs_repo)
238 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json")
239 data = json.loads(r.output)
240 assert "exit_code" in data
241
242 def test_write_path_exit_code_zero(self, cs_repo: pathlib.Path) -> None:
243 v1 = _v1_commit_id(cs_repo)
244 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json")
245 assert r.exit_code == 0
246 data = json.loads(r.output)
247 assert data["exit_code"] == 0
248
249 def test_noop_path_has_exit_code(self, cs_repo: pathlib.Path) -> None:
250 r_log = runner.invoke(None, ["log", "--json"], env=_env(cs_repo))
251 head_id = json.loads(r_log.output)["commits"][0]["commit_id"]
252 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", head_id, "--json")
253 data = json.loads(r.output)
254 assert "exit_code" in data
255
256 def test_noop_path_exit_code_zero(self, cs_repo: pathlib.Path) -> None:
257 r_log = runner.invoke(None, ["log", "--json"], env=_env(cs_repo))
258 head_id = json.loads(r_log.output)["commits"][0]["commit_id"]
259 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", head_id, "--json")
260 assert r.exit_code == 0
261 data = json.loads(r.output)
262 assert data["exit_code"] == 0
263
264 def test_exit_code_mirrors_process_exit_dry_run(self, cs_repo: pathlib.Path) -> None:
265 v1 = _v1_commit_id(cs_repo)
266 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json")
267 data = json.loads(r.output)
268 assert data["exit_code"] == r.exit_code
269
270 def test_exit_code_mirrors_process_exit_write(self, cs_repo: pathlib.Path) -> None:
271 v1 = _v1_commit_id(cs_repo)
272 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json")
273 data = json.loads(r.output)
274 assert data["exit_code"] == r.exit_code
275
276
277 # ---------------------------------------------------------------------------
278 # TestTypedDicts — _CheckoutSymbolOutputJson carries the new fields
279 # ---------------------------------------------------------------------------
280
281
282 class TestTypedDicts:
283 """_CheckoutSymbolOutputJson must carry exit_code and duration_ms."""
284
285 def test_checkout_symbol_output_json_exists(self) -> None:
286 from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson # noqa: F401
287
288 def test_has_exit_code_annotation(self) -> None:
289 from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson
290 assert "exit_code" in _CheckoutSymbolOutputJson.__annotations__
291
292 def test_has_duration_ms_annotation(self) -> None:
293 from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson
294 assert "duration_ms" in _CheckoutSymbolOutputJson.__annotations__
295
296 def test_retains_address_annotation(self) -> None:
297 from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson
298 assert "address" in _CheckoutSymbolOutputJson.__annotations__
299
300 def test_retains_changed_annotation(self) -> None:
301 from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson
302 assert "changed" in _CheckoutSymbolOutputJson.__annotations__
303
304 def test_retains_dry_run_annotation(self) -> None:
305 from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson
306 assert "dry_run" in _CheckoutSymbolOutputJson.__annotations__
307
308 def test_retains_verified_annotation(self) -> None:
309 from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson
310 assert "verified" in _CheckoutSymbolOutputJson.__annotations__
311
312
313 # ---------------------------------------------------------------------------
314 # TestDocstrings — run() docstring documents new fields
315 # ---------------------------------------------------------------------------
316
317
318 class TestDocstrings:
319 """run() must document exit_code and duration_ms."""
320
321 def test_run_docstring_mentions_exit_code(self) -> None:
322 from muse.cli.commands.checkout_symbol import run
323 assert run.__doc__ is not None
324 assert "exit_code" in run.__doc__
325
326 def test_run_docstring_mentions_duration_ms(self) -> None:
327 from muse.cli.commands.checkout_symbol import run
328 assert run.__doc__ is not None
329 assert "duration_ms" in run.__doc__
330
331
332 # ---------------------------------------------------------------------------
333 # TestAnsiSanitization — no escape codes in JSON output
334 # ---------------------------------------------------------------------------
335
336
337 class TestAnsiSanitization:
338 """No ANSI escape sequences anywhere in the JSON output."""
339
340 def test_dry_run_json_no_ansi(self, cs_repo: pathlib.Path) -> None:
341 v1 = _v1_commit_id(cs_repo)
342 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json")
343 assert "\x1b" not in r.output
344
345 def test_write_json_no_ansi(self, cs_repo: pathlib.Path) -> None:
346 v1 = _v1_commit_id(cs_repo)
347 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json")
348 assert "\x1b" not in r.output
349
350 def test_noop_json_no_ansi(self, cs_repo: pathlib.Path) -> None:
351 r_log = runner.invoke(None, ["log", "--json"], env=_env(cs_repo))
352 head_id = json.loads(r_log.output)["commits"][0]["commit_id"]
353 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", head_id, "--json")
354 assert "\x1b" not in r.output
355
356
357 # ---------------------------------------------------------------------------
358 # TestPerformance — duration_ms under 2000 ms for a small repo
359 # ---------------------------------------------------------------------------
360
361
362 class TestPerformance:
363 """duration_ms must stay under 2000 ms for small repos."""
364
365 def test_dry_run_duration_under_2000ms(self, cs_repo: pathlib.Path) -> None:
366 v1 = _v1_commit_id(cs_repo)
367 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json")
368 data = json.loads(r.output)
369 assert data["duration_ms"] < 2000
370
371 def test_write_duration_under_2000ms(self, cs_repo: pathlib.Path) -> None:
372 v1 = _v1_commit_id(cs_repo)
373 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json")
374 data = json.loads(r.output)
375 assert data["duration_ms"] < 2000
376
377 def test_duration_ms_is_float_not_int(self, cs_repo: pathlib.Path) -> None:
378 v1 = _v1_commit_id(cs_repo)
379 r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json")
380 data = json.loads(r.output)
381 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