"""Regression checks for whole-pipeline assembly and terminal outcomes.""" import asyncio import unittest from types import SimpleNamespace from unittest.mock import AsyncMock, patch from models import AssistantConfig from pipecat.frames.frames import CancelFrame, EndFrame, ErrorFrame, StartFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.worker import PipelineWorker from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from services.pipecat import pipeline from services.pipecat.pipeline_runner import run_media_worker class PipelineAssemblyTest(unittest.IsolatedAsyncioTestCase): async def test_realtime_startup_preserves_client_capabilities(self): brain = SimpleNamespace( spec=SimpleNamespace(supported_runtime_modes={"realtime"}), system_prompt=lambda cfg: "test", greeting=AsyncMock(return_value=""), setup_realtime=AsyncMock(), on_session_update=AsyncMock(), ) transport = SimpleNamespace( input=lambda: FrameProcessor(), output=lambda: FrameProcessor(), event_handler=lambda name: lambda handler: handler, ) with ( patch.object(pipeline, "build_brain", return_value=brain), patch.object(pipeline, "create_realtime_service", return_value=FrameProcessor()), patch.object(pipeline.ConversationRecorder, "start", new=AsyncMock(return_value=None)), patch.object(pipeline, "run_media_worker", new=AsyncMock()) as run, ): await pipeline.run_pipeline( transport, AssistantConfig(runtimeMode="realtime"), client_tool_names={"show_message"}, ) run.assert_awaited_once() runtime = brain.setup_realtime.await_args.args[1] self.assertTrue(runtime.client_tools.supports("show_message")) self.assertFalse(runtime.client_tools.supports("unregistered_tool")) class TerminalProcessor(FrameProcessor): def __init__(self, terminal): super().__init__() self.terminal = terminal async def process_frame(self, frame, direction): await super().process_frame(frame, direction) await self.push_frame(frame, direction) if isinstance(frame, StartFrame): if isinstance(self.terminal, ErrorFrame): await self.push_frame(self.terminal, FrameDirection.UPSTREAM) else: await self.push_frame(self.terminal) class PipelineOutcomeTest(unittest.IsolatedAsyncioTestCase): async def test_fatal_error_is_failed_even_when_runner_returns_normally(self): worker = PipelineWorker( Pipeline([TerminalProcessor(ErrorFrame(error="provider failed", fatal=True))]), enable_rtvi=False, ) recorder = SimpleNamespace(finish=AsyncMock()) await asyncio.wait_for(run_media_worker(worker, recorder), 3) recorder.finish.assert_awaited_once_with(status="failed") async def test_normal_end_and_client_disconnect_are_completed(self): for terminal in (EndFrame(), CancelFrame(reason="client_disconnected")): with self.subTest(terminal=type(terminal).__name__): worker = PipelineWorker(Pipeline([Passthrough()]), enable_rtvi=False) recorder = SimpleNamespace(finish=AsyncMock()) await worker.queue_frame(terminal) await asyncio.wait_for(run_media_worker(worker, recorder), 3) recorder.finish.assert_awaited_once_with(status="completed") async def test_server_task_cancellation_remains_failed_and_propagates(self): worker = PipelineWorker(Pipeline([Passthrough()]), enable_rtvi=False) started = asyncio.Event() @worker.event_handler("on_pipeline_started") async def on_started(_worker, _frame): started.set() recorder = SimpleNamespace(finish=AsyncMock()) task = asyncio.create_task(run_media_worker(worker, recorder)) await asyncio.wait_for(started.wait(), 3) task.cancel() with self.assertRaises(asyncio.CancelledError): await asyncio.wait_for(task, 3) recorder.finish.assert_awaited_once_with(status="failed") class Passthrough(FrameProcessor): async def process_frame(self, frame, direction): await super().process_frame(frame, direction) await self.push_frame(frame, direction)