""" Tests for the openai_endpoint API key UX fix (issue #3875). Validates that the openai_endpoint API key setting has the correct type and a user-friendly description that makes clear it can be optional. """ import json import os from pathlib import Path import pytest from local_deep_research.settings.manager import SettingsManager DEFAULTS_PATH = ( Path(__file__).parent.parent / "src" / "local_deep_research" / "defaults" / "default_settings.json" ) @pytest.fixture(autouse=True) def clean_env(): """Save/clear/restore LDR_* env vars to prevent pollution across tests.""" original_env = {k: v for k, v in os.environ.items() if k.startswith("LDR_")} for key in list(os.environ.keys()): if key.startswith("LDR_"): os.environ.pop(key, None) yield for key in list(os.environ.keys()): if key.startswith("LDR_"): os.environ.pop(key, None) for key, value in original_env.items(): os.environ[key] = value @pytest.fixture(scope="module") def defaults(): """Load all default settings.""" manager = SettingsManager(db_session=None) return manager.default_settings @pytest.fixture(scope="module") def raw_json(): """Load the raw default_settings.json file.""" with open(DEFAULTS_PATH, encoding="utf-8") as f: return json.load(f) class TestOpenAIEndpointApiKeyType: """Test that openai_endpoint API key has correct type 'LLM'.""" def test_openai_endpoint_api_key_type_is_llm(self, defaults): """The openai_endpoint api_key should have type 'LLM', not 'SEARCH'.""" setting = defaults["llm.openai_endpoint.api_key"] assert setting["type"] == "LLM", ( f"Expected type 'LLM' but got '{setting['type']}'. " f"API keys for LLM providers should be categorized as 'LLM'." ) class TestOpenAIEndpointApiKeyDescription: """Test that the description clarifies the key is optional for local servers.""" def test_description_mentions_optional(self, defaults): """The description should mention that the key is optional / can be left empty.""" setting = defaults["llm.openai_endpoint.api_key"] description = setting["description"].lower() assert ( "optional" in description or "leave empty" in description or "leave this blank" in description or "leave blank" in description ), ( f"Description should mention 'optional', 'leave empty', or 'leave (this) blank'. " f"Got: {setting['description']!r}" ) def test_description_no_longer_says_not_needed(self, defaults): """The description should NOT say 'use any non-empty value like not-needed'.""" setting = defaults["llm.openai_endpoint.api_key"] description = setting["description"].lower() assert "not-needed" not in description, ( f"Description should not suggest using 'not-needed' as a placeholder. " f"Got: {setting['description']!r}" ) class TestAllLlmApiKeyTypes: """Test that llm.*.api_key settings used for optional endpoints have type 'LLM'.""" def test_local_llm_api_keys_have_type_llm(self, defaults): """API keys for local/optional LLM providers must be categorized as 'LLM'. Cloud-only providers (google, openrouter, ionos, anthropic, openai, deepseek, xai) legitimately use SEARCH type since a key is always required. But providers that support local servers (openai_endpoint, ollama, llamacpp, lmstudio) should use LLM type. """ optional_providers = { "llm.openai_endpoint.api_key", "llm.ollama.api_key", "llm.llamacpp.api_key", "llm.lmstudio.api_key", } errors = [] for key, setting in defaults.items(): if key in optional_providers: if setting["type"] != "LLM": errors.append( f"{key}: expected type 'LLM', got '{setting['type']}'" ) assert not errors, ( f"{len(errors)} optional-provider api_key setting(s) have wrong type:\n" + "\n".join(f" {e}" for e in errors) )