From 824916f8639f1faf01ec0b24cdfec45d82543700 Mon Sep 17 00:00:00 2001 From: ashishpatel26 Date: Mon, 15 Jun 2026 10:23:01 +0530 Subject: [PATCH] fix: avoid 'success' as error text when errors list is empty (#1031) When the CLI emits is_error=True with an empty errors[] array and subtype='success', the SDK was producing the misleading message 'Claude Code returned an error result: success'. The fallback now skips 'success' as a subtype label and uses 'unknown error' instead. --- src/claude_agent_sdk/_internal/query.py | 8 ++- tests/test_query.py | 93 +++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/claude_agent_sdk/_internal/query.py b/src/claude_agent_sdk/_internal/query.py index 7a4f8a447..bc5001ff9 100644 --- a/src/claude_agent_sdk/_internal/query.py +++ b/src/claude_agent_sdk/_internal/query.py @@ -303,9 +303,13 @@ async def _read_messages(self) -> None: self._first_result_event.set() if message.get("is_error"): errors = message.get("errors") or [] - self._last_error_result_text = "; ".join(errors) or str( - message.get("subtype", "unknown error") + subtype = message.get("subtype", "") + fallback = ( + subtype + if subtype and subtype != "success" + else "unknown error" ) + self._last_error_result_text = "; ".join(errors) or fallback else: self._last_error_result_text = None elif not ( diff --git a/tests/test_query.py b/tests/test_query.py index 3ae65093d..9ee817aad 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -817,6 +817,99 @@ def test_buffered_messages_drain_after_close_trio(self): self._run_buffered_drain_after_close("trio") +class TestErrorResultText: + """Regression tests for #1031: misleading error text when errors[] is empty.""" + + def _run_query_with_result_message(self, result_message: dict) -> "Query": + """Run a Query against a transport that yields the given result message + and return the Query instance so callers can inspect _last_error_result_text.""" + + async def _run(): + mock_transport = _make_mock_transport( + messages=[ + { + "type": "result", + **result_message, + } + ] + ) + q = Query(transport=mock_transport, is_streaming_mode=True) + await q.start() + async for _ in q.receive_messages(): + pass + return q + + return anyio.run(_run) + + def test_empty_errors_and_success_subtype_gives_unknown_error(self): + """When is_error=True, errors=[], subtype='success', the error text + must not be 'success' — it should fall back to 'unknown error'.""" + q = self._run_query_with_result_message( + { + "subtype": "success", + "is_error": True, + "errors": [], + "duration_ms": 100, + "duration_api_ms": 80, + "num_turns": 1, + "session_id": "test", + "total_cost_usd": 0.001, + } + ) + assert q._last_error_result_text is not None + assert q._last_error_result_text != "success" + assert q._last_error_result_text == "unknown error" + + def test_empty_errors_with_meaningful_subtype_uses_subtype(self): + """When is_error=True, errors=[], subtype is not 'success', the subtype + should be used as the fallback error text.""" + q = self._run_query_with_result_message( + { + "subtype": "error_during_execution", + "is_error": True, + "errors": [], + "duration_ms": 100, + "duration_api_ms": 80, + "num_turns": 1, + "session_id": "test", + "total_cost_usd": 0.001, + } + ) + assert q._last_error_result_text == "error_during_execution" + + def test_non_empty_errors_list_uses_errors(self): + """When is_error=True and errors is non-empty, those errors are joined + and used as the error text regardless of subtype.""" + q = self._run_query_with_result_message( + { + "subtype": "success", + "is_error": True, + "errors": ["something went wrong", "details here"], + "duration_ms": 100, + "duration_api_ms": 80, + "num_turns": 1, + "session_id": "test", + "total_cost_usd": 0.001, + } + ) + assert q._last_error_result_text == "something went wrong; details here" + + def test_no_error_clears_last_error_result_text(self): + """When is_error=False (success result), _last_error_result_text is None.""" + q = self._run_query_with_result_message( + { + "subtype": "success", + "is_error": False, + "duration_ms": 100, + "duration_api_ms": 80, + "num_turns": 1, + "session_id": "test", + "total_cost_usd": 0.001, + } + ) + assert q._last_error_result_text is None + + class TestControlCancelRequest: """Tests for control_cancel_request handling (issue #739).