from __future__ import annotations import unittest from types import SimpleNamespace from fastapi import HTTPException from routes.assistants import _validate_startup_actions from schemas import AssistantUpsert from services.config_resolver import _runtime_tool_ids def startup_body( *, phase: str = "opening", bind_tool: bool = False, ) -> AssistantUpsert: return AssistantUpsert( name="启动动作测试", type="prompt", runtimeMode="pipeline", toolIds=["tool_message"] if bind_tool else [], startup={ "executionMode": "sequential", "actions": [ { "id": "opening_message", "phase": phase, "toolId": "tool_message", "arguments": { "title": "重要提示", "message": "请确认已阅读。", "actions": [ { "id": "confirmed", "label": "确认", "style": "primary", } ], "dismissible": False, }, "required": True, } ], }, ) class FakeSession: def __init__(self, tool): self.tool = tool async def get(self, _model, tool_id): return self.tool if self.tool is not None and tool_id == self.tool.id else None class StartupActionValidationTests(unittest.IsolatedAsyncioTestCase): def test_runtime_pool_includes_bound_and_startup_only_tools(self): self.assertEqual( _runtime_tool_ids( ["conversation_tool", "shared_tool"], { "actions": [ {"tool_id": "opening_tool"}, {"toolId": "shared_tool"}, ] }, ), ["conversation_tool", "shared_tool", "opening_tool"], ) def test_realtime_rejects_startup_actions(self): with self.assertRaisesRegex(ValueError, "Realtime"): AssistantUpsert( name="Realtime 启动动作", type="prompt", runtimeMode="realtime", startup=startup_body().startup, ) def test_realtime_rejects_builtin_opening_message(self): with self.assertRaisesRegex(ValueError, "Realtime"): AssistantUpsert( name="Realtime 开场消息", type="prompt", runtimeMode="realtime", startup={ "openingMessage": { "title": "重要提示", "message": "请确认已阅读。", "confirmLabel": "确认", } }, ) async def test_builtin_opening_message_does_not_reference_a_tool(self): body = AssistantUpsert( name="内置开场消息", type="prompt", startup={ "openingMessage": { "title": "重要提示", "message": "请确认已阅读。", "confirmLabel": "确认", } }, ) await _validate_startup_actions(FakeSession(None), body) self.assertEqual(body.startup.actions, []) self.assertEqual(body.startup.opening_message.confirm_label, "确认") async def test_startup_tool_does_not_need_conversation_binding(self): tool = SimpleNamespace( id="tool_message", name="重要提示", function_name="show_message", type="client", status="active", definition={ "config": { "wait_for_response": True, "response_wait_mode": "session", } }, ) body = startup_body(bind_tool=False) await _validate_startup_actions(FakeSession(tool), body) self.assertEqual(body.tool_ids, []) async def test_show_message_requires_session_wait(self): tool = SimpleNamespace( id="tool_message", name="重要提示", function_name="show_message", type="client", status="active", definition={ "config": { "wait_for_response": True, "response_wait_mode": "timeout", } }, ) with self.assertRaisesRegex(HTTPException, "会话内等待"): await _validate_startup_actions(FakeSession(tool), startup_body()) tool.definition["config"]["response_wait_mode"] = "session" await _validate_startup_actions(FakeSession(tool), startup_body()) async def test_preflight_rejects_client_tools(self): tool = SimpleNamespace( id="tool_message", name="重要提示", function_name="show_message", type="client", status="active", definition={ "config": { "wait_for_response": True, "response_wait_mode": "session", } }, ) with self.assertRaisesRegex(HTTPException, "preflight"): await _validate_startup_actions( FakeSession(tool), startup_body(phase="preflight"), ) if __name__ == "__main__": unittest.main()