# ========= 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 asyncio import os import tempfile from collections.abc import AsyncGenerator, Generator from pathlib import Path from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch import pytest from dotenv import load_dotenv from fastapi import FastAPI from fastapi.testclient import TestClient # Load environment variables load_dotenv() def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption( "--full-test-mode", action="store_true", help="Run all tests" ) parser.addoption( "--default-test-mode", action="store_true", help="Run all tests except the very slow ones", ) parser.addoption( "--fast-test-mode", action="store_true", help="Run only tests without LLM inference", ) parser.addoption( "--llm-test-only", action="store_true", help="Run only tests with LLM inference except the very slow ones", ) parser.addoption( "--very-slow-test-only", action="store_true", help="Run only the very slow tests", ) def pytest_collection_modifyitems( config: pytest.Config, items: list[pytest.Item] ) -> None: if config.getoption("--llm-test-only"): skip_fast = pytest.mark.skip(reason="Skipped for llm test only") for item in items: if "model_backend" not in item.keywords: item.add_marker(skip_fast) return elif config.getoption("--very-slow-test-only"): skip_fast = pytest.mark.skip(reason="Skipped for very slow test only") for item in items: if "very_slow" not in item.keywords: item.add_marker(skip_fast) return # Run all tests in full test mode elif config.getoption("--full-test-mode"): return # Skip all tests involving LLM inference both remote # (including OpenAI API) and local ones, since they are slow # and may drain money if fast test mode is enabled. elif config.getoption("--fast-test-mode"): skip = pytest.mark.skip(reason="Skipped for fast test mode") for item in items: if "optional" in item.keywords or "model_backend" in item.keywords: item.add_marker(skip) return else: skip_full_test = pytest.mark.skip( reason="Very slow test runs only in full test mode" ) for item in items: if "very_slow" in item.keywords: item.add_marker(skip_full_test) return @pytest.fixture def temp_dir() -> Generator[Path, None, None]: """Create a temporary directory for test files.""" with tempfile.TemporaryDirectory() as temp_dir: yield Path(temp_dir) @pytest.fixture def sample_file_path(temp_dir: Path) -> Path: """Create a sample file for testing.""" file_path = temp_dir / "test_file.txt" file_path.write_text("Sample content for testing") return file_path @pytest.fixture def sample_env_path(temp_dir: Path) -> Path: """Create a sample .env file for testing.""" env_path = temp_dir / ".env" env_path.write_text("SAMPLE_ENV_VAR=test_value\nOPENAI_API_KEY=test_key") return env_path @pytest.fixture def mock_openai_api(): """Mock OpenAI API calls.""" with patch("openai.OpenAI") as mock_openai: mock_client = MagicMock() mock_openai.return_value = mock_client # Mock chat completion mock_response = MagicMock() mock_response.choices = [MagicMock()] mock_response.choices[0].message.content = "Test response" mock_response.usage.total_tokens = 100 mock_client.chat.completions.create.return_value = mock_response yield mock_client @pytest.fixture def mock_model_backend(): """Mock model backend for testing.""" with patch("camel.models.ModelFactory.create") as mock_create: backend = MagicMock() backend.model_type = "gpt-4" backend.model_config_dict = {"max_tokens": 4096} backend.current_model = MagicMock() backend.current_model.model_type = "gpt-4" mock_create.return_value = backend yield backend @pytest.fixture def mock_camel_agent(): """Mock CAMEL agent for testing.""" agent = MagicMock() # Use MagicMock instead of AsyncMock agent.role_name = "test_agent" agent.agent_id = "test_agent_123" # Make step method return proper structure with both .msg and .msgs[0] mock_response = MagicMock() mock_message = MagicMock() mock_message.content = "Test agent response" mock_message.parsed = None mock_response.msg = mock_message mock_response.msgs = [ mock_message ] # msgs[0] should point to the same content mock_response.info = {"usage": {"total_tokens": 50}} agent.step.return_value = mock_response agent.astep = AsyncMock() agent.astep.return_value.msg.content = "Test async agent response" agent.astep.return_value.msg.parsed = None agent.astep.return_value.info = {"usage": {"total_tokens": 50}} agent.add_tools = MagicMock() # Add this for install_mcp tests agent.chat_history = [] # Add this for chat history tests return agent @pytest.fixture def mock_task(): """Mock CAMEL Task for testing.""" task = MagicMock() task.id = "test_task_123" task.content = "Test task content" task.result = None task.state = "OPEN" # Changed from CREATED to OPEN task.additional_info = {} task.parent = None task.subtasks = [] return task @pytest.fixture def mock_request(): """Mock FastAPI Request object.""" request = AsyncMock() request.is_disconnected = AsyncMock(return_value=False) request.state = SimpleNamespace() return request @pytest.fixture def app() -> FastAPI: """Create FastAPI test application.""" from fastapi import FastAPI from app.controller.chat_controller import router as chat_router from app.controller.model_controller import router as model_router from app.controller.task_controller import router as task_router from app.controller.tool_controller import router as tool_router app = FastAPI() app.include_router(chat_router) app.include_router(model_router) app.include_router(task_router) app.include_router(tool_router) return app @pytest.fixture def client(app: FastAPI) -> Generator[TestClient, None, None]: """Create test client.""" with TestClient(app) as test_client: yield test_client @pytest.fixture def mock_task_lock(): """Mock TaskLock for testing.""" task_lock = MagicMock() task_lock.id = "test_task_123" task_lock.status = "OPEN" # Changed from CREATED to OPEN task_lock.queue = asyncio.Queue() task_lock.get_queue = AsyncMock() task_lock.put_queue = AsyncMock() task_lock.put_human_input = AsyncMock() task_lock.add_background_task = MagicMock() return task_lock @pytest.fixture def mock_workforce(): """Mock Workforce for testing.""" workforce = MagicMock() workforce._running = False workforce.eigent_make_sub_tasks = MagicMock(return_value=[]) workforce.eigent_start = AsyncMock() workforce.add_single_agent_worker = MagicMock() workforce.pause = MagicMock() workforce.resume = MagicMock() workforce.stop = MagicMock() workforce.stop_gracefully = MagicMock() return workforce @pytest.fixture def mock_worker_with_agent(): """Mock worker with agent_id for SingleAgentWorker tests.""" worker = MagicMock() worker.agent_id = "test_agent_123" worker.astep = AsyncMock() worker.step = MagicMock() # Mock response structure mock_response = MagicMock() mock_response.msg = MagicMock() mock_response.msg.content = "Test worker response" mock_response.msg.parsed = {"result": "test"} mock_response.info = {"usage": {"total_tokens": 50}} worker.astep.return_value = mock_response worker.step.return_value = mock_response return worker @pytest.fixture(scope="function") def event_loop(): """Create an instance of the default event loop for the test session.""" loop = asyncio.new_event_loop() yield loop loop.close() @pytest.fixture def mock_environment_variables(): """Mock environment variables for testing.""" env_vars = { "OPENAI_API_KEY": "test_key", "OPENAI_API_BASE_URL": "https://api.openai.com/v1", "CAMEL_MODEL_LOG_ENABLED": "true", "CAMEL_LOG_DIR": "/tmp/test_logs", "file_save_path": "/tmp/test_files", "browser_port": "8080", } with patch.dict(os.environ, env_vars, clear=False): yield env_vars @pytest.fixture def sample_chat_data(): """Sample chat data for testing.""" return { "task_id": "test_task_123", "project_id": "test_project_456", "email": "test@example.com", "question": "Create a simple Python script", "attaches": [], "model_type": "gpt-4", "model_platform": "openai", "api_key": "test_key", "api_url": "https://api.openai.com/v1", "new_agents": [], "env_path": ".env", "browser_port": 8080, "summary_prompt": "", } @pytest.fixture def sample_task_content(): """Sample task content for testing.""" return { "id": "test_task_123", "content": "Test task content", "state": "OPEN", # Changed from CREATED to OPEN } # Async fixtures @pytest.fixture async def async_mock_agent() -> AsyncGenerator[AsyncMock, None]: """Async mock agent for testing.""" agent = AsyncMock() agent.role_name = "async_test_agent" agent.agent_id = "async_test_agent_456" # Mock async step method mock_response = MagicMock() mock_response.msg.content = "Async test response" mock_response.msg.parsed = {"test": "data"} mock_response.info = {"usage": {"total_tokens": 75}} agent.astep.return_value = mock_response yield agent # Safety net: clean up any MagicMock-named directories that tests may # accidentally create when mock objects are used as file paths. @pytest.fixture(autouse=True, scope="session") def _cleanup_magicmock_dirs(): """Remove MagicMock-named directories from backend/ after test session.""" yield import shutil backend_dir = Path(__file__).parent.parent for entry in backend_dir.iterdir(): if "MagicMock" in entry.name: shutil.rmtree(entry, ignore_errors=True) # Markers for test categorization pytest_plugins = ["pytest_asyncio"] def pytest_configure(config): """Configure pytest markers.""" config.addinivalue_line( "markers", "model_backend: mark test as requiring model backend" ) config.addinivalue_line( "markers", "very_slow: mark test as very slow (requires full test mode)", ) config.addinivalue_line( "markers", "optional: mark test as optional (skipped in fast mode)" ) config.addinivalue_line( "markers", "integration: mark test as integration test" ) config.addinivalue_line("markers", "unit: mark test as unit test")