mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-23 12:44:45 +00:00
Co-authored-by: Douglas <douglas.ym.lai@gmail.com> Co-authored-by: Douglas Lai <115660088+Douglasymlai@users.noreply.github.com>
444 lines
16 KiB
Python
444 lines
16 KiB
Python
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
|
|
import os
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.controller import tool_controller
|
|
from app.controller.tool_controller import install_tool
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestToolController:
|
|
"""Test cases for tool controller endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_notion_tool_success(self):
|
|
tool_name = "notion"
|
|
mock_toolkit = AsyncMock()
|
|
mock_tools = [MagicMock(), MagicMock()]
|
|
for tool, name in zip(mock_tools, ["create_page", "update_page"]):
|
|
tool.func.__name__ = name
|
|
mock_toolkit.get_tools = MagicMock(return_value=mock_tools)
|
|
with patch(
|
|
"app.controller.tool_controller.NotionMCPToolkit",
|
|
return_value=mock_toolkit,
|
|
):
|
|
result = await install_tool(tool_name)
|
|
assert result["success"] is True
|
|
assert result["tools"] == ["create_page", "update_page"]
|
|
assert result["count"] == 2
|
|
assert result["toolkit_name"] == "NotionMCPToolkit"
|
|
mock_toolkit.connect.assert_called_once()
|
|
mock_toolkit.disconnect.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_unknown_tool(self):
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await install_tool("unknown_tool")
|
|
assert exc_info.value.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_notion_tool_connection_failure(self):
|
|
mock_toolkit = AsyncMock()
|
|
mock_toolkit.connect.side_effect = Exception("Connection failed")
|
|
with patch(
|
|
"app.controller.tool_controller.NotionMCPToolkit",
|
|
return_value=mock_toolkit,
|
|
):
|
|
result = await install_tool("notion")
|
|
assert result["success"] is True
|
|
assert result["tools"] == []
|
|
assert result["count"] == 0
|
|
assert "warning" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_notion_tool_get_tools_failure(self):
|
|
mock_toolkit = AsyncMock()
|
|
mock_toolkit.get_tools = MagicMock(
|
|
side_effect=Exception("Failed to get tools")
|
|
)
|
|
with patch(
|
|
"app.controller.tool_controller.NotionMCPToolkit",
|
|
return_value=mock_toolkit,
|
|
):
|
|
result = await install_tool("notion")
|
|
assert result["success"] is True
|
|
assert result["tools"] == []
|
|
assert result["count"] == 0
|
|
assert "warning" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_notion_tool_disconnect_failure(self):
|
|
mock_toolkit = AsyncMock()
|
|
mock_tools = [MagicMock()]
|
|
mock_tools[0].func.__name__ = "test_tool"
|
|
mock_toolkit.get_tools = MagicMock(return_value=mock_tools)
|
|
mock_toolkit.disconnect.side_effect = Exception("Disconnect failed")
|
|
with patch(
|
|
"app.controller.tool_controller.NotionMCPToolkit",
|
|
return_value=mock_toolkit,
|
|
):
|
|
result = await install_tool("notion")
|
|
assert result["success"] is True
|
|
assert result["tools"] == []
|
|
assert result["count"] == 0
|
|
assert "warning" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_notion_tool_empty_tools(self):
|
|
mock_toolkit = AsyncMock()
|
|
mock_toolkit.get_tools = MagicMock(return_value=[])
|
|
with patch(
|
|
"app.controller.tool_controller.NotionMCPToolkit",
|
|
return_value=mock_toolkit,
|
|
):
|
|
result = await install_tool("notion")
|
|
assert result["success"] is True
|
|
assert result["tools"] == []
|
|
assert result["count"] == 0
|
|
mock_toolkit.connect.assert_called_once()
|
|
mock_toolkit.disconnect.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_notion_tool_with_complex_tools(self):
|
|
mock_toolkit = AsyncMock()
|
|
names = [
|
|
"create_database",
|
|
"query_database",
|
|
"update_block",
|
|
"delete_page",
|
|
]
|
|
mock_tools = []
|
|
for name in names:
|
|
mt = MagicMock()
|
|
mt.func.__name__ = name
|
|
mock_tools.append(mt)
|
|
mock_toolkit.get_tools = MagicMock(return_value=mock_tools)
|
|
with patch(
|
|
"app.controller.tool_controller.NotionMCPToolkit",
|
|
return_value=mock_toolkit,
|
|
):
|
|
result = await install_tool("notion")
|
|
assert result["success"] is True
|
|
assert result["tools"] == names
|
|
assert result["count"] == 4
|
|
mock_toolkit.connect.assert_called_once()
|
|
mock_toolkit.disconnect.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_launch_cdp_browser_uses_remote_hands_endpoint(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
class _FakeRemoteHands:
|
|
def get_capability_manifest(self):
|
|
return {"deployment": "remote_cluster"}
|
|
|
|
def acquire_resource(
|
|
self, resource_type: str, session_id: str, **kwargs
|
|
):
|
|
_ = (resource_type, session_id, kwargs)
|
|
return "http://worker-17:9222"
|
|
|
|
monkeypatch.delenv("EIGENT_CDP_URL", raising=False)
|
|
tool_controller._web_cdp_browser_meta = None
|
|
request = SimpleNamespace(
|
|
state=SimpleNamespace(hands=_FakeRemoteHands())
|
|
)
|
|
|
|
response = await tool_controller.launch_cdp_browser(request)
|
|
|
|
assert response["success"] is True
|
|
assert response["endpoint"] == "http://worker-17:9222"
|
|
assert response["browser"]["managedBy"] == "remote"
|
|
assert response["browser"]["host"] == "worker-17"
|
|
assert os.environ["EIGENT_CDP_URL"] == "http://worker-17:9222"
|
|
assert os.environ["browser_port"] == "9222"
|
|
|
|
tool_controller._clear_connected_cdp_browser()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_open_browser_login_uses_dedicated_cookie_port_when_existing(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
monkeypatch.delenv("EIGENT_LOGIN_BROWSER_CDP_PORT", raising=False)
|
|
|
|
with patch(
|
|
"app.controller.tool_controller._is_port_in_use",
|
|
return_value=True,
|
|
):
|
|
response = await tool_controller.open_browser_login()
|
|
|
|
assert response["success"] is True
|
|
assert response["cdp_port"] == 9323
|
|
assert response["session_id"] == "user_login"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_browser_status_uses_dedicated_cookie_port(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
monkeypatch.delenv("EIGENT_LOGIN_BROWSER_CDP_PORT", raising=False)
|
|
|
|
with patch(
|
|
"app.controller.tool_controller._is_port_in_use",
|
|
return_value=True,
|
|
) as is_port_in_use:
|
|
response = await tool_controller.browser_status()
|
|
|
|
assert response == {"is_open": True, "cdp_port": 9323}
|
|
is_port_in_use.assert_called_once_with(9323)
|
|
|
|
def test_remote_browser_hands_rejects_async_manifest(self):
|
|
class _AsyncManifestHands:
|
|
async def get_capability_manifest(self):
|
|
return {"deployment": "remote_cluster"}
|
|
|
|
assert not tool_controller._is_remote_browser_hands(
|
|
_AsyncManifestHands()
|
|
)
|
|
|
|
def test_remote_cdp_endpoint_uses_shared_validation(self):
|
|
with patch(
|
|
"app.controller.tool_controller.is_cdp_url_available",
|
|
return_value=True,
|
|
) as is_cdp_url_available:
|
|
assert tool_controller._is_cdp_endpoint_available(
|
|
"http://worker-17:9222"
|
|
)
|
|
|
|
is_cdp_url_available.assert_called_once_with("http://worker-17:9222")
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestToolControllerIntegration:
|
|
"""Integration tests for tool controller."""
|
|
|
|
def test_install_notion_tool_endpoint_integration(
|
|
self, client: TestClient
|
|
):
|
|
"""Test install Notion tool endpoint through FastAPI test client."""
|
|
tool_name = "notion"
|
|
|
|
mock_toolkit = AsyncMock()
|
|
mock_tools = [MagicMock(), MagicMock()]
|
|
mock_tools[0].func.__name__ = "create_page"
|
|
mock_tools[1].func.__name__ = "update_page"
|
|
mock_toolkit.get_tools = MagicMock(return_value=mock_tools)
|
|
|
|
with patch(
|
|
"app.controller.tool_controller.NotionMCPToolkit",
|
|
return_value=mock_toolkit,
|
|
):
|
|
response = client.post(f"/install/tool/{tool_name}")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["tools"] == ["create_page", "update_page"]
|
|
assert data["count"] == 2
|
|
|
|
def test_install_unknown_tool_endpoint_integration(
|
|
self, client: TestClient
|
|
):
|
|
"""Test install unknown tool endpoint through FastAPI test client."""
|
|
tool_name = "unknown_tool"
|
|
|
|
response = client.post(f"/install/tool/{tool_name}")
|
|
|
|
assert response.status_code == 404
|
|
|
|
def test_install_notion_tool_endpoint_with_connection_error(
|
|
self, client: TestClient
|
|
):
|
|
"""Test install Notion tool endpoint when connection fails."""
|
|
tool_name = "notion"
|
|
|
|
mock_toolkit = AsyncMock()
|
|
mock_toolkit.connect.side_effect = Exception("Connection failed")
|
|
|
|
with patch(
|
|
"app.controller.tool_controller.NotionMCPToolkit",
|
|
return_value=mock_toolkit,
|
|
):
|
|
response = client.post(f"/install/tool/{tool_name}")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["tools"] == []
|
|
assert "warning" in data
|
|
|
|
def test_launch_cdp_browser_endpoint_integration(
|
|
self, client: TestClient, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
monkeypatch.delenv("EIGENT_CDP_URL", raising=False)
|
|
tool_controller._web_cdp_browser_meta = None
|
|
|
|
with patch(
|
|
"app.controller.tool_controller.ensure_cdp_browser_endpoint",
|
|
return_value="http://127.0.0.1:9222",
|
|
):
|
|
response = client.post("/browser/cdp/launch")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["port"] == 9222
|
|
assert data["browser"]["id"] == "web-cdp-9222"
|
|
assert tool_controller._get_connected_cdp_port() == 9222
|
|
|
|
tool_controller._clear_connected_cdp_browser()
|
|
|
|
def test_connect_list_and_disconnect_cdp_browser_endpoints(
|
|
self, client: TestClient, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
monkeypatch.delenv("EIGENT_CDP_URL", raising=False)
|
|
tool_controller._web_cdp_browser_meta = None
|
|
|
|
with patch(
|
|
"app.controller.tool_controller._is_cdp_available",
|
|
return_value=True,
|
|
):
|
|
connect_response = client.post(
|
|
"/browser/cdp/connect",
|
|
json={"port": 9333, "name": "External Browser (9333)"},
|
|
)
|
|
|
|
assert connect_response.status_code == 200
|
|
connect_data = connect_response.json()
|
|
assert connect_data["success"] is True
|
|
assert connect_data["browser"]["port"] == 9333
|
|
assert connect_data["browser"]["isExternal"] is True
|
|
|
|
list_response = client.get("/browser/cdp/list")
|
|
assert list_response.status_code == 200
|
|
assert list_response.json() == [connect_data["browser"]]
|
|
|
|
disconnect_response = client.delete("/browser/cdp/9333")
|
|
assert disconnect_response.status_code == 200
|
|
assert disconnect_response.json()["success"] is True
|
|
assert tool_controller._get_connected_cdp_port() is None
|
|
|
|
def test_connect_cdp_browser_endpoint_returns_error_when_unreachable(
|
|
self, client: TestClient, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
monkeypatch.delenv("EIGENT_CDP_URL", raising=False)
|
|
tool_controller._web_cdp_browser_meta = None
|
|
|
|
with patch(
|
|
"app.controller.tool_controller._is_cdp_available",
|
|
return_value=False,
|
|
):
|
|
response = client.post("/browser/cdp/connect", json={"port": 9555})
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is False
|
|
assert "9555" in data["error"]
|
|
|
|
|
|
@pytest.mark.model_backend
|
|
class TestToolControllerWithRealMCP:
|
|
"""Tests that require real MCP connections (marked for selective running)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_notion_tool_with_real_connection(self):
|
|
"""Test Notion tool installation with real MCP connection."""
|
|
|
|
# This test would connect to real Notion MCP server
|
|
# Requires actual MCP server setup and credentials
|
|
# Marked as model_backend test for selective execution
|
|
assert True # Placeholder
|
|
|
|
@pytest.mark.very_slow
|
|
async def test_install_and_test_all_notion_tools(self):
|
|
"""Test installation and functionality of all Notion tools (very slow test)."""
|
|
# This test would install and test each Notion tool individually
|
|
# Marked as very_slow for execution only in full test mode
|
|
assert True # Placeholder
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestToolControllerErrorCases:
|
|
"""Test error and edge cases for tool installation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_tool_with_malformed_tool_response(self):
|
|
mock_toolkit = AsyncMock()
|
|
tools = [MagicMock(), object()] # Second item lacks func
|
|
tools[0].func.__name__ = "valid_tool"
|
|
mock_toolkit.get_tools = MagicMock(return_value=tools)
|
|
with patch(
|
|
"app.controller.tool_controller.NotionMCPToolkit",
|
|
return_value=mock_toolkit,
|
|
):
|
|
# Inner except catches the AttributeError and returns success with empty tools
|
|
result = await install_tool("notion")
|
|
assert result["success"] is True
|
|
assert result["tools"] == []
|
|
assert "warning" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_tool_with_none_toolkit(self):
|
|
with patch(
|
|
"app.controller.tool_controller.NotionMCPToolkit",
|
|
return_value=None,
|
|
):
|
|
# Inner except catches AttributeError on None.connect()
|
|
result = await install_tool("notion")
|
|
assert result["success"] is True
|
|
assert result["tools"] == []
|
|
assert "warning" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_tool_with_special_characters_in_name(self):
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await install_tool("notion@#$%")
|
|
assert exc_info.value.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_tool_with_empty_string_name(self):
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await install_tool("")
|
|
assert exc_info.value.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_tool_with_none_name(self):
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await install_tool(None)
|
|
assert exc_info.value.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_notion_tool_partial_failure(self):
|
|
mock_toolkit = AsyncMock()
|
|
mock_toolkit.connect.return_value = None
|
|
tools = [MagicMock(), MagicMock(), MagicMock()]
|
|
tools[0].func.__name__ = "create_page"
|
|
tools[1].func.__name__ = "update_page"
|
|
tools[2].func = None
|
|
mock_toolkit.get_tools = MagicMock(return_value=tools)
|
|
mock_toolkit.disconnect.return_value = None
|
|
with patch(
|
|
"app.controller.tool_controller.NotionMCPToolkit",
|
|
return_value=mock_toolkit,
|
|
):
|
|
# Inner except catches the AttributeError from tools[2].func
|
|
result = await install_tool("notion")
|
|
assert result["success"] is True
|
|
assert result["tools"] == []
|
|
assert "warning" in result
|