feat(07-03): classifier wired to load_provider_config + ai_config stub removed — D-04/D-06
- Remove _ProviderConfigStub from services/ai_config.py (replaced by real ProviderConfig) - Add module-level import: from ai.provider_config import ProviderConfig, PROVIDER_DEFAULTS - load_provider_config() now returns ProviderConfig (no lazy import inside function body) - classifier.py: replace inline _settings dict with load_provider_config(session) call (D-06) - Per-user override path builds ProviderConfig from PROVIDER_DEFAULTS (no api_key — T-07-06) - Fallback to app_settings defaults when load_provider_config returns None (D-15) - Truncation delegated to provider._truncate() — no more text slices in classifier (D-12/D-13) - Promote test_api_key_encrypt_decrypt to passing (round-trip + domain salt isolation) - Update test_classifier.py: test_per_user_provider + test_default_provider_fallback use ProviderConfig assertions
This commit is contained in:
@@ -119,13 +119,17 @@ async def test_classifier_with_mock_provider(isolated_data_dir):
|
||||
@pytest.mark.asyncio
|
||||
async def test_per_user_provider(db_session):
|
||||
"""When ai_provider='openai' and ai_model='gpt-4o' are passed to the classifier,
|
||||
it resolves _settings['active_provider'] == 'openai'.
|
||||
it builds a ProviderConfig with provider_id='openai' and model='gpt-4o'.
|
||||
|
||||
DOC-03: AI provider/model comes from the user's DB record (passed through from
|
||||
_run) not from global config or the retired load_settings() flat file (D-14).
|
||||
|
||||
Plan 07-03: get_provider now receives a ProviderConfig object (D-06), not a
|
||||
raw dict. Assertions updated accordingly.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
from ai.base import ClassificationResult
|
||||
from ai.provider_config import ProviderConfig
|
||||
from services.classifier import classify_document
|
||||
import uuid
|
||||
|
||||
@@ -136,10 +140,10 @@ async def test_per_user_provider(db_session):
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.user_id = user_id
|
||||
|
||||
captured_settings = {}
|
||||
captured_configs = []
|
||||
|
||||
def capture_get_provider(settings):
|
||||
captured_settings.update(settings)
|
||||
def capture_get_provider(config):
|
||||
captured_configs.append(config)
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.classify = AsyncMock(return_value=ClassificationResult(
|
||||
topics=[], suggested_new_topics=[], reasoning=""
|
||||
@@ -156,9 +160,11 @@ async def test_per_user_provider(db_session):
|
||||
patch("services.classifier.get_provider", side_effect=capture_get_provider):
|
||||
await classify_document(mock_session, doc_id, ai_provider="openai", ai_model="gpt-4o")
|
||||
|
||||
assert captured_settings.get("active_provider") == "openai"
|
||||
assert "openai" in captured_settings.get("providers", {})
|
||||
assert captured_settings["providers"]["openai"]["model"] == "gpt-4o"
|
||||
assert len(captured_configs) == 1
|
||||
config = captured_configs[0]
|
||||
assert isinstance(config, ProviderConfig), "get_provider must receive a ProviderConfig (D-06)"
|
||||
assert config.provider_id == "openai"
|
||||
assert config.model == "gpt-4o"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -224,13 +230,18 @@ async def test_celery_task_uses_user_provider(db_session):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_provider_fallback(db_session):
|
||||
"""When user.ai_provider is None, the classifier receives config.settings.default_ai_provider.
|
||||
"""When user.ai_provider is None, the classifier uses the system_settings DB row or env fallback.
|
||||
|
||||
D-15: fallback chain is user.ai_provider → DEFAULT_AI_PROVIDER env var →
|
||||
D-15: fallback chain is user.ai_provider → system_settings DB → DEFAULT_AI_PROVIDER env var →
|
||||
code default 'ollama' (CONTEXT.md D-15).
|
||||
|
||||
Plan 07-03: get_provider now receives a ProviderConfig (D-06). When ai_provider=None
|
||||
and load_provider_config returns None, the classifier falls back to
|
||||
app_settings.default_ai_provider. Assertions updated from dict to ProviderConfig.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
from ai.base import ClassificationResult
|
||||
from ai.provider_config import ProviderConfig
|
||||
from services.classifier import classify_document
|
||||
import uuid
|
||||
|
||||
@@ -241,10 +252,10 @@ async def test_default_provider_fallback(db_session):
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.user_id = user_id
|
||||
|
||||
captured_settings = {}
|
||||
captured_configs = []
|
||||
|
||||
def capture_get_provider(settings):
|
||||
captured_settings.update(settings)
|
||||
def capture_get_provider(config):
|
||||
captured_configs.append(config)
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.classify = AsyncMock(return_value=ClassificationResult(
|
||||
topics=[], suggested_new_topics=[], reasoning=""
|
||||
@@ -255,6 +266,7 @@ async def test_default_provider_fallback(db_session):
|
||||
patch("services.classifier.storage.load_topics_for_user", AsyncMock(return_value=[])), \
|
||||
patch("services.classifier.storage.load_topics", AsyncMock(return_value=[])), \
|
||||
patch("services.classifier.storage.update_document_topics", AsyncMock(return_value=None)), \
|
||||
patch("services.classifier.load_provider_config", AsyncMock(return_value=None)), \
|
||||
patch("services.classifier.get_provider", side_effect=capture_get_provider):
|
||||
|
||||
mock_session = AsyncMock()
|
||||
@@ -262,5 +274,8 @@ async def test_default_provider_fallback(db_session):
|
||||
# Pass ai_provider=None to trigger the default fallback (D-15)
|
||||
await classify_document(mock_session, doc_id, ai_provider=None, ai_model=None)
|
||||
|
||||
assert len(captured_configs) == 1
|
||||
config = captured_configs[0]
|
||||
assert isinstance(config, ProviderConfig), "get_provider must receive a ProviderConfig (D-06)"
|
||||
# Should fall back to app_settings.default_ai_provider = "ollama"
|
||||
assert captured_settings.get("active_provider") == "ollama"
|
||||
assert config.provider_id == "ollama"
|
||||
|
||||
Reference in New Issue
Block a user