Files
pipecat/tests/test_bus.py
Aleix Conchillo Flaqué b03247f360 Rename BaseTask → BaseWorker and reserve "task" for asyncio
Replaces every "task" identifier that referred to the BaseTask
abstraction with "worker". Asyncio task plumbing (asyncio.Task,
BaseTaskManager, TaskManager, create_task, cancel_task, etc.) stays
untouched. Highlights:

- Classes: BaseTask → BaseWorker, PipelineTask → PipelineWorker,
  LLMTask → LLMWorker, LLMContextTask → LLMContextWorker, TaskBus →
  WorkerBus, TaskRegistry → WorkerRegistry, TaskActivationArgs →
  WorkerActivationArgs, TaskReadyData → WorkerReadyData,
  TaskRegistryEntry → WorkerRegistryEntry, TaskObserver →
  WorkerObserver, all Bus*TaskMessage → Bus*WorkerMessage,
  BusAddTaskMessage.task field → worker, BusWorkerRegistryMessage.tasks
  field → workers.
- Methods/decorators: activate_task → activate_worker, deactivate_task
  → deactivate_worker, add_task → add_worker, watch_task →
  watch_worker, @task_ready → @worker_ready, setup_pipeline_task hook
  → setup_pipeline_worker.
- Params/fields: FrameProcessorSetup.pipeline_task and
  FunctionCallParams.pipeline_task → pipeline_worker. Parameter names
  like task_name → worker_name; spawn/run accept worker:.
- Files: pipeline/base_task.py → base_worker.py, pipeline/task.py →
  worker.py (plus a re-export shim at pipeline/task.py),
  task_observer.py → worker_observer.py, task_ready_decorator.py →
  worker_ready_decorator.py, pipecat.tasks → pipecat.workers,
  llm_task.py → llm_worker.py, llm_context_task.py →
  llm_context_worker.py, examples/multi-task → examples/multi-worker.

Back-compat:
- PipelineTask kept as a deprecated subclass of PipelineWorker that
  warns on construction.
- pipecat.pipeline.task re-exports PipelineWorker/PipelineTask/etc. so
  existing user imports keep working.
- FrameProcessor.pipeline_task kept as a deprecated property that
  forwards to pipeline_worker.

Local variables in examples that hold a worker (task = PipelineTask(...))
are renamed to worker = PipelineWorker(...). Asyncio-task locals
(runner_task, etc.) are preserved.
2026-05-21 19:07:13 -07:00

275 lines
8.6 KiB
Python

