Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions claudecode/github_action_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,19 @@ def main():
print(json.dumps({'error': f'Security audit failed: {error_msg}'}))
sys.exit(EXIT_GENERAL_ERROR)

# When Claude hits token limits or errors mid-run, it can return success with no actual review.
# Fail the step and surface it so CI/logs show the problem instead of silently passing (see issue #63).
analysis_summary = results.get('analysis_summary', {})
if not analysis_summary.get('review_completed', True) and analysis_summary.get('files_reviewed', 0) == 0:
msg = (
"Security review did not complete (e.g. token limit or truncated output). "
"No files were reviewed. Failing the step so the run is visible in CI."
)
logger.warning(msg)
print(msg, file=sys.stderr)
print(json.dumps({'error': msg, 'analysis_summary': analysis_summary}))
sys.exit(EXIT_GENERAL_ERROR)

# Filter findings to reduce false positives
original_findings = results.get('findings', [])

Expand Down
50 changes: 50 additions & 0 deletions claudecode/test_main_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,3 +556,53 @@ def test_audit_failure(self, mock_client_class, mock_runner_class,
output = json.loads(captured.out)
assert 'Security audit failed' in output['error']
assert 'Claude execution failed' in output['error']

@patch('pathlib.Path.cwd')
@patch('claudecode.github_action_audit.get_security_audit_prompt')
@patch('claudecode.github_action_audit.FindingsFilter')
@patch('claudecode.github_action_audit.SimpleClaudeRunner')
@patch('claudecode.github_action_audit.GitHubActionClient')
def test_incomplete_review_fails_step(self, mock_client_class, mock_runner_class,
mock_filter_class, mock_prompt_func,
mock_cwd, capsys):
"""When review did not complete (e.g. token limit), fail the step so CI shows it (issue #63)."""
mock_client = Mock()
mock_client.get_pr_data.return_value = {'number': 123, 'title': 'Test', 'body': ''}
mock_client.get_pr_diff.return_value = "diff"
mock_client._is_excluded.return_value = False
mock_client_class.return_value = mock_client

mock_runner = Mock()
mock_runner.validate_claude_available.return_value = (True, "")
mock_runner.run_security_audit.return_value = (
True,
"",
{
'findings': [],
'analysis_summary': {
'files_reviewed': 0,
'high_severity': 0,
'medium_severity': 0,
'low_severity': 0,
'review_completed': False,
}
}
)
mock_runner_class.return_value = mock_runner
mock_filter_class.return_value = Mock()
mock_prompt_func.return_value = "prompt"
mock_cwd.return_value = Path('/tmp')

with patch.dict(os.environ, {
'GITHUB_REPOSITORY': 'owner/repo',
'PR_NUMBER': '123',
'GITHUB_TOKEN': 'test-token'
}):
with pytest.raises(SystemExit) as exc_info:
main()

assert exc_info.value.code == 1
captured = capsys.readouterr()
output = json.loads(captured.out)
assert 'error' in output
assert 'did not complete' in output['error']