gabriel / muse public
test_code_check_supercharge.py python
339 lines 13.5 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Supercharge tests for ``muse code code-check`` — agent-usability gaps.
2
3 The existing test_cmd_code_check.py covers correctness, JSON schema, filters,
4 --diff, --rules, security, edge cases, and stress. This file targets only
5 the gaps those tests leave open:
6
7 Coverage matrix
8 ---------------
9 - --json / -j: -j alias works identically to --json
10 - exit_code: JSON output includes exit_code mirroring process exit
11 (0 normally; 1 when --strict and has_errors)
12 - duration_ms: JSON output includes non-negative float duration_ms
13 - TypedDicts: _CodeCheckOutputJson 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 Critical distinction: exit_code is NOT hardcoded to 0. When --strict is
19 active and violations with severity=error are found, both the process and
20 the JSON exit_code equal 1.
21 """
22
23 from __future__ import annotations
24 from collections.abc import Mapping
25
26 import json
27 import pathlib
28 import textwrap
29
30 import pytest
31
32 from tests.cli_test_helper import CliRunner
33
34 runner = CliRunner()
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41
42 def _env(root: pathlib.Path) -> Mapping[str, str]:
43 return {"MUSE_REPO_ROOT": str(root)}
44
45
46 def _run(root: pathlib.Path, *args: str):
47 return runner.invoke(None, list(args), env=_env(root))
48
49
50 def _stage_commit(root: pathlib.Path, msg: str = "commit") -> None:
51 r = _run(root, "code", "add", ".")
52 assert r.exit_code == 0, r.output
53 r2 = _run(root, "commit", "-m", msg)
54 assert r2.exit_code == 0, r2.output
55
56
57 # ---------------------------------------------------------------------------
58 # Fixtures
59 # ---------------------------------------------------------------------------
60
61
62 @pytest.fixture()
63 def clean_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
64 """Code-domain repo with one clean Python file — no violations."""
65 monkeypatch.chdir(tmp_path)
66 r = _run(tmp_path, "init", "--domain", "code")
67 assert r.exit_code == 0, r.output
68 (tmp_path / "clean.py").write_text("def hello():\n return 'hello'\n")
69 _stage_commit(tmp_path, "clean")
70 return tmp_path
71
72
73 def _complex_func(n_branches: int = 12) -> str:
74 """Python source with cyclomatic complexity > 10 (triggers max_complexity rule)."""
75 lines = ["def heavy(x: int) -> int:", " if x == 1:", " return 1"]
76 for i in range(2, n_branches + 1):
77 lines.append(f" elif x == {i}:")
78 lines.append(f" return {i}")
79 lines.append(" return 0")
80 return "\n".join(lines) + "\n"
81
82
83 @pytest.fixture()
84 def violation_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
85 """Repo whose HEAD commit has a high-complexity function → error violation.
86
87 Writes a custom code_invariants.toml that raises max_complexity to severity=error
88 so that --strict exits 1 and has_errors is True.
89 """
90 monkeypatch.chdir(tmp_path)
91 r = _run(tmp_path, "init", "--domain", "code")
92 assert r.exit_code == 0, r.output
93 muse_dir = tmp_path / ".muse"
94 muse_dir.mkdir(exist_ok=True)
95 (muse_dir / "code_invariants.toml").write_text(
96 '[[rule]]\nname = "complexity_gate"\nseverity = "error"\n'
97 'rule_type = "max_complexity"\n[rule.params]\nthreshold = 10\n'
98 )
99 (tmp_path / "complex.py").write_text(_complex_func())
100 _stage_commit(tmp_path, "add complex function")
101 return tmp_path
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_exits_zero_clean(self, clean_repo: pathlib.Path) -> None:
113 r = _run(clean_repo, "code", "code-check", "-j")
114 assert r.exit_code == 0, r.output
115
116 def test_j_alias_valid_json(self, clean_repo: pathlib.Path) -> None:
117 r = _run(clean_repo, "code", "code-check", "-j")
118 json.loads(r.output) # must not raise
119
120 def test_j_alias_has_violations_key(self, clean_repo: pathlib.Path) -> None:
121 r = _run(clean_repo, "code", "code-check", "-j")
122 data = json.loads(r.output)
123 assert "violations" in data
124
125 def test_j_alias_has_has_errors_key(self, clean_repo: pathlib.Path) -> None:
126 r = _run(clean_repo, "code", "code-check", "-j")
127 data = json.loads(r.output)
128 assert "has_errors" in data
129
130 def test_j_alias_same_top_level_keys_as_json_flag(self, clean_repo: pathlib.Path) -> None:
131 r1 = _run(clean_repo, "code", "code-check", "--json")
132 r2 = _run(clean_repo, "code", "code-check", "-j")
133 d1 = json.loads(r1.output)
134 d2 = json.loads(r2.output)
135 d1.pop("duration_ms", None)
136 d2.pop("duration_ms", None)
137 assert set(d1.keys()) == set(d2.keys())
138
139 def test_j_alias_violation_count_matches(self, violation_repo: pathlib.Path) -> None:
140 r1 = _run(violation_repo, "code", "code-check", "--json")
141 r2 = _run(violation_repo, "code", "code-check", "-j")
142 d1 = json.loads(r1.output)
143 d2 = json.loads(r2.output)
144 assert len(d1["violations"]) == len(d2["violations"])
145
146 def test_j_alias_with_strict(self, clean_repo: pathlib.Path) -> None:
147 r = _run(clean_repo, "code", "code-check", "-j", "--strict")
148 assert r.exit_code == 0, r.output
149 json.loads(r.output)
150
151
152 # ---------------------------------------------------------------------------
153 # TestDurationMs — JSON output must include duration_ms
154 # ---------------------------------------------------------------------------
155
156
157 class TestDurationMs:
158 """JSON output must include a non-negative float duration_ms."""
159
160 def test_json_has_duration_ms(self, clean_repo: pathlib.Path) -> None:
161 r = _run(clean_repo, "code", "code-check", "--json")
162 data = json.loads(r.output)
163 assert "duration_ms" in data
164
165 def test_json_duration_ms_nonnegative(self, clean_repo: pathlib.Path) -> None:
166 r = _run(clean_repo, "code", "code-check", "--json")
167 data = json.loads(r.output)
168 assert data["duration_ms"] >= 0
169
170 def test_json_duration_ms_is_float(self, clean_repo: pathlib.Path) -> None:
171 r = _run(clean_repo, "code", "code-check", "--json")
172 data = json.loads(r.output)
173 assert isinstance(data["duration_ms"], float)
174
175 def test_j_alias_duration_ms_present(self, clean_repo: pathlib.Path) -> None:
176 r = _run(clean_repo, "code", "code-check", "-j")
177 data = json.loads(r.output)
178 assert "duration_ms" in data
179
180 def test_duration_ms_with_violations(self, violation_repo: pathlib.Path) -> None:
181 r = _run(violation_repo, "code", "code-check", "--json")
182 data = json.loads(r.output)
183 assert "duration_ms" in data
184 assert data["duration_ms"] >= 0
185
186 def test_duration_ms_with_filter(self, clean_repo: pathlib.Path) -> None:
187 r = _run(clean_repo, "code", "code-check", "--json", "--filter", "error")
188 data = json.loads(r.output)
189 assert "duration_ms" in data
190 assert data["duration_ms"] >= 0
191
192
193 # ---------------------------------------------------------------------------
194 # TestExitCode — JSON includes exit_code mirroring process exit
195 # ---------------------------------------------------------------------------
196
197
198 class TestExitCode:
199 """JSON exit_code must mirror the actual process exit code.
200
201 Without --strict: always 0, even if violations exist.
202 With --strict and error-severity violations: 1.
203 """
204
205 def test_json_has_exit_code(self, clean_repo: pathlib.Path) -> None:
206 r = _run(clean_repo, "code", "code-check", "--json")
207 data = json.loads(r.output)
208 assert "exit_code" in data
209
210 def test_json_exit_code_zero_clean_no_strict(self, clean_repo: pathlib.Path) -> None:
211 r = _run(clean_repo, "code", "code-check", "--json")
212 assert r.exit_code == 0
213 data = json.loads(r.output)
214 assert data["exit_code"] == 0
215
216 def test_json_exit_code_is_int(self, clean_repo: pathlib.Path) -> None:
217 r = _run(clean_repo, "code", "code-check", "--json")
218 data = json.loads(r.output)
219 assert isinstance(data["exit_code"], int)
220
221 def test_j_alias_exit_code_present(self, clean_repo: pathlib.Path) -> None:
222 r = _run(clean_repo, "code", "code-check", "-j")
223 data = json.loads(r.output)
224 assert "exit_code" in data
225
226 def test_exit_code_zero_with_violations_no_strict(
227 self, violation_repo: pathlib.Path
228 ) -> None:
229 """Violations without --strict → process exits 0, JSON exit_code == 0."""
230 r = _run(violation_repo, "code", "code-check", "--json")
231 assert r.exit_code == 0
232 data = json.loads(r.output)
233 assert data["exit_code"] == 0
234
235 def test_exit_code_one_strict_with_errors(self, violation_repo: pathlib.Path) -> None:
236 """--strict + error violations → process exits 1, JSON exit_code == 1."""
237 r = _run(violation_repo, "code", "code-check", "--json", "--strict")
238 assert r.exit_code == 1
239 data = json.loads(r.output)
240 assert data["exit_code"] == 1
241
242 def test_exit_code_mirrors_process_exit_clean(self, clean_repo: pathlib.Path) -> None:
243 r = _run(clean_repo, "code", "code-check", "--json")
244 data = json.loads(r.output)
245 assert data["exit_code"] == r.exit_code
246
247 def test_exit_code_mirrors_process_exit_strict_violations(
248 self, violation_repo: pathlib.Path
249 ) -> None:
250 r = _run(violation_repo, "code", "code-check", "--json", "--strict")
251 data = json.loads(r.output)
252 assert data["exit_code"] == r.exit_code
253
254 def test_exit_code_not_hardcoded_zero(self, violation_repo: pathlib.Path) -> None:
255 """Prove exit_code is computed, not a literal 0."""
256 r = _run(violation_repo, "code", "code-check", "--json", "--strict")
257 data = json.loads(r.output)
258 assert data["exit_code"] != 0
259
260
261 # ---------------------------------------------------------------------------
262 # TestTypedDicts — _CodeCheckOutputJson carries the new fields
263 # ---------------------------------------------------------------------------
264
265
266 class TestTypedDicts:
267 """_CodeCheckOutputJson must carry exit_code and duration_ms annotations."""
268
269 def test_code_check_output_json_exists(self) -> None:
270 from muse.cli.commands.code_check import _CodeCheckOutputJson # noqa: F401
271
272 def test_has_exit_code_annotation(self) -> None:
273 from muse.cli.commands.code_check import _CodeCheckOutputJson
274 assert "exit_code" in _CodeCheckOutputJson.__annotations__
275
276 def test_has_duration_ms_annotation(self) -> None:
277 from muse.cli.commands.code_check import _CodeCheckOutputJson
278 assert "duration_ms" in _CodeCheckOutputJson.__annotations__
279
280 def test_retains_violations_annotation(self) -> None:
281 from muse.cli.commands.code_check import _CodeCheckOutputJson
282 assert "violations" in _CodeCheckOutputJson.__annotations__
283
284 def test_retains_has_errors_annotation(self) -> None:
285 from muse.cli.commands.code_check import _CodeCheckOutputJson
286 assert "has_errors" in _CodeCheckOutputJson.__annotations__
287
288 def test_retains_commit_id_annotation(self) -> None:
289 from muse.cli.commands.code_check import _CodeCheckOutputJson
290 assert "commit_id" in _CodeCheckOutputJson.__annotations__
291
292 def test_retains_rules_checked_annotation(self) -> None:
293 from muse.cli.commands.code_check import _CodeCheckOutputJson
294 assert "rules_checked" in _CodeCheckOutputJson.__annotations__
295
296
297 # ---------------------------------------------------------------------------
298 # TestAnsiSanitization — no escape codes in JSON output
299 # ---------------------------------------------------------------------------
300
301
302 class TestAnsiSanitization:
303 """No ANSI escape sequences anywhere in the JSON output."""
304
305 def test_json_output_no_ansi_clean(self, clean_repo: pathlib.Path) -> None:
306 r = _run(clean_repo, "code", "code-check", "--json")
307 assert "\x1b" not in r.output
308
309 def test_j_alias_output_no_ansi(self, clean_repo: pathlib.Path) -> None:
310 r = _run(clean_repo, "code", "code-check", "-j")
311 assert "\x1b" not in r.output
312
313 def test_json_output_no_ansi_violations(self, violation_repo: pathlib.Path) -> None:
314 r = _run(violation_repo, "code", "code-check", "--json")
315 assert "\x1b" not in r.output
316
317
318 # ---------------------------------------------------------------------------
319 # TestPerformance — duration_ms under 2000 ms for a small repo
320 # ---------------------------------------------------------------------------
321
322
323 class TestPerformance:
324 """duration_ms must stay under 2000 ms for small repos."""
325
326 def test_json_duration_under_2000ms(self, clean_repo: pathlib.Path) -> None:
327 r = _run(clean_repo, "code", "code-check", "--json")
328 data = json.loads(r.output)
329 assert data["duration_ms"] < 2000
330
331 def test_j_alias_duration_under_2000ms(self, clean_repo: pathlib.Path) -> None:
332 r = _run(clean_repo, "code", "code-check", "-j")
333 data = json.loads(r.output)
334 assert data["duration_ms"] < 2000
335
336 def test_duration_ms_is_float_not_int(self, clean_repo: pathlib.Path) -> None:
337 r = _run(clean_repo, "code", "code-check", "--json")
338 data = json.loads(r.output)
339 assert isinstance(data["duration_ms"], float)
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago