diff --git a/pyproject.toml b/pyproject.toml index 98385e32..11061163 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,3 +40,15 @@ include = ["tradingagents*", "cli*"] [tool.setuptools.package-data] cli = ["static/*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra --strict-markers" +markers = [ + "unit: fast isolated unit tests", + "integration: tests requiring external services", + "smoke: quick sanity-check tests", +] +filterwarnings = [ + "ignore::DeprecationWarning", +] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..67554305 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,67 @@ +"""Shared pytest fixtures that prevent CI hangs when API keys are absent.""" + +import os +from unittest.mock import MagicMock, patch + +import pytest + +# --------------------------------------------------------------------------- +# Custom markers +# --------------------------------------------------------------------------- + + +def pytest_configure(config): + for marker in ("unit", "integration", "smoke"): + config.addinivalue_line("markers", f"{marker}: {marker}-level tests") + + +# --------------------------------------------------------------------------- +# Auto-use: placeholder API keys so LLM client init never blocks +# --------------------------------------------------------------------------- + +_API_KEY_ENV_VARS = [ + "OPENAI", + "GOOGLE", + "ANTHROPIC", + "XAI", + "ALPHA_VANTAGE", +] + + +@pytest.fixture(autouse=True) +def _dummy_api_keys(monkeypatch): + for provider in _API_KEY_ENV_VARS: + env_var = f"{provider}_API_KEY" + monkeypatch.setenv(env_var, os.environ.get(env_var, "placeholder")) + + +# --------------------------------------------------------------------------- +# Auto-use: safe DEFAULT_CONFIG override (no real API calls) +# --------------------------------------------------------------------------- + +_SAFE_CONFIG = { + "llm_provider": "openai", + "deep_think_llm": "gpt-5.4-mini", + "quick_think_llm": "gpt-5.4-mini", + "max_debate_rounds": 1, + "max_risk_discuss_rounds": 1, +} + + +@pytest.fixture(autouse=True) +def _safe_default_config(): + with patch.dict("tradingagents.default_config.DEFAULT_CONFIG", _SAFE_CONFIG): + yield + + +# --------------------------------------------------------------------------- +# Reusable mock LLM client +# --------------------------------------------------------------------------- + + +@pytest.fixture() +def mock_llm_client(): + client = MagicMock() + client.get_llm.return_value = MagicMock() + with patch("tradingagents.llm_clients.create_llm_client", return_value=client): + yield client diff --git a/tests/test_google_api_key.py b/tests/test_google_api_key.py index e1607c49..53376ab1 100644 --- a/tests/test_google_api_key.py +++ b/tests/test_google_api_key.py @@ -1,9 +1,12 @@ import unittest from unittest.mock import patch +import pytest + from tradingagents.llm_clients.google_client import GoogleClient +@pytest.mark.unit class TestGoogleApiKeyStandardization(unittest.TestCase): """Verify GoogleClient accepts unified api_key parameter.""" diff --git a/tests/test_model_validation.py b/tests/test_model_validation.py index 50f26318..5392d7cd 100644 --- a/tests/test_model_validation.py +++ b/tests/test_model_validation.py @@ -1,6 +1,8 @@ import unittest import warnings +import pytest + from tradingagents.llm_clients.base_client import BaseLLMClient from tradingagents.llm_clients.model_catalog import get_known_models from tradingagents.llm_clients.validators import validate_model @@ -19,6 +21,7 @@ class DummyLLMClient(BaseLLMClient): return validate_model(self.provider, self.model) +@pytest.mark.unit class ModelValidationTests(unittest.TestCase): def test_cli_catalog_models_are_all_validator_approved(self): for provider, models in get_known_models().items(): diff --git a/tests/test_ticker_symbol_handling.py b/tests/test_ticker_symbol_handling.py index 858d26cd..7fbe5315 100644 --- a/tests/test_ticker_symbol_handling.py +++ b/tests/test_ticker_symbol_handling.py @@ -1,9 +1,12 @@ import unittest +import pytest + from cli.utils import normalize_ticker_symbol from tradingagents.agents.utils.agent_utils import build_instrument_context +@pytest.mark.unit class TickerSymbolHandlingTests(unittest.TestCase): def test_normalize_ticker_symbol_preserves_exchange_suffix(self): self.assertEqual(normalize_ticker_symbol(" cnc.to "), "CNC.TO") diff --git a/tradingagents/llm_clients/factory.py b/tradingagents/llm_clients/factory.py index a9a7e83d..49041f29 100644 --- a/tradingagents/llm_clients/factory.py +++ b/tradingagents/llm_clients/factory.py @@ -1,10 +1,6 @@ from typing import Optional from .base_client import BaseLLMClient -from .openai_client import OpenAIClient -from .anthropic_client import AnthropicClient -from .google_client import GoogleClient -from .azure_client import AzureOpenAIClient # Providers that use the OpenAI-compatible chat completions API _OPENAI_COMPATIBLE = ( @@ -20,6 +16,9 @@ def create_llm_client( ) -> BaseLLMClient: """Create an LLM client for the specified provider. + Client modules are imported lazily so that collecting tests or importing + the package does not trigger heavy LLM SDK initialization. + Args: provider: LLM provider name model: Model name/identifier @@ -35,15 +34,19 @@ def create_llm_client( provider_lower = provider.lower() if provider_lower in _OPENAI_COMPATIBLE: + from .openai_client import OpenAIClient return OpenAIClient(model, base_url, provider=provider_lower, **kwargs) if provider_lower == "anthropic": + from .anthropic_client import AnthropicClient return AnthropicClient(model, base_url, **kwargs) if provider_lower == "google": + from .google_client import GoogleClient return GoogleClient(model, base_url, **kwargs) if provider_lower == "azure": + from .azure_client import AzureOpenAIClient return AzureOpenAIClient(model, base_url, **kwargs) raise ValueError(f"Unsupported LLM provider: {provider}")