2026-10-23 15:55:08 +08:00
|
|
|
"""Real Pipecat scheduling with an explicitly simulated remote executor."""
|
|
|
|
|
import asyncio
|
|
|
|
|
import json
|
|
|
|
|
import unittest
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from pipecat.frames.frames import CancelFrame
|
|
|
|
|
from pipecat.pipeline.pipeline import Pipeline
|
|
|
|
|
from pipecat.pipeline.worker import PipelineWorker
|
|
|
|
|
from pipecat.processors.frame_processor import FrameProcessor
|
|
|
|
|
from services.background_tasks.native import NativeTaskRuntime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Passthrough(FrameProcessor):
|
|
|
|
|
async def process_frame(self, frame, direction):
|
|
|
|
|
await super().process_frame(frame, direction)
|
|
|
|
|
await self.push_frame(frame, direction)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NativeJobTest(unittest.IsolatedAsyncioTestCase):
|
2026-10-23 16:27:00 +08:00
|
|
|
async def test_native_timeout_calls_remote_cleanup(self):
|
|
|
|
|
cancelled = asyncio.Event()
|
|
|
|
|
|
|
|
|
|
async def execute(message, worker):
|
|
|
|
|
await asyncio.Event().wait()
|
|
|
|
|
|
|
|
|
|
async def cleanup(message):
|
|
|
|
|
cancelled.set()
|
|
|
|
|
|
|
|
|
|
async def ignore(message):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
runtime = NativeTaskRuntime(execute, cleanup, ignore, ignore)
|
|
|
|
|
await runtime.start()
|
|
|
|
|
try:
|
|
|
|
|
await runtime.submit("timeout-test", 0.05)
|
|
|
|
|
await asyncio.wait_for(cancelled.wait(), 2)
|
|
|
|
|
finally:
|
|
|
|
|
await runtime.close()
|
|
|
|
|
|
2026-10-23 15:55:08 +08:00
|
|
|
async def test_progress_completion_and_media_disconnect(self):
|
|
|
|
|
progress, finished, release = asyncio.Event(), asyncio.Event(), asyncio.Event()
|
|
|
|
|
results = []
|
|
|
|
|
|
|
|
|
|
async def execute(message, worker):
|
|
|
|
|
# SIMULATED executor; no remote OpenCode or model is called here.
|
|
|
|
|
await worker.send_job_update(message.job_id, {"step": "working"})
|
|
|
|
|
await release.wait()
|
|
|
|
|
return {"result": "done"}
|
|
|
|
|
|
|
|
|
|
async def cancelled(message):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def update(message):
|
|
|
|
|
progress.set()
|
|
|
|
|
|
|
|
|
|
async def result(message):
|
|
|
|
|
results.append(message)
|
|
|
|
|
finished.set()
|
|
|
|
|
|
|
|
|
|
runtime = NativeTaskRuntime(execute, cancelled, update, result)
|
|
|
|
|
await runtime.start()
|
|
|
|
|
try:
|
|
|
|
|
job_id = await runtime.submit("test-task", 5)
|
|
|
|
|
await asyncio.wait_for(progress.wait(), 2)
|
|
|
|
|
self.assertFalse(finished.is_set())
|
|
|
|
|
media = PipelineWorker(Pipeline([Passthrough()]), enable_rtvi=False)
|
|
|
|
|
await runtime.runner.add_workers(media)
|
|
|
|
|
await media.queue_frame(CancelFrame(reason="client_disconnected"))
|
|
|
|
|
await asyncio.wait_for(media.wait(), 2)
|
|
|
|
|
self.assertIn(job_id, runtime.executor.active_jobs)
|
|
|
|
|
release.set()
|
|
|
|
|
await asyncio.wait_for(finished.wait(), 2)
|
|
|
|
|
self.assertEqual(results[0].status, "completed")
|
|
|
|
|
self.assertEqual(results[0].response, {"result": "done"})
|
|
|
|
|
artifact = Path(__file__).resolve().parents[2] / "implement-stage/native-jobs-smoke.json"
|
|
|
|
|
artifact.write_text(json.dumps({"simulated_executor": True, "native_job": "completed", "survives_media_disconnect": True}) + "\n")
|
|
|
|
|
finally:
|
|
|
|
|
await runtime.close()
|