Skip to content

Commit 80f1dcf

Browse files
committed
Address remaining PR #21 review feedback
1 parent 1ac0c46 commit 80f1dcf

File tree

6 files changed

+119
-7
lines changed

6 files changed

+119
-7
lines changed

claudecode/github_action_audit.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ def _is_excluded(self, filepath: str) -> bool:
234234
return True
235235

236236
return False
237+
238+
def is_excluded(self, filepath: str) -> bool:
239+
"""Public wrapper for exclusion checks."""
240+
return self._is_excluded(filepath)
237241

238242
def _filter_generated_files(self, diff_text: str) -> str:
239243
"""Filter out generated files and excluded directories from diff content."""
@@ -329,7 +333,7 @@ def run_prompt(self, repo_dir: Path, prompt: str, model: Optional[str] = None) -
329333
if (isinstance(parsed_result, dict) and
330334
parsed_result.get('type') == 'result' and
331335
parsed_result.get('subtype') == 'error_during_execution' and
332-
attempt == 0):
336+
attempt < NUM_RETRIES - 1):
333337
continue
334338

335339
if isinstance(parsed_result, dict) and 'result' in parsed_result and isinstance(parsed_result['result'], str):

claudecode/prompts.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ def _build_hybrid_diff_section(pr_diff: str, max_lines: int) -> str:
1919
"""Build a bounded inline diff section while requiring tool-based context reads."""
2020
if not pr_diff:
2121
return "\nNo inline diff available. Use repository tools to inspect changed files.\n"
22+
if max_lines == 0:
23+
return (
24+
"\nInline diff intentionally omitted (max-diff-lines=0). "
25+
"Use repository tools to inspect changed files and context.\n"
26+
)
2227

2328
lines = pr_diff.splitlines()
2429
if max_lines > 0 and len(lines) > max_lines:

