from unittest.mock import AsyncMock, Mock, patch import pytest from core.config import config from models.models import MCPResponse, ToolDefinitionModel from services.custom_tool_service import CustomToolService from services.resources.custom_tools import get_custom_tools from services.tools.execute_custom_tool import execute_custom_tool class _DummyMcp: def custom_route(self, _path, methods=None): # noqa: ARG002 def _decorator(fn): return fn return _decorator @pytest.mark.asyncio async def test_list_registered_tools_threads_user_id_to_plugin_hub(): service = CustomToolService(_DummyMcp()) with patch("services.custom_tool_service.PluginHub.get_tools_for_project", new_callable=AsyncMock) as mock_get: mock_get.return_value = [] await service.list_registered_tools("project-hash", user_id="user-1") mock_get.assert_awaited_once_with("project-hash", user_id="user-1") @pytest.mark.asyncio async def test_get_tool_definition_threads_user_id_to_plugin_hub(): service = CustomToolService(_DummyMcp()) with patch("services.custom_tool_service.PluginHub.get_tool_definition", new_callable=AsyncMock) as mock_get: mock_get.return_value = None await service.get_tool_definition("project-hash", "my_tool", user_id="user-1") mock_get.assert_awaited_once_with("project-hash", "my_tool", user_id="user-1") @pytest.mark.asyncio async def test_execute_tool_threads_user_id_to_definition_lookup_and_transport(): service = CustomToolService(_DummyMcp()) definition = ToolDefinitionModel(name="my_tool", description="My tool", requires_polling=False) with patch.object(service, "get_tool_definition", new_callable=AsyncMock) as mock_get_definition: with patch("services.custom_tool_service.send_with_unity_instance", new_callable=AsyncMock) as mock_send: mock_get_definition.return_value = definition mock_send.return_value = {"success": True, "message": "ok"} await service.execute_tool( "project-hash", "my_tool", "Project@project-hash", {"x": 1}, user_id="user-1", ) mock_get_definition.assert_awaited_once_with("project-hash", "my_tool", user_id="user-1") mock_send.assert_awaited_once() assert mock_send.call_args.kwargs["user_id"] == "user-1" @pytest.mark.asyncio async def test_execute_custom_tool_threads_user_id_from_context(monkeypatch): monkeypatch.setattr(config, "http_remote_hosted", True) ctx = Mock() state = {"unity_instance": "Project@project-hash", "user_id": "user-1"} ctx.get_state = AsyncMock(side_effect=lambda key, default=None: state.get(key, default)) service = Mock() service.execute_tool = AsyncMock(return_value=MCPResponse(success=True, message="ok")) with patch("services.tools.execute_custom_tool.resolve_project_id_for_unity_instance", return_value="project-hash"): with patch("services.tools.execute_custom_tool.CustomToolService.get_instance", return_value=service): await execute_custom_tool(ctx, "my_tool", {}) service.execute_tool.assert_awaited_once_with( "project-hash", "my_tool", "Project@project-hash", {}, user_id="user-1", ) @pytest.mark.asyncio async def test_custom_tools_resource_threads_user_id_from_context(monkeypatch): monkeypatch.setattr(config, "http_remote_hosted", True) ctx = Mock() state = {"unity_instance": "Project@project-hash", "user_id": "user-1"} ctx.get_state = AsyncMock(side_effect=lambda key, default=None: state.get(key, default)) service = Mock() service.list_registered_tools = AsyncMock( return_value=[ToolDefinitionModel(name="my_tool", description="My tool")] ) with patch("services.resources.custom_tools.resolve_project_id_for_unity_instance", return_value="project-hash"): with patch("services.resources.custom_tools.CustomToolService.get_instance", return_value=service): await get_custom_tools(ctx) service.list_registered_tools.assert_awaited_once_with("project-hash", user_id="user-1")