gabriel / muse public

test_cmd_code_check.py file-level

at sha256:2 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:0 chore: bump version to 0.2.0rc14 · gabriel · Jun 20, 2026
1 """Comprehensive tests for ``muse code code-check``.
2
3 Review findings addressed
4 --------------------------
5 Bug fixes
6 * Error messages (no commit, bad rules path) now write to stderr, not stdout.
7 * Dead import ``CodeChecker`` removed from the CLI module.
8 * Non-existent ``--rules`` file now produces an explicit error instead of
9 silently falling back to built-in defaults.
10
11 New capabilities
12 * ``--filter error|warning|info``: show only violations of the given severity.
13 * ``--diff <ref>``: show only violations that are NEW since a reference commit
14 (CI ratchet β€” fail only on regressions, not pre-existing noise).
15 * ``--diff`` + ``--json``: emits ``diff_ref`` field in JSON payload.
16 * ``make_report`` + ``diff_reports`` added to ``muse.core.invariants``.
17
18 Test categories
19 ---------------
20 I Core behaviour β€” HEAD, specific commit, text output shape.
21 II JSON output β€” required keys, has_errors, has_warnings, counts.
22 III --filter flag β€” error/warning/info filtering, interaction with --strict.
23 IV --diff flag β€” new-only violations, CI ratchet, JSON shape, bad ref.
24 V --rules flag β€” custom file, path traversal, missing file.
25 VI Security β€” path traversal, absolute paths, dot-dot escapes.
26 VII Edge cases β€” no commits, fresh repo, header format, consistency.
27 VIII Stress β€” 100 violation files, 50-file cycle chain, large rule sets.
28 """
29
30 from __future__ import annotations
31 from collections.abc import Mapping
32
33 import argparse
34 import json
35 import pathlib
36
37 import pytest
38
39 from muse.core.types import Manifest, split_id
40
41 from tests.cli_test_helper import CliRunner
42
43 runner = CliRunner()
44 cli = None
45
46
47 # ---------------------------------------------------------------------------
48 # Helpers
49 # ---------------------------------------------------------------------------
50
51
52 def _env(root: pathlib.Path) -> Manifest:
53 return {"MUSE_REPO_ROOT": str(root)}
54
55
56 def _run(root: pathlib.Path, *args: str) -> tuple[int, str]:
57 result = runner.invoke(cli, list(args), env=_env(root), catch_exceptions=False)
58 return result.exit_code, result.output + result.stderr
59
60
61 def _run_unchecked(root: pathlib.Path, *args: str) -> tuple[int, str]:
62 result = runner.invoke(cli, list(args), env=_env(root))
63 return result.exit_code, result.output + result.stderr
64
65
66 def _stage_commit(root: pathlib.Path, msg: str = "commit") -> None:
67 """Stage all modified files and commit."""
68 code, out = _run(root, "code", "add", ".")
69 assert code == 0, f"add failed: {out}"
70 code, out = _run(root, "commit", "-m", msg)
71 assert code == 0, f"commit failed: {out}"
72
73
74 def _complex_func(n_branches: int = 12) -> str:
75 """Return Python source for a function with cyclomatic complexity > 10.
76
77 Builds the string line-by-line to guarantee correct indentation β€”
78 f-string multiline substitution does NOT propagate indentation to
79 continuation lines, so the naive template approach produces broken Python.
80 """
81 lines = [
82 "def heavy(x: int) -> int:",
83 " if x == 1:",
84 " return 1",
85 ]
86 for i in range(2, n_branches + 1):
87 lines.append(f" elif x == {i}:")
88 lines.append(f" return {i}")
89 lines.append(" return 0")
90 return "\n".join(lines) + "\n"
91
92
93 # ---------------------------------------------------------------------------
94 # Fixtures
95 # ---------------------------------------------------------------------------
96
97
98 @pytest.fixture()
99 def clean_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
100 """Repo with one clean Python file (no violations)."""
101 monkeypatch.chdir(tmp_path)
102 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
103 assert r.exit_code == 0, r.output
104 (tmp_path / "clean.py").write_text("def greet(name: str) -> str:\n return f'hello {name}'\n")
105 _stage_commit(tmp_path, "init clean")
106 return tmp_path
107
108
109 @pytest.fixture()
110 def violation_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
111 """Repo with one clean commit then a second commit adding violations.
112
113 Commit 1: clean.py only (no complexity violations).
114 Commit 2: adds complex_mod.py with a function of complexity > 10.
115
116 Both commits stage ALL files (muse code add .) so the snapshot contains
117 the accumulated working-tree state, not just the delta.
118 """
119 monkeypatch.chdir(tmp_path)
120 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
121 assert r.exit_code == 0, r.output
122
123 # Commit 1 β€” simple function, no complexity violation
124 (tmp_path / "clean.py").write_text("def _add(a: int, b: int) -> int:\n return a + b\n")
125 _stage_commit(tmp_path, "init clean")
126
127 # Commit 2 β€” add a high-complexity function (complexity violation)
128 (tmp_path / "complex_mod.py").write_text(_complex_func(12))
129 _stage_commit(tmp_path, "add complex_mod")
130 return tmp_path
131
132
133 @pytest.fixture()
134 def cycle_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
135 """Repo with two files that import each other (circular import β†’ error)."""
136 monkeypatch.chdir(tmp_path)
137 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
138 assert r.exit_code == 0, r.output
139 (tmp_path / "alpha.py").write_text("import beta\n\ndef from_alpha() -> str:\n return 'alpha'\n")
140 (tmp_path / "beta.py").write_text("import alpha\n\ndef from_beta() -> str:\n return 'beta'\n")
141 _stage_commit(tmp_path, "add cycle")
142 return tmp_path
143
144
145 # ===========================================================================
146 # I Core behaviour
147 # ===========================================================================
148
149
150 class TestCoreBehaviourI:
151 def test_I1_clean_repo_exits_0(self, clean_repo: pathlib.Path) -> None:
152 code, out = _run(clean_repo, "code", "code-check")
153 assert code == 0, out
154
155 def test_I2_clean_repo_reports_no_error_violations(self, clean_repo: pathlib.Path) -> None:
156 """A simple single-file repo may have dead_export warnings (the exported
157 function is never imported elsewhere), but it must have zero errors."""
158 _, out = _run(clean_repo, "code", "code-check", "--json")
159 d = json.loads(out.strip())
160 assert d["has_errors"] is False
161
162 def test_I3_complexity_violation_detected(self, violation_repo: pathlib.Path) -> None:
163 _, out = _run(violation_repo, "code", "code-check")
164 assert "complexity" in out.lower() or "heavy" in out.lower()
165
166 def test_I4_specific_commit_id_accepted(self, violation_repo: pathlib.Path) -> None:
167 # Get the current HEAD commit ID from JSON output
168 code, out = _run(violation_repo, "code", "code-check", "--json")
169 assert code == 0, out
170 d = json.loads(out.strip().splitlines()[-1] if "\n" in out else out)
171 commit_id = d["commit_id"]
172 # Run with the explicit commit ID (short hex prefix, no algo: prefix)
173 code2, out2 = _run(violation_repo, "code", "code-check", split_id(commit_id)[1][:8])
174 assert code2 == 0, out2
175
176 def test_I5_without_strict_exits_0_even_with_violations(
177 self, violation_repo: pathlib.Path
178 ) -> None:
179 code, _ = _run(violation_repo, "code", "code-check")
180 assert code == 0
181
182 def test_I6_strict_exits_1_on_error_severity(self, cycle_repo: pathlib.Path) -> None:
183 """Circular imports produce error-severity violations; --strict must exit 1."""
184 code, out = _run_unchecked(cycle_repo, "code", "code-check", "--strict")
185 assert code == 1, f"expected exit 1, got {code}:\n{out}"
186
187 def test_I7_text_output_header_contains_commit_short(
188 self, clean_repo: pathlib.Path
189 ) -> None:
190 code, out = _run(clean_repo, "code", "code-check", "--json")
191 d = json.loads(out.strip().splitlines()[-1] if "\n" in out else out)
192 commit_prefix = d["commit_id"][:8]
193 _, text_out = _run(clean_repo, "code", "code-check")
194 assert commit_prefix in text_out
195
196 def test_I8_error_to_no_commit_goes_to_stderr(
197 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
198 ) -> None:
199 """When there are no commits, error must go to stderr, not stdout."""
200 monkeypatch.chdir(tmp_path)
201 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
202 assert r.exit_code == 0, r.output
203 result = runner.invoke(cli, ["code", "code-check"], env=_env(tmp_path))
204 assert result.exit_code != 0
205 assert "❌" in result.stderr or "no commit" in result.stderr.lower()
206
207
208 # ===========================================================================
209 # II JSON output
210 # ===========================================================================
211
212
213 class TestJsonOutputII:
214 def test_II1_json_has_required_keys(self, clean_repo: pathlib.Path) -> None:
215 code, out = _run(clean_repo, "code", "code-check", "--json")
216 assert code == 0, out
217 d = json.loads(out.strip())
218 for key in ("commit_id", "domain", "violations", "rules_checked",
219 "has_errors", "has_warnings"):
220 assert key in d, f"missing key: {key}"
221
222 def test_II2_json_violations_is_list(self, clean_repo: pathlib.Path) -> None:
223 _, out = _run(clean_repo, "code", "code-check", "--json")
224 d = json.loads(out.strip())
225 assert isinstance(d["violations"], list)
226
227 def test_II3_json_has_errors_true_when_circular(self, cycle_repo: pathlib.Path) -> None:
228 _, out = _run(cycle_repo, "code", "code-check", "--json")
229 d = json.loads(out.strip())
230 assert d["has_errors"] is True
231
232 def test_II4_json_has_errors_false_for_clean(self, clean_repo: pathlib.Path) -> None:
233 _, out = _run(clean_repo, "code", "code-check", "--json")
234 d = json.loads(out.strip())
235 assert d["has_errors"] is False
236
237 def test_II5_json_violation_entries_have_required_fields(
238 self, violation_repo: pathlib.Path
239 ) -> None:
240 _, out = _run(violation_repo, "code", "code-check", "--json")
241 d = json.loads(out.strip())
242 for v in d["violations"]:
243 for field in ("rule_name", "severity", "address", "description"):
244 assert field in v, f"violation missing field {field!r}: {v}"
245
246 def test_II6_strict_json_exits_1_on_error(self, cycle_repo: pathlib.Path) -> None:
247 code, out = _run_unchecked(cycle_repo, "code", "code-check", "--json", "--strict")
248 assert code == 1, out
249 # Output is still valid JSON
250 d = json.loads(out.strip())
251 assert d["has_errors"] is True
252
253 def test_II7_json_rules_checked_nonzero(self, clean_repo: pathlib.Path) -> None:
254 _, out = _run(clean_repo, "code", "code-check", "--json")
255 d = json.loads(out.strip())
256 assert d["rules_checked"] > 0
257
258 def test_II8_json_domain_is_code(self, clean_repo: pathlib.Path) -> None:
259 _, out = _run(clean_repo, "code", "code-check", "--json")
260 d = json.loads(out.strip())
261 assert d["domain"] == "code"
262
263
264 # ===========================================================================
265 # III --filter flag
266 # ===========================================================================
267
268
269 class TestFilterFlagIII:
270 def test_III1_filter_error_hides_warnings(self, violation_repo: pathlib.Path) -> None:
271 """complexity_gate produces warnings; --filter error should show none."""
272 _, out = _run(violation_repo, "code", "code-check", "--filter", "error", "--json")
273 d = json.loads(out.strip())
274 for v in d["violations"]:
275 assert v["severity"] == "error", f"non-error slipped through: {v}"
276
277 def test_III2_filter_warning_hides_errors(self, cycle_repo: pathlib.Path) -> None:
278 """Circular imports produce errors; --filter warning should hide them."""
279 _, out = _run(cycle_repo, "code", "code-check", "--filter", "warning", "--json")
280 d = json.loads(out.strip())
281 for v in d["violations"]:
282 assert v["severity"] == "warning", f"non-warning slipped through: {v}"
283
284 def test_III3_filter_error_clean_repo_zero_violations(
285 self, clean_repo: pathlib.Path
286 ) -> None:
287 code, out = _run(clean_repo, "code", "code-check", "--filter", "error", "--json")
288 assert code == 0, out
289 d = json.loads(out.strip())
290 assert d["violations"] == []
291
292 def test_III4_filter_error_strict_still_exits_1(self, cycle_repo: pathlib.Path) -> None:
293 code, out = _run_unchecked(
294 cycle_repo, "code", "code-check", "--filter", "error", "--strict"
295 )
296 assert code == 1, out
297
298 def test_III5_filter_warning_strict_exits_0_for_cycle_repo(
299 self, cycle_repo: pathlib.Path
300 ) -> None:
301 """Filtering to warnings only means the error violations are hidden;
302 --strict should NOT fire since the filtered report has no errors."""
303 code, _ = _run_unchecked(
304 cycle_repo, "code", "code-check", "--filter", "warning", "--strict"
305 )
306 assert code == 0
307
308 def test_III6_filter_appears_in_text_header(self, clean_repo: pathlib.Path) -> None:
309 _, out = _run(clean_repo, "code", "code-check", "--filter", "error")
310 assert "[error only]" in out
311
312 def test_III7_filter_json_has_severity_filter_key(self, clean_repo: pathlib.Path) -> None:
313 _, out = _run(clean_repo, "code", "code-check", "--filter", "error", "--json")
314 d = json.loads(out.strip())
315 assert d.get("severity_filter") == "error"
316
317
318 # ===========================================================================
319 # IV --diff flag
320 # ===========================================================================
321
322
323 class TestDiffFlagIV:
324 def test_IV1_diff_against_self_shows_zero_new_violations(
325 self, violation_repo: pathlib.Path
326 ) -> None:
327 """Comparing HEAD vs HEAD should produce no new violations."""
328 # Get HEAD commit ID
329 _, jout = _run(violation_repo, "code", "code-check", "--json")
330 head_id = json.loads(jout.strip())["commit_id"]
331 _, out = _run(violation_repo, "code", "code-check", "--diff", head_id, "--json")
332 d = json.loads(out.strip())
333 assert d["violations"] == [], f"expected 0 new violations, got: {d['violations']}"
334
335 def test_IV2_diff_shows_only_new_violations(self, violation_repo: pathlib.Path) -> None:
336 """violation_repo has 2 commits: clean then complex_mod.
337 --diff against the first commit should surface the new complexity violations."""
338 # Get the first (clean) commit ID via log
339 code, log_out = _run(violation_repo, "log", "--json")
340 assert code == 0, log_out
341 commits = json.loads(log_out)["commits"]
342 assert len(commits) >= 2
343 # commits is newest-first; the second entry is the clean commit
344 clean_commit_id = commits[1]["commit_id"]
345
346 code, out = _run(
347 violation_repo, "code", "code-check", "--diff", clean_commit_id, "--json"
348 )
349 assert code == 0, out
350 d = json.loads(out.strip())
351 # There should be some new violations (complexity warnings from complex_mod.py)
352 assert len(d["violations"]) > 0
353
354 def test_IV3_diff_json_has_diff_ref_key(self, violation_repo: pathlib.Path) -> None:
355 _, jout = _run(violation_repo, "code", "code-check", "--json")
356 head_id = json.loads(jout.strip())["commit_id"]
357 _, out = _run(violation_repo, "code", "code-check", "--diff", head_id, "--json")
358 d = json.loads(out.strip())
359 assert "diff_ref" in d
360 assert d["diff_ref"] == head_id
361
362 def test_IV4_diff_text_header_contains_diff_label(
363 self, violation_repo: pathlib.Path
364 ) -> None:
365 _, jout = _run(violation_repo, "code", "code-check", "--json")
366 head_id = json.loads(jout.strip())["commit_id"]
367 _, out = _run(violation_repo, "code", "code-check", "--diff", head_id)
368 assert "new since" in out.lower()
369
370 def test_IV5_diff_with_bad_ref_exits_1(self, clean_repo: pathlib.Path) -> None:
371 code, out = _run_unchecked(
372 clean_repo, "code", "code-check", "--diff", "deadbeef00000000"
373 )
374 assert code == 1, out
375
376 def test_IV6_diff_strict_fires_only_on_new_errors(
377 self, violation_repo: pathlib.Path
378 ) -> None:
379 """Diff vs self β†’ 0 new violations β†’ --strict must NOT exit 1."""
380 _, jout = _run(violation_repo, "code", "code-check", "--json")
381 head_id = json.loads(jout.strip())["commit_id"]
382 code, _ = _run_unchecked(
383 violation_repo, "code", "code-check", "--diff", head_id, "--strict"
384 )
385 assert code == 0
386
387 def test_IV7_diff_and_filter_combined(self, violation_repo: pathlib.Path) -> None:
388 """--diff and --filter compose: only new + matching-severity violations."""
389 _, jout = _run(violation_repo, "code", "code-check", "--json")
390 head_id = json.loads(jout.strip())["commit_id"]
391 code, out = _run(
392 violation_repo, "code", "code-check",
393 "--diff", head_id, "--filter", "error", "--json"
394 )
395 assert code == 0, out
396 d = json.loads(out.strip())
397 assert d["violations"] == []
398 assert d.get("severity_filter") == "error"
399 assert "diff_ref" in d
400
401
402 # ===========================================================================
403 # V --rules flag
404 # ===========================================================================
405
406
407 class TestRulesFlagV:
408 def test_V1_custom_rules_file_used(self, clean_repo: pathlib.Path) -> None:
409 """A custom rules file with zero rules produces zero violations and rules_checked=0.
410
411 An explicit empty file must NOT fall back to built-in defaults.
412 """
413 rules = clean_repo / "rules.toml"
414 rules.write_text("# no rules\n")
415 code, out = _run(clean_repo, "code", "code-check", "--rules", "rules.toml", "--json")
416 assert code == 0, out
417 d = json.loads(out.strip())
418 # Zero rules β†’ zero violations regardless of file content
419 assert d["violations"] == []
420 assert d["rules_checked"] == 0
421
422 def test_V2_custom_rules_complexity_threshold_0(self, clean_repo: pathlib.Path) -> None:
423 """Threshold of 0 flags every function (complexity β‰₯ 1 > 0)."""
424 rules = clean_repo / "tight.toml"
425 rules.write_text(
426 '[[rule]]\nname = "strict_complexity"\nseverity = "error"\n'
427 'rule_type = "max_complexity"\n[rule.params]\nthreshold = 0\n'
428 )
429 _, out = _run(clean_repo, "code", "code-check", "--rules", "tight.toml", "--json")
430 d = json.loads(out.strip())
431 assert len(d["violations"]) > 0
432
433 def test_V3_missing_rules_file_exits_1(self, clean_repo: pathlib.Path) -> None:
434 code, out = _run_unchecked(
435 clean_repo, "code", "code-check", "--rules", "nonexistent.toml"
436 )
437 assert code == 1, out
438
439 def test_V4_path_traversal_in_rules_exits_1(self, clean_repo: pathlib.Path) -> None:
440 code, out = _run_unchecked(
441 clean_repo, "code", "code-check", "--rules", "../../etc/passwd"
442 )
443 assert code == 1, out
444 assert "❌" in out or "traversal" in out.lower() or "escape" in out.lower()
445
446 def test_V5_rules_zero_rules_rules_checked_is_0(self, clean_repo: pathlib.Path) -> None:
447 rules = clean_repo / "empty.toml"
448 rules.write_text("")
449 _, out = _run(clean_repo, "code", "code-check", "--rules", "empty.toml", "--json")
450 d = json.loads(out.strip())
451 assert d["rules_checked"] == 0
452
453
454 # ===========================================================================
455 # VI Security
456 # ===========================================================================
457
458
459 class TestSecurityVI:
460 def test_VI1_absolute_path_outside_repo_rejected(self, clean_repo: pathlib.Path) -> None:
461 """An absolute path to /etc/hosts must be rejected by contain_path."""
462 code, out = _run_unchecked(
463 clean_repo, "code", "code-check", "--rules", "/etc/hosts"
464 )
465 assert code == 1, out
466 assert "❌" in out or "traversal" in out.lower() or "escape" in out.lower()
467
468 def test_VI2_dotdot_traversal_rejected(self, clean_repo: pathlib.Path) -> None:
469 code, out = _run_unchecked(
470 clean_repo, "code", "code-check", "--rules", "../../../etc/passwd"
471 )
472 assert code == 1, out
473
474 def test_VI3_null_byte_in_rules_path_rejected(self, clean_repo: pathlib.Path) -> None:
475 """Null bytes in file paths must not silently succeed."""
476 code, _ = _run_unchecked(
477 clean_repo, "code", "code-check", "--rules", "rules\x00.toml"
478 )
479 assert code == 1
480
481 def test_VI4_error_output_goes_to_stderr(self, clean_repo: pathlib.Path) -> None:
482 """Path traversal error must appear on stderr, not stdout."""
483 result = runner.invoke(
484 cli,
485 ["code", "code-check", "--rules", "../../etc/passwd"],
486 env=_env(clean_repo),
487 )
488 assert result.exit_code == 1
489 assert "❌" in result.stderr or "traversal" in result.stderr.lower()
490
491 def test_VI5_repo_without_commits_does_not_panic(
492 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
493 ) -> None:
494 monkeypatch.chdir(tmp_path)
495 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
496 assert r.exit_code == 0, r.output
497 result = runner.invoke(cli, ["code", "code-check"], env=_env(tmp_path))
498 assert result.exit_code == 1
499 # Must not raise an unhandled exception
500 assert result.exception is None
501
502
503 # ===========================================================================
504 # VII Edge cases
505 # ===========================================================================
506
507
508 class TestEdgeCasesVII:
509 def test_VII1_single_file_dead_export_warning(self, clean_repo: pathlib.Path) -> None:
510 """greet in clean.py is never imported β€” expect dead_exports warning."""
511 _, out = _run(clean_repo, "code", "code-check", "--json")
512 d = json.loads(out.strip())
513 # dead_exports may or may not fire depending on how the rule counts imports;
514 # the important invariant is that we get a valid report.
515 assert isinstance(d["violations"], list)
516
517 def test_VII2_text_and_json_violation_counts_agree(
518 self, violation_repo: pathlib.Path
519 ) -> None:
520 code_t, text_out = _run(violation_repo, "code", "code-check")
521 code_j, json_out = _run(violation_repo, "code", "code-check", "--json")
522 assert code_t == 0
523 assert code_j == 0
524 d = json.loads(json_out.strip())
525 count = len(d["violations"])
526 # Text output summary line contains the violation count
527 assert f"{count} violation" in text_out
528
529 def test_VII3_cycle_repo_no_cycles_rule_fires(self, cycle_repo: pathlib.Path) -> None:
530 _, out = _run(cycle_repo, "code", "code-check", "--json")
531 d = json.loads(out.strip())
532 cycle_violations = [v for v in d["violations"] if v["rule_name"] == "no_cycles"]
533 assert len(cycle_violations) > 0
534
535 def test_VII4_commit_id_in_json_is_full_sha(self, clean_repo: pathlib.Path) -> None:
536 _, out = _run(clean_repo, "code", "code-check", "--json")
537 d = json.loads(out.strip())
538 assert len(d["commit_id"]) >= 40
539
540 def test_VII5_filter_and_json_compose_cleanly(self, cycle_repo: pathlib.Path) -> None:
541 """--filter warning + --json should produce valid JSON with only warnings."""
542 code, out = _run(cycle_repo, "code", "code-check", "--filter", "warning", "--json")
543 assert code == 0, out
544 d = json.loads(out.strip())
545 for v in d["violations"]:
546 assert v["severity"] == "warning"
547
548 def test_VII6_rules_checked_matches_builtin_default_count(
549 self, clean_repo: pathlib.Path
550 ) -> None:
551 _, out = _run(clean_repo, "code", "code-check", "--json")
552 d = json.loads(out.strip())
553 # Built-in defaults have 3 rules
554 assert d["rules_checked"] == 3
555
556 def test_VII7_second_run_produces_identical_output(
557 self, clean_repo: pathlib.Path
558 ) -> None:
559 """Output is deterministic: two back-to-back runs must be identical."""
560 _, out1 = _run(clean_repo, "code", "code-check", "--json")
561 _, out2 = _run(clean_repo, "code", "code-check", "--json")
562 d1 = json.loads(out1.strip())
563 d2 = json.loads(out2.strip())
564 assert d1["violations"] == d2["violations"]
565 assert d1["rules_checked"] == d2["rules_checked"]
566
567
568 # ===========================================================================
569 # VIII Stress
570 # ===========================================================================
571
572
573 class TestStressVIII:
574 def test_VIII1_100_complex_files_all_violations_reported(
575 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
576 ) -> None:
577 """100 files each containing a high-complexity function β€” all should fire."""
578 monkeypatch.chdir(tmp_path)
579 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
580 assert r.exit_code == 0, r.output
581
582 for i in range(100):
583 # Each file has a uniquely-named complex function
584 src = f"def heavy_{i}(x: int) -> int:\n"
585 src += " if x == 0:\n return 0\n"
586 for j in range(1, 13):
587 src += f" elif x == {j}:\n return {j}\n"
588 src += " return -1\n"
589 (tmp_path / f"mod_{i:03d}.py").write_text(src)
590
591 _stage_commit(tmp_path, "100 complex files")
592
593 code, out = _run(tmp_path, "code", "code-check", "--json")
594 assert code == 0, out
595 d = json.loads(out.strip())
596 complexity_violations = [
597 v for v in d["violations"] if v["rule_name"] == "complexity_gate"
598 ]
599 # Every file should have at least one complexity violation
600 assert len(complexity_violations) >= 100
601
602 def test_VIII2_large_custom_rule_set(
603 self, clean_repo: pathlib.Path
604 ) -> None:
605 """A TOML file with 50 identical rules β€” all 50 should be evaluated."""
606 rules_toml = ""
607 for i in range(50):
608 rules_toml += (
609 f'[[rule]]\nname = "complexity_{i}"\nseverity = "warning"\n'
610 f'rule_type = "max_complexity"\n[rule.params]\nthreshold = 100\n\n'
611 )
612 (clean_repo / "big_rules.toml").write_text(rules_toml)
613
614 code, out = _run(
615 clean_repo, "code", "code-check", "--rules", "big_rules.toml", "--json"
616 )
617 assert code == 0, out
618 d = json.loads(out.strip())
619 assert d["rules_checked"] == 50
620
621 def test_VIII3_diff_on_100_file_repo(
622 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
623 ) -> None:
624 """--diff on a 100-file repo must complete and return valid JSON."""
625 monkeypatch.chdir(tmp_path)
626 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
627 assert r.exit_code == 0, r.output
628
629 # Commit 1: 100 clean files
630 for i in range(100):
631 (tmp_path / f"mod_{i:03d}.py").write_text(f"def fn_{i}() -> int:\n return {i}\n")
632 _stage_commit(tmp_path, "100 clean files")
633
634 # Save first commit ID
635 code, jout = _run(tmp_path, "code", "code-check", "--json")
636 assert code == 0
637 base_commit = json.loads(jout.strip())["commit_id"]
638
639 # Commit 2: add one complex file
640 (tmp_path / "complex_new.py").write_text(_complex_func(15))
641 _stage_commit(tmp_path, "add complex file")
642
643 code, out = _run(
644 tmp_path, "code", "code-check", "--diff", base_commit, "--json"
645 )
646 assert code == 0, out
647 d = json.loads(out.strip())
648 # Should show new violations only from complex_new.py
649 addresses = [v["address"] for v in d["violations"]]
650 assert any("complex_new" in a for a in addresses)
651 # Should NOT include violations from other files (they existed in base)
652 non_new = [a for a in addresses if "complex_new" not in a]
653 assert non_new == [], f"unexpected non-new violations: {non_new}"
654
655 def test_VIII4_stress_diff_vs_self_always_zero(
656 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
657 ) -> None:
658 """50 iterations of diff-vs-self β€” always 0 new violations."""
659 monkeypatch.chdir(tmp_path)
660 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
661 assert r.exit_code == 0, r.output
662 for i in range(20):
663 (tmp_path / f"mod_{i}.py").write_text(_complex_func(12))
664 _stage_commit(tmp_path, "bulk commit")
665
666 _, jout = _run(tmp_path, "code", "code-check", "--json")
667 head = json.loads(jout.strip())["commit_id"]
668
669 for _ in range(50):
670 _, out = _run(tmp_path, "code", "code-check", "--diff", head, "--json")
671 d = json.loads(out.strip())
672 assert d["violations"] == [], "diff vs self must always be empty"
673
674 def test_VIII5_filter_stress_all_severities_preserved(
675 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
676 ) -> None:
677 """Filter by each severity and union equals total violations."""
678 monkeypatch.chdir(tmp_path)
679 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
680 assert r.exit_code == 0, r.output
681 (tmp_path / "alpha.py").write_text("import beta\n\ndef a() -> None: pass\n")
682 (tmp_path / "beta.py").write_text("import alpha\n\ndef b() -> None: pass\n")
683 _stage_commit(tmp_path, "cycle + warnings")
684
685 _, total_out = _run(tmp_path, "code", "code-check", "--json")
686 total = json.loads(total_out.strip())
687 total_count = len(total["violations"])
688
689 union: list[Mapping[str, str]] = []
690 for sev in ("error", "warning", "info"):
691 _, sout = _run(tmp_path, "code", "code-check", "--filter", sev, "--json")
692 d = json.loads(sout.strip())
693 for v in d["violations"]:
694 assert v["severity"] == sev
695 union.extend(d["violations"])
696
697 assert len(union) == total_count, (
698 f"union of filtered severities ({len(union)}) != total ({total_count})"
699 )
700
701
702 # ---------------------------------------------------------------------------
703 # TestRegisterFlags
704 # ---------------------------------------------------------------------------
705
706
707 class TestRegisterFlags:
708 """register() wires --json / -j correctly."""
709
710 def _parse(self, *args: str) -> argparse.Namespace:
711 from muse.cli.commands.code_check import register
712 p = argparse.ArgumentParser()
713 sub = p.add_subparsers()
714 register(sub)
715 return p.parse_args(["code-check", *args])
716
717 def test_default_json_out_is_false(self) -> None:
718 ns = self._parse()
719 assert ns.json_out is False
720
721 def test_json_flag_sets_json_out(self) -> None:
722 ns = self._parse("--json")
723 assert ns.json_out is True
724
725 def test_j_shorthand_sets_json_out(self) -> None:
726 ns = self._parse("-j")
727 assert ns.json_out is True