fix(yuanbao): await the forwarded-records loading heartbeat

ForwardedRecordsParseMiddleware.handle() called the coroutine function
_send_loading_heartbeat() without awaiting it, so the coroutine was built
and dropped. The RUNNING heartbeat never reached the client and Python
raised "coroutine was never awaited".

Forwarded-record parsing is the slow inbound path, which is where the
loading bubble matters most: the user sees nothing while the deep parse
runs. Awaiting is safe, since the helper already swallows every exception
and the call sits inside the middleware's own try block.
This commit is contained in:
MaxFreedomPollard 2026-07-31 01:15:34 -04:00 committed by Teknium
parent 7729c183b4
commit 87f5c5351a
2 changed files with 75 additions and 1 deletions

View File

@ -2160,7 +2160,7 @@ class ForwardedRecordsParseMiddleware(InboundMiddleware):
async def handle(self, ctx: InboundContext, next_fn) -> None:
try:
if ctx.forwarded_records:
self._send_loading_heartbeat(ctx)
await self._send_loading_heartbeat(ctx)
ctx.raw_text = self.build_forward_text(ctx.forwarded_records, ctx=ctx, is_dispatch=True)
except Exception as exc:
# Degrade gracefully: leave ctx.raw_text as-is.

View File

@ -0,0 +1,74 @@
"""ForwardedRecordsParseMiddleware must actually send its loading heartbeat.
``_send_loading_heartbeat`` is a coroutine function. Calling it without
``await`` builds a coroutine object and drops it, so the RUNNING heartbeat is
never sent and Python raises "coroutine was never awaited" at collection time.
Forwarded-record parsing is the slow inbound path, which is exactly where the
user needs the loading bubble.
"""
import warnings
import pytest
from gateway.platforms.yuanbao import (
ForwardedRecordsParseMiddleware,
InboundContext,
WS_HEARTBEAT_RUNNING,
)
class _Heartbeat:
def __init__(self):
self.calls = []
async def send_heartbeat_once(self, chat_id, state):
self.calls.append((chat_id, state))
class _Outbound:
def __init__(self):
self.heartbeat = _Heartbeat()
class _Adapter:
name = "yuanbao"
def __init__(self):
self._outbound = _Outbound()
@pytest.mark.asyncio
async def test_forwarded_records_send_the_loading_heartbeat():
adapter = _Adapter()
ctx = InboundContext(adapter=adapter)
ctx.chat_id = "chat-1"
ctx.forwarded_records = [{"msgContent": {"text": "hello"}}]
called = []
async def _next():
called.append(True)
with warnings.catch_warnings():
# A dropped coroutine surfaces here as a RuntimeWarning rather than a
# failure; promote it so the regression cannot pass silently.
warnings.simplefilter("error", RuntimeWarning)
await ForwardedRecordsParseMiddleware().handle(ctx, _next)
assert adapter._outbound.heartbeat.calls == [("chat-1", WS_HEARTBEAT_RUNNING)]
assert called, "middleware must still call next_fn"
@pytest.mark.asyncio
async def test_no_forwarded_records_sends_no_heartbeat():
adapter = _Adapter()
ctx = InboundContext(adapter=adapter)
ctx.chat_id = "chat-1"
async def _next():
pass
await ForwardedRecordsParseMiddleware().handle(ctx, _next)
assert adapter._outbound.heartbeat.calls == []