Skip to content

Commit 66fdece

Browse files
committed
Remove remaining single-pass compatibility paths
1 parent df89a93 commit 66fdece

File tree

5 files changed

+81
-143
lines changed

5 files changed

+81
-143
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Nutrient Code Reviewer
22

3-
An AI-powered code review GitHub Action using Claude to analyze code changes. Uses a unified multi-agent approach for both code quality (correctness, reliability, performance, maintainability, testing) and security in a single pass. This action provides intelligent, context-aware review for pull requests using Anthropic's Claude Code tool for deep semantic analysis.
3+
An AI-powered code review GitHub Action using Claude to analyze code changes. Uses a unified multi-agent, multi-phase approach for both code quality (correctness, reliability, performance, maintainability, testing) and security. This action provides intelligent, context-aware review for pull requests using Anthropic's Claude Code tool for deep semantic analysis.
44

55
Based on the original work from [anthropics/claude-code-security-review](https://github.com/anthropics/claude-code-security-review).
66

claudecode/review_orchestrator.py

Lines changed: 7 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,11 @@ def __init__(
6363
self.max_diff_lines = max(0, max_diff_lines)
6464

6565
def _run_phase(self, repo_dir: Path, prompt: str, model: str, phase_name: str) -> Tuple[bool, Dict[str, Any], str]:
66-
raw_result = None
67-
supports_run_prompt = callable(getattr(type(self.claude_runner), "run_prompt", None))
68-
if supports_run_prompt:
69-
raw_result = self.claude_runner.run_prompt(repo_dir, prompt, model=model)
70-
elif hasattr(self.claude_runner, "run_code_review"):
71-
try:
72-
raw_result = self.claude_runner.run_code_review(repo_dir, prompt, model=model)
73-
except TypeError:
74-
# Backward compatibility for legacy mocks/runners that don't accept model parameter.
75-
raw_result = self.claude_runner.run_code_review(repo_dir, prompt)
66+
run_prompt = getattr(self.claude_runner, "run_prompt", None)
67+
if not callable(run_prompt):
68+
return False, {}, f"Runner missing run_prompt for {phase_name}"
69+
70+
raw_result = run_prompt(repo_dir, prompt, model=model)
7671
if not (isinstance(raw_result, tuple) and len(raw_result) == 3):
7772
return False, {}, f"Invalid runner response for {phase_name}"
7873

@@ -128,44 +123,8 @@ def run(
128123
if not ok:
129124
return False, {}, f"Triage phase failed: {err}"
130125

131-
# Backward-compatible fast path for legacy single-pass outputs.
132-
if isinstance(triage_result, dict) and "findings" in triage_result:
133-
original_count = len([f for f in triage_result.get("findings", []) if isinstance(f, dict)])
134-
legacy_findings = []
135-
for finding in triage_result.get("findings", []):
136-
if isinstance(finding, dict) and not self._is_excluded(finding.get("file", "")):
137-
legacy_findings.append(self._ensure_review_type(finding))
138-
pr_context = {
139-
"repo_name": pr_data.get("head", {}).get("repo", {}).get("full_name", "unknown"),
140-
"pr_number": pr_data.get("number"),
141-
"title": pr_data.get("title", ""),
142-
"description": pr_data.get("body", ""),
143-
}
144-
filter_response = self.findings_filter.filter_findings(legacy_findings, pr_context)
145-
if isinstance(filter_response, tuple) and len(filter_response) == 3:
146-
filter_success, filter_results, _ = filter_response
147-
if filter_success and isinstance(filter_results, dict):
148-
legacy_findings = filter_results.get("filtered_findings", legacy_findings)
149-
legacy_findings = [self._ensure_review_type(f) for f in legacy_findings if isinstance(f, dict)]
150-
high = len([f for f in legacy_findings if str(f.get("severity", "")).upper() == "HIGH"])
151-
medium = len([f for f in legacy_findings if str(f.get("severity", "")).upper() == "MEDIUM"])
152-
low = len([f for f in legacy_findings if str(f.get("severity", "")).upper() == "LOW"])
153-
return True, {
154-
"findings": legacy_findings,
155-
"analysis_summary": {
156-
"files_reviewed": triage_result.get("analysis_summary", {}).get("files_reviewed", pr_data.get("changed_files", 0)),
157-
"high_severity": high,
158-
"medium_severity": medium,
159-
"low_severity": low,
160-
"review_completed": True,
161-
},
162-
"filtering_summary": {
163-
"total_original_findings": original_count,
164-
"excluded_findings": max(0, original_count - len(legacy_findings)),
165-
"kept_findings": len(legacy_findings),
166-
},
167-
"triage": {"skip_review": False, "reason": "legacy_single_pass", "risk_level": "medium"},
168-
}, ""
126+
if not isinstance(triage_result, dict) or "skip_review" not in triage_result:
127+
return False, {}, "Triage phase returned invalid schema"
169128

170129
if triage_result.get("skip_review") is True:
171130
logger.info("Skipping review based on triage decision: %s", triage_result.get("reason", ""))

claudecode/test_main_function.py

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,28 @@
1212
from claudecode.github_action_audit import main
1313

1414

15+
def _mock_multiphase_success(mock_runner, findings=None):
16+
"""Configure runner mock for strict multi-phase orchestration."""
17+
findings = findings or []
18+
mock_runner.run_prompt.side_effect = [
19+
(True, "", {"skip_review": False, "reason": "", "risk_level": "medium"}), # triage
20+
(True, "", {"claude_md_files": [], "change_summary": "", "hotspots": [], "priority_files": []}), # context
21+
(True, "", {"findings": findings}), # compliance
22+
(True, "", {"findings": []}), # quality
23+
(True, "", {"findings": []}), # security
24+
(
25+
True,
26+
"",
27+
{
28+
"validated_findings": [
29+
{"finding_index": idx, "keep": True, "confidence": 0.95, "reason": "valid"}
30+
for idx in range(len(findings))
31+
]
32+
},
33+
), # validation
34+
]
35+
36+
1537
class TestMainFunction:
1638
"""Test main function execution flow."""
1739

@@ -232,20 +254,7 @@ def test_main_successful_audit_no_findings(self, mock_client_class, mock_runner_
232254

233255
mock_runner = Mock()
234256
mock_runner.validate_claude_available.return_value = (True, "")
235-
# Unified review - single call
236-
mock_runner.run_code_review.return_value = (
237-
True,
238-
"",
239-
{
240-
'findings': [],
241-
'analysis_summary': {
242-
'files_reviewed': 5,
243-
'high_severity': 0,
244-
'medium_severity': 0,
245-
'low_severity': 0
246-
}
247-
}
248-
)
257+
_mock_multiphase_success(mock_runner, findings=[])
249258
mock_runner_class.return_value = mock_runner
250259

251260
mock_filter = Mock()
@@ -299,6 +308,7 @@ def test_main_successful_audit_with_findings(self, mock_client_class, mock_runne
299308
}
300309
mock_client.get_pr_diff.return_value = "diff content"
301310
mock_client._is_excluded.return_value = False # Don't exclude any files in tests
311+
mock_client.is_excluded.return_value = False
302312
mock_client_class.return_value = mock_client
303313

304314
findings = [
@@ -320,20 +330,7 @@ def test_main_successful_audit_with_findings(self, mock_client_class, mock_runne
320330

321331
mock_runner = Mock()
322332
mock_runner.validate_claude_available.return_value = (True, "")
323-
# Unified review - single call with all findings
324-
mock_runner.run_code_review.return_value = (
325-
True,
326-
"",
327-
{
328-
'findings': findings,
329-
'analysis_summary': {
330-
'files_reviewed': 2,
331-
'high_severity': 1,
332-
'medium_severity': 1,
333-
'low_severity': 0
334-
}
335-
}
336-
)
333+
_mock_multiphase_success(mock_runner, findings=findings)
337334
mock_runner_class.return_value = mock_runner
338335

339336
# Filter keeps only high severity
@@ -391,13 +388,14 @@ def test_main_with_full_filter(self, mock_client_class, mock_runner_class,
391388
}
392389
mock_client.get_pr_diff.return_value = "diff content"
393390
mock_client._is_excluded.return_value = False # Don't exclude any files in tests
391+
mock_client.is_excluded.return_value = False
394392
mock_client_class.return_value = mock_client
395393

396394
findings = [{'file': 'test.py', 'line': 10, 'severity': 'HIGH', 'description': 'Issue'}]
397395

398396
mock_runner = Mock()
399397
mock_runner.validate_claude_available.return_value = (True, "")
400-
mock_runner.run_code_review.return_value = (True, "", {'findings': findings})
398+
_mock_multiphase_success(mock_runner, findings=findings)
401399
mock_runner_class.return_value = mock_runner
402400

403401
mock_prompt_func.return_value = "prompt"
@@ -450,6 +448,7 @@ def test_main_filter_failure_keeps_all_findings(self, mock_client_class, mock_ru
450448
mock_client.get_pr_data.return_value = {'number': 123, 'title': 'Test', 'body': ''}
451449
mock_client.get_pr_diff.return_value = "diff"
452450
mock_client._is_excluded.return_value = False # Don't exclude any files in tests
451+
mock_client.is_excluded.return_value = False
453452
mock_client_class.return_value = mock_client
454453

455454
findings = [
@@ -459,7 +458,7 @@ def test_main_filter_failure_keeps_all_findings(self, mock_client_class, mock_ru
459458

460459
mock_runner = Mock()
461460
mock_runner.validate_claude_available.return_value = (True, "")
462-
mock_runner.run_code_review.return_value = (True, "", {'findings': findings})
461+
_mock_multiphase_success(mock_runner, findings=findings)
463462
mock_runner_class.return_value = mock_runner
464463

465464
mock_prompt_func.return_value = "prompt"
@@ -539,11 +538,7 @@ def test_audit_failure(self, mock_client_class, mock_runner_class,
539538

540539
mock_runner = Mock()
541540
mock_runner.validate_claude_available.return_value = (True, "")
542-
mock_runner.run_code_review.return_value = (
543-
False,
544-
"Claude execution failed",
545-
{}
546-
)
541+
mock_runner.run_prompt.return_value = (False, "Claude execution failed", {})
547542
mock_runner_class.return_value = mock_runner
548543

549544
mock_filter_class.return_value = Mock()
@@ -582,6 +577,7 @@ def test_findings_have_correct_review_type(self, mock_client_class, mock_runner_
582577
mock_client.get_pr_data.return_value = {'number': 123, 'title': 'Test', 'body': ''}
583578
mock_client.get_pr_diff.return_value = "diff"
584579
mock_client._is_excluded.return_value = False
580+
mock_client.is_excluded.return_value = False
585581
mock_client_class.return_value = mock_client
586582

587583
# Mixed findings - security and non-security
@@ -592,7 +588,7 @@ def test_findings_have_correct_review_type(self, mock_client_class, mock_runner_
592588

593589
mock_runner = Mock()
594590
mock_runner.validate_claude_available.return_value = (True, "")
595-
mock_runner.run_code_review.return_value = (True, "", {'findings': findings})
591+
_mock_multiphase_success(mock_runner, findings=findings)
596592
mock_runner_class.return_value = mock_runner
597593

598594
# Filter passes through all findings
@@ -644,6 +640,7 @@ def test_review_type_not_overwritten_if_already_set(self, mock_client_class, moc
644640
mock_client.get_pr_data.return_value = {'number': 123, 'title': 'Test', 'body': ''}
645641
mock_client.get_pr_diff.return_value = "diff"
646642
mock_client._is_excluded.return_value = False
643+
mock_client.is_excluded.return_value = False
647644
mock_client_class.return_value = mock_client
648645

649646
# Finding already has review_type set (e.g., from a custom analyzer)
@@ -654,7 +651,7 @@ def test_review_type_not_overwritten_if_already_set(self, mock_client_class, moc
654651

655652
mock_runner = Mock()
656653
mock_runner.validate_claude_available.return_value = (True, "")
657-
mock_runner.run_code_review.return_value = (True, "", {'findings': findings})
654+
_mock_multiphase_success(mock_runner, findings=findings)
658655
mock_runner_class.return_value = mock_runner
659656

660657
mock_filter = Mock()

claudecode/test_review_orchestrator.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,16 @@ def test_review_model_config_from_env_with_fallbacks():
2020
assert cfg.validation == "global-model"
2121

2222

23-
def test_orchestrator_legacy_single_pass_path():
23+
def test_orchestrator_invalid_triage_schema_fails():
2424
runner = Mock()
25-
runner.run_code_review.return_value = (
25+
runner.run_prompt.return_value = (
2626
True,
2727
"",
28-
{
29-
"findings": [
30-
{"file": "a.py", "line": 3, "severity": "HIGH", "category": "security", "title": "Issue"}
31-
],
32-
"analysis_summary": {"files_reviewed": 1},
33-
},
28+
{"findings": [{"file": "a.py", "line": 3, "severity": "HIGH", "category": "security", "title": "Issue"}]},
3429
)
3530
findings_filter = Mock()
36-
findings_filter.filter_findings.return_value = (
37-
True,
38-
{"filtered_findings": runner.run_code_review.return_value[2]["findings"]},
39-
Mock(),
40-
)
4131
github_client = Mock()
42-
github_client._is_excluded.return_value = False
32+
github_client.is_excluded.return_value = False
4333
cfg = ReviewModelConfig("t", "c", "q", "s", "v")
4434

4535
orchestrator = ReviewOrchestrator(runner, findings_filter, github_client, cfg, 100)
@@ -49,7 +39,6 @@ def test_orchestrator_legacy_single_pass_path():
4939
pr_diff="diff --git a/a.py b/a.py",
5040
)
5141

52-
assert ok is True
53-
assert err == ""
54-
assert len(result["findings"]) == 1
55-
assert result["findings"][0]["review_type"] == "security"
42+
assert ok is False
43+
assert result == {}
44+
assert "invalid schema" in err.lower()

0 commit comments

Comments
 (0)