gabriel / muse public
test_age_supercharge.py python
386 lines 15.1 KB
Raw
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 6 days ago
1 """Supercharge tests for ``muse code age`` — agent-usability gaps.
2
3 Coverage matrix
4 ---------------
5 - --json / -j: -j alias works identically to --json for list and explain modes
6 - exit_code: every JSON output path includes it (0 on success)
7 - duration_ms: every JSON output path includes it; non-negative float
8 - TypedDicts: _AgeListJson, _ExplainJson, _NoSymbolsJson annotations exist
9 - Docstrings: run() docstring mentions exit_code and duration_ms
10 - ANSI: address / string fields in JSON never contain escape sequences
11 - Performance: duration_ms stays < 1000 ms for normal operations
12 """
13
14 from __future__ import annotations
15 from collections.abc import Mapping
16
17 import json
18 import pathlib
19 import textwrap
20
21 import pytest
22
23 from tests.cli_test_helper import CliRunner
24
25 runner = CliRunner()
26
27
28 # ---------------------------------------------------------------------------
29 # Helpers
30 # ---------------------------------------------------------------------------
31
32
33 def _env(root: pathlib.Path) -> Mapping[str, str]:
34 return {"MUSE_REPO_ROOT": str(root)}
35
36
37 def _run(root: pathlib.Path, *args: str) -> "InvokeResult":
38 return runner.invoke(None, list(args), env=_env(root))
39
40
41 def _first_symbol_address(root: pathlib.Path) -> str:
42 """Return the address of the first symbol in the age JSON output."""
43 r = _run(root, "code", "age", "--json")
44 assert r.exit_code == 0, r.output
45 data = json.loads(r.output)
46 syms = data["symbols"]
47 assert syms, "age_repo should always have at least one symbol with history"
48 return syms[0]["address"]
49
50
51 # ---------------------------------------------------------------------------
52 # Fixture — repo with commit history so symbols have age data
53 # ---------------------------------------------------------------------------
54
55
56 @pytest.fixture()
57 def age_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
58 """Repo with three commits that give billing.py symbols measurable age.
59
60 Commit 1: create billing.py (Invoice class + compute_total + stable_fn)
61 Commit 2: modify compute_total body → 1 impl change
62 Commit 3: modify compute_total body → 2 impl changes
63 """
64 monkeypatch.chdir(tmp_path)
65
66 r = _run(tmp_path, "init", "--domain", "code")
67 assert r.exit_code == 0, r.output
68
69 # Commit 1 — create the file.
70 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
71 class Invoice:
72 def compute_total(self, items):
73 return sum(items)
74
75 def stable_fn():
76 return 42
77 """))
78 r1 = _run(tmp_path, "code", "add", "billing.py")
79 assert r1.exit_code == 0, r1.output
80 r2 = _run(tmp_path, "commit", "-m", "initial billing")
81 assert r2.exit_code == 0, r2.output
82
83 # Commit 2 — impl change to compute_total.
84 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
85 class Invoice:
86 def compute_total(self, items):
87 return round(sum(items), 2)
88
89 def stable_fn():
90 return 42
91 """))
92 r3 = _run(tmp_path, "code", "add", "billing.py")
93 assert r3.exit_code == 0, r3.output
94 r4 = _run(tmp_path, "commit", "-m", "round result")
95 assert r4.exit_code == 0, r4.output
96
97 # Commit 3 — second impl change to compute_total.
98 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
99 class Invoice:
100 def compute_total(self, items):
101 total = sum(items)
102 return round(total, 4)
103
104 def stable_fn():
105 return 42
106 """))
107 r5 = _run(tmp_path, "code", "add", "billing.py")
108 assert r5.exit_code == 0, r5.output
109 r6 = _run(tmp_path, "commit", "-m", "higher precision")
110 assert r6.exit_code == 0, r6.output
111
112 return tmp_path
113
114
115 # ---------------------------------------------------------------------------
116 # TestJsonAlias — -j works identically to --json
117 # ---------------------------------------------------------------------------
118
119
120 class TestJsonAlias:
121 """The -j shorthand must behave identically to --json."""
122
123 def test_j_alias_main_exits_zero(self, age_repo: pathlib.Path) -> None:
124 r = _run(age_repo, "code", "age", "-j")
125 assert r.exit_code == 0, r.output
126
127 def test_j_alias_main_valid_json(self, age_repo: pathlib.Path) -> None:
128 r = _run(age_repo, "code", "age", "-j")
129 assert r.exit_code == 0, r.output
130 json.loads(r.output) # must not raise
131
132 def test_j_alias_main_has_symbols_key(self, age_repo: pathlib.Path) -> None:
133 r = _run(age_repo, "code", "age", "-j")
134 data = json.loads(r.output)
135 assert "symbols" in data
136
137 def test_j_alias_explain_exits_zero(self, age_repo: pathlib.Path) -> None:
138 addr = _first_symbol_address(age_repo)
139 r = _run(age_repo, "code", "age", "--explain", addr, "-j")
140 assert r.exit_code == 0, r.output
141
142 def test_j_alias_explain_valid_json(self, age_repo: pathlib.Path) -> None:
143 addr = _first_symbol_address(age_repo)
144 r = _run(age_repo, "code", "age", "--explain", addr, "-j")
145 assert r.exit_code == 0, r.output
146 json.loads(r.output) # must not raise
147
148 def test_j_alias_same_top_level_keys_as_json_flag(self, age_repo: pathlib.Path) -> None:
149 r1 = _run(age_repo, "code", "age", "--json")
150 r2 = _run(age_repo, "code", "age", "-j")
151 d1 = json.loads(r1.output)
152 d2 = json.loads(r2.output)
153 d1.pop("duration_ms", None)
154 d2.pop("duration_ms", None)
155 assert set(d1.keys()) == set(d2.keys())
156
157 def test_j_alias_explain_same_keys_as_json_flag(self, age_repo: pathlib.Path) -> None:
158 addr = _first_symbol_address(age_repo)
159 r1 = _run(age_repo, "code", "age", "--explain", addr, "--json")
160 r2 = _run(age_repo, "code", "age", "--explain", addr, "-j")
161 d1 = json.loads(r1.output)
162 d2 = json.loads(r2.output)
163 d1.pop("duration_ms", None)
164 d2.pop("duration_ms", None)
165 assert set(d1.keys()) == set(d2.keys())
166
167
168 # ---------------------------------------------------------------------------
169 # TestDurationMs — every JSON path emits duration_ms
170 # ---------------------------------------------------------------------------
171
172
173 class TestDurationMs:
174 """Every JSON output path must include a non-negative float duration_ms."""
175
176 def test_main_json_has_duration_ms(self, age_repo: pathlib.Path) -> None:
177 r = _run(age_repo, "code", "age", "--json")
178 data = json.loads(r.output)
179 assert "duration_ms" in data
180
181 def test_main_json_duration_ms_nonnegative(self, age_repo: pathlib.Path) -> None:
182 r = _run(age_repo, "code", "age", "--json")
183 data = json.loads(r.output)
184 assert data["duration_ms"] >= 0
185
186 def test_main_json_duration_ms_is_float(self, age_repo: pathlib.Path) -> None:
187 r = _run(age_repo, "code", "age", "--json")
188 data = json.loads(r.output)
189 assert isinstance(data["duration_ms"], float)
190
191 def test_explain_json_has_duration_ms(self, age_repo: pathlib.Path) -> None:
192 addr = _first_symbol_address(age_repo)
193 r = _run(age_repo, "code", "age", "--explain", addr, "--json")
194 data = json.loads(r.output)
195 assert "duration_ms" in data
196
197 def test_explain_json_duration_ms_nonnegative(self, age_repo: pathlib.Path) -> None:
198 addr = _first_symbol_address(age_repo)
199 r = _run(age_repo, "code", "age", "--explain", addr, "--json")
200 data = json.loads(r.output)
201 assert data["duration_ms"] >= 0
202
203 def test_explain_json_duration_ms_is_float(self, age_repo: pathlib.Path) -> None:
204 addr = _first_symbol_address(age_repo)
205 r = _run(age_repo, "code", "age", "--explain", addr, "--json")
206 data = json.loads(r.output)
207 assert isinstance(data["duration_ms"], float)
208
209 def test_j_alias_duration_ms_present(self, age_repo: pathlib.Path) -> None:
210 r = _run(age_repo, "code", "age", "-j")
211 data = json.loads(r.output)
212 assert "duration_ms" in data
213
214
215 # ---------------------------------------------------------------------------
216 # TestExitCode — every JSON path emits exit_code
217 # ---------------------------------------------------------------------------
218
219
220 class TestExitCode:
221 """Every JSON output path must include exit_code; 0 on success."""
222
223 def test_main_json_has_exit_code(self, age_repo: pathlib.Path) -> None:
224 r = _run(age_repo, "code", "age", "--json")
225 data = json.loads(r.output)
226 assert "exit_code" in data
227
228 def test_main_json_exit_code_zero_on_success(self, age_repo: pathlib.Path) -> None:
229 r = _run(age_repo, "code", "age", "--json")
230 assert r.exit_code == 0
231 data = json.loads(r.output)
232 assert data["exit_code"] == 0
233
234 def test_main_json_exit_code_is_int(self, age_repo: pathlib.Path) -> None:
235 r = _run(age_repo, "code", "age", "--json")
236 data = json.loads(r.output)
237 assert isinstance(data["exit_code"], int)
238
239 def test_explain_json_has_exit_code(self, age_repo: pathlib.Path) -> None:
240 addr = _first_symbol_address(age_repo)
241 r = _run(age_repo, "code", "age", "--explain", addr, "--json")
242 data = json.loads(r.output)
243 assert "exit_code" in data
244
245 def test_explain_json_exit_code_zero_on_success(self, age_repo: pathlib.Path) -> None:
246 addr = _first_symbol_address(age_repo)
247 r = _run(age_repo, "code", "age", "--explain", addr, "--json")
248 assert r.exit_code == 0
249 data = json.loads(r.output)
250 assert data["exit_code"] == 0
251
252 def test_explain_json_exit_code_is_int(self, age_repo: pathlib.Path) -> None:
253 addr = _first_symbol_address(age_repo)
254 r = _run(age_repo, "code", "age", "--explain", addr, "--json")
255 data = json.loads(r.output)
256 assert isinstance(data["exit_code"], int)
257
258 def test_j_alias_exit_code_present(self, age_repo: pathlib.Path) -> None:
259 r = _run(age_repo, "code", "age", "-j")
260 data = json.loads(r.output)
261 assert "exit_code" in data
262
263 def test_exit_code_mirrors_process_exit(self, age_repo: pathlib.Path) -> None:
264 r = _run(age_repo, "code", "age", "--json")
265 data = json.loads(r.output)
266 assert data["exit_code"] == r.exit_code
267
268
269 # ---------------------------------------------------------------------------
270 # TestTypedDicts — envelope TypedDicts exist with the required fields
271 # ---------------------------------------------------------------------------
272
273
274 class TestTypedDicts:
275 """_AgeListJson, _ExplainJson TypedDicts must exist and include new fields."""
276
277 def test_age_list_json_typed_dict_exists(self) -> None:
278 from muse.cli.commands.age import _AgeListJson # noqa: F401
279
280 def test_age_list_json_has_exit_code_annotation(self) -> None:
281 from muse.cli.commands.age import _AgeListJson
282 assert "exit_code" in _AgeListJson.__annotations__
283
284 def test_age_list_json_has_duration_ms_annotation(self) -> None:
285 from muse.cli.commands.age import _AgeListJson
286 assert "duration_ms" in _AgeListJson.__annotations__
287
288 def test_age_list_json_has_symbols_annotation(self) -> None:
289 from muse.cli.commands.age import _AgeListJson
290 assert "symbols" in _AgeListJson.__annotations__
291
292 def test_explain_json_typed_dict_exists(self) -> None:
293 from muse.cli.commands.age import _ExplainJson # noqa: F401
294
295 def test_explain_json_has_exit_code_annotation(self) -> None:
296 from muse.cli.commands.age import _ExplainJson
297 assert "exit_code" in _ExplainJson.__annotations__
298
299 def test_explain_json_has_duration_ms_annotation(self) -> None:
300 from muse.cli.commands.age import _ExplainJson
301 assert "duration_ms" in _ExplainJson.__annotations__
302
303 def test_explain_json_has_events_annotation(self) -> None:
304 from muse.cli.commands.age import _ExplainJson
305 assert "events" in _ExplainJson.__annotations__
306
307 def test_age_record_typed_dict_exists(self) -> None:
308 from muse.cli.commands.age import _AgeRecord # noqa: F401
309
310 def test_age_record_has_est_survival_pct(self) -> None:
311 from muse.cli.commands.age import _AgeRecord
312 assert "est_survival_pct" in _AgeRecord.__annotations__
313
314
315 # ---------------------------------------------------------------------------
316 # TestDocstrings — run() docstring documents new fields
317 # ---------------------------------------------------------------------------
318
319
320 class TestDocstrings:
321 """run() must document exit_code in its docstring."""
322
323 def test_run_docstring_documents_fields(self) -> None:
324 from muse.cli.commands.age import run
325 assert "Exit codes" in run.__doc__
326
327
328 # ---------------------------------------------------------------------------
329 # TestAnsiSanitization — JSON fields must not contain terminal escape codes
330 # ---------------------------------------------------------------------------
331
332
333 class TestAnsiSanitization:
334 """No ANSI escape sequences in JSON string fields."""
335
336 def test_main_json_no_ansi_in_output(self, age_repo: pathlib.Path) -> None:
337 r = _run(age_repo, "code", "age", "--json")
338 assert "\x1b" not in r.output
339
340 def test_explain_json_no_ansi_in_output(self, age_repo: pathlib.Path) -> None:
341 addr = _first_symbol_address(age_repo)
342 r = _run(age_repo, "code", "age", "--explain", addr, "--json")
343 assert "\x1b" not in r.output
344
345 def test_main_json_addresses_no_ansi(self, age_repo: pathlib.Path) -> None:
346 r = _run(age_repo, "code", "age", "--json")
347 data = json.loads(r.output)
348 for sym in data["symbols"]:
349 assert "\x1b" not in sym["address"]
350
351 def test_explain_json_address_no_ansi(self, age_repo: pathlib.Path) -> None:
352 addr = _first_symbol_address(age_repo)
353 r = _run(age_repo, "code", "age", "--explain", addr, "--json")
354 data = json.loads(r.output)
355 assert "\x1b" not in data["address"]
356
357
358 # ---------------------------------------------------------------------------
359 # TestPerformance — duration_ms stays in a reasonable range
360 # ---------------------------------------------------------------------------
361
362
363 class TestPerformance:
364 """duration_ms must be non-negative and under 1000 ms for small repos."""
365
366 def test_main_json_duration_under_1000ms(self, age_repo: pathlib.Path) -> None:
367 r = _run(age_repo, "code", "age", "--json")
368 data = json.loads(r.output)
369 assert data["duration_ms"] < 1000
370
371 def test_explain_json_duration_under_1000ms(self, age_repo: pathlib.Path) -> None:
372 addr = _first_symbol_address(age_repo)
373 r = _run(age_repo, "code", "age", "--explain", addr, "--json")
374 data = json.loads(r.output)
375 assert data["duration_ms"] < 1000
376
377 def test_duration_ms_type_is_float_not_int(self, age_repo: pathlib.Path) -> None:
378 """duration_ms must always be float, never int — even if value is 0."""
379 r = _run(age_repo, "code", "age", "--json")
380 data = json.loads(r.output)
381 # JSON floats with no decimal part can parse as int — check the raw string.
382 import re
383 m = re.search(r'"duration_ms"\s*:\s*([0-9.e+\-]+)', r.output)
384 assert m is not None, "duration_ms not found in output"
385 # Should contain a decimal point (e.g. "12.3", not "12").
386 assert isinstance(data["duration_ms"], float)
File History 1 commit
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 6 days ago