121 lines
6.9 KiB
Python
121 lines
6.9 KiB
Python
"""F0: explicit simulated backend, real application-owned Pipecat Jobs."""
|
|
import asyncio
|
|
import json
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from models import RuntimeTaskBackend
|
|
from services.background_tasks.service import TaskService
|
|
from tests.test_background_tasks import MemoryRepository, SimulatedExecutor
|
|
from tests.test_reasoner_backend import SimulatedLLM, backend_config
|
|
|
|
|
|
class TaskBackendSpineTest(unittest.IsolatedAsyncioTestCase):
|
|
async def test_backend_resource_reaches_native_job_without_tool_definition(self):
|
|
SimulatedExecutor.release = asyncio.Event()
|
|
SimulatedExecutor.confirms_stop = True
|
|
service = TaskService(MemoryRepository(), SimulatedExecutor)
|
|
backend = SimpleNamespace(id="tb_smoke", type="opencode", config={"url": "https://simulated.test", "directory": "/demo", "timeout_seconds": 30}, secrets={})
|
|
await service.start()
|
|
try:
|
|
task = await service.submit(assistant_id="asst_smoke", conversation_id="conv_smoke", backend=backend, instruction="SIMULATED task", invocation_id="smoke")
|
|
SimulatedExecutor.release.set()
|
|
async with asyncio.timeout(3):
|
|
while (await service.get(task["id"])).status != "succeeded":
|
|
await asyncio.sleep(.005)
|
|
record = await service.get(task["id"])
|
|
self.assertEqual(record.connection["_backend_type"], "opencode")
|
|
self.assertTrue(record.job_id)
|
|
artifact = Path(__file__).resolve().parents[2] / "implement-stage/task-backends/backend-spine-smoke.json"
|
|
artifact.write_text(json.dumps({"simulated_remote": True, "native_job": "completed", "backend_id": backend.id, "tool_definition_required": False}) + "\n")
|
|
finally:
|
|
await service.close()
|
|
|
|
|
|
class ReasonerServiceTest(unittest.IsolatedAsyncioTestCase):
|
|
"""Full business lifecycle with a real native worker and simulated model."""
|
|
|
|
async def asyncSetUp(self):
|
|
self.service = TaskService(MemoryRepository())
|
|
await self.service.start()
|
|
self.backend = RuntimeTaskBackend.model_validate(backend_config()["_backend"])
|
|
|
|
async def asyncTearDown(self):
|
|
await self.service.close()
|
|
|
|
async def wait_task(self, task_id, predicate):
|
|
async with asyncio.timeout(5):
|
|
while True:
|
|
record = await self.service.get(task_id)
|
|
if predicate(record):
|
|
return record
|
|
await asyncio.sleep(.005)
|
|
|
|
async def submit(self, *, variable_state=None):
|
|
return await self.service.submit(
|
|
assistant_id="asst_reasoner", conversation_id="conv_reasoner", backend=self.backend,
|
|
instruction="显式模拟任务", invocation_id="call_reasoner", variable_state=variable_state,
|
|
)
|
|
|
|
async def test_snapshot_is_frozen_and_business_projection_has_no_secrets(self):
|
|
model = SimulatedLLM([[('complete_task', {"status": "succeeded", "result": "模拟任务完成"})]])
|
|
self.backend.instructions = "为 {{customer}} 核查结果"
|
|
self.backend.config["instructions"] = "字面模板 {{customer}}"
|
|
state = {"values": {"customer": "before"}, "definitions": {"customer": {"type": "string"}}, "secrets": {"secret__token": "private-token"}}
|
|
with patch("services.background_tasks.reasoner.create_reasoner_llm", return_value=model):
|
|
task = await self.submit(variable_state=state)
|
|
self.backend.config["skills"][0]["content"] = "changed after submission"
|
|
state["values"]["customer"] = "changed after submission"
|
|
record = await self.wait_task(task["id"], lambda item: item.status == "succeeded")
|
|
self.assertEqual(record.backend_type, "reasoner")
|
|
self.assertEqual(record.connection["_variables"]["customer"], "before")
|
|
self.assertEqual(record.connection["_secret_variables"]["secret__token"], "private-token")
|
|
self.assertNotIn("changed after submission", str(model.contexts))
|
|
self.assertIn("为 before 核查结果", model.contexts[0][0]["content"])
|
|
self.assertIn("字面模板 {{customer}}", model.contexts[0][0]["content"])
|
|
self.assertNotIn("private-token", str(record.public()))
|
|
self.assertNotIn("simulated-key", str(record.public()))
|
|
self.assertNotIn("_backend", record.public())
|
|
|
|
async def test_immediate_next_question_survives_reply_projection_update(self):
|
|
model = SimulatedLLM([
|
|
[('ask_user', {"question": "选择日期?"})],
|
|
[('ask_user', {"question": "选择范围?"})],
|
|
[('complete_task', {"status": "succeeded", "result": "两个问题均已回答"})],
|
|
])
|
|
with patch("services.background_tasks.reasoner.create_reasoner_llm", return_value=model):
|
|
task = await self.submit()
|
|
first = await self.wait_task(task["id"], lambda item: item.status == "waiting_input")
|
|
await self.service.send_input(task["id"], {"answers": [["周一"]]})
|
|
second = await self.wait_task(task["id"], lambda item: item.status == "waiting_input" and item.input_request.get("id") != first.input_request["id"])
|
|
self.assertEqual(second.input_request["questions"][0]["question"], "选择范围?")
|
|
await self.service.send_input(task["id"], {"answers": [["本部门"]]})
|
|
finished = await self.wait_task(task["id"], lambda item: item.status == "succeeded")
|
|
self.assertEqual(finished.result, "两个问题均已回答")
|
|
self.assertFalse(finished.input_request)
|
|
|
|
async def test_shutdown_restart_does_not_replay_reasoner(self):
|
|
model = SimulatedLLM([[('ask_user', {"question": "需要补充信息"})]])
|
|
with patch("services.background_tasks.reasoner.create_reasoner_llm", return_value=model) as factory:
|
|
task = await self.submit()
|
|
await self.wait_task(task["id"], lambda item: item.status == "waiting_input")
|
|
await self.service.close()
|
|
await self.service.start()
|
|
record = await self.wait_task(task["id"], lambda item: item.status == "failed")
|
|
self.assertIn("未自动重放工具", record.summary)
|
|
self.assertEqual(factory.call_count, 1)
|
|
|
|
async def test_business_cancel_cleans_only_current_reasoner(self):
|
|
model = SimulatedLLM([[('ask_user', {"question": "需要补充信息"})]])
|
|
with patch("services.background_tasks.reasoner.create_reasoner_llm", return_value=model):
|
|
task = await self.submit()
|
|
await self.wait_task(task["id"], lambda item: item.status == "waiting_input")
|
|
executor = self.service._executors[task["id"]]
|
|
await self.service.cancel(task["id"])
|
|
record = await self.wait_task(task["id"], lambda item: item.status == "cancelled")
|
|
self.assertIsNone(executor._worker)
|
|
self.assertIn("已发出的工具请求", record.summary)
|
|
self.assertTrue(self.service.runtime._run and not self.service.runtime._run.done())
|