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
364 lines
13 KiB
Python
364 lines
13 KiB
Python
"""Tests for llm_utils module."""
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
from local_deep_research.utilities.llm_utils import (
|
|
fetch_ollama_models,
|
|
get_model_identifier,
|
|
get_ollama_base_url,
|
|
get_server_url,
|
|
)
|
|
|
|
|
|
class _FakeModelName:
|
|
def __init__(self, model_name):
|
|
self.model_name = model_name
|
|
|
|
|
|
class _FakeModelOnly:
|
|
def __init__(self, model):
|
|
self.model = model
|
|
|
|
|
|
class _FakeBoth:
|
|
def __init__(self, model_name, model):
|
|
self.model_name = model_name
|
|
self.model = model
|
|
|
|
|
|
class _FakeNeither:
|
|
pass
|
|
|
|
|
|
class _FakeWrapper:
|
|
def __init__(self, base_llm):
|
|
self.base_llm = base_llm
|
|
|
|
|
|
class TestGetModelIdentifier:
|
|
"""Regression tests for the identifier helper that drives the
|
|
Tier 4 LLM cache predicate. Earlier code used ``getattr(llm, "name",
|
|
str(llm))`` which always fell back to ``str(wrapper)`` — a repr
|
|
string that poisoned the quality_model column."""
|
|
|
|
def test_prefers_model_name(self):
|
|
assert get_model_identifier(_FakeModelName("gpt-4")) == "gpt-4"
|
|
|
|
def test_falls_back_to_model(self):
|
|
assert (
|
|
get_model_identifier(_FakeModelOnly("claude-opus-4-7"))
|
|
== "claude-opus-4-7"
|
|
)
|
|
|
|
def test_prefers_model_name_over_model(self):
|
|
# If both exist, model_name wins. ChatOpenAI / ChatAnthropic
|
|
# expose model_name; ChatOllama exposes model.
|
|
llm = _FakeBoth(model_name="gpt-4", model="ignored")
|
|
assert get_model_identifier(llm) == "gpt-4"
|
|
|
|
def test_class_name_fallback(self):
|
|
# Never return a repr() with object address — fall back cleanly.
|
|
result = get_model_identifier(_FakeNeither())
|
|
assert result == "_FakeNeither"
|
|
assert "object at 0x" not in result
|
|
|
|
def test_unwraps_processing_wrapper(self):
|
|
wrapped = _FakeWrapper(_FakeModelOnly("mistral"))
|
|
assert get_model_identifier(wrapped) == "mistral"
|
|
|
|
def test_none_values_skipped(self):
|
|
class _NonePair:
|
|
model_name = None
|
|
model = "real-model"
|
|
|
|
assert get_model_identifier(_NonePair()) == "real-model"
|
|
|
|
|
|
class TestGetOllamaBaseUrl:
|
|
"""Tests for get_ollama_base_url function."""
|
|
|
|
def test_returns_default_without_settings(self):
|
|
"""Should return default URL when no settings provided."""
|
|
with patch(
|
|
"local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
|
|
) as mock_get:
|
|
mock_get.return_value = None
|
|
result = get_ollama_base_url()
|
|
assert result == "http://localhost:11434"
|
|
|
|
def test_uses_embeddings_ollama_url(self):
|
|
"""Should use embeddings.ollama.url setting."""
|
|
with patch(
|
|
"local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
|
|
) as mock_get:
|
|
mock_get.return_value = "http://custom:11434"
|
|
result = get_ollama_base_url()
|
|
assert result == "http://custom:11434"
|
|
|
|
def test_normalizes_url(self):
|
|
"""Should normalize URL without scheme."""
|
|
with patch(
|
|
"local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
|
|
) as mock_get:
|
|
mock_get.return_value = "localhost:11434"
|
|
result = get_ollama_base_url()
|
|
assert result == "http://localhost:11434"
|
|
|
|
def test_passes_settings_snapshot(self):
|
|
"""Should pass settings snapshot to get_setting_from_snapshot."""
|
|
snapshot = {"key": "value"}
|
|
with patch(
|
|
"local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
|
|
) as mock_get:
|
|
mock_get.return_value = "http://localhost:11434"
|
|
get_ollama_base_url(settings_snapshot=snapshot)
|
|
# Should have been called with snapshot
|
|
assert any(
|
|
call.kwargs.get("settings_snapshot") == snapshot
|
|
for call in mock_get.call_args_list
|
|
)
|
|
|
|
|
|
class TestGetServerUrl:
|
|
"""Tests for get_server_url function."""
|
|
|
|
def test_returns_default_without_settings(self):
|
|
"""Should return default URL when no settings provided."""
|
|
result = get_server_url()
|
|
assert result == "http://127.0.0.1:5000/"
|
|
|
|
def test_uses_server_url_from_snapshot(self):
|
|
"""Should use direct server_url from snapshot."""
|
|
snapshot = {"server_url": "https://custom.example.com/"}
|
|
result = get_server_url(settings_snapshot=snapshot)
|
|
assert result == "https://custom.example.com/"
|
|
|
|
def test_uses_system_server_url(self):
|
|
"""Should use system.server_url setting."""
|
|
snapshot = {"system": {"server_url": "https://system.example.com/"}}
|
|
result = get_server_url(settings_snapshot=snapshot)
|
|
assert result == "https://system.example.com/"
|
|
|
|
def test_constructs_url_from_web_settings(self):
|
|
"""Should construct URL from web.host, web.port, web.use_https."""
|
|
# Need to provide snapshot so it goes through web settings path
|
|
snapshot = {"_trigger_web_settings": True}
|
|
with patch(
|
|
"local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
|
|
) as mock_get:
|
|
|
|
def side_effect(key, snapshot_arg=None, default=None):
|
|
settings = {
|
|
"web.host": "0.0.0.0",
|
|
"web.port": 8080,
|
|
"web.use_https": True,
|
|
}
|
|
return settings.get(key, default)
|
|
|
|
mock_get.side_effect = side_effect
|
|
result = get_server_url(settings_snapshot=snapshot)
|
|
# 0.0.0.0 should be converted to 127.0.0.1
|
|
assert result == "https://127.0.0.1:8080/"
|
|
|
|
def test_uses_http_when_use_https_false(self):
|
|
"""Should use http scheme when use_https is False."""
|
|
snapshot = {"_trigger_web_settings": True}
|
|
with patch(
|
|
"local_deep_research.utilities.llm_utils.get_setting_from_snapshot"
|
|
) as mock_get:
|
|
|
|
def side_effect(key, snapshot_arg=None, default=None):
|
|
settings = {
|
|
"web.host": "192.168.1.1", # Not localhost, so it won't default
|
|
"web.port": 5000,
|
|
"web.use_https": False,
|
|
}
|
|
return settings.get(key, default)
|
|
|
|
mock_get.side_effect = side_effect
|
|
result = get_server_url(settings_snapshot=snapshot)
|
|
assert result == "http://192.168.1.1:5000/"
|
|
|
|
def test_priority_order(self):
|
|
"""Should check server_url before system before web settings."""
|
|
# Direct server_url takes priority
|
|
snapshot = {
|
|
"server_url": "https://direct/",
|
|
"system": {"server_url": "https://system/"},
|
|
}
|
|
result = get_server_url(settings_snapshot=snapshot)
|
|
assert result == "https://direct/"
|
|
|
|
def test_returns_fallback_with_trailing_slash(self):
|
|
"""Fallback URL should have trailing slash."""
|
|
result = get_server_url()
|
|
assert result.endswith("/")
|
|
|
|
|
|
class TestFetchOllamaModels:
|
|
"""Tests for fetch_ollama_models function."""
|
|
|
|
def test_returns_empty_list_on_connection_error(self):
|
|
"""Should return empty list on connection error."""
|
|
import requests
|
|
|
|
with patch.object(requests, "get") as mock_get:
|
|
mock_get.side_effect = Exception("Connection refused")
|
|
result = fetch_ollama_models("http://localhost:11434")
|
|
assert result == []
|
|
|
|
def test_returns_empty_list_on_non_200(self):
|
|
"""Should return empty list on non-200 status."""
|
|
import requests
|
|
|
|
with patch.object(requests, "get") as mock_get:
|
|
mock_response = Mock()
|
|
mock_response.status_code = 404
|
|
mock_get.return_value = mock_response
|
|
result = fetch_ollama_models("http://localhost:11434")
|
|
assert result == []
|
|
|
|
def test_parses_models_from_response(self):
|
|
"""Should parse models from API response."""
|
|
import requests
|
|
|
|
with patch.object(requests, "get") as mock_get:
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"models": [
|
|
{"name": "llama2"},
|
|
{"name": "mistral"},
|
|
]
|
|
}
|
|
mock_get.return_value = mock_response
|
|
|
|
result = fetch_ollama_models("http://localhost:11434")
|
|
|
|
assert len(result) == 2
|
|
assert {"value": "llama2", "label": "llama2"} in result
|
|
assert {"value": "mistral", "label": "mistral"} in result
|
|
|
|
def test_handles_older_api_format(self):
|
|
"""Should handle older API format (list directly)."""
|
|
import requests
|
|
|
|
with patch.object(requests, "get") as mock_get:
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = [
|
|
{"name": "model1"},
|
|
{"name": "model2"},
|
|
]
|
|
mock_get.return_value = mock_response
|
|
|
|
result = fetch_ollama_models("http://localhost:11434")
|
|
|
|
assert len(result) == 2
|
|
|
|
def test_skips_models_without_name(self):
|
|
"""Should skip models without name field."""
|
|
import requests
|
|
|
|
with patch.object(requests, "get") as mock_get:
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"models": [
|
|
{"name": "valid"},
|
|
{"other": "field"}, # No name
|
|
{"name": ""}, # Empty name
|
|
]
|
|
}
|
|
mock_get.return_value = mock_response
|
|
|
|
result = fetch_ollama_models("http://localhost:11434")
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["value"] == "valid"
|
|
|
|
def test_uses_custom_timeout(self):
|
|
"""Should use custom timeout."""
|
|
import requests
|
|
|
|
with patch.object(requests, "get") as mock_get:
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"models": []}
|
|
mock_get.return_value = mock_response
|
|
|
|
fetch_ollama_models("http://localhost:11434", timeout=10.0)
|
|
|
|
mock_get.assert_called_once()
|
|
assert mock_get.call_args.kwargs["timeout"] == 10.0
|
|
|
|
def test_uses_auth_headers(self):
|
|
"""Should pass auth headers to request."""
|
|
import requests
|
|
|
|
with patch.object(requests, "get") as mock_get:
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"models": []}
|
|
mock_get.return_value = mock_response
|
|
|
|
headers = {"Authorization": "Bearer token"}
|
|
fetch_ollama_models("http://localhost:11434", auth_headers=headers)
|
|
|
|
mock_get.assert_called_once()
|
|
# safe_get wraps requests.get and injects a project User-Agent
|
|
# alongside caller-provided headers, so we check the auth
|
|
# header is present rather than doing an exact dict match.
|
|
sent_headers = mock_get.call_args.kwargs["headers"]
|
|
assert sent_headers["Authorization"] == "Bearer token"
|
|
|
|
def test_constructs_correct_url(self):
|
|
"""Should construct correct API URL."""
|
|
import requests
|
|
|
|
with patch.object(requests, "get") as mock_get:
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"models": []}
|
|
mock_get.return_value = mock_response
|
|
|
|
fetch_ollama_models("http://localhost:11434")
|
|
|
|
mock_get.assert_called_once()
|
|
assert (
|
|
mock_get.call_args.args[0] == "http://localhost:11434/api/tags"
|
|
)
|
|
|
|
def test_returns_correct_format(self):
|
|
"""Should return models in correct format with value and label."""
|
|
import requests
|
|
|
|
with patch.object(requests, "get") as mock_get:
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"models": [{"name": "test-model"}]
|
|
}
|
|
mock_get.return_value = mock_response
|
|
|
|
result = fetch_ollama_models("http://localhost:11434")
|
|
|
|
assert len(result) == 1
|
|
assert "value" in result[0]
|
|
assert "label" in result[0]
|
|
assert result[0]["value"] == result[0]["label"]
|
|
|
|
def test_handles_empty_models_list(self):
|
|
"""Should handle empty models list."""
|
|
import requests
|
|
|
|
with patch.object(requests, "get") as mock_get:
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"models": []}
|
|
mock_get.return_value = mock_response
|
|
|
|
result = fetch_ollama_models("http://localhost:11434")
|
|
|
|
assert result == []
|