Files
pipecat/tests/test_idle_frame_processor.py
iamjr15 565349d332 Fix asyncio.Event race conditions in idle processors
Move event.clear() from finally block to success path in
IdleFrameProcessor and UserIdleProcessor._idle_task_handler().
The finally block unconditionally cleared signals set during
async timeout callbacks, causing false-positive idle detection.

Closes #3402
2026-04-09 13:41:01 -07:00

105 lines
2.9 KiB
Python

#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import asyncio
import unittest
from pipecat.frames.frames import (
TextFrame,
TranscriptionFrame,
)
from pipecat.processors.idle_frame_processor import IdleFrameProcessor
from pipecat.tests.utils import SleepFrame, run_test
class TestIdleFrameProcessor(unittest.IsolatedAsyncioTestCase):
async def test_basic_idle_detection(self):
"""Test that idle callback is triggered after timeout."""
callback_called = asyncio.Event()
async def idle_callback(processor):
callback_called.set()
processor = IdleFrameProcessor(callback=idle_callback, timeout=0.1)
await run_test(
processor,
frames_to_send=[SleepFrame(sleep=0.2)],
expected_down_frames=[],
)
assert callback_called.is_set(), "Idle callback was not called"
async def test_activity_resets_timer(self):
"""Test that receiving frames resets the idle timer."""
callback_called = asyncio.Event()
async def idle_callback(processor):
callback_called.set()
processor = IdleFrameProcessor(callback=idle_callback, timeout=0.2)
await run_test(
processor,
frames_to_send=[
SleepFrame(sleep=0.1),
TextFrame(text="hello"),
SleepFrame(sleep=0.1),
TextFrame(text="world"),
SleepFrame(sleep=0.1),
],
expected_down_frames=[TextFrame, TextFrame],
)
assert not callback_called.is_set()
async def test_signal_preserved_during_callback(self):
"""Regression: event set during callback must not be cleared."""
class _Sentinel(Exception):
pass
async def callback(proc):
proc._idle_event.set()
raise _Sentinel()
processor = IdleFrameProcessor(callback=callback, timeout=0.01)
processor._idle_event = asyncio.Event()
with self.assertRaises(_Sentinel):
await processor._idle_task_handler()
self.assertTrue(processor._idle_event.is_set())
async def test_specific_frame_types(self):
"""Test that only monitored frame types reset the idle timer."""
callback_called = asyncio.Event()
async def idle_callback(processor):
callback_called.set()
processor = IdleFrameProcessor(
callback=idle_callback,
timeout=0.15,
types=[TranscriptionFrame],
)
await run_test(
processor,
frames_to_send=[
SleepFrame(sleep=0.1),
TextFrame(text="this should not reset"),
SleepFrame(sleep=0.2),
],
expected_down_frames=[TextFrame],
)
assert callback_called.is_set()
if __name__ == "__main__":
unittest.main()