diff --git a/openevolve/llm/claude_code.py b/openevolve/llm/claude_code.py index 5bd3c9fb3..c42ae8dcf 100644 --- a/openevolve/llm/claude_code.py +++ b/openevolve/llm/claude_code.py @@ -125,8 +125,9 @@ def _run_cli(self, cmd: list, timeout: int, prompt: Optional[str] = None) -> str ) if result.returncode != 0: stderr = result.stderr.strip() - if stderr: - logger.warning(f"Claude CLI stderr: {stderr[:500]}") + raise RuntimeError( + f"Claude CLI exited with code {result.returncode}. stderr: {stderr[:500]}" + ) output = result.stdout.strip() if not output: raise RuntimeError(f"Empty response from Claude CLI. stderr: {result.stderr[:500]}") diff --git a/tests/test_claude_code_llm.py b/tests/test_claude_code_llm.py index d803aa4a6..7c5829726 100644 --- a/tests/test_claude_code_llm.py +++ b/tests/test_claude_code_llm.py @@ -1,6 +1,7 @@ """Tests for the Claude Code CLI LLM backend.""" import asyncio +import sys import unittest from unittest.mock import MagicMock, patch @@ -81,6 +82,38 @@ def test_empty_response_raises(self, mock_run): with self.assertRaises(RuntimeError): asyncio.run(llm.generate("test prompt")) + def test_nonzero_exit_with_stdout_raises(self): + llm = ClaudeCodeLLM(self._make_cfg()) + command = [ + sys.executable, + "-c", + "import sys; print('partial output'); " + "print('provider failed', file=sys.stderr); sys.exit(7)", + ] + with self.assertRaisesRegex(RuntimeError, "exited with code 7.*provider failed"): + llm._run_cli(command, timeout=5) + + @patch("openevolve.llm.claude_code.subprocess.run") + def test_retry_after_failure_with_stdout(self, mock_run): + mock_run.side_effect = [ + MagicMock(returncode=7, stdout="partial output", stderr="transient error"), + MagicMock(returncode=0, stdout="complete response", stderr=""), + ] + llm = ClaudeCodeLLM(self._make_cfg(timeout=10, retries=1, retry_delay=0)) + result = asyncio.run(llm.generate("test prompt")) + self.assertEqual(result, "complete response") + self.assertEqual(mock_run.call_count, 2) + + @patch("openevolve.llm.claude_code.subprocess.run") + def test_nonzero_exit_with_stdout_exhausts_retries(self, mock_run): + mock_run.return_value = MagicMock( + returncode=7, stdout="partial output", stderr="persistent error" + ) + llm = ClaudeCodeLLM(self._make_cfg(timeout=10, retries=2, retry_delay=0)) + with self.assertRaisesRegex(RuntimeError, "exited with code 7"): + asyncio.run(llm.generate("test prompt")) + self.assertEqual(mock_run.call_count, 3) + @patch("openevolve.llm.claude_code.subprocess.run") def test_retry_on_failure(self, mock_run): mock_run.side_effect = [