From 39b8d1d313841acfe83ab9178ea57280ea766fdf Mon Sep 17 00:00:00 2001 From: AhmetArif0 <147827411+AhmetArif0@users.noreply.github.com> Date: Sat, 23 May 2026 12:54:21 +0300 Subject: [PATCH] fix(dingtalk): finalize open streaming cards before disconnect AI Card "tool progress" cards created with finalize=False were left in streaming state on DingTalk's UI after a gateway restart because disconnect() called _streaming_cards.clear() without first closing them via _close_streaming_siblings. Move the finalization loop before self._http_client.aclose() so the HTTP client is still available when the finalize requests are sent. Adds a regression test that asserts the HTTP client is alive during finalization. --- gateway/platforms/dingtalk.py | 13 +++++++++++++ tests/gateway/test_dingtalk.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/gateway/platforms/dingtalk.py b/gateway/platforms/dingtalk.py index 6e599ed2210..0b3c7f52ace 100644 --- a/gateway/platforms/dingtalk.py +++ b/gateway/platforms/dingtalk.py @@ -358,6 +358,19 @@ class DingTalkAdapter(BasePlatformAdapter): await asyncio.gather(*self._bg_tasks, return_exceptions=True) self._bg_tasks.clear() + # Finalize any open streaming cards before the HTTP client closes so + # they don't stay stuck in streaming state on DingTalk's UI after + # a gateway restart. _close_streaming_siblings handles its own + # per-card exceptions; the outer try is a safety net for token fetch. + for _chat_id in list(self._streaming_cards): + try: + await self._close_streaming_siblings(_chat_id) + except Exception as _exc: + logger.debug( + "[%s] Failed to finalize streaming card on disconnect for %s: %s", + self.name, _chat_id, _exc, + ) + if self._http_client: await self._http_client.aclose() self._http_client = None diff --git a/tests/gateway/test_dingtalk.py b/tests/gateway/test_dingtalk.py index 6b2db13299d..2da55a00979 100644 --- a/tests/gateway/test_dingtalk.py +++ b/tests/gateway/test_dingtalk.py @@ -407,6 +407,36 @@ class TestConnect: assert len(adapter._dedup._seen) == 0 assert adapter._http_client is None + @pytest.mark.asyncio + async def test_disconnect_finalizes_open_streaming_cards(self): + """Streaming cards must be finalized before HTTP client closes.""" + from unittest.mock import AsyncMock, patch + from gateway.platforms.dingtalk import DingTalkAdapter + adapter = DingTalkAdapter(PlatformConfig(enabled=True)) + adapter._http_client = AsyncMock() + adapter._stream_task = None + adapter._streaming_cards = { + "chat-1": {"track-a": "last content"}, + "chat-2": {"track-b": "other"}, + } + + close_calls = [] + + async def fake_close_siblings(chat_id): + # HTTP client must still be alive at call time. + assert adapter._http_client is not None, ( + "HTTP client was already closed before card finalization" + ) + close_calls.append(chat_id) + adapter._streaming_cards.pop(chat_id, None) + + with patch.object(adapter, "_close_streaming_siblings", side_effect=fake_close_siblings): + await adapter.disconnect() + + assert set(close_calls) == {"chat-1", "chat-2"} + assert adapter._streaming_cards == {} + assert adapter._http_client is None + # --------------------------------------------------------------------------- # Platform enum