7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
615 lines
22 KiB
Python
615 lines
22 KiB
Python
"""Tests for Google/Gemini LLM provider."""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch
|
|
|
|
from local_deep_research.llm.providers.implementations.google import (
|
|
GoogleProvider,
|
|
)
|
|
|
|
|
|
class TestGoogleProviderMetadata:
|
|
"""Tests for GoogleProvider class metadata."""
|
|
|
|
def test_provider_name(self):
|
|
"""Provider name is correct."""
|
|
assert GoogleProvider.provider_name == "Google Gemini"
|
|
|
|
def test_provider_key(self):
|
|
"""Provider key is correct."""
|
|
assert GoogleProvider.provider_key == "GOOGLE"
|
|
|
|
def test_is_cloud(self):
|
|
"""Google is a cloud provider."""
|
|
assert GoogleProvider.is_cloud is True
|
|
|
|
def test_company_name(self):
|
|
"""Company name is Google."""
|
|
assert GoogleProvider.company_name == "Google"
|
|
|
|
def test_api_key_setting(self):
|
|
"""API key setting is correct."""
|
|
assert GoogleProvider.api_key_setting == "llm.google.api_key"
|
|
|
|
def test_default_model(self):
|
|
"""Default model is empty by design — users must explicitly pick one."""
|
|
assert GoogleProvider.default_model == ""
|
|
|
|
def test_default_base_url(self):
|
|
"""Default base URL is Google's OpenAI-compatible endpoint."""
|
|
assert (
|
|
"generativelanguage.googleapis.com"
|
|
in GoogleProvider.default_base_url
|
|
)
|
|
assert "openai" in GoogleProvider.default_base_url
|
|
|
|
|
|
class TestGoogleRequiresAuth:
|
|
"""Tests for requires_auth_for_models method."""
|
|
|
|
def test_requires_auth_for_models_returns_true(self):
|
|
"""Google requires authentication for model listing."""
|
|
assert GoogleProvider.requires_auth_for_models() is True
|
|
|
|
|
|
class TestGoogleListModelsForApi:
|
|
"""Tests for list_models_for_api method (custom implementation)."""
|
|
|
|
def test_returns_empty_without_api_key(self):
|
|
"""Returns empty list when no API key provided."""
|
|
result = GoogleProvider.list_models_for_api(api_key=None)
|
|
assert result == []
|
|
|
|
def test_returns_empty_with_empty_api_key(self):
|
|
"""Returns empty list when API key is empty string."""
|
|
result = GoogleProvider.list_models_for_api(api_key="")
|
|
assert result == []
|
|
|
|
def test_lists_models_with_valid_key(self):
|
|
"""Returns models when valid API key provided."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"models": [
|
|
{
|
|
"name": "models/gemini-1.5-flash",
|
|
"supportedGenerationMethods": ["generateContent"],
|
|
},
|
|
{
|
|
"name": "models/gemini-1.5-pro",
|
|
"supportedGenerationMethods": ["generateContent"],
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(
|
|
api_key="test-google-key"
|
|
)
|
|
|
|
assert len(result) == 2
|
|
assert {
|
|
"value": "gemini-1.5-flash",
|
|
"label": "gemini-1.5-flash",
|
|
} in result
|
|
assert {
|
|
"value": "gemini-1.5-pro",
|
|
"label": "gemini-1.5-pro",
|
|
} in result
|
|
|
|
def test_strips_models_prefix_from_name(self):
|
|
"""Strips 'models/' prefix from model names."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"models": [
|
|
{
|
|
"name": "models/gemini-2.0-flash-exp",
|
|
"supportedGenerationMethods": ["generateContent"],
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
# Should strip "models/" prefix
|
|
assert result[0]["value"] == "gemini-2.0-flash-exp"
|
|
assert "models/" not in result[0]["value"]
|
|
|
|
def test_handles_model_without_prefix(self):
|
|
"""Handles model names that don't have 'models/' prefix."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"models": [
|
|
{
|
|
"name": "gemini-custom",
|
|
"supportedGenerationMethods": ["generateContent"],
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
assert result[0]["value"] == "gemini-custom"
|
|
|
|
def test_filters_embedding_models(self):
|
|
"""Filters out models that don't support generateContent."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"models": [
|
|
{
|
|
"name": "models/gemini-1.5-flash",
|
|
"supportedGenerationMethods": ["generateContent"],
|
|
},
|
|
{
|
|
"name": "models/embedding-001",
|
|
"supportedGenerationMethods": [
|
|
"embedContent"
|
|
], # Embedding model
|
|
},
|
|
{
|
|
"name": "models/text-embedding-004",
|
|
"supportedGenerationMethods": [
|
|
"embedContent",
|
|
"countTokens",
|
|
],
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
# Only gemini-1.5-flash should be included
|
|
assert len(result) == 1
|
|
assert result[0]["value"] == "gemini-1.5-flash"
|
|
|
|
def test_includes_multimethod_models_with_generate(self):
|
|
"""Includes models that support generateContent plus other methods."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"models": [
|
|
{
|
|
"name": "models/gemini-1.5-pro",
|
|
"supportedGenerationMethods": [
|
|
"generateContent",
|
|
"countTokens",
|
|
"createCachedContent",
|
|
],
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["value"] == "gemini-1.5-pro"
|
|
|
|
def test_passes_api_key_via_header_not_url(self):
|
|
"""Passes API key via the ``x-goog-api-key`` header, not as a
|
|
``?key=...`` query parameter. See issue #4184: the URL is embedded
|
|
in ``requests``/``urllib3`` exception messages, but headers are
|
|
not — so keeping the key out of the URL is the primary
|
|
prevention-by-construction control against credential leakage
|
|
through logs.
|
|
"""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"models": []}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
GoogleProvider.list_models_for_api(api_key="my-test-key")
|
|
|
|
url = mock_get.call_args[0][0]
|
|
headers = mock_get.call_args[1].get("headers") or {}
|
|
|
|
assert "my-test-key" not in url, (
|
|
"API key must not appear in the URL — it would leak "
|
|
"through HTTP exception messages. See issue #4184."
|
|
)
|
|
assert headers.get("x-goog-api-key") == "my-test-key", (
|
|
f"API key must be passed via the x-goog-api-key header. "
|
|
f"Got headers: {headers!r}"
|
|
)
|
|
|
|
def test_uses_native_gemini_api_endpoint(self):
|
|
"""Uses native Gemini API endpoint, not OpenAI-compatible."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"models": []}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
call_args = mock_get.call_args[0]
|
|
url = call_args[0]
|
|
# Should use native API, not /openai endpoint
|
|
assert "generativelanguage.googleapis.com" in url
|
|
assert "/v1beta/models" in url
|
|
assert "/openai" not in url
|
|
|
|
def test_returns_empty_on_non_200_status(self):
|
|
"""Returns empty list when API returns non-200 status."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 401
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="invalid-key")
|
|
|
|
assert result == []
|
|
|
|
def test_returns_empty_on_400_status(self):
|
|
"""Returns empty list on 400 Bad Request."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 400
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
assert result == []
|
|
|
|
def test_returns_empty_on_500_status(self):
|
|
"""Returns empty list on server error."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 500
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
assert result == []
|
|
|
|
def test_returns_empty_on_exception(self):
|
|
"""Returns empty list when exception occurs."""
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.side_effect = Exception("Network error")
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
assert result == []
|
|
|
|
def test_returns_empty_on_connection_error(self):
|
|
"""Returns empty list on connection error."""
|
|
import requests
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.side_effect = requests.exceptions.ConnectionError()
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
assert result == []
|
|
|
|
def test_returns_empty_on_timeout(self):
|
|
"""Returns empty list on timeout."""
|
|
import requests
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.side_effect = requests.exceptions.Timeout()
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
assert result == []
|
|
|
|
def test_handles_empty_models_list(self):
|
|
"""Handles response with empty models list."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"models": []}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
assert result == []
|
|
|
|
def test_handles_missing_models_key(self):
|
|
"""Handles response missing 'models' key."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {} # No 'models' key
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
assert result == []
|
|
|
|
def test_handles_model_missing_supported_methods(self):
|
|
"""Handles model without supportedGenerationMethods."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"models": [
|
|
{
|
|
"name": "models/gemini-1.5-flash",
|
|
# Missing supportedGenerationMethods
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
# Model without supportedGenerationMethods should be skipped
|
|
assert result == []
|
|
|
|
def test_handles_model_missing_name(self):
|
|
"""Handles model without name field."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"models": [
|
|
{
|
|
"supportedGenerationMethods": ["generateContent"],
|
|
# Missing 'name'
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
result = GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
# Model without name should be skipped
|
|
assert result == []
|
|
|
|
def test_ignores_base_url_parameter(self):
|
|
"""Ignores base_url parameter (uses fixed Google endpoint)."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"models": []}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
# Pass a custom base_url - should be ignored
|
|
GoogleProvider.list_models_for_api(
|
|
api_key="test-key", base_url="https://custom-endpoint.com"
|
|
)
|
|
|
|
call_args = mock_get.call_args[0]
|
|
url = call_args[0]
|
|
# Should still use Google's native API
|
|
assert "generativelanguage.googleapis.com" in url
|
|
|
|
def test_uses_10_second_timeout(self):
|
|
"""Uses 10 second timeout for API call."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"models": []}
|
|
|
|
with patch("local_deep_research.security.safe_get") as mock_get:
|
|
mock_get.return_value = mock_response
|
|
|
|
GoogleProvider.list_models_for_api(api_key="test-key")
|
|
|
|
call_kwargs = mock_get.call_args[1]
|
|
assert call_kwargs["timeout"] == 10
|
|
|
|
|
|
class TestGoogleCreateLLM:
|
|
"""Tests for create_llm method (inherited from base with Google settings)."""
|
|
|
|
def test_create_llm_raises_without_api_key(self):
|
|
"""Raises ValueError when API key not configured."""
|
|
with patch(
|
|
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
|
|
) as mock_get_setting:
|
|
mock_get_setting.return_value = None
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
GoogleProvider.create_llm()
|
|
|
|
assert "api key" in str(exc_info.value).lower()
|
|
|
|
def test_create_llm_with_valid_api_key(self):
|
|
"""Successfully creates ChatOpenAI instance with valid API key."""
|
|
|
|
def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
|
|
settings_map = {
|
|
"llm.google.api_key": "test-google-key",
|
|
"llm.max_tokens": None,
|
|
"llm.streaming": None,
|
|
"llm.max_retries": None,
|
|
"llm.request_timeout": None,
|
|
}
|
|
return settings_map.get(key, default)
|
|
|
|
with patch(
|
|
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
|
|
) as mock_get_setting:
|
|
mock_get_setting.side_effect = mock_get_setting_side_effect
|
|
|
|
with patch(
|
|
"local_deep_research.llm.providers.openai_base.ChatOpenAI"
|
|
) as mock_chat:
|
|
mock_llm = Mock()
|
|
mock_chat.return_value = mock_llm
|
|
|
|
result = GoogleProvider.create_llm(model_name="test-model")
|
|
|
|
assert result is mock_llm
|
|
mock_chat.assert_called_once()
|
|
|
|
def test_create_llm_uses_default_model(self):
|
|
"""Raises ValueError when no model name is provided (no silent default)."""
|
|
|
|
def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
|
|
settings_map = {
|
|
"llm.google.api_key": "test-key",
|
|
"llm.max_tokens": None,
|
|
"llm.streaming": None,
|
|
"llm.max_retries": None,
|
|
"llm.request_timeout": None,
|
|
}
|
|
return settings_map.get(key, default)
|
|
|
|
with patch(
|
|
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
|
|
) as mock_get_setting:
|
|
mock_get_setting.side_effect = mock_get_setting_side_effect
|
|
|
|
with pytest.raises(ValueError, match="model not configured"):
|
|
GoogleProvider.create_llm()
|
|
|
|
def test_create_llm_with_custom_model(self):
|
|
"""Uses custom Gemini model when specified."""
|
|
|
|
def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
|
|
settings_map = {
|
|
"llm.google.api_key": "test-key",
|
|
"llm.max_tokens": None,
|
|
"llm.streaming": None,
|
|
"llm.max_retries": None,
|
|
"llm.request_timeout": None,
|
|
}
|
|
return settings_map.get(key, default)
|
|
|
|
with patch(
|
|
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
|
|
) as mock_get_setting:
|
|
mock_get_setting.side_effect = mock_get_setting_side_effect
|
|
|
|
with patch(
|
|
"local_deep_research.llm.providers.openai_base.ChatOpenAI"
|
|
) as mock_chat:
|
|
GoogleProvider.create_llm(model_name="gemini-1.5-pro")
|
|
|
|
call_kwargs = mock_chat.call_args[1]
|
|
assert call_kwargs["model"] == "gemini-1.5-pro"
|
|
|
|
def test_create_llm_uses_google_base_url(self):
|
|
"""Uses Google's OpenAI-compatible endpoint as base URL."""
|
|
|
|
def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
|
|
settings_map = {
|
|
"llm.google.api_key": "test-key",
|
|
"llm.max_tokens": None,
|
|
"llm.streaming": None,
|
|
"llm.max_retries": None,
|
|
"llm.request_timeout": None,
|
|
}
|
|
return settings_map.get(key, default)
|
|
|
|
with patch(
|
|
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
|
|
) as mock_get_setting:
|
|
mock_get_setting.side_effect = mock_get_setting_side_effect
|
|
|
|
with patch(
|
|
"local_deep_research.llm.providers.openai_base.ChatOpenAI"
|
|
) as mock_chat:
|
|
with patch(
|
|
"local_deep_research.llm.providers.openai_base.normalize_url",
|
|
side_effect=lambda x: x,
|
|
):
|
|
GoogleProvider.create_llm(model_name="test-model")
|
|
|
|
call_kwargs = mock_chat.call_args[1]
|
|
assert (
|
|
"generativelanguage.googleapis.com"
|
|
in call_kwargs["base_url"]
|
|
)
|
|
|
|
def test_create_llm_passes_temperature(self):
|
|
"""Passes temperature parameter."""
|
|
|
|
def mock_get_setting_side_effect(key, default=None, *args, **kwargs):
|
|
settings_map = {
|
|
"llm.google.api_key": "test-key",
|
|
"llm.max_tokens": None,
|
|
"llm.streaming": None,
|
|
"llm.max_retries": None,
|
|
"llm.request_timeout": None,
|
|
}
|
|
return settings_map.get(key, default)
|
|
|
|
with patch(
|
|
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
|
|
) as mock_get_setting:
|
|
mock_get_setting.side_effect = mock_get_setting_side_effect
|
|
|
|
with patch(
|
|
"local_deep_research.llm.providers.openai_base.ChatOpenAI"
|
|
) as mock_chat:
|
|
GoogleProvider.create_llm(
|
|
model_name="test-model", temperature=0.3
|
|
)
|
|
|
|
call_kwargs = mock_chat.call_args[1]
|
|
assert call_kwargs["temperature"] == 0.3
|
|
|
|
|
|
class TestGoogleIsAvailable:
|
|
"""Tests for is_available method."""
|
|
|
|
def test_is_available_true_when_key_exists(self):
|
|
"""Returns True when API key is configured."""
|
|
with patch(
|
|
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
|
|
) as mock_get_setting:
|
|
mock_get_setting.return_value = "test-google-key"
|
|
|
|
result = GoogleProvider.is_available()
|
|
assert result is True
|
|
|
|
def test_is_available_false_when_no_key(self):
|
|
"""Returns False when API key is not configured."""
|
|
with patch(
|
|
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
|
|
) as mock_get_setting:
|
|
mock_get_setting.return_value = None
|
|
|
|
result = GoogleProvider.is_available()
|
|
assert result is False
|
|
|
|
def test_is_available_false_when_empty_key(self):
|
|
"""Returns False when API key is empty string."""
|
|
with patch(
|
|
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
|
|
) as mock_get_setting:
|
|
mock_get_setting.return_value = ""
|
|
|
|
result = GoogleProvider.is_available()
|
|
assert result is False
|
|
|
|
def test_is_available_false_on_exception(self):
|
|
"""Returns False when exception occurs."""
|
|
with patch(
|
|
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
|
|
) as mock_get_setting:
|
|
mock_get_setting.side_effect = Exception("Settings error")
|
|
|
|
result = GoogleProvider.is_available()
|
|
assert result is False
|