Files
ai-video-fullstack/backend/tests/test_task_backend_spine.py
T

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', {"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', {"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())