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
229 lines
8.6 KiB
Python
229 lines
8.6 KiB
Python
"""
|
||
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
|