Files
signal-platform/tests/unit/test_sentiment_provider_service.py
T
dennisthiessen ffb609d38f
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 31s
Deploy / deploy (push) Successful in 23s
Fix xAI sentiment: use Agent Tools web_search (Live Search deprecated)
xAI returned 410 — search_parameters/Live Search is retired. Route xAI
through the Responses API web_search tool instead (same path as OpenAI):
- OpenAISentimentProvider parametrized with base_url / tool_type / source
- xAI builds it against https://api.x.ai/v1 with the web_search tool
- Drop the dead Live Search code from the generic compatible provider
- Frontend label: "xAI Grok — web search"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 14:30:45 +02:00

131 lines
5.8 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_via_responses_web_search(self, session: AsyncSession):
await sps.update_sentiment_config(session, provider="xai", api_key="xai-x")
provider = await sps.build_sentiment_provider(session)
# xAI grounds via the Responses API web_search tool
assert type(provider).__name__ == "OpenAISentimentProvider"
assert provider._tool_type == "web_search"
assert provider._source == "xai"
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"