gabriel / muse public

test_checkout_symbol_supercharge.py file-level

at sha256:f · View file ↗ · Intel ↗

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