#
# Copyright (c) 2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import asyncio
import itertools
import unittest
from pipecat.bus import (
AsyncQueueBus,
BusCancelMessage,
BusCancelWorkerMessage,
BusDataMessage,
BusJobCancelMessage,
BusSubscriber,
)
from pipecat.utils.asyncio.task_manager import TaskManager, TaskManagerParams
async def create_test_bus():
"""Create an AsyncQueueBus with a TaskManager for testing."""
bus = AsyncQueueBus()
tm = TaskManager()
tm.setup(TaskManagerParams(loop=asyncio.get_running_loop()))
await bus.setup(tm)
return bus, tm
class _CollectorSub(BusSubscriber):
"""Subscriber that collects messages into a list."""
_counter = itertools.count()
def __init__(self):
self._name = f"collector_{next(self._counter)}"
self.received = []
@property
def name(self) -> str:
return self._name
async def on_bus_message(self, message):
self.received.append(message)
class TestAsyncQueueBus(unittest.IsolatedAsyncioTestCase):
async def test_send_delivers_to_subscriber(self):
"""send() delivers a message to a subscriber."""
bus, _ = await create_test_bus()
sub = _CollectorSub()
await bus.subscribe(sub)
await bus.start()
msg = BusDataMessage(source="task_a")
await bus.send(msg)
await asyncio.sleep(0.05)
await bus.stop()
self.assertEqual(len(sub.received), 1)
self.assertIs(sub.received[0], msg)
async def test_multiple_messages_in_order(self):
"""Messages are dispatched in FIFO order."""
bus, _ = await create_test_bus()
sub = _CollectorSub()
await bus.subscribe(sub)
await bus.start()
msgs = [BusDataMessage(source=f"task_{i}") for i in range(5)]
for m in msgs:
await bus.send(m)
await asyncio.sleep(0.1)
await bus.stop()
self.assertEqual(len(sub.received), 5)
for sent, got in zip(msgs, sub.received):
self.assertIs(sent, got)
async def test_start_stop_lifecycle(self):
"""start() begins dispatch tasks, stop() cancels them cleanly."""
bus, _ = await create_test_bus()
sub = _CollectorSub()
await bus.subscribe(sub)
await bus.start()
await bus.send(BusDataMessage(source="a"))
await asyncio.sleep(0.05)
self.assertEqual(len(sub.received), 1)
await bus.stop()
await bus.send(BusDataMessage(source="b"))
await asyncio.sleep(0.05)
# After stop, messages are not dispatched
self.assertEqual(len(sub.received), 1)
class TestBusSubscriber(unittest.IsolatedAsyncioTestCase):
async def test_subscribe_calls_on_bus_message(self):
"""subscribe() delivers messages to subscriber's on_bus_message."""
bus, _ = await create_test_bus()
sub = _CollectorSub()
await bus.subscribe(sub)
await bus.start()
msg = BusDataMessage(source="task_a")
await bus.send(msg)
await asyncio.sleep(0.05)
await bus.stop()
self.assertEqual(len(sub.received), 1)
self.assertIs(sub.received[0], msg)
async def test_multiple_subscribers_independent(self):
"""Two subscribers each get every message on their own task."""
bus, _ = await create_test_bus()
sub1 = _CollectorSub()
sub2 = _CollectorSub()
await bus.subscribe(sub1)
await bus.subscribe(sub2)
await bus.start()
msg = BusDataMessage(source="task_a")
await bus.send(msg)
await asyncio.sleep(0.05)
await bus.stop()
self.assertEqual(len(sub1.received), 1)
self.assertEqual(len(sub2.received), 1)
self.assertIs(sub1.received[0], msg)
self.assertIs(sub2.received[0], msg)
async def test_unsubscribe_stops_delivery(self):
"""unsubscribe() prevents further message delivery."""
bus, _ = await create_test_bus()
sub = _CollectorSub()
await bus.subscribe(sub)
await bus.start()
await bus.send(BusDataMessage(source="a"))
await asyncio.sleep(0.05)
self.assertEqual(len(sub.received), 1)
await bus.unsubscribe(sub)
await bus.send(BusDataMessage(source="b"))
await asyncio.sleep(0.05)
await bus.stop()
self.assertEqual(len(sub.received), 1)
async def test_slow_subscriber_does_not_block_others(self):
"""A slow subscriber does not block a fast subscriber."""
bus, _ = await create_test_bus()
fast_received = []
fast_done = asyncio.Event()
class SlowSub(BusSubscriber):
@property
def name(self) -> str:
return "slow"
async def on_bus_message(self, message):
await asyncio.sleep(0.5)
class FastSub(BusSubscriber):
@property
def name(self) -> str:
return "fast"
async def on_bus_message(self, message):
fast_received.append(message)
fast_done.set()
await bus.subscribe(SlowSub())
await bus.subscribe(FastSub())
await bus.start()
await bus.send(BusDataMessage(source="a"))
await asyncio.wait_for(fast_done.wait(), timeout=0.1)
await bus.stop()
self.assertEqual(len(fast_received), 1)
class TestBusMessagePriority(unittest.IsolatedAsyncioTestCase):
async def test_system_message_preempts_data_messages(self):
"""System messages are delivered before data messages queued earlier."""
bus, _ = await create_test_bus()
sub = _CollectorSub()
await bus.subscribe(sub)
# Queue messages before starting dispatch
for i in range(5):
await bus.send(BusDataMessage(source=f"data_{i}"))
await bus.send(BusCancelMessage(source="runner", reason="urgent"))
await bus.start()
await asyncio.sleep(0.1)
await bus.stop()
# System message should be first
self.assertIsInstance(sub.received[0], BusCancelMessage)
self.assertEqual(sub.received[0].source, "runner")
async def test_data_messages_preserve_fifo_order(self):
"""Data messages maintain FIFO order among themselves."""
bus, _ = await create_test_bus()
sub = _CollectorSub()
await bus.subscribe(sub)
await bus.start()
for i in range(5):
await bus.send(BusDataMessage(source=f"task_{i}"))
await asyncio.sleep(0.1)
await bus.stop()
sources = [m.source for m in sub.received]
self.assertEqual(sources, [f"task_{i}" for i in range(5)])
async def test_system_messages_preserve_fifo_order(self):
"""Multiple system messages maintain FIFO order among themselves."""
bus, _ = await create_test_bus()
sub = _CollectorSub()
await bus.subscribe(sub)
await bus.start()
await bus.send(BusCancelMessage(source="first", reason="a"))
await bus.send(BusCancelMessage(source="second", reason="b"))
await asyncio.sleep(0.1)
await bus.stop()
self.assertEqual(sub.received[0].source, "first")
self.assertEqual(sub.received[1].source, "second")
async def test_mixed_messages_system_first(self):
"""When data and system messages are queued, all system come first."""
bus, _ = await create_test_bus()
sub = _CollectorSub()
await bus.subscribe(sub)
# Queue before starting so all messages are pending
await bus.send(BusDataMessage(source="data_1"))
await bus.send(BusDataMessage(source="data_2"))
await bus.send(BusCancelMessage(source="cancel_1"))
await bus.send(BusDataMessage(source="data_3"))
await bus.send(BusCancelWorkerMessage(source="cancel_2", target="task"))
await bus.start()
await asyncio.sleep(0.1)
await bus.stop()
sources = [m.source for m in sub.received]
self.assertEqual(sources, ["cancel_1", "cancel_2", "data_1", "data_2", "data_3"])
async def test_task_cancel_is_system_priority(self):
"""BusJobCancelMessage has system priority."""
bus, _ = await create_test_bus()
sub = _CollectorSub()
await bus.subscribe(sub)
await bus.send(BusDataMessage(source="data"))
await bus.send(BusJobCancelMessage(source="parent", target="worker", job_id="t1"))
await bus.start()
await asyncio.sleep(0.1)
await bus.stop()
self.assertIsInstance(sub.received[0], BusJobCancelMessage)
if __name__ == "__main__":
unittest.main()