|
1 | 1 | import pytest |
2 | 2 | import sys |
| 3 | +import types |
| 4 | +import importlib.util |
| 5 | +from pathlib import Path |
3 | 6 | from unittest.mock import AsyncMock, MagicMock, patch, Mock, PropertyMock |
4 | 7 |
|
5 | | -# Mock consts module first to avoid ModuleNotFoundError |
6 | | -consts_mock = MagicMock() |
7 | | -consts_mock.const = MagicMock() |
8 | | -# Set required constants in consts.const |
9 | | -consts_mock.const.MINIO_ENDPOINT = "http://localhost:9000" |
10 | | -consts_mock.const.MINIO_ACCESS_KEY = "test_access_key" |
11 | | -consts_mock.const.MINIO_SECRET_KEY = "test_secret_key" |
12 | | -consts_mock.const.MINIO_REGION = "us-east-1" |
13 | | -consts_mock.const.MINIO_DEFAULT_BUCKET = "test-bucket" |
14 | | -consts_mock.const.POSTGRES_HOST = "localhost" |
15 | | -consts_mock.const.POSTGRES_USER = "test_user" |
16 | | -consts_mock.const.NEXENT_POSTGRES_PASSWORD = "test_password" |
17 | | -consts_mock.const.POSTGRES_DB = "test_db" |
18 | | -consts_mock.const.POSTGRES_PORT = 5432 |
19 | | -consts_mock.const.DEFAULT_TENANT_ID = "default_tenant" |
20 | | -consts_mock.const.LOCAL_MCP_SERVER = "http://localhost:5011" |
21 | | -consts_mock.const.MODEL_CONFIG_MAPPING = {"llm": "llm_config"} |
22 | | -consts_mock.const.LANGUAGE = {"ZH": "zh"} |
23 | | - |
24 | | -# Add the mocked consts module to sys.modules |
25 | | -sys.modules['consts'] = consts_mock |
26 | | -sys.modules['consts.const'] = consts_mock.const |
| 8 | +TEST_ROOT = Path(__file__).resolve().parents[2] |
| 9 | +PROJECT_ROOT = TEST_ROOT.parent |
| 10 | + |
| 11 | +# Ensure project backend package is found before test/backend |
| 12 | +for _path in (str(PROJECT_ROOT), str(TEST_ROOT)): |
| 13 | + if _path not in sys.path: |
| 14 | + sys.path.insert(0, _path) |
| 15 | + |
| 16 | +from test.common.env_test_utils import bootstrap_env |
| 17 | + |
| 18 | +env_state = bootstrap_env() |
| 19 | +consts_const = env_state["mock_const"] |
| 20 | + |
| 21 | +# Utilities --------------------------------------------------------------- |
| 22 | +def _create_stub_module(name: str, **attrs): |
| 23 | + """Return a lightweight module stub with the provided attributes.""" |
| 24 | + module = types.ModuleType(name) |
| 25 | + for attr_name, attr_value in attrs.items(): |
| 26 | + setattr(module, attr_name, attr_value) |
| 27 | + return module |
| 28 | + |
| 29 | + |
| 30 | +# Configure required constants via shared bootstrap env |
| 31 | +consts_const.MINIO_ENDPOINT = "http://localhost:9000" |
| 32 | +consts_const.MINIO_ACCESS_KEY = "test_access_key" |
| 33 | +consts_const.MINIO_SECRET_KEY = "test_secret_key" |
| 34 | +consts_const.MINIO_REGION = "us-east-1" |
| 35 | +consts_const.MINIO_DEFAULT_BUCKET = "test-bucket" |
| 36 | +consts_const.POSTGRES_HOST = "localhost" |
| 37 | +consts_const.POSTGRES_USER = "test_user" |
| 38 | +consts_const.NEXENT_POSTGRES_PASSWORD = "test_password" |
| 39 | +consts_const.POSTGRES_DB = "test_db" |
| 40 | +consts_const.POSTGRES_PORT = 5432 |
| 41 | +consts_const.DEFAULT_TENANT_ID = "default_tenant" |
| 42 | +consts_const.LOCAL_MCP_SERVER = "http://localhost:5011" |
| 43 | +consts_const.MODEL_CONFIG_MAPPING = {"llm": "llm_config"} |
| 44 | +consts_const.LANGUAGE = {"ZH": "zh"} |
| 45 | +consts_const.DATA_PROCESS_SERVICE = "https://example.com/data-process" |
27 | 46 |
|
28 | 47 | # Mock utils module |
29 | 48 | utils_mock = MagicMock() |
|
38 | 57 | # if the testing environment does not have it available. |
39 | 58 | boto3_mock = MagicMock() |
40 | 59 | sys.modules['boto3'] = boto3_mock |
| 60 | +sys.modules['dotenv'] = MagicMock(load_dotenv=MagicMock()) |
41 | 61 |
|
42 | 62 | # Mock the entire client module |
43 | 63 | client_mock = MagicMock() |
|
49 | 69 |
|
50 | 70 | # Add the mocked client module to sys.modules |
51 | 71 | sys.modules['backend.database.client'] = client_mock |
| 72 | +sys.modules['database.client'] = _create_stub_module( |
| 73 | + "database.client", |
| 74 | + minio_client=MagicMock(), |
| 75 | + postgres_client=MagicMock(), |
| 76 | + db_client=MagicMock(), |
| 77 | + get_db_session=MagicMock(), |
| 78 | + as_dict=MagicMock(), |
| 79 | +) |
52 | 80 |
|
53 | 81 | # Mock external dependencies before imports |
54 | | -sys.modules['nexent.core.utils.observer'] = MagicMock() |
| 82 | +mock_message_observer = MagicMock() |
| 83 | +sys.modules['nexent.core.utils.observer'] = MagicMock(MessageObserver=mock_message_observer) |
55 | 84 | sys.modules['nexent.core.agents.agent_model'] = MagicMock() |
56 | 85 | sys.modules['smolagents.agents'] = MagicMock() |
57 | 86 | sys.modules['smolagents.utils'] = MagicMock() |
58 | 87 | sys.modules['services.remote_mcp_service'] = MagicMock() |
| 88 | +database_module = _create_stub_module("database") |
| 89 | +sys.modules['database'] = database_module |
59 | 90 | sys.modules['database.agent_db'] = MagicMock() |
60 | 91 | sys.modules['database.tool_db'] = MagicMock() |
61 | 92 | sys.modules['database.model_management_db'] = MagicMock() |
|
67 | 98 | sys.modules['utils.model_name_utils'] = MagicMock() |
68 | 99 | sys.modules['langchain_core.tools'] = MagicMock() |
69 | 100 | sys.modules['services.memory_config_service'] = MagicMock() |
| 101 | +# Build services module hierarchy with minimal functionality |
| 102 | +services_module = _create_stub_module("services") |
| 103 | +sys.modules['services'] = services_module |
| 104 | +sys.modules['services.image_service'] = _create_stub_module( |
| 105 | + "services.image_service", get_vlm_model=MagicMock(return_value="stub_vlm") |
| 106 | +) |
| 107 | +sys.modules['services.file_management_service'] = _create_stub_module( |
| 108 | + "services.file_management_service", |
| 109 | + get_llm_model=MagicMock(return_value="stub_llm_model"), |
| 110 | +) |
| 111 | +sys.modules['services.tool_configuration_service'] = _create_stub_module( |
| 112 | + "services.tool_configuration_service", |
| 113 | + initialize_tools_on_startup=AsyncMock(), |
| 114 | +) |
| 115 | +# Build top-level nexent module to avoid importing the real package |
| 116 | +nexent_module = _create_stub_module( |
| 117 | + "nexent", |
| 118 | + MessageObserver=mock_message_observer, |
| 119 | +) |
| 120 | +sys.modules['nexent'] = nexent_module |
| 121 | + |
| 122 | +# Create nested modules for nexent.core to satisfy imports safely |
| 123 | +sys.modules['nexent.core'] = _create_stub_module("nexent.core") |
| 124 | +sys.modules['nexent.core.agents'] = _create_stub_module("nexent.core.agents") |
| 125 | +sys.modules['nexent.core.utils'] = _create_stub_module("nexent.core.utils") |
70 | 126 | sys.modules['nexent.memory.memory_service'] = MagicMock() |
71 | 127 |
|
72 | 128 | # Create mock classes that might be imported |
73 | 129 | mock_agent_config = MagicMock() |
74 | 130 | mock_model_config = MagicMock() |
75 | 131 | mock_tool_config = MagicMock() |
76 | 132 | mock_agent_run_info = MagicMock() |
77 | | -mock_message_observer = MagicMock() |
78 | 133 |
|
79 | 134 | sys.modules['nexent.core.agents.agent_model'].AgentConfig = mock_agent_config |
80 | 135 | sys.modules['nexent.core.agents.agent_model'].ModelConfig = mock_model_config |
|
85 | 140 | # Mock BASE_BUILTIN_MODULES |
86 | 141 | sys.modules['smolagents.utils'].BASE_BUILTIN_MODULES = ["os", "sys", "json"] |
87 | 142 |
|
88 | | -# Now import the module under test |
| 143 | +# Provide lightweight smolagents package to prevent circular imports |
| 144 | +smolagents_module = _create_stub_module("smolagents") |
| 145 | +smolagents_tools_module = _create_stub_module("smolagents.tools", Tool=MagicMock()) |
| 146 | +smolagents_module.tools = smolagents_tools_module |
| 147 | +sys.modules['smolagents'] = smolagents_module |
| 148 | +sys.modules['smolagents.tools'] = smolagents_tools_module |
| 149 | + |
| 150 | +# Ensure real backend.agents.create_agent_info is available and uses our stubs |
| 151 | +backend_pkg = sys.modules.get("backend") |
| 152 | +if backend_pkg is None: |
| 153 | + backend_pkg = types.ModuleType("backend") |
| 154 | + backend_pkg.__path__ = [str((TEST_ROOT.parent) / "backend")] |
| 155 | + sys.modules["backend"] = backend_pkg |
| 156 | + |
| 157 | +agents_pkg = sys.modules.get("backend.agents") |
| 158 | +if agents_pkg is None: |
| 159 | + agents_pkg = types.ModuleType("backend.agents") |
| 160 | + agents_pkg.__path__ = [str((TEST_ROOT.parent) / "backend" / "agents")] |
| 161 | + sys.modules["backend.agents"] = agents_pkg |
| 162 | + setattr(backend_pkg, "agents", agents_pkg) |
| 163 | + |
| 164 | +create_agent_info_path = (TEST_ROOT.parent / "backend" / "agents" / "create_agent_info.py") |
| 165 | +spec = importlib.util.spec_from_file_location( |
| 166 | + "backend.agents.create_agent_info", create_agent_info_path |
| 167 | +) |
| 168 | +create_agent_info_module = importlib.util.module_from_spec(spec) |
| 169 | +sys.modules["backend.agents.create_agent_info"] = create_agent_info_module |
| 170 | +assert spec.loader is not None |
| 171 | +spec.loader.exec_module(create_agent_info_module) |
| 172 | +setattr(agents_pkg, "create_agent_info", create_agent_info_module) |
| 173 | + |
| 174 | +# Now import the symbols under test |
89 | 175 | from backend.agents.create_agent_info import ( |
90 | 176 | discover_langchain_tools, |
91 | 177 | create_tool_config_list, |
@@ -282,6 +368,43 @@ async def test_create_tool_config_list_with_knowledge_base_tool(self): |
282 | 368 | last_call = mock_tool_config.call_args_list[-1] |
283 | 369 | assert last_call[1]['class_name'] == "KnowledgeBaseSearchTool" |
284 | 370 |
|
| 371 | + @pytest.mark.asyncio |
| 372 | + async def test_create_tool_config_list_with_analyze_text_file_tool(self): |
| 373 | + """Ensure AnalyzeTextFileTool receives text-specific metadata.""" |
| 374 | + mock_tool_instance = MagicMock() |
| 375 | + mock_tool_instance.class_name = "AnalyzeTextFileTool" |
| 376 | + mock_tool_config.return_value = mock_tool_instance |
| 377 | + |
| 378 | + with patch('backend.agents.create_agent_info.discover_langchain_tools', return_value=[]), \ |
| 379 | + patch('backend.agents.create_agent_info.search_tools_for_sub_agent') as mock_search_tools, \ |
| 380 | + patch('backend.agents.create_agent_info.get_llm_model') as mock_get_llm_model, \ |
| 381 | + patch('backend.agents.create_agent_info.minio_client', new_callable=MagicMock) as mock_minio_client: |
| 382 | + |
| 383 | + mock_search_tools.return_value = [ |
| 384 | + { |
| 385 | + "class_name": "AnalyzeTextFileTool", |
| 386 | + "name": "analyze_text_file", |
| 387 | + "description": "Analyze text file tool", |
| 388 | + "inputs": "string", |
| 389 | + "output_type": "array", |
| 390 | + "params": [{"name": "prompt", "default": "describe"}], |
| 391 | + "source": "local", |
| 392 | + "usage": None |
| 393 | + } |
| 394 | + ] |
| 395 | + mock_get_llm_model.return_value = "mock_llm_model" |
| 396 | + |
| 397 | + result = await create_tool_config_list("agent_1", "tenant_1", "user_1") |
| 398 | + |
| 399 | + assert len(result) == 1 |
| 400 | + assert result[0] is mock_tool_instance |
| 401 | + mock_get_llm_model.assert_called_once_with(tenant_id="tenant_1") |
| 402 | + assert mock_tool_instance.metadata == { |
| 403 | + "llm_model": "mock_llm_model", |
| 404 | + "storage_client": mock_minio_client, |
| 405 | + "data_process_service_url": consts_const.DATA_PROCESS_SERVICE, |
| 406 | + } |
| 407 | + |
285 | 408 |
|
286 | 409 | class TestCreateAgentConfig: |
287 | 410 | """Tests for the create_agent_config function""" |
|
0 commit comments