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