Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

333 lines
13 KiB
Python

"""High-value edge case tests for LM Studio, OpenRouter, xAI, and IONOS providers.
Focuses on error handling paths, cross-provider consistency, edge cases in
configuration, list_models behavior, and provider-specific quirks.
"""
import pytest
from unittest.mock import Mock, patch
from local_deep_research.llm.providers.implementations.lmstudio import (
LMStudioProvider,
)
from local_deep_research.llm.providers.implementations.openrouter import (
OpenRouterProvider,
)
from local_deep_research.llm.providers.implementations.xai import (
XAIProvider,
)
from local_deep_research.llm.providers.implementations.ionos import (
IONOSProvider,
)
from local_deep_research.llm.providers.openai_base import (
OpenAICompatibleProvider,
)
from local_deep_research.llm.providers.base import BaseLLMProvider
def _lmstudio_settings(url_value, extras=None):
"""Side-effect factory for LM Studio (no API key, URL-based)."""
base = {
"llm.lmstudio.url": url_value,
"llm.max_tokens": None,
}
if extras:
base.update(extras)
def _side_effect(*args, **kwargs):
key = args[0] if args else kwargs.get("key", "")
default = args[1] if len(args) > 1 else kwargs.get("default", None)
return base.get(key, default)
return _side_effect
def _cloud_settings(api_key_setting, api_key_value, extras=None):
"""Build a mock side-effect for cloud provider settings."""
base = {
api_key_setting: api_key_value,
"llm.max_tokens": None,
"llm.streaming": None,
"llm.max_retries": None,
"llm.request_timeout": None,
}
if extras:
base.update(extras)
def _side_effect(key, default=None, *args, **kwargs):
return base.get(key, default)
return _side_effect
# ===========================================================================
# LM Studio edge cases
# ===========================================================================
class TestLMStudioEdgeCases:
"""Edge cases unique to the LM Studio provider."""
def test_is_available_with_non_200_status_codes(self):
"""Various non-200 codes return False."""
for status in [301, 403, 404, 502, 503]:
with patch(
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
) as mock_gs:
mock_gs.side_effect = _lmstudio_settings(
"http://localhost:1234/v1"
)
with patch("local_deep_research.security.safe_get") as mock_get:
mock_response = Mock(status_code=status)
mock_get.return_value = mock_response
assert LMStudioProvider.is_available() is False, (
f"Expected False for HTTP {status}"
)
def test_is_available_timeout_returns_false(self):
"""A connection timeout is handled gracefully."""
with patch(
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
) as mock_gs:
mock_gs.side_effect = _lmstudio_settings("http://localhost:1234/v1")
with patch("local_deep_research.security.safe_get") as mock_get:
mock_get.side_effect = TimeoutError("Connection timed out")
assert LMStudioProvider.is_available() is False
def test_lmstudio_inherits_from_openai_compatible(self):
"""LM Studio provider inherits from OpenAICompatibleProvider."""
assert issubclass(LMStudioProvider, OpenAICompatibleProvider)
assert issubclass(LMStudioProvider, BaseLLMProvider)
# ===========================================================================
# OpenRouter edge cases
# ===========================================================================
class TestOpenRouterEdgeCases:
"""Edge cases unique to the OpenRouter provider."""
def test_create_llm_with_whitespace_only_api_key(self):
"""Whitespace-only API key is normalized as missing and raises (required cloud provider)."""
with patch(
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
) as mock_gs:
mock_gs.side_effect = _cloud_settings(
"llm.openrouter.api_key", " "
)
with pytest.raises(ValueError, match="API key not configured"):
OpenRouterProvider.create_llm(model_name="test-model")
def test_create_llm_error_message_contains_setting_name(self):
"""Error message tells user which setting to configure."""
with patch(
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
) as mock_gs:
mock_gs.side_effect = _cloud_settings(
"llm.openrouter.api_key", None
)
with pytest.raises(ValueError) as exc_info:
OpenRouterProvider.create_llm()
assert "llm.openrouter.api_key" in str(exc_info.value)
def test_list_models_for_api_connection_error_returns_empty(self):
"""Network error returns empty list, not exception."""
with patch("openai.OpenAI") as mock_openai:
mock_openai.side_effect = ConnectionError("Network unreachable")
result = OpenRouterProvider.list_models_for_api(api_key="key")
assert result == []
def test_openrouter_inherits_from_openai_compatible(self):
"""OpenRouter provider inherits from OpenAICompatibleProvider."""
assert issubclass(OpenRouterProvider, OpenAICompatibleProvider)
assert issubclass(OpenRouterProvider, BaseLLMProvider)
def test_create_llm_passes_max_tokens_when_configured(self):
"""max_tokens from settings is forwarded to ChatOpenAI."""
with patch(
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
) as mock_gs:
mock_gs.side_effect = _cloud_settings(
"llm.openrouter.api_key",
"test-key",
extras={"llm.max_tokens": 2048},
)
with patch(
"local_deep_research.llm.providers.openai_base.ChatOpenAI"
) as mock_chat:
mock_chat.return_value = Mock()
OpenRouterProvider.create_llm(model_name="test-model")
call_kwargs = mock_chat.call_args[1]
assert call_kwargs["max_tokens"] == 2048
# ===========================================================================
# xAI edge cases
# ===========================================================================
class TestXAIEdgeCases:
"""Edge cases unique to the xAI Grok provider."""
def test_create_llm_with_empty_string_api_key(self):
"""Empty string API key raises ValueError."""
with patch(
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
) as mock_gs:
mock_gs.side_effect = _cloud_settings("llm.xai.api_key", "")
with pytest.raises(ValueError, match="(?i)api key"):
XAIProvider.create_llm()
def test_create_llm_error_message_contains_xai_setting(self):
"""Error message references the correct xAI setting key."""
with patch(
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
) as mock_gs:
mock_gs.side_effect = _cloud_settings("llm.xai.api_key", None)
with pytest.raises(ValueError) as exc_info:
XAIProvider.create_llm()
assert "llm.xai.api_key" in str(exc_info.value)
def test_list_models_for_api_requires_key(self):
"""xAI requires auth so list_models_for_api returns [] without key."""
result = XAIProvider.list_models_for_api(api_key=None)
assert result == []
def test_is_available_with_exception_in_settings(self):
"""Exception during settings retrieval returns False."""
with patch(
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
) as mock_gs:
mock_gs.side_effect = RuntimeError("settings DB corrupted")
assert XAIProvider.is_available() is False
def test_xai_inherits_from_openai_compatible(self):
"""xAI provider inherits from OpenAICompatibleProvider."""
assert issubclass(XAIProvider, OpenAICompatibleProvider)
assert issubclass(XAIProvider, BaseLLMProvider)
# ===========================================================================
# IONOS edge cases
# ===========================================================================
class TestIONOSEdgeCases:
"""Edge cases unique to the IONOS provider."""
def test_create_llm_with_none_api_key_raises(self):
"""Explicitly None API key raises ValueError."""
with patch(
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
) as mock_gs:
mock_gs.side_effect = _cloud_settings("llm.ionos.api_key", None)
with pytest.raises(ValueError, match="(?i)api key"):
IONOSProvider.create_llm()
def test_create_llm_error_message_references_ionos_setting(self):
"""Error message tells the user exactly which setting is missing."""
with patch(
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
) as mock_gs:
mock_gs.side_effect = _cloud_settings("llm.ionos.api_key", None)
with pytest.raises(ValueError) as exc_info:
IONOSProvider.create_llm()
assert "llm.ionos.api_key" in str(exc_info.value)
def test_list_models_for_api_without_key_returns_empty(self):
"""IONOS requires auth so returns [] without a key."""
result = IONOSProvider.list_models_for_api(api_key=None)
assert result == []
def test_is_available_with_whitespace_only_key(self):
"""Whitespace-only API key is treated as unavailable."""
with patch(
"local_deep_research.config.thread_settings.get_setting_from_snapshot"
) as mock_gs:
mock_gs.return_value = " "
assert IONOSProvider.is_available() is False
def test_ionos_inherits_from_openai_compatible(self):
"""IONOS provider inherits from OpenAICompatibleProvider."""
assert issubclass(IONOSProvider, OpenAICompatibleProvider)
assert issubclass(IONOSProvider, BaseLLMProvider)
# ===========================================================================
# Cross-provider consistency tests
# ===========================================================================
class TestCrossProviderConsistency:
"""Verify consistent behaviour across all four providers."""
@pytest.mark.parametrize(
"provider_cls,expected_key",
[
(LMStudioProvider, "LMSTUDIO"),
(OpenRouterProvider, "OPENROUTER"),
(XAIProvider, "XAI"),
(IONOSProvider, "IONOS"),
],
)
def test_provider_key_is_uppercase(self, provider_cls, expected_key):
"""All provider_key values should be uppercase identifiers."""
assert provider_cls.provider_key == expected_key
@pytest.mark.parametrize(
"provider_cls,is_cloud",
[
(LMStudioProvider, False),
(OpenRouterProvider, True),
(XAIProvider, True),
(IONOSProvider, True),
],
)
def test_cloud_flag_consistency(self, provider_cls, is_cloud):
"""Cloud flag matches expected value for each provider."""
assert provider_cls.is_cloud is is_cloud
@pytest.mark.parametrize(
"provider_cls",
[LMStudioProvider, OpenRouterProvider, XAIProvider, IONOSProvider],
)
def test_default_base_url_is_valid(self, provider_cls):
"""All providers must have a non-empty default_base_url with a scheme."""
url = provider_cls.default_base_url
assert url is not None
assert url.startswith("http://") or url.startswith("https://")
@pytest.mark.parametrize(
"provider_cls",
[LMStudioProvider, OpenRouterProvider, XAIProvider, IONOSProvider],
)
def test_default_model_is_empty(self, provider_cls):
"""Every provider must declare an empty default model — users must
explicitly pick one. No silent fallbacks."""
assert provider_cls.default_model == ""
@pytest.mark.parametrize(
"provider_cls",
[OpenRouterProvider, XAIProvider, IONOSProvider],
)
def test_cloud_providers_require_api_key_setting(self, provider_cls):
"""Cloud providers must define an api_key_setting."""
assert provider_cls.api_key_setting is not None
assert provider_cls.api_key_setting.startswith("llm.")
def test_local_provider_does_not_require_api_key(self):
"""LM Studio (local) declares its setting key but treats it as optional."""
assert LMStudioProvider.api_key_setting == "llm.lmstudio.api_key"
assert LMStudioProvider.api_key_optional is True
@pytest.mark.parametrize(
"provider_cls",
[LMStudioProvider, OpenRouterProvider, XAIProvider, IONOSProvider],
)
def test_all_providers_have_company_name(self, provider_cls):
"""Every provider must have a company_name for UI display."""
assert provider_cls.company_name
assert isinstance(provider_cls.company_name, str)
assert len(provider_cls.company_name) > 0