"""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())