""" Deep coverage tests for the Ollama LLM provider. Targets paths not exercised by test_ollama_provider.py: - _get_auth_headers: api_key=None, settings_snapshot=None → empty dict - _get_auth_headers: api_key from settings_snapshot is empty string → no header - is_available: URL configured but safe_get raises unexpected exception - is_available: safe_get returns non-200 response - create_llm: model not found in older API format (no "models" key) - create_llm: explicit None context window resolves to the helper default - list_models_for_api: label ":latest" stripped correctly - requires_auth_for_models: always returns False """ from unittest.mock import Mock, patch import requests from local_deep_research.llm.providers.implementations.ollama import ( OllamaProvider, ) MODULE = "local_deep_research.llm.providers.implementations.ollama" GET_SETTING = f"{MODULE}.get_setting_from_snapshot" # --------------------------------------------------------------------------- # _get_auth_headers edge cases # --------------------------------------------------------------------------- class TestGetAuthHeadersEdgeCases: def test_both_none_returns_empty(self): headers = OllamaProvider._get_auth_headers( api_key=None, settings_snapshot=None ) assert headers == {} def test_empty_string_api_key_from_settings_returns_empty(self): """When settings return empty string, no Authorization header should be added.""" with patch(GET_SETTING, return_value=""): headers = OllamaProvider._get_auth_headers( settings_snapshot={"llm.ollama.api_key": ""} ) assert headers == {} def test_explicit_api_key_takes_precedence_over_snapshot(self): """Explicit api_key must not be overwritten by settings lookup.""" with patch(GET_SETTING, return_value="settings-key") as mock_get: headers = OllamaProvider._get_auth_headers( api_key="explicit-key", settings_snapshot={"llm.ollama.api_key": "settings-key"}, ) # get_setting_from_snapshot should NOT be called because api_key provided mock_get.assert_not_called() assert headers["Authorization"] == "Bearer explicit-key" # --------------------------------------------------------------------------- # is_available – additional paths # --------------------------------------------------------------------------- class TestIsAvailableAdditional: def test_unexpected_exception_from_safe_get_returns_false(self): with patch(GET_SETTING, return_value="http://localhost:11434"): with patch( f"{MODULE}.safe_get", side_effect=Exception("unexpected") ): result = OllamaProvider.is_available() assert result is False def test_non_200_response_returns_false(self): with patch(GET_SETTING, return_value="http://localhost:11434"): mock_resp = Mock() mock_resp.status_code = 503 with patch(f"{MODULE}.safe_get", return_value=mock_resp): result = OllamaProvider.is_available() assert result is False def test_request_exception_returns_false(self): with patch(GET_SETTING, return_value="http://localhost:11434"): with patch( f"{MODULE}.safe_get", side_effect=requests.exceptions.RequestException("timeout"), ): result = OllamaProvider.is_available() assert result is False # --------------------------------------------------------------------------- # create_llm – older API format (no "models" key in response) # --------------------------------------------------------------------------- class TestCreateLlmOlderApiFormat: def test_create_llm_succeeds_without_preflight(self): """create_llm now skips pre-flight checks and creates ChatOllama directly.""" def _get_setting(key, default=None, *args, **kwargs): return { "llm.ollama.url": "http://localhost:11434", "llm.local_context_window_size": 4096, "llm.supports_max_tokens": True, "llm.max_tokens": 100000, }.get(key, default) with patch(GET_SETTING, side_effect=_get_setting): with patch(f"{MODULE}.ChatOllama") as mock_chat: mock_chat.return_value = Mock() OllamaProvider.create_llm(model_name="gemma3:12b") mock_chat.assert_called_once() # --------------------------------------------------------------------------- # create_llm – context_window_size is None # --------------------------------------------------------------------------- class TestCreateLlmRequiresModelName: """Direct callers of OllamaProvider.create_llm() must specify a model.""" def test_raises_when_no_model_name(self): import pytest def _get_setting(key, default=None, *args, **kwargs): return {"llm.ollama.url": "http://localhost:11434"}.get( key, default ) with patch(GET_SETTING, side_effect=_get_setting): with pytest.raises(ValueError, match="Ollama model not configured"): OllamaProvider.create_llm() def test_raises_when_model_name_empty_string(self): import pytest def _get_setting(key, default=None, *args, **kwargs): return {"llm.ollama.url": "http://localhost:11434"}.get( key, default ) with patch(GET_SETTING, side_effect=_get_setting): with pytest.raises(ValueError, match="Ollama model not configured"): OllamaProvider.create_llm(model_name="") class TestCreateLlmContextWindowNone: def test_explicit_none_window_resolves_to_helper_default(self): """Explicit None resolves through the shared helper to its default, matching what context_limit bookkeeping reports for overflow detection. Previously num_ctx was omitted here (Ollama server default ~2048) while context_limit claimed 8192 — overflows between the two went undetected. No max_tokens kwarg: ChatOllama ignores it. Uses a real settings_snapshot (no get_setting patch) so the explicit-None value actually reaches the shared helper's function-local thread_settings lookup. """ from local_deep_research.llm.providers._helpers import ( DEFAULT_LOCAL_CONTEXT_WINDOW_SIZE, ) snapshot = { "llm.ollama.url": "http://localhost:11434", "llm.local_context_window_size": None, "llm.supports_max_tokens": True, "llm.max_tokens": 100000, } with patch(f"{MODULE}.ChatOllama") as mock_chat: mock_chat.return_value = Mock() OllamaProvider.create_llm( model_name="gemma3:12b", settings_snapshot=snapshot ) call_kwargs = mock_chat.call_args[1] assert call_kwargs["num_ctx"] == DEFAULT_LOCAL_CONTEXT_WINDOW_SIZE assert "max_tokens" not in call_kwargs # --------------------------------------------------------------------------- # list_models_for_api – label formatting # --------------------------------------------------------------------------- class TestListModelsLabelFormatting: def test_latest_tag_stripped_from_label(self): with patch( "local_deep_research.utilities.llm_utils.fetch_ollama_models" ) as mock_fetch: mock_fetch.return_value = [ {"value": "llama2:latest", "label": "llama2:latest"} ] result = OllamaProvider.list_models_for_api( base_url="http://localhost:11434" ) assert len(result) == 1 # ":latest" should be stripped from the label assert ":latest" not in result[0]["label"] def test_colon_replaced_with_space_in_label(self): with patch( "local_deep_research.utilities.llm_utils.fetch_ollama_models" ) as mock_fetch: mock_fetch.return_value = [ {"value": "model:7b", "label": "model:7b"} ] result = OllamaProvider.list_models_for_api( base_url="http://localhost:11434" ) assert ":" not in result[0]["label"].replace("(Ollama)", "") # --------------------------------------------------------------------------- # requires_auth_for_models # --------------------------------------------------------------------------- class TestRequiresAuthForModels: def test_always_returns_false(self): assert OllamaProvider.requires_auth_for_models() is False