@@ -302,6 +302,37 @@ async def slow(data: RpcInvocationData) -> str:
302302 assert info .value .code == rtc .RpcError .ErrorCode .RESPONSE_TIMEOUT
303303
304304
305+ async def test_chain_finishing_as_the_deadline_fires_keeps_its_result () -> None :
306+ """The deadline timer can run in the same loop iteration the chain completes in, before
307+ the waiting task resumes: its cancel is rejected (the chain is done) and the result is
308+ the caller's. A zero deadline with an immediate handler pins that ordering."""
309+ lp = _participant ()
310+ lp ._rpc_handlers ["fast" ] = lambda data : "ok"
311+
312+ result = await lp ._run_incoming_chain (
313+ RpcInvocationData ("r1" , "alice" , "{}" , 0.0 , method = "fast" )
314+ )
315+ assert result == "ok"
316+
317+
318+ async def test_cancelled_error_escaping_the_chain_is_an_application_error (
319+ caplog : pytest .LogCaptureFixture ,
320+ ) -> None :
321+ """A CancelledError that an interceptor or the handler lets escape on its own is not a
322+ deadline nor a disconnect; the caller must still get an answer rather than wait out its
323+ timeout because the cancellation slipped past the response code."""
324+ lp = _participant ()
325+
326+ async def leaks_cancel (data : RpcInvocationData ) -> str :
327+ raise asyncio .CancelledError ()
328+
329+ lp ._rpc_handlers ["leaky" ] = leaks_cancel
330+ with pytest .raises (rtc .RpcError ) as info :
331+ await lp ._run_incoming_chain (RpcInvocationData ("r1" , "alice" , "{}" , 5.0 , method = "leaky" ))
332+ assert info .value .code == rtc .RpcError .ErrorCode .APPLICATION_ERROR
333+ assert any ("leaky" in r .getMessage () for r in caplog .records )
334+
335+
305336async def test_outside_cancellation_maps_to_recipient_disconnected () -> None :
306337 lp = _participant ()
307338 started = asyncio .Event ()
0 commit comments