import os import sys import types import pytest os.environ["DEBUG"] = "false" if "tavily" not in sys.modules: fake_tavily = types.ModuleType("tavily") class _BootstrapTavilyClient: def __init__(self, api_key, api_base_url=None): self.api_key = api_key self.api_base_url = api_base_url def search(self, **kwargs): return {"results": []} fake_tavily.TavilyClient = _BootstrapTavilyClient sys.modules["tavily"] = fake_tavily from landppt.services import deep_research_service as drs from landppt.services.deep_research_service import ( DEEPResearchService, _is_tavily_auth_error, _normalize_secret_value, ) def test_normalize_secret_value_filters_empty_and_masked_values(): assert _normalize_secret_value(None) is None assert _normalize_secret_value("") is None assert _normalize_secret_value(" ") is None assert _normalize_secret_value("********") is None assert _normalize_secret_value(" tvly-good ") == "tvly-good" def test_is_tavily_auth_error_matches_common_messages(): assert _is_tavily_auth_error(Exception("Unauthorized: missing or invalid API key.")) is True assert _is_tavily_auth_error(Exception("invalid_api_key")) is True assert _is_tavily_auth_error(Exception("timeout")) is False @pytest.mark.asyncio async def test_tavily_search_retries_next_key_on_auth_error(monkeypatch): service = DEEPResearchService() async def fake_candidates(): return [ ("user database override", "bad-key"), ("system database default", "good-key"), ] class FakeTavilyClient: def __init__(self, api_key, api_base_url=None): self.api_key = api_key self.api_base_url = api_base_url def search(self, **kwargs): if self.api_key == "bad-key": raise Exception("Unauthorized: missing or invalid API key.") return { "results": [ { "title": "SkillNet", "url": "https://example.com", "content": "ok", "score": 0.9, "published_date": "2026-03-08", } ] } monkeypatch.setattr(service, "_get_tavily_api_key_candidates_async", fake_candidates) monkeypatch.setattr(drs, "TavilyClient", FakeTavilyClient) monkeypatch.setattr(drs.ai_config, "tavily_include_domains", None, raising=False) monkeypatch.setattr(drs.ai_config, "tavily_exclude_domains", None, raising=False) results = await service._tavily_search("SkillNet", "zh") assert results == [ { "title": "SkillNet", "url": "https://example.com", "content": "ok", "score": 0.9, "published_date": "2026-03-08", } ] assert service._active_tavily_key_source == "system database default" @pytest.mark.asyncio async def test_tavily_search_uses_runtime_base_url(monkeypatch): service = DEEPResearchService() created_clients = [] async def fake_candidates(): return [("process environment", "good-key")] async def fake_runtime_config(): return { "base_url": "https://gateway.example.com/tavily", "max_results": 7, "search_depth": "advanced", "include_domains": ["example.com"], "exclude_domains": None, } class FakeTavilyClient: def __init__(self, api_key, api_base_url=None): self.api_key = api_key self.api_base_url = api_base_url created_clients.append(self) def search(self, **kwargs): assert kwargs["max_results"] == 7 assert kwargs["search_depth"] == "advanced" assert kwargs["include_domains"] == ["example.com"] return {"results": []} monkeypatch.setattr(service, "_get_tavily_api_key_candidates_async", fake_candidates) monkeypatch.setattr(service, "_get_tavily_runtime_config_async", fake_runtime_config) monkeypatch.setattr(drs, "TavilyClient", FakeTavilyClient) results = await service._tavily_search("SkillNet", "zh") assert results == [] assert created_clients[0].api_base_url == "https://gateway.example.com/tavily" @pytest.mark.asyncio async def test_tavily_runtime_config_ignores_blank_db_base_url(monkeypatch): import landppt.database.database as database_mod import landppt.database.repositories as repo_mod service = DEEPResearchService(user_id=123) class FakeSession: async def __aenter__(self): return self async def __aexit__(self, exc_type, exc, tb): return False class FakeRepo: def __init__(self, session): self.session = session async def get_all_configs(self, user_id=None): return { "tavily_base_url": { "value": "", "type": "url", "category": "generation_params", "is_user_override": True, } } monkeypatch.setattr(database_mod, "AsyncSessionLocal", lambda: FakeSession()) monkeypatch.setattr(repo_mod, "UserConfigRepository", FakeRepo) config = await service._get_tavily_runtime_config_async() assert config["base_url"] == "https://api.tavily.com"