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