claudecode/review_orchestrator.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,10 @@ def __init__(
6464

6565
def _run_phase(self, repo_dir: Path, prompt: str, model: str, phase_name: str) -> Tuple[bool, Dict[str, Any], str]:
6666
raw_result = None
67-
if hasattr(self.claude_runner, "run_prompt"):
67+
supports_run_prompt = callable(getattr(type(self.claude_runner), "run_prompt", None))
68+
if supports_run_prompt:
6869
raw_result = self.claude_runner.run_prompt(repo_dir, prompt, model=model)
69-
if not (isinstance(raw_result, tuple) and len(raw_result) == 3) and hasattr(self.claude_runner, "run_code_review"):
70+
elif hasattr(self.claude_runner, "run_code_review"):
7071
try:
7172
raw_result = self.claude_runner.run_code_review(repo_dir, prompt, model=model)
7273
except TypeError:
@@ -87,6 +88,13 @@ def _run_phase(self, repo_dir: Path, prompt: str, model: str, phase_name: str) -
8788
return False, {}, f"Failed to parse {phase_name} output"
8889
return True, parsed, ""
8990

91+
def _is_excluded(self, filepath: str) -> bool:
92+
checker = getattr(self.github_client, "is_excluded", None)
93+
if callable(checker):
94+
return bool(checker(filepath))
95+
# Fallback for current client/tests that still expose private method.
96+
return bool(self.github_client._is_excluded(filepath))
97+
9098
def _collect_phase_findings(self, phase_result: Dict[str, Any], source_agent: str) -> List[Dict[str, Any]]:
9199
findings: List[Dict[str, Any]] = []
92100
for finding in phase_result.get("findings", []):
@@ -125,7 +133,7 @@ def run(
125133
original_count = len([f for f in triage_result.get("findings", []) if isinstance(f, dict)])
126134
legacy_findings = []
127135
for finding in triage_result.get("findings", []):
128-
if isinstance(finding, dict) and not self.github_client._is_excluded(finding.get("file", "")):
136+
if isinstance(finding, dict) and not self._is_excluded(finding.get("file", "")):
129137
legacy_findings.append(self._ensure_review_type(finding))
130138
pr_context = {
131139
"repo_name": pr_data.get("head", {}).get("repo", {}).get("full_name", "unknown"),
@@ -255,7 +263,7 @@ def run(
255263
"description": pr_data.get("body", ""),
256264
}
257265
kept_findings = validated
258-
original_count = len(validated)
266+
original_count = len(all_candidates)
259267
filter_response = self.findings_filter.filter_findings(validated, pr_context)
260268
if isinstance(filter_response, tuple) and len(filter_response) == 3:
261269
filter_success, filter_results, _ = filter_response
@@ -266,7 +274,7 @@ def run(
266274
for finding in kept_findings:
267275
if not isinstance(finding, dict):
268276
continue
269-
if self.github_client._is_excluded(finding.get("file", "")):
277+
if self._is_excluded(finding.get("file", "")):
270278
continue
271279
final_findings.append(self._ensure_review_type(finding))
272280

claudecode/test_findings_merge.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Unit tests for findings_merge utilities."""
2+
3+
from claudecode.findings_merge import merge_findings
4+
5+
6+
def test_merge_findings_empty_input():
7+
assert merge_findings([]) == []
8+
9+
10+
def test_merge_findings_keeps_higher_severity_then_confidence():
11+
findings = [
12+
{"file": "a.py", "line": 10, "category": "security", "title": "Issue", "severity": "MEDIUM", "confidence": 0.9},
13+
{"file": "a.py", "line": 10, "category": "security", "title": "Issue", "severity": "HIGH", "confidence": 0.8},
14+
{"file": "a.py", "line": 10, "category": "security", "title": "Issue", "severity": "HIGH", "confidence": 0.95},
15+
]
16+
17+
merged = merge_findings(findings)
18+
assert len(merged) == 1
19+
assert merged[0]["severity"] == "HIGH"
20+
assert merged[0]["confidence"] == 0.95
21+
22+
23+
def test_merge_findings_separate_keys_are_preserved():
24+
findings = [
25+
{"file": "a.py", "line": 1, "category": "correctness", "title": "One", "severity": "LOW"},
26+
{"file": "a.py", "line": 2, "category": "correctness", "title": "Two", "severity": "LOW"},
27+
{"file": "b.py", "line": 1, "category": "security", "title": "One", "severity": "MEDIUM"},
28+
]
29+
30+
merged = merge_findings(findings)
31+
assert len(merged) == 3
32+

claudecode/test_prompts.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,12 @@ def test_get_unified_review_prompt_includes_summary_guidelines(self):
192192

193193
assert "PR SUMMARY GUIDELINES:" in prompt
194194
assert "2-4 sentences" in prompt
195-
assert "~10 words" in prompt
195+
assert "~10 words" in prompt
196+
197+
def test_build_hybrid_diff_section_max_lines_zero_omits_inline_diff(self):
198+
from claudecode.prompts import _build_hybrid_diff_section
199+
200+
section = _build_hybrid_diff_section("diff --git a/a.py b/a.py\n+print('x')", 0)
201+
202+
assert "intentionally omitted" in section
203+
assert "```diff" not in section
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Unit tests for review_orchestrator."""
2+
3+
from pathlib import Path
4+
from unittest.mock import Mock
5+
6+
from claudecode.review_orchestrator import ReviewModelConfig, ReviewOrchestrator
7+
8+
9+
def test_review_model_config_from_env_with_fallbacks():
10+
env = {
11+
"CLAUDE_MODEL": "global-model",
12+
"MODEL_TRIAGE": "triage-model",
13+
"MODEL_SECURITY": "security-model",
14+
}
15+
cfg = ReviewModelConfig.from_env(env, "default-model")
16+
assert cfg.triage == "triage-model"
17+
assert cfg.compliance == "global-model"
18+
assert cfg.quality == "global-model"
19+
assert cfg.security == "security-model"
20+
assert cfg.validation == "global-model"
21+
22+
23+
def test_orchestrator_legacy_single_pass_path():
24+
runner = Mock()
25+
runner.run_code_review.return_value = (
26+
True,
27+
"",
28+
{
29+
"findings": [
30+
{"file": "a.py", "line": 3, "severity": "HIGH", "category": "security", "title": "Issue"}
31+
],
32+
"analysis_summary": {"files_reviewed": 1},
33+
},
34+
)
35+
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+
)
41+
github_client = Mock()
42+
github_client._is_excluded.return_value = False
43+
cfg = ReviewModelConfig("t", "c", "q", "s", "v")
44+
45+
orchestrator = ReviewOrchestrator(runner, findings_filter, github_client, cfg, 100)
46+
ok, result, err = orchestrator.run(
47+
repo_dir=Path("."),
48+
pr_data={"number": 1, "changed_files": 1, "head": {"repo": {"full_name": "x/y"}}},
49+
pr_diff="diff --git a/a.py b/a.py",
50+
)
51+
52+
assert ok is True
53+
assert err == ""
54+
assert len(result["findings"]) == 1
55+
assert result["findings"][0]["review_type"] == "security"

0 commit comments

Comments
 (0)