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

861 lines
34 KiB
Python

"""
Tests for Ollama embedding provider.
"""
from unittest.mock import Mock, patch
import requests
from local_deep_research.embeddings.providers.implementations.ollama import (
OllamaEmbeddingsProvider,
)
# ── helpers ──────────────────────────────────────────────────────────────
_OLLAMA_MODULE = (
"local_deep_research.embeddings.providers.implementations.ollama"
)
_LLM_UTILS = "local_deep_research.utilities.llm_utils"
def _mock_capabilities(caps_by_model):
"""Return a side_effect function for _get_model_capabilities.
``caps_by_model`` maps model names to capability lists
(e.g. {"nomic-embed-text:latest": ["embedding"]}).
"""
def _side_effect(_base_url, model_name):
return caps_by_model.get(model_name)
return _side_effect
# ── metadata ─────────────────────────────────────────────────────────────
class TestOllamaEmbeddingsProviderMetadata:
"""Tests for OllamaEmbeddingsProvider class metadata."""
def test_provider_name(self):
"""Provider name is 'Ollama'."""
assert OllamaEmbeddingsProvider.provider_name == "Ollama"
def test_provider_key(self):
"""Provider key is 'OLLAMA'."""
assert OllamaEmbeddingsProvider.provider_key == "OLLAMA"
def test_requires_api_key_false(self):
"""Does not require API key."""
assert OllamaEmbeddingsProvider.requires_api_key is False
def test_supports_local_true(self):
"""Supports local execution."""
assert OllamaEmbeddingsProvider.supports_local is True
def test_default_model(self):
"""Has a default embedding model."""
assert OllamaEmbeddingsProvider.default_model == "nomic-embed-text"
# ── is_available ─────────────────────────────────────────────────────────
class TestOllamaEmbeddingsIsAvailable:
"""Tests for is_available method."""
def test_available_when_server_responds(self):
"""Returns True when Ollama server responds."""
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch("requests.get") as mock_get:
mock_response = Mock()
mock_response.status_code = 200
mock_get.return_value = mock_response
result = OllamaEmbeddingsProvider.is_available()
assert result is True
def test_not_available_when_server_error(self):
"""Returns False when server returns error."""
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch("requests.get") as mock_get:
mock_response = Mock()
mock_response.status_code = 500
mock_get.return_value = mock_response
result = OllamaEmbeddingsProvider.is_available()
assert result is False
def test_not_available_when_connection_fails(self):
"""Returns False when connection fails."""
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch("requests.get") as mock_get:
mock_get.side_effect = requests.exceptions.ConnectionError()
result = OllamaEmbeddingsProvider.is_available()
assert result is False
def test_not_available_when_timeout(self):
"""Returns False when request times out."""
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch("requests.get") as mock_get:
mock_get.side_effect = requests.exceptions.Timeout()
result = OllamaEmbeddingsProvider.is_available()
assert result is False
# ── create_embeddings ────────────────────────────────────────────────────
def _make_settings_side_effect(values):
"""Build a side_effect for get_setting_from_snapshot that dispatches by key.
``values`` maps setting keys (e.g. "embeddings.ollama.model") to the value
that lookup should return. Keys absent from ``values`` fall back to the
caller-provided ``default`` argument.
"""
def _side_effect(key, default=None, **_kwargs):
return values.get(key, default)
return _side_effect
class TestOllamaEmbeddingsCreate:
"""Tests for create_embeddings method."""
def test_create_with_default_model(self):
"""Creates embeddings with default model."""
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{
"embeddings.ollama.model": "nomic-embed-text",
"embeddings.ollama.num_ctx": 8192,
}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
mock_instance = Mock()
mock_ollama.return_value = mock_instance
result = OllamaEmbeddingsProvider.create_embeddings()
assert result is mock_instance
mock_ollama.assert_called_once()
call_kwargs = mock_ollama.call_args[1]
assert call_kwargs["model"] == "nomic-embed-text"
def test_create_with_custom_model(self):
"""Creates embeddings with custom model."""
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{"embeddings.ollama.num_ctx": 8192}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
mock_instance = Mock()
mock_ollama.return_value = mock_instance
OllamaEmbeddingsProvider.create_embeddings(
model="mxbai-embed-large"
)
call_kwargs = mock_ollama.call_args[1]
assert call_kwargs["model"] == "mxbai-embed-large"
def test_create_with_custom_base_url(self):
"""Creates embeddings with custom base URL."""
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{"embeddings.ollama.num_ctx": 8192}
)
with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
mock_instance = Mock()
mock_ollama.return_value = mock_instance
OllamaEmbeddingsProvider.create_embeddings(
model="nomic-embed-text",
base_url="http://custom:8080",
)
call_kwargs = mock_ollama.call_args[1]
assert call_kwargs["base_url"] == "http://custom:8080"
def test_create_uses_settings_snapshot(self):
"""Uses settings snapshot when provided."""
mock_settings = {"embeddings.ollama.model": "custom-model"}
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{
"embeddings.ollama.model": "custom-model",
"embeddings.ollama.num_ctx": 8192,
}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings"):
OllamaEmbeddingsProvider.create_embeddings(
settings_snapshot=mock_settings
)
# Verify get_setting_from_snapshot was called with settings
mock_get_setting.assert_called()
def test_create_passes_num_ctx_when_set(self):
"""Forwards num_ctx to OllamaEmbeddings when configured."""
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{
"embeddings.ollama.model": "nomic-embed-text",
"embeddings.ollama.num_ctx": 8192,
}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
OllamaEmbeddingsProvider.create_embeddings()
call_kwargs = mock_ollama.call_args[1]
assert call_kwargs["num_ctx"] == 8192
def test_create_omits_num_ctx_when_unset(self):
"""Does not pass num_ctx when the setting is None."""
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{
"embeddings.ollama.model": "nomic-embed-text",
"embeddings.ollama.num_ctx": None,
}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
OllamaEmbeddingsProvider.create_embeddings()
call_kwargs = mock_ollama.call_args[1]
assert "num_ctx" not in call_kwargs
def test_create_passes_custom_num_ctx(self):
"""Forwards a non-default num_ctx (proves the value is pass-through, not hardcoded)."""
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{
"embeddings.ollama.model": "nomic-embed-text",
"embeddings.ollama.num_ctx": 16384,
}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
OllamaEmbeddingsProvider.create_embeddings()
call_kwargs = mock_ollama.call_args[1]
assert call_kwargs["num_ctx"] == 16384
def test_create_coerces_string_num_ctx_to_int(self):
"""Coerces string num_ctx to int (settings storage may return strings)."""
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{
"embeddings.ollama.model": "nomic-embed-text",
"embeddings.ollama.num_ctx": "8192",
}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
OllamaEmbeddingsProvider.create_embeddings()
call_kwargs = mock_ollama.call_args[1]
assert call_kwargs["num_ctx"] == 8192
assert isinstance(call_kwargs["num_ctx"], int)
def test_create_coerces_float_num_ctx_to_int(self):
"""Coerces float num_ctx to int (JSON deserialization may yield floats)."""
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{
"embeddings.ollama.model": "nomic-embed-text",
"embeddings.ollama.num_ctx": 8192.0,
}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
OllamaEmbeddingsProvider.create_embeddings()
call_kwargs = mock_ollama.call_args[1]
assert call_kwargs["num_ctx"] == 8192
assert isinstance(call_kwargs["num_ctx"], int)
def test_create_combines_custom_model_and_num_ctx(self):
"""Custom model arg and num_ctx setting both flow through."""
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{"embeddings.ollama.num_ctx": 4096}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
OllamaEmbeddingsProvider.create_embeddings(
model="bge-m3",
)
call_kwargs = mock_ollama.call_args[1]
assert call_kwargs["model"] == "bge-m3"
assert call_kwargs["num_ctx"] == 4096
def test_create_uses_default_num_ctx_when_snapshot_missing_key(self):
"""Falls back to default=8192 when snapshot doesn't contain num_ctx."""
# _make_settings_side_effect returns the caller's `default=` for
# absent keys, mirroring what the real settings system does when
# neither DB nor JSON defaults have the key (shouldn't happen in
# practice once the JSON ships, but verifies the explicit default
# in create_embeddings is wired correctly).
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{"embeddings.ollama.model": "nomic-embed-text"}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
with patch(f"{_OLLAMA_MODULE}.OllamaEmbeddings") as mock_ollama:
OllamaEmbeddingsProvider.create_embeddings()
call_kwargs = mock_ollama.call_args[1]
assert call_kwargs["num_ctx"] == 8192
# ── weakref.finalize safety net ─────────────────────────────────────────
class TestOllamaEmbeddingsFinalizerSafetyNet:
"""The provider factory registers a ``weakref.finalize`` so callers
that bypass ``LocalEmbeddingManager`` (e.g. the programmatic-API
example scripts under ``examples/api_usage/``, direct test
constructions) still get cleanup at GC time.
The manager-driven explicit close path remains the load-bearing
primary cleanup; the finalizer is the last-resort safety net only.
"""
def test_create_registers_finalizer_with_inner_clients(self):
"""Provider passes the inner sync/async ``ollama.Client`` objects to
``weakref.finalize`` — not the wrapping ``OllamaEmbeddings`` itself.
Passing the instance would defeat the finalizer's purpose by keeping
the instance alive through the registry's strong-ref."""
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{
"embeddings.ollama.model": "nomic-embed-text",
"embeddings.ollama.num_ctx": 8192,
}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
# A simple object (not Mock) so the auto-attr behavior of
# Mock doesn't mask a missing _client/_async_client.
class _FakeOllamaEmbeddings:
def __init__(self):
self._client = object()
self._async_client = object()
fake_instance = _FakeOllamaEmbeddings()
with patch(
f"{_OLLAMA_MODULE}.OllamaEmbeddings",
return_value=fake_instance,
):
with patch(
f"{_OLLAMA_MODULE}.weakref.finalize"
) as mock_finalize:
result = OllamaEmbeddingsProvider.create_embeddings()
assert result is fake_instance
mock_finalize.assert_called_once()
args, _ = mock_finalize.call_args
# weakref.finalize(instance, callback, sync, async)
assert args[0] is fake_instance
assert args[2] is fake_instance._client
assert args[3] is fake_instance._async_client
def test_finalizer_does_not_crash_if_inner_attrs_missing(self):
"""A future langchain_ollama version might rename _client /
_async_client. The factory must not crash in that case — the
explicit close path (or the upstream's own cleanup) remains
responsible for resource release.
"""
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{
"embeddings.ollama.model": "nomic-embed-text",
"embeddings.ollama.num_ctx": 8192,
}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:11434"
# Slotted class with no _client / _async_client → attribute
# access raises AttributeError, exercising the except branch.
class _ReshapedEmbeddings:
__slots__ = ()
with patch(
f"{_OLLAMA_MODULE}.OllamaEmbeddings",
return_value=_ReshapedEmbeddings(),
):
# Must not raise.
result = OllamaEmbeddingsProvider.create_embeddings()
assert result is not None
def test_real_ollama_embeddings_finalizer_closes_clients_on_gc(self):
"""End-to-end: construct a real ``langchain_ollama.OllamaEmbeddings``
via the provider, drop the reference, force GC, and verify both
inner httpx clients are closed. This is the canary for the
#3816-shaped FD leak on the embeddings side.
"""
import gc
import weakref as _weakref
with patch(
f"{_OLLAMA_MODULE}.get_setting_from_snapshot"
) as mock_get_setting:
mock_get_setting.side_effect = _make_settings_side_effect(
{
"embeddings.ollama.model": "nomic-embed-text",
"embeddings.ollama.num_ctx": 8192,
}
)
with patch(f"{_OLLAMA_MODULE}.get_ollama_base_url") as mock_get_url:
mock_get_url.return_value = "http://localhost:1"
# No OllamaEmbeddings patch — uses the real class.
instance = OllamaEmbeddingsProvider.create_embeddings()
sync_httpx = instance._client._client
async_httpx = instance._async_client._client
assert sync_httpx.is_closed is False
assert async_httpx.is_closed is False
instance_ref = _weakref.ref(instance)
# End the patches before dropping the ref so the finalizer
# runs against the real ``_close_inner_ollama_clients``.
del instance
gc.collect()
assert instance_ref() is None, (
"OllamaEmbeddings instance must be GC'able — the finalizer "
"must not strong-ref it"
)
assert sync_httpx.is_closed is True
assert async_httpx.is_closed is True
# ── _get_model_capabilities ─────────────────────────────────────────────
class TestGetModelCapabilities:
"""Tests for _get_model_capabilities private helper."""
def test_returns_capabilities_on_success(self):
"""Returns capability list from /api/show response."""
mock_resp = Mock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"capabilities": ["embedding"]}
with patch(f"{_OLLAMA_MODULE}.safe_post", return_value=mock_resp):
caps = OllamaEmbeddingsProvider._get_model_capabilities(
"http://localhost:11434", "nomic-embed-text"
)
assert caps == ["embedding"]
def test_returns_none_on_http_error(self):
"""Returns None when server returns non-200."""
mock_resp = Mock()
mock_resp.status_code = 404
with patch(f"{_OLLAMA_MODULE}.safe_post", return_value=mock_resp):
caps = OllamaEmbeddingsProvider._get_model_capabilities(
"http://localhost:11434", "nonexistent-model"
)
assert caps is None
def test_returns_none_on_exception(self):
"""Returns None when request raises exception."""
with patch(
f"{_OLLAMA_MODULE}.safe_post",
side_effect=Exception("connection refused"),
):
caps = OllamaEmbeddingsProvider._get_model_capabilities(
"http://localhost:11434", "nomic-embed-text"
)
assert caps is None
def test_returns_none_when_capabilities_missing(self):
"""Returns None when response lacks capabilities field."""
mock_resp = Mock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"details": {"family": "nomic-bert"}}
with patch(f"{_OLLAMA_MODULE}.safe_post", return_value=mock_resp):
caps = OllamaEmbeddingsProvider._get_model_capabilities(
"http://localhost:11434", "nomic-embed-text"
)
assert caps is None
def test_returns_completion_for_llm(self):
"""Returns completion capabilities for LLM models."""
mock_resp = Mock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"capabilities": ["completion", "tools"]}
with patch(f"{_OLLAMA_MODULE}.safe_post", return_value=mock_resp):
caps = OllamaEmbeddingsProvider._get_model_capabilities(
"http://localhost:11434", "qwen3:4b"
)
assert caps == ["completion", "tools"]
# ── is_embedding_model ───────────────────────────────────────────────────
class TestIsEmbeddingModel:
"""Tests for is_embedding_model method."""
def test_returns_true_for_embedding_model(self):
"""Returns True when capabilities include 'embedding'."""
with patch(
f"{_OLLAMA_MODULE}.get_ollama_base_url",
return_value="http://localhost:11434",
):
with patch.object(
OllamaEmbeddingsProvider,
"_get_model_capabilities",
return_value=["embedding"],
):
result = OllamaEmbeddingsProvider.is_embedding_model(
"nomic-embed-text"
)
assert result is True
def test_returns_false_for_llm_model(self):
"""Returns False when capabilities don't include 'embedding'."""
with patch(
f"{_OLLAMA_MODULE}.get_ollama_base_url",
return_value="http://localhost:11434",
):
with patch.object(
OllamaEmbeddingsProvider,
"_get_model_capabilities",
return_value=["completion", "tools"],
):
result = OllamaEmbeddingsProvider.is_embedding_model("qwen3:4b")
assert result is False
def test_returns_none_when_capabilities_unavailable(self):
"""Older Ollama (no capabilities in /api/show) → None, not a guess.
We refuse to guess from the model name; the caller can decide
whether to tag the model, hide it, or just trust the user.
"""
with patch(
f"{_OLLAMA_MODULE}.get_ollama_base_url",
return_value="http://localhost:11434",
):
with patch.object(
OllamaEmbeddingsProvider,
"_get_model_capabilities",
return_value=None,
):
assert (
OllamaEmbeddingsProvider.is_embedding_model(
"nomic-embed-text"
)
is None
)
assert (
OllamaEmbeddingsProvider.is_embedding_model(
"deepseek-r1:32b"
)
is None
)
# ── get_available_models ─────────────────────────────────────────────────
class TestOllamaEmbeddingsGetAvailableModels:
"""Tests for get_available_models method."""
def _setup_mocks(self, all_models, caps_by_model):
"""Helper: patch base_url, fetch_ollama_models, and capabilities."""
return (
patch(
f"{_OLLAMA_MODULE}.get_ollama_base_url",
return_value="http://localhost:11434",
),
patch(
f"{_LLM_UTILS}.fetch_ollama_models",
return_value=all_models,
),
patch.object(
OllamaEmbeddingsProvider,
"_get_model_capabilities",
side_effect=_mock_capabilities(caps_by_model),
),
)
def test_embedding_models_sorted_first(self):
"""Embedding models appear before LLM models."""
all_models = [
{"value": "qwen3:4b", "label": "qwen3:4b"},
{
"value": "nomic-embed-text:latest",
"label": "nomic-embed-text:latest",
},
{"value": "deepseek-r1:32b", "label": "deepseek-r1:32b"},
]
caps = {
"qwen3:4b": ["completion", "tools"],
"nomic-embed-text:latest": ["embedding"],
"deepseek-r1:32b": ["completion"],
}
p1, p2, p3 = self._setup_mocks(all_models, caps)
with p1, p2, p3:
result = OllamaEmbeddingsProvider.get_available_models()
assert result[0]["value"] == "nomic-embed-text:latest"
assert result[0]["is_embedding"] is True
def test_all_models_returned_with_flag(self):
"""All models are returned, each with an is_embedding flag."""
all_models = [
{
"value": "nomic-embed-text:latest",
"label": "nomic-embed-text:latest",
},
{"value": "qwen3:4b", "label": "qwen3:4b"},
]
caps = {
"nomic-embed-text:latest": ["embedding"],
"qwen3:4b": ["completion"],
}
p1, p2, p3 = self._setup_mocks(all_models, caps)
with p1, p2, p3:
result = OllamaEmbeddingsProvider.get_available_models()
assert len(result) == 2
assert all("is_embedding" in m for m in result)
embed_models = [m for m in result if m["is_embedding"]]
llm_models = [m for m in result if not m["is_embedding"]]
assert len(embed_models) == 1
assert len(llm_models) == 1
def test_empty_model_list(self):
"""Returns empty list when no models available."""
p1, p2, p3 = self._setup_mocks([], {})
with p1, p2, p3:
result = OllamaEmbeddingsProvider.get_available_models()
assert result == []
def test_all_llms_still_returned(self):
"""Even if no embedding models exist, all LLMs are returned."""
all_models = [
{"value": "qwen3:4b", "label": "qwen3:4b"},
{"value": "deepseek-r1:32b", "label": "deepseek-r1:32b"},
]
caps = {
"qwen3:4b": ["completion"],
"deepseek-r1:32b": ["completion"],
}
p1, p2, p3 = self._setup_mocks(all_models, caps)
with p1, p2, p3:
result = OllamaEmbeddingsProvider.get_available_models()
assert len(result) == 2
assert all(m["is_embedding"] is False for m in result)
def test_capabilities_unavailable_returns_untagged_models(self):
"""Older Ollama (no /api/show capabilities) → every model is
still listed, just without an ``is_embedding`` flag. The
provider doesn't guess from the name."""
all_models = [
{
"value": "nomic-embed-text:latest",
"label": "nomic-embed-text:latest",
},
{"value": "qwen3:4b", "label": "qwen3:4b"},
]
caps = {} # No model has capabilities → all untagged
p1, p2, p3 = self._setup_mocks(all_models, caps)
with p1, p2, p3:
result = OllamaEmbeddingsProvider.get_available_models()
assert {m["value"] for m in result} == {
"nomic-embed-text:latest",
"qwen3:4b",
}
assert all("is_embedding" not in m for m in result)
def test_multiple_embedding_models(self):
"""Multiple embedding models are all sorted first."""
all_models = [
{"value": "qwen3:4b", "label": "qwen3:4b"},
{
"value": "nomic-embed-text:latest",
"label": "nomic-embed-text:latest",
},
{
"value": "mxbai-embed-large:latest",
"label": "mxbai-embed-large:latest",
},
{"value": "deepseek-r1:32b", "label": "deepseek-r1:32b"},
]
caps = {
"qwen3:4b": ["completion"],
"nomic-embed-text:latest": ["embedding"],
"mxbai-embed-large:latest": ["embedding"],
"deepseek-r1:32b": ["completion"],
}
p1, p2, p3 = self._setup_mocks(all_models, caps)
with p1, p2, p3:
result = OllamaEmbeddingsProvider.get_available_models()
# First two should be embedding models
assert result[0]["is_embedding"] is True
assert result[1]["is_embedding"] is True
assert result[2]["is_embedding"] is False
assert result[3]["is_embedding"] is False
def test_result_preserves_value_and_label(self):
"""Original value and label fields are preserved."""
all_models = [
{
"value": "nomic-embed-text:latest",
"label": "nomic-embed-text:latest",
},
]
caps = {"nomic-embed-text:latest": ["embedding"]}
p1, p2, p3 = self._setup_mocks(all_models, caps)
with p1, p2, p3:
result = OllamaEmbeddingsProvider.get_available_models()
assert result[0]["value"] == "nomic-embed-text:latest"
assert result[0]["label"] == "nomic-embed-text:latest"
assert result[0]["is_embedding"] is True
# ── provider info & config ───────────────────────────────────────────────
class TestOllamaEmbeddingsProviderInfo:
"""Tests for get_provider_info method."""
def test_provider_info_structure(self):
"""get_provider_info returns expected structure."""
info = OllamaEmbeddingsProvider.get_provider_info()
assert "name" in info
assert "key" in info
assert "requires_api_key" in info
assert "supports_local" in info
assert "default_model" in info
assert info["name"] == "Ollama"
assert info["key"] == "OLLAMA"
assert info["requires_api_key"] is False
assert info["supports_local"] is True
class TestOllamaEmbeddingsValidateConfig:
"""Tests for validate_config method."""
def test_validate_config_when_available(self):
"""validate_config returns True when available."""
with patch.object(
OllamaEmbeddingsProvider, "is_available", return_value=True
):
is_valid, error = OllamaEmbeddingsProvider.validate_config()
assert is_valid is True
assert error is None
def test_validate_config_when_not_available(self):
"""validate_config returns False when not available."""
with patch.object(
OllamaEmbeddingsProvider, "is_available", return_value=False
):
is_valid, error = OllamaEmbeddingsProvider.validate_config()
assert is_valid is False
assert error is not None
assert "not available" in error