gabriel / muse public
test_contract_supercharge.py python
382 lines 14.7 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
1 """Supercharge tests for ``muse code contract`` — agent-usability gaps.
2
3 The existing TestContract suite in test_code_commands.py covers correctness,
4 JSON schema, all field keys, parameter schema, history schema, stability values,
5 arg observations, and error paths. This file targets only the gaps those
6 tests leave open:
7
8 Coverage matrix
9 ---------------
10 - --json / -j: -j alias works identically to --json
11 - exit_code: JSON output includes exit_code = 0 on success
12 - duration_ms: JSON output includes non-negative float duration_ms
13 - TypedDicts: _ContractJson gains exit_code/duration_ms annotations
14 - Docstrings: run() docstring mentions exit_code and duration_ms
15 - ANSI: JSON output never contains terminal escape sequences
16 - Performance: duration_ms stays under 2000 ms for a small repo
17 """
18
19 from __future__ import annotations
20
21 import json
22 import os
23 import pathlib
24 import textwrap
25
26 import pytest
27
28 from tests.cli_test_helper import CliRunner
29
30 runner = CliRunner()
31
32 _ADDR = "billing.py::compute_total"
33
34
35 # ---------------------------------------------------------------------------
36 # Helpers
37 # ---------------------------------------------------------------------------
38
39
40 def _env(root: pathlib.Path) -> dict[str, str]:
41 return {"MUSE_REPO_ROOT": str(root)}
42
43
44 def _run(root: pathlib.Path, *args: str):
45 return runner.invoke(None, list(args), env=_env(root))
46
47
48 # ---------------------------------------------------------------------------
49 # Fixture — multi-commit repo with a real call graph + test assertions
50 # ---------------------------------------------------------------------------
51
52
53 @pytest.fixture()
54 def contract_repo(
55 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
56 ) -> pathlib.Path:
57 """Repo exercising every dimension of ``muse code contract``.
58
59 Layout::
60
61 billing.py — compute_total(items, currency="USD") → float
62 services.py — place_order() calls compute_total → stored
63 audit.py — run_audit() calls compute_total → discarded
64 tests/test_billing.py — test functions with assertions
65
66 Commit history::
67
68 1. readme.txt seed commit
69 2. billing.py added — compute_total created
70 3. callers + tests added
71 4. billing.py body rewrite (PATCH)
72 5. billing.py currency param added (MINOR)
73 """
74 monkeypatch.chdir(tmp_path)
75
76 r = _run(tmp_path, "init", "--domain", "code")
77 assert r.exit_code == 0, r.output
78
79 # commit 1 — seed
80 (tmp_path / "readme.txt").write_text("# contract test repo\n")
81 r = _run(tmp_path, "code", "add", ".")
82 assert r.exit_code == 0, r.output
83 r = _run(tmp_path, "commit", "-m", "seed: initial readme")
84 assert r.exit_code == 0, r.output
85
86 # commit 2 — add compute_total
87 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
88 def compute_total(items):
89 return sum(i["price"] for i in items)
90 """))
91 r = _run(tmp_path, "code", "add", ".")
92 assert r.exit_code == 0, r.output
93 r = _run(tmp_path, "commit", "-m", "feat: add compute_total")
94 assert r.exit_code == 0, r.output
95
96 # commit 3 — callers + tests
97 os.makedirs(tmp_path / "tests", exist_ok=True)
98 (tmp_path / "services.py").write_text(textwrap.dedent("""\
99 from billing import compute_total
100
101 def place_order(items):
102 total = compute_total(items, currency="EUR")
103 return total
104 """))
105 (tmp_path / "audit.py").write_text(textwrap.dedent("""\
106 from billing import compute_total
107
108 def run_audit(items):
109 compute_total(items)
110 """))
111 (tmp_path / "tests" / "test_billing.py").write_text(textwrap.dedent("""\
112 from billing import compute_total
113
114 def test_compute_total_basic():
115 result = compute_total([{"price": 10}, {"price": 5}])
116 assert result == 15
117 assert result > 0
118 assert isinstance(result, (int, float))
119
120 def test_compute_total_empty():
121 result = compute_total([])
122 assert result == 0
123 """))
124 r = _run(tmp_path, "code", "add", ".")
125 assert r.exit_code == 0, r.output
126 r = _run(tmp_path, "commit", "-m", "feat: add callers and tests")
127 assert r.exit_code == 0, r.output
128
129 # commit 4 — body rewrite (PATCH)
130 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
131 def compute_total(items):
132 total = 0.0
133 for item in items:
134 total += float(item["price"])
135 return total
136 """))
137 r = _run(tmp_path, "code", "add", ".")
138 assert r.exit_code == 0, r.output
139 r = _run(tmp_path, "commit", "-m", "perf: vectorise compute_total")
140 assert r.exit_code == 0, r.output
141
142 # commit 5 — add currency param (MINOR)
143 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
144 def compute_total(items, currency="USD"):
145 total = 0.0
146 for item in items:
147 total += float(item["price"])
148 return total
149 """))
150 r = _run(tmp_path, "code", "add", ".")
151 assert r.exit_code == 0, r.output
152 r = _run(tmp_path, "commit", "-m", "feat: add optional currency param")
153 assert r.exit_code == 0, r.output
154
155 return tmp_path
156
157
158 # ---------------------------------------------------------------------------
159 # TestJsonAlias — -j works identically to --json
160 # ---------------------------------------------------------------------------
161
162
163 class TestJsonAlias:
164 """-j shorthand must behave identically to --json."""
165
166 def test_j_alias_exits_zero(self, contract_repo: pathlib.Path) -> None:
167 r = _run(contract_repo, "code", "contract", _ADDR, "-j")
168 assert r.exit_code == 0, r.output
169
170 def test_j_alias_valid_json(self, contract_repo: pathlib.Path) -> None:
171 r = _run(contract_repo, "code", "contract", _ADDR, "-j")
172 json.loads(r.output) # must not raise
173
174 def test_j_alias_has_address_key(self, contract_repo: pathlib.Path) -> None:
175 r = _run(contract_repo, "code", "contract", _ADDR, "-j")
176 assert "address" in json.loads(r.output)
177
178 def test_j_alias_has_ops_key(self, contract_repo: pathlib.Path) -> None:
179 r = _run(contract_repo, "code", "contract", _ADDR, "-j")
180 data = json.loads(r.output)
181 assert "stability" in data
182
183 def test_j_alias_same_top_level_keys_as_json_flag(
184 self, contract_repo: pathlib.Path
185 ) -> None:
186 r1 = _run(contract_repo, "code", "contract", _ADDR, "--json")
187 r2 = _run(contract_repo, "code", "contract", _ADDR, "-j")
188 d1 = json.loads(r1.output)
189 d2 = json.loads(r2.output)
190 d1.pop("duration_ms", None)
191 d2.pop("duration_ms", None)
192 assert set(d1.keys()) == set(d2.keys())
193
194 def test_j_alias_address_matches(self, contract_repo: pathlib.Path) -> None:
195 r = _run(contract_repo, "code", "contract", _ADDR, "-j")
196 assert json.loads(r.output)["address"] == _ADDR
197
198 def test_j_alias_has_history_key(self, contract_repo: pathlib.Path) -> None:
199 r = _run(contract_repo, "code", "contract", _ADDR, "-j")
200 assert "history" in json.loads(r.output)
201
202
203 # ---------------------------------------------------------------------------
204 # TestDurationMs — JSON output must include duration_ms
205 # ---------------------------------------------------------------------------
206
207
208 class TestDurationMs:
209 """JSON output must include a non-negative float duration_ms."""
210
211 def test_json_has_duration_ms(self, contract_repo: pathlib.Path) -> None:
212 r = _run(contract_repo, "code", "contract", _ADDR, "--json")
213 assert "duration_ms" in json.loads(r.output)
214
215 def test_json_duration_ms_nonnegative(self, contract_repo: pathlib.Path) -> None:
216 r = _run(contract_repo, "code", "contract", _ADDR, "--json")
217 assert json.loads(r.output)["duration_ms"] >= 0
218
219 def test_json_duration_ms_is_float(self, contract_repo: pathlib.Path) -> None:
220 r = _run(contract_repo, "code", "contract", _ADDR, "--json")
221 assert isinstance(json.loads(r.output)["duration_ms"], float)
222
223 def test_j_alias_duration_ms_present(self, contract_repo: pathlib.Path) -> None:
224 r = _run(contract_repo, "code", "contract", _ADDR, "-j")
225 assert "duration_ms" in json.loads(r.output)
226
227 def test_duration_ms_with_max_commits_1(self, contract_repo: pathlib.Path) -> None:
228 """duration_ms present even with --max-commits 1."""
229 r = _run(contract_repo, "code", "contract", _ADDR, "--json", "--max-commits", "1")
230 data = json.loads(r.output)
231 assert "duration_ms" in data
232 assert data["duration_ms"] >= 0
233
234 def test_duration_ms_not_zero_for_real_work(self, contract_repo: pathlib.Path) -> None:
235 """Non-trivial analysis should take measurable time."""
236 r = _run(contract_repo, "code", "contract", _ADDR, "--json")
237 # Allow 0.0 only in very fast CI — just confirm type and sign
238 assert isinstance(json.loads(r.output)["duration_ms"], float)
239
240
241 # ---------------------------------------------------------------------------
242 # TestExitCode — JSON includes exit_code = 0 on success
243 # ---------------------------------------------------------------------------
244
245
246 class TestExitCode:
247 """JSON exit_code must be 0 on success."""
248
249 def test_json_has_exit_code(self, contract_repo: pathlib.Path) -> None:
250 r = _run(contract_repo, "code", "contract", _ADDR, "--json")
251 assert "exit_code" in json.loads(r.output)
252
253 def test_json_exit_code_zero(self, contract_repo: pathlib.Path) -> None:
254 r = _run(contract_repo, "code", "contract", _ADDR, "--json")
255 assert r.exit_code == 0
256 assert json.loads(r.output)["exit_code"] == 0
257
258 def test_json_exit_code_is_int(self, contract_repo: pathlib.Path) -> None:
259 r = _run(contract_repo, "code", "contract", _ADDR, "--json")
260 assert isinstance(json.loads(r.output)["exit_code"], int)
261
262 def test_j_alias_exit_code_present(self, contract_repo: pathlib.Path) -> None:
263 r = _run(contract_repo, "code", "contract", _ADDR, "-j")
264 assert "exit_code" in json.loads(r.output)
265
266 def test_exit_code_mirrors_process_exit(self, contract_repo: pathlib.Path) -> None:
267 r = _run(contract_repo, "code", "contract", _ADDR, "--json")
268 assert json.loads(r.output)["exit_code"] == r.exit_code
269
270 def test_exit_code_zero_with_max_commits(self, contract_repo: pathlib.Path) -> None:
271 r = _run(contract_repo, "code", "contract", _ADDR, "--json", "--max-commits", "3")
272 assert r.exit_code == 0
273 assert json.loads(r.output)["exit_code"] == 0
274
275 def test_exit_code_not_present_in_error_path(
276 self, contract_repo: pathlib.Path
277 ) -> None:
278 """Error paths raise SystemExit before JSON emits — no JSON to check."""
279 r = _run(contract_repo, "code", "contract", "billing.py::nonexistent", "--json")
280 assert r.exit_code != 0
281
282
283 # ---------------------------------------------------------------------------
284 # TestTypedDicts — _ContractJson carries the new fields
285 # ---------------------------------------------------------------------------
286
287
288 class TestTypedDicts:
289 """_ContractJson must carry exit_code and duration_ms annotations."""
290
291 def test_contract_json_typeddict_exists(self) -> None:
292 from muse.cli.commands.contract import _ContractJson # noqa: F401
293
294 def test_has_exit_code_annotation(self) -> None:
295 from muse.cli.commands.contract import _ContractJson
296 assert "exit_code" in _ContractJson.__annotations__
297
298 def test_has_duration_ms_annotation(self) -> None:
299 from muse.cli.commands.contract import _ContractJson
300 assert "duration_ms" in _ContractJson.__annotations__
301
302 def test_retains_address_annotation(self) -> None:
303 from muse.cli.commands.contract import _ContractJson
304 assert "address" in _ContractJson.__annotations__
305
306 def test_retains_history_annotation(self) -> None:
307 from muse.cli.commands.contract import _ContractJson
308 assert "history" in _ContractJson.__annotations__
309
310 def test_retains_stability_annotation(self) -> None:
311 from muse.cli.commands.contract import _ContractJson
312 assert "stability" in _ContractJson.__annotations__
313
314 def test_retains_warnings_annotation(self) -> None:
315 from muse.cli.commands.contract import _ContractJson
316 assert "warnings" in _ContractJson.__annotations__
317
318
319 # ---------------------------------------------------------------------------
320 # TestDocstrings — run() docstring documents new fields
321 # ---------------------------------------------------------------------------
322
323
324 class TestDocstrings:
325 """run() must document exit_code and duration_ms."""
326
327 def test_run_docstring_mentions_exit_code(self) -> None:
328 from muse.cli.commands.contract import run
329 assert run.__doc__ is not None
330 assert "exit_code" in run.__doc__
331
332 def test_run_docstring_mentions_duration_ms(self) -> None:
333 from muse.cli.commands.contract import run
334 assert run.__doc__ is not None
335 assert "duration_ms" in run.__doc__
336
337
338 # ---------------------------------------------------------------------------
339 # TestAnsiSanitization — no escape codes in JSON output
340 # ---------------------------------------------------------------------------
341
342
343 class TestAnsiSanitization:
344 """No ANSI escape sequences anywhere in the JSON output."""
345
346 def test_json_output_no_ansi(self, contract_repo: pathlib.Path) -> None:
347 r = _run(contract_repo, "code", "contract", _ADDR, "--json")
348 assert "\x1b" not in r.output
349
350 def test_j_alias_output_no_ansi(self, contract_repo: pathlib.Path) -> None:
351 r = _run(contract_repo, "code", "contract", _ADDR, "-j")
352 assert "\x1b" not in r.output
353
354 def test_json_output_no_ansi_with_max_commits(
355 self, contract_repo: pathlib.Path
356 ) -> None:
357 r = _run(
358 contract_repo, "code", "contract", _ADDR,
359 "--json", "--max-commits", "2",
360 )
361 assert "\x1b" not in r.output
362
363
364 # ---------------------------------------------------------------------------
365 # TestPerformance — duration_ms under 2000 ms for a small repo
366 # ---------------------------------------------------------------------------
367
368
369 class TestPerformance:
370 """duration_ms must stay under 2000 ms for small repos."""
371
372 def test_json_duration_under_2000ms(self, contract_repo: pathlib.Path) -> None:
373 r = _run(contract_repo, "code", "contract", _ADDR, "--json")
374 assert json.loads(r.output)["duration_ms"] < 2000
375
376 def test_j_alias_duration_under_2000ms(self, contract_repo: pathlib.Path) -> None:
377 r = _run(contract_repo, "code", "contract", _ADDR, "-j")
378 assert json.loads(r.output)["duration_ms"] < 2000
379
380 def test_duration_ms_is_float_not_int(self, contract_repo: pathlib.Path) -> None:
381 r = _run(contract_repo, "code", "contract", _ADDR, "--json")
382 assert isinstance(json.loads(r.output)["duration_ms"], float)
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago