126c3b3c17
Providers (admin-switchable, no redeploy): - DeepSeek and any OpenAI-compatible endpoint (OpenRouter, Together, Groq, local Ollama) via a generic Chat Completions adapter + base_url - xAI Grok with Live Search (search_parameters web+X, citations) — grounded tier alongside OpenAI and Gemini - DeepSeek / generic compatible endpoints are ungrounded (no web search); UI shows an amber warning and labels each provider's grounding - Optional env fallbacks DEEPSEEK_API_KEY / XAI_API_KEY UI: replace native <select> (unstyleable white popup on Windows) with a custom dark Dropdown component everywhere — sentiment provider, scanner filters, market sort, indicators, admin universe, user role. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
131 lines
5.9 KiB
Python
131 lines
5.9 KiB
Python
"""Unit tests for runtime sentiment provider configuration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.exceptions import ProviderError, ValidationError
|
|
from app.models.settings import SystemSetting
|
|
from app.services import sentiment_provider_service as sps
|
|
|
|
|
|
@pytest.fixture
|
|
async def session() -> AsyncSession:
|
|
from tests.conftest import _test_session_factory
|
|
|
|
async with _test_session_factory() as session:
|
|
yield session
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_env_keys(monkeypatch):
|
|
"""Default: no env keys, so DB-only behavior is deterministic."""
|
|
monkeypatch.setattr(sps.settings, "openai_api_key", "")
|
|
monkeypatch.setattr(sps.settings, "gemini_api_key", "")
|
|
|
|
|
|
class TestGetConfig:
|
|
async def test_defaults_no_key(self, session: AsyncSession):
|
|
config = await sps.get_sentiment_config(session)
|
|
assert config["provider"] == "openai"
|
|
assert config["model"] == "gpt-4o-mini"
|
|
assert config["api_key_configured"] is False
|
|
assert config["api_key_source"] == "none"
|
|
assert config["web_search"] is True
|
|
assert set(config["valid_providers"]) == {"openai", "gemini", "deepseek", "xai", "openai_compatible"}
|
|
|
|
async def test_never_returns_raw_key(self, session: AsyncSession):
|
|
await sps.update_sentiment_config(session, api_key="sk-secret-123")
|
|
config = await sps.get_sentiment_config(session)
|
|
# No field should leak the key
|
|
assert "sk-secret-123" not in str(config)
|
|
assert config["api_key_configured"] is True
|
|
assert config["api_key_source"] == "database"
|
|
|
|
async def test_env_fallback_reported(self, session: AsyncSession, monkeypatch):
|
|
monkeypatch.setattr(sps.settings, "openai_api_key", "sk-from-env")
|
|
config = await sps.get_sentiment_config(session)
|
|
assert config["api_key_configured"] is True
|
|
assert config["api_key_source"] == "environment"
|
|
|
|
|
|
class TestUpdateConfig:
|
|
async def test_update_provider_and_model(self, session: AsyncSession):
|
|
result = await sps.update_sentiment_config(
|
|
session, provider="gemini", model="gemini-2.0-flash"
|
|
)
|
|
assert result["provider"] == "gemini"
|
|
assert result["model"] == "gemini-2.0-flash"
|
|
|
|
async def test_empty_key_does_not_overwrite(self, session: AsyncSession):
|
|
await sps.update_sentiment_config(session, api_key="sk-original")
|
|
# Subsequent save without a key must keep the original
|
|
await sps.update_sentiment_config(session, provider="openai", api_key="")
|
|
result = await session.execute(
|
|
select(SystemSetting).where(SystemSetting.key == sps.KEY_API_KEY)
|
|
)
|
|
assert result.scalar_one().value == "sk-original"
|
|
|
|
async def test_rejects_invalid_provider(self, session: AsyncSession):
|
|
with pytest.raises(ValidationError):
|
|
await sps.update_sentiment_config(session, provider="anthropic")
|
|
|
|
|
|
class TestBuildProvider:
|
|
async def test_raises_without_key(self, session: AsyncSession):
|
|
with pytest.raises(ProviderError):
|
|
await sps.build_sentiment_provider(session)
|
|
|
|
async def test_builds_openai(self, session: AsyncSession):
|
|
await sps.update_sentiment_config(session, provider="openai", api_key="sk-x")
|
|
provider = await sps.build_sentiment_provider(session)
|
|
assert type(provider).__name__ == "OpenAISentimentProvider"
|
|
|
|
async def test_builds_gemini(self, session: AsyncSession):
|
|
await sps.update_sentiment_config(session, provider="gemini", api_key="g-x")
|
|
provider = await sps.build_sentiment_provider(session)
|
|
assert type(provider).__name__ == "GeminiSentimentProvider"
|
|
|
|
async def test_uses_env_key_when_db_empty(self, session: AsyncSession, monkeypatch):
|
|
monkeypatch.setattr(sps.settings, "openai_api_key", "sk-env")
|
|
await sps.update_sentiment_config(session, provider="openai")
|
|
provider = await sps.build_sentiment_provider(session)
|
|
assert type(provider).__name__ == "OpenAISentimentProvider"
|
|
|
|
async def test_builds_deepseek_with_fixed_base_url(self, session: AsyncSession):
|
|
await sps.update_sentiment_config(session, provider="deepseek", api_key="ds-x")
|
|
provider = await sps.build_sentiment_provider(session)
|
|
assert type(provider).__name__ == "OpenAICompatibleSentimentProvider"
|
|
config = await sps.get_sentiment_config(session)
|
|
assert config["base_url"] == "https://api.deepseek.com"
|
|
assert config["web_search"] is False
|
|
|
|
async def test_builds_xai_with_live_search(self, session: AsyncSession):
|
|
await sps.update_sentiment_config(session, provider="xai", api_key="xai-x")
|
|
provider = await sps.build_sentiment_provider(session)
|
|
assert type(provider).__name__ == "OpenAICompatibleSentimentProvider"
|
|
# xAI is wired with Live Search enabled
|
|
assert provider._live_search is True
|
|
assert provider._extra_body == {"search_parameters": {"mode": "auto", "return_citations": True}}
|
|
config = await sps.get_sentiment_config(session)
|
|
assert config["base_url"] == "https://api.x.ai/v1"
|
|
assert config["web_search"] is True
|
|
|
|
async def test_openai_compatible_requires_base_url(self, session: AsyncSession):
|
|
await sps.update_sentiment_config(session, provider="openai_compatible", api_key="x")
|
|
with pytest.raises(ProviderError):
|
|
await sps.build_sentiment_provider(session)
|
|
|
|
async def test_openai_compatible_with_base_url(self, session: AsyncSession):
|
|
await sps.update_sentiment_config(
|
|
session,
|
|
provider="openai_compatible",
|
|
api_key="x",
|
|
model="llama-3.1-70b",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
)
|
|
provider = await sps.build_sentiment_provider(session)
|
|
assert type(provider).__name__ == "OpenAICompatibleSentimentProvider"
|