[Concept,16/37] u_boot_pylib: Catch API errors from the Claude Agent SDK

Message ID 20260404213020.372253-17-sjg@u-boot.org
State New
Headers
Series patman: Autolink fixes and AI-assisted patch review |

Commit Message

Simon Glass April 4, 2026, 9:28 p.m. UTC
  From: Simon Glass <sjg@chromium.org>

The Claude Agent SDK raises a bare Exception on API errors (e.g. 500
Internal Server Error), but run_agent_collect() only catches
RuntimeError, ValueError and OSError. This causes the entire review to
crash when a single agent call fails, even though the review loop
already handles the failure gracefully with a placeholder message.

Catch bare Exceptions whose message indicates an API or process error
(containing 'API Error' or 'exit code') while re-raising unexpected
exceptions that indicate real bugs.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

 tools/u_boot_pylib/claude.py      |  5 +++++
 tools/u_boot_pylib/test_claude.py | 31 +++++++++++++++++++++++++++++++
 2 files changed, 36 insertions(+)
  

Patch

diff --git a/tools/u_boot_pylib/claude.py b/tools/u_boot_pylib/claude.py
index 29dff1d1d4b..6ba980b6b2e 100644
--- a/tools/u_boot_pylib/claude.py
+++ b/tools/u_boot_pylib/claude.py
@@ -62,3 +62,8 @@  async def run_agent_collect(prompt, options):
     except (RuntimeError, ValueError, OSError) as exc:
         tout.error(f'Agent failed: {exc}')
         return False, '\n\n'.join(conversation_log)
+    except Exception as exc:
+        if 'API Error' in str(exc) or 'exit code' in str(exc):
+            tout.error(f'Agent failed: {exc}')
+            return False, '\n\n'.join(conversation_log)
+        raise
diff --git a/tools/u_boot_pylib/test_claude.py b/tools/u_boot_pylib/test_claude.py
index c564dddb70e..2a666e6c396 100644
--- a/tools/u_boot_pylib/test_claude.py
+++ b/tools/u_boot_pylib/test_claude.py
@@ -83,6 +83,37 @@  class TestClaude(unittest.TestCase):
 
         self.assertFalse(success)
 
+    def test_run_agent_collect_handles_api_error(self):
+        """run_agent_collect() catches SDK API errors"""
+        # pylint: disable=W0613,W0719
+        async def mock_query(**kwargs):
+            raise Exception(
+                'Command failed with exit code 1 (exit code: 1)')
+            yield  # pylint: disable=W0101
+
+        self._setup_claude_with_mock_query(mock_query)
+        loop = asyncio.new_event_loop()
+        with terminal.capture():
+            success, _ = loop.run_until_complete(
+                claude.run_agent_collect('test prompt', MagicMock()))
+        loop.close()
+
+        self.assertFalse(success)
+
+    def test_run_agent_collect_reraises_unknown(self):
+        """run_agent_collect() re-raises unexpected exceptions"""
+        # pylint: disable=W0613
+        async def mock_query(**kwargs):
+            raise TypeError('unexpected bug')
+            yield  # pylint: disable=W0101
+
+        self._setup_claude_with_mock_query(mock_query)
+        loop = asyncio.new_event_loop()
+        with self.assertRaises(TypeError):
+            loop.run_until_complete(
+                claude.run_agent_collect('test prompt', MagicMock()))
+        loop.close()
+
     def test_run_agent_collect_skips_non_text_blocks(self):
         """run_agent_collect() ignores blocks without text attribute"""
         text_block = MagicMock()