chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:08:55 +08:00
commit 7a0da7932b
2985 changed files with 1049377 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Tests for API module."""
+106
View File
@@ -0,0 +1,106 @@
"""Fixtures for API module tests."""
import pytest
from unittest.mock import MagicMock, patch
@pytest.fixture
def mock_llm():
"""Create a mock LLM."""
mock = MagicMock()
mock.invoke.return_value = MagicMock(content="Mocked LLM response")
return mock
@pytest.fixture
def mock_search_engine():
"""Create a mock search engine."""
mock = MagicMock()
mock.run.return_value = [
{
"title": "Result 1",
"link": "https://example.com/1",
"snippet": "Snippet 1",
},
{
"title": "Result 2",
"link": "https://example.com/2",
"snippet": "Snippet 2",
},
]
return mock
@pytest.fixture
def mock_search_system():
"""Create a mock AdvancedSearchSystem."""
mock = MagicMock()
mock.analyze_topic.return_value = {
"current_knowledge": "Summary of findings",
"findings": [{"content": "Finding 1"}, {"content": "Finding 2"}],
"iterations": 2,
"questions": {"1": ["Q1", "Q2"]},
"formatted_findings": "Formatted findings text",
"all_links_of_system": [
{"title": "Source 1", "link": "https://example.com/1"},
],
}
mock.max_iterations = 2
mock.questions_per_iteration = 3
mock.model = MagicMock()
return mock
@pytest.fixture
def mock_report_generator():
"""Create a mock report generator."""
mock = MagicMock()
mock.generate_report.return_value = {
"content": "# Report\n\nReport content",
"metadata": {"generated": "2024-01-01T00:00:00Z"},
}
return mock
@pytest.fixture
def sample_settings_snapshot():
"""Create a sample settings snapshot."""
return {
"llm.provider": {"value": "ollama", "ui_element": "select"},
"llm.model": {"value": "gemma3:12b", "ui_element": "text"},
"llm.temperature": {"value": 0.7, "ui_element": "range"},
"search.tool": {"value": "searxng", "ui_element": "select"},
"search.iterations": {"value": 2, "ui_element": "number"},
"search.questions_per_iteration": {"value": 3, "ui_element": "number"},
"search.max_results": {"value": 20, "ui_element": "number"},
}
@pytest.fixture
def mock_get_llm(mock_llm):
"""Mock get_llm function."""
with patch(
"local_deep_research.api.research_functions.get_llm",
return_value=mock_llm,
):
yield mock_llm
@pytest.fixture
def mock_get_search(mock_search_engine):
"""Mock get_search function."""
with patch(
"local_deep_research.api.research_functions.get_search",
return_value=mock_search_engine,
):
yield mock_search_engine
@pytest.fixture
def mock_advanced_search_system(mock_search_system):
"""Mock AdvancedSearchSystem class."""
with patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem",
return_value=mock_search_system,
):
yield mock_search_system
+185
View File
@@ -0,0 +1,185 @@
"""Tests for benchmark API functions."""
from unittest.mock import patch
class TestGetAvailableBenchmarks:
def test_returns_list(self):
from local_deep_research.api.benchmark_functions import (
get_available_benchmarks,
)
result = get_available_benchmarks()
assert isinstance(result, list)
def test_contains_simpleqa(self):
from local_deep_research.api.benchmark_functions import (
get_available_benchmarks,
)
result = get_available_benchmarks()
ids = [b["id"] for b in result]
assert "simpleqa" in ids
def test_contains_browsecomp(self):
from local_deep_research.api.benchmark_functions import (
get_available_benchmarks,
)
result = get_available_benchmarks()
ids = [b["id"] for b in result]
assert "browsecomp" in ids
def test_contains_xbench(self):
from local_deep_research.api.benchmark_functions import (
get_available_benchmarks,
)
result = get_available_benchmarks()
ids = [b["id"] for b in result]
assert "xbench_deepsearch" in ids
def test_benchmarks_have_required_fields(self):
from local_deep_research.api.benchmark_functions import (
get_available_benchmarks,
)
result = get_available_benchmarks()
for benchmark in result:
assert "id" in benchmark
assert "name" in benchmark
assert "description" in benchmark
assert "recommended_examples" in benchmark
class TestEvaluateSimpleqa:
def test_calls_run_benchmark(self):
from local_deep_research.api.benchmark_functions import (
evaluate_simpleqa,
)
with patch(
"local_deep_research.api.benchmark_functions.run_simpleqa_benchmark"
) as mock:
mock.return_value = {"status": "complete"}
result = evaluate_simpleqa(num_examples=10)
mock.assert_called_once()
assert result["status"] == "complete"
def test_passes_search_config(self):
from local_deep_research.api.benchmark_functions import (
evaluate_simpleqa,
)
with patch(
"local_deep_research.api.benchmark_functions.run_simpleqa_benchmark"
) as mock:
mock.return_value = {}
evaluate_simpleqa(search_iterations=5, questions_per_iteration=7)
call_args = mock.call_args
assert call_args.kwargs["search_config"]["iterations"] == 5
assert (
call_args.kwargs["search_config"]["questions_per_iteration"]
== 7
)
def test_passes_evaluation_config(self):
from local_deep_research.api.benchmark_functions import (
evaluate_simpleqa,
)
with patch(
"local_deep_research.api.benchmark_functions.run_simpleqa_benchmark"
) as mock:
mock.return_value = {}
evaluate_simpleqa(
evaluation_model="gpt-4", evaluation_provider="openai"
)
call_args = mock.call_args
assert (
call_args.kwargs["evaluation_config"]["model_name"] == "gpt-4"
)
assert call_args.kwargs["evaluation_config"]["provider"] == "openai"
class TestEvaluateBrowsecomp:
def test_calls_run_benchmark(self):
from local_deep_research.api.benchmark_functions import (
evaluate_browsecomp,
)
with patch(
"local_deep_research.api.benchmark_functions.run_browsecomp_benchmark"
) as mock:
mock.return_value = {"status": "complete"}
result = evaluate_browsecomp(num_examples=10)
mock.assert_called_once()
assert result["status"] == "complete"
class TestEvaluateXbenchDeepsearch:
def test_calls_run_benchmark(self):
from local_deep_research.api.benchmark_functions import (
evaluate_xbench_deepsearch,
)
with patch(
"local_deep_research.api.benchmark_functions.run_xbench_deepsearch_benchmark"
) as mock:
mock.return_value = {"status": "complete"}
result = evaluate_xbench_deepsearch(num_examples=10)
mock.assert_called_once()
assert result["status"] == "complete"
class TestCompareConfigurations:
def test_returns_dict(self):
from local_deep_research.api.benchmark_functions import (
compare_configurations,
)
with patch(
"local_deep_research.api.benchmark_functions.run_benchmark"
) as mock_run:
with patch(
"local_deep_research.security.file_write_verifier.write_file_verified"
):
mock_run.return_value = {"metrics": {"accuracy": 0.8}}
result = compare_configurations(num_examples=5)
assert isinstance(result, dict)
assert "status" in result
def test_uses_default_configurations(self):
from local_deep_research.api.benchmark_functions import (
compare_configurations,
)
with patch(
"local_deep_research.api.benchmark_functions.run_benchmark"
) as mock_run:
with patch(
"local_deep_research.security.file_write_verifier.write_file_verified"
):
mock_run.return_value = {"metrics": {}}
result = compare_configurations(num_examples=5)
assert result["configurations_tested"] == 3
def test_custom_configurations(self):
from local_deep_research.api.benchmark_functions import (
compare_configurations,
)
custom_configs = [
{"name": "Custom", "search_tool": "wikipedia", "iterations": 2}
]
with patch(
"local_deep_research.api.benchmark_functions.run_benchmark"
) as mock_run:
with patch(
"local_deep_research.security.file_write_verifier.write_file_verified"
):
mock_run.return_value = {"metrics": {}}
result = compare_configurations(
num_examples=5, configurations=custom_configs
)
assert result["configurations_tested"] == 1
@@ -0,0 +1,275 @@
"""
Tests for uncovered code paths in api/benchmark_functions.py.
Targets:
- evaluate_simpleqa: evaluation_config with model only, provider only
- evaluate_browsecomp: evaluation_config branches
- evaluate_xbench_deepsearch: evaluation_config branches
- compare_configurations: default configs, report generation, metrics formatting
"""
from unittest.mock import patch
MODULE = "local_deep_research.api.benchmark_functions"
class TestEvaluateSimpleqa:
@patch(f"{MODULE}.run_simpleqa_benchmark")
def test_evaluation_config_with_model_only(self, mock_run):
"""evaluation_config is set when only model is provided."""
from local_deep_research.api.benchmark_functions import (
evaluate_simpleqa,
)
mock_run.return_value = {"status": "complete"}
evaluate_simpleqa(
num_examples=5,
evaluation_model="gpt-4",
)
call_kwargs = mock_run.call_args[1]
assert call_kwargs["evaluation_config"] == {"model_name": "gpt-4"}
@patch(f"{MODULE}.run_simpleqa_benchmark")
def test_evaluation_config_with_provider_only(self, mock_run):
"""evaluation_config is set when only provider is provided."""
from local_deep_research.api.benchmark_functions import (
evaluate_simpleqa,
)
mock_run.return_value = {"status": "complete"}
evaluate_simpleqa(
num_examples=5,
evaluation_provider="openai",
)
call_kwargs = mock_run.call_args[1]
assert call_kwargs["evaluation_config"] == {"provider": "openai"}
@patch(f"{MODULE}.run_simpleqa_benchmark")
def test_evaluation_config_with_both(self, mock_run):
"""evaluation_config includes both model and provider."""
from local_deep_research.api.benchmark_functions import (
evaluate_simpleqa,
)
mock_run.return_value = {"status": "complete"}
evaluate_simpleqa(
num_examples=5,
evaluation_model="gpt-4",
evaluation_provider="openai",
)
call_kwargs = mock_run.call_args[1]
assert call_kwargs["evaluation_config"]["model_name"] == "gpt-4"
assert call_kwargs["evaluation_config"]["provider"] == "openai"
@patch(f"{MODULE}.run_simpleqa_benchmark")
def test_no_evaluation_config(self, mock_run):
"""evaluation_config is None when neither model nor provider."""
from local_deep_research.api.benchmark_functions import (
evaluate_simpleqa,
)
mock_run.return_value = {"status": "complete"}
evaluate_simpleqa(num_examples=5)
call_kwargs = mock_run.call_args[1]
assert call_kwargs["evaluation_config"] is None
class TestEvaluateBrowsecomp:
@patch(f"{MODULE}.run_browsecomp_benchmark")
def test_evaluation_config_with_model(self, mock_run):
"""evaluation_config with model for browsecomp."""
from local_deep_research.api.benchmark_functions import (
evaluate_browsecomp,
)
mock_run.return_value = {"status": "complete"}
evaluate_browsecomp(
num_examples=5,
evaluation_model="claude-3",
)
call_kwargs = mock_run.call_args[1]
assert call_kwargs["evaluation_config"]["model_name"] == "claude-3"
@patch(f"{MODULE}.run_browsecomp_benchmark")
def test_evaluation_config_with_provider(self, mock_run):
"""evaluation_config with provider for browsecomp."""
from local_deep_research.api.benchmark_functions import (
evaluate_browsecomp,
)
mock_run.return_value = {"status": "complete"}
evaluate_browsecomp(
num_examples=5,
evaluation_provider="anthropic",
)
call_kwargs = mock_run.call_args[1]
assert call_kwargs["evaluation_config"]["provider"] == "anthropic"
class TestEvaluateXbenchDeepsearch:
@patch(f"{MODULE}.run_xbench_deepsearch_benchmark")
def test_evaluation_config_with_model(self, mock_run):
"""evaluation_config with model for xbench."""
from local_deep_research.api.benchmark_functions import (
evaluate_xbench_deepsearch,
)
mock_run.return_value = {"status": "complete"}
evaluate_xbench_deepsearch(
num_examples=5,
evaluation_model="gpt-4o",
)
call_kwargs = mock_run.call_args[1]
assert call_kwargs["evaluation_config"]["model_name"] == "gpt-4o"
@patch(f"{MODULE}.run_xbench_deepsearch_benchmark")
def test_evaluation_config_with_provider(self, mock_run):
"""evaluation_config with provider for xbench."""
from local_deep_research.api.benchmark_functions import (
evaluate_xbench_deepsearch,
)
mock_run.return_value = {"status": "complete"}
evaluate_xbench_deepsearch(
num_examples=5,
evaluation_provider="openai",
)
call_kwargs = mock_run.call_args[1]
assert call_kwargs["evaluation_config"]["provider"] == "openai"
class TestCompareConfigurations:
@patch(
"local_deep_research.security.file_write_verifier.write_file_verified"
)
@patch(f"{MODULE}.run_benchmark")
def test_default_configurations(self, mock_run, mock_write):
"""Uses default configurations when none provided."""
from local_deep_research.api.benchmark_functions import (
compare_configurations,
)
mock_run.return_value = {
"metrics": {"accuracy": 0.85, "average_processing_time": 1.5},
"total_examples": 20,
}
result = compare_configurations(
dataset_type="simpleqa",
num_examples=5,
output_dir="/tmp/test_bench",
)
assert result["status"] == "complete"
assert result["configurations_tested"] == 3 # 3 default configs
assert mock_run.call_count == 3
@patch(
"local_deep_research.security.file_write_verifier.write_file_verified"
)
@patch(f"{MODULE}.run_benchmark")
def test_custom_configurations(self, mock_run, mock_write):
"""Passes custom configurations correctly."""
from local_deep_research.api.benchmark_functions import (
compare_configurations,
)
mock_run.return_value = {
"metrics": {"accuracy": 0.9, "average_processing_time": 2.0},
"total_examples": 10,
}
configs = [
{
"name": "Fast",
"search_tool": "wikipedia",
"iterations": 1,
"questions_per_iteration": 2,
},
]
result = compare_configurations(
configurations=configs,
num_examples=10,
output_dir="/tmp/test_bench",
)
assert result["configurations_tested"] == 1
@patch(
"local_deep_research.security.file_write_verifier.write_file_verified"
)
@patch(f"{MODULE}.run_benchmark")
def test_report_written(self, mock_run, mock_write):
"""Comparison report is written to file."""
from local_deep_research.api.benchmark_functions import (
compare_configurations,
)
mock_run.return_value = {
"metrics": {"accuracy": 0.85, "average_processing_time": 1.5},
"total_examples": 20,
}
result = compare_configurations(
num_examples=5,
output_dir="/tmp/test_bench",
)
mock_write.assert_called_once()
# Report content should contain markdown table
report_content = mock_write.call_args[0][1]
assert "Configuration" in report_content
assert "Accuracy" in report_content
assert result["report_path"].endswith(".md")
@patch(
"local_deep_research.security.file_write_verifier.write_file_verified"
)
@patch(f"{MODULE}.run_benchmark")
def test_extra_config_items_passed(self, mock_run, mock_write):
"""Extra config items beyond standard ones are passed through."""
from local_deep_research.api.benchmark_functions import (
compare_configurations,
)
mock_run.return_value = {
"metrics": {"accuracy": 0.5},
"total_examples": 5,
}
configs = [
{
"name": "Custom",
"search_tool": "searxng",
"iterations": 1,
"questions_per_iteration": 1,
"custom_param": "value",
},
]
compare_configurations(
configurations=configs,
num_examples=5,
output_dir="/tmp/test_bench",
)
call_kwargs = mock_run.call_args[1]
assert call_kwargs["search_config"]["custom_param"] == "value"
+662
View File
@@ -0,0 +1,662 @@
"""Tests for LDRClient HTTP client."""
import pytest
from unittest.mock import MagicMock, Mock, patch
from local_deep_research.api.client import LDRClient, quick_query
class TestLDRClientInit:
"""Tests for LDRClient initialization."""
def test_default_base_url(self):
"""Test default base URL is localhost:5000."""
client = LDRClient()
assert client.base_url == "http://localhost:5000"
def test_custom_base_url(self):
"""Test custom base URL."""
client = LDRClient(base_url="http://example.com:8080")
assert client.base_url == "http://example.com:8080"
def test_initial_state(self):
"""Test initial state of client."""
client = LDRClient()
assert client.csrf_token is None
assert client.logged_in is False
assert client.username is None
def test_creates_session(self):
"""Test that a session is created."""
client = LDRClient()
assert client.session is not None
class TestLDRClientLogin:
"""Tests for LDRClient login."""
def test_login_success(self):
"""Test successful login."""
client = LDRClient()
# Mock the session responses
login_page_response = Mock()
login_page_response.text = (
'<input name="csrf_token" value="test_csrf_token"/>'
)
login_response = Mock()
login_response.status_code = 200
csrf_response = Mock()
csrf_response.status_code = 200
csrf_response.json.return_value = {"csrf_token": "api_csrf_token"}
client.session = Mock()
client.session.get.side_effect = [login_page_response, csrf_response]
client.session.post.return_value = login_response
result = client.login("testuser", "testpass")
assert result is True
assert client.logged_in is True
assert client.username == "testuser"
assert client.csrf_token == "api_csrf_token"
def test_login_no_csrf_in_page(self):
"""Test login failure when CSRF token not found in page."""
client = LDRClient()
login_page_response = Mock()
login_page_response.text = "<html>No CSRF here</html>"
client.session = Mock()
client.session.get.return_value = login_page_response
result = client.login("testuser", "testpass")
assert result is False
assert client.logged_in is False
def test_login_http_error(self):
"""Test login failure on HTTP error."""
client = LDRClient()
login_page_response = Mock()
login_page_response.text = (
'<input name="csrf_token" value="test_csrf"/>'
)
login_response = Mock()
login_response.status_code = 401 # Unauthorized
client.session = Mock()
client.session.get.return_value = login_page_response
client.session.post.return_value = login_response
result = client.login("testuser", "wrongpass")
assert result is False
def test_login_csrf_fetch_fails(self):
"""Test login succeeds even if CSRF fetch fails."""
client = LDRClient()
login_page_response = Mock()
login_page_response.text = (
'<input name="csrf_token" value="test_csrf"/>'
)
login_response = Mock()
login_response.status_code = 200
csrf_response = Mock()
csrf_response.status_code = 500 # Failed
client.session = Mock()
client.session.get.side_effect = [login_page_response, csrf_response]
client.session.post.return_value = login_response
result = client.login("testuser", "testpass")
# Should still succeed but without API CSRF token
assert result is True
assert client.logged_in is True
assert client.csrf_token is None
def test_login_exception(self):
"""Test login handles exceptions."""
client = LDRClient()
client.session = Mock()
client.session.get.side_effect = Exception("Network error")
result = client.login("testuser", "testpass")
assert result is False
class TestLDRClientApiHeaders:
"""Tests for LDRClient API headers."""
def test_api_headers_with_token(self):
"""Test API headers when CSRF token is set."""
client = LDRClient()
client.csrf_token = "test_token"
headers = client._api_headers()
assert headers == {"X-CSRF-Token": "test_token"}
def test_api_headers_without_token(self):
"""Test API headers when no CSRF token."""
client = LDRClient()
headers = client._api_headers()
assert headers == {}
class TestLDRClientQuickResearch:
"""Tests for LDRClient quick_research."""
def test_requires_login(self):
"""Test that quick_research requires login."""
client = LDRClient()
with pytest.raises(RuntimeError, match="Not logged in"):
client.quick_research("test query")
def test_starts_research(self):
"""Test that research is started."""
client = LDRClient()
client.logged_in = True
start_response = Mock()
start_response.status_code = 200
start_response.json.return_value = {"research_id": "test-id"}
status_response = Mock()
status_response.status_code = 200
status_response.json.return_value = {"status": "completed"}
result_response = Mock()
result_response.status_code = 200
result_response.json.return_value = {"summary": "Test summary"}
client.session = Mock()
client.session.post.return_value = start_response
client.session.get.side_effect = [status_response, result_response]
result = client.quick_research("test query")
assert result == {"summary": "Test summary"}
def test_returns_research_id_when_not_waiting(self):
"""Test returning research_id when not waiting."""
client = LDRClient()
client.logged_in = True
start_response = Mock()
start_response.status_code = 200
start_response.json.return_value = {"research_id": "test-id"}
client.session = Mock()
client.session.post.return_value = start_response
result = client.quick_research("test query", wait_for_result=False)
assert result == {"research_id": "test-id"}
def test_raises_on_start_error(self):
"""Test raising error on start failure."""
client = LDRClient()
client.logged_in = True
error_response = Mock()
error_response.status_code = 500
error_response.json.return_value = [{"message": "Server error"}]
client.session = Mock()
client.session.post.return_value = error_response
with pytest.raises(RuntimeError, match="Failed to start research"):
client.quick_research("test query")
def test_raises_on_no_research_id(self):
"""Test raising error when no research_id returned."""
client = LDRClient()
client.logged_in = True
response = Mock()
response.status_code = 200
response.json.return_value = {} # No research_id
client.session = Mock()
client.session.post.return_value = response
with pytest.raises(RuntimeError, match="No research ID"):
client.quick_research("test query")
class TestLDRClientWaitForResearch:
"""Tests for LDRClient wait_for_research."""
def test_returns_result_on_completion(self):
"""Test returning result when research completes."""
client = LDRClient()
status_response = Mock()
status_response.status_code = 200
status_response.json.return_value = {"status": "completed"}
result_response = Mock()
result_response.status_code = 200
result_response.json.return_value = {"summary": "Result"}
client.session = Mock()
client.session.get.side_effect = [status_response, result_response]
result = client.wait_for_research("test-id", timeout=10)
assert result == {"summary": "Result"}
def test_raises_on_failure(self):
"""Test raising error on research failure."""
client = LDRClient()
status_response = Mock()
status_response.status_code = 200
status_response.json.return_value = {
"status": "failed",
"error": "Test error",
}
client.session = Mock()
client.session.get.return_value = status_response
with pytest.raises(RuntimeError, match="Research failed"):
client.wait_for_research("test-id", timeout=10)
def test_raises_on_timeout(self):
"""Test raising error on timeout."""
client = LDRClient()
status_response = Mock()
status_response.status_code = 200
status_response.json.return_value = {"status": "in_progress"}
client.session = Mock()
client.session.get.return_value = status_response
with pytest.raises(RuntimeError, match="timed out"):
client.wait_for_research("test-id", timeout=1)
def test_raises_on_result_fetch_error(self):
"""Test raising error when result fetch fails."""
client = LDRClient()
status_response = Mock()
status_response.status_code = 200
status_response.json.return_value = {"status": "completed"}
result_response = Mock()
result_response.status_code = 500
client.session = Mock()
client.session.get.side_effect = [status_response, result_response]
with pytest.raises(RuntimeError, match="Failed to get results"):
client.wait_for_research("test-id", timeout=10)
class TestLDRClientGetSettings:
"""Tests for LDRClient get_settings."""
def test_requires_login(self):
"""Test that get_settings requires login."""
client = LDRClient()
with pytest.raises(RuntimeError, match="Not logged in"):
client.get_settings()
def test_returns_settings(self):
"""Test returning settings."""
client = LDRClient()
client.logged_in = True
response = Mock()
response.status_code = 200
response.json.return_value = {"llm": {"model": "test"}}
client.session = Mock()
client.session.get.return_value = response
result = client.get_settings()
assert result == {"llm": {"model": "test"}}
def test_raises_on_error(self):
"""Test raising error on failure."""
client = LDRClient()
client.logged_in = True
response = Mock()
response.status_code = 500
client.session = Mock()
client.session.get.return_value = response
with pytest.raises(RuntimeError, match="Failed to get settings"):
client.get_settings()
class TestLDRClientUpdateSetting:
"""Tests for LDRClient update_setting."""
def test_requires_login(self):
"""Test that update_setting requires login."""
client = LDRClient()
with pytest.raises(RuntimeError, match="Not logged in"):
client.update_setting("key", "value")
def test_returns_true_on_success(self):
"""Test returning True on success."""
client = LDRClient()
client.logged_in = True
client.csrf_token = "token"
response = Mock()
response.status_code = 200
client.session = Mock()
client.session.put.return_value = response
result = client.update_setting("llm.model", "new_model")
assert result is True
def test_returns_false_on_failure(self):
"""Test returning False on failure."""
client = LDRClient()
client.logged_in = True
response = Mock()
response.status_code = 400
client.session = Mock()
client.session.put.return_value = response
result = client.update_setting("llm.model", "new_model")
assert result is False
class TestLDRClientGetHistory:
"""Tests for LDRClient get_history."""
def test_requires_login(self):
"""Test that get_history requires login."""
client = LDRClient()
with pytest.raises(RuntimeError, match="Not logged in"):
client.get_history()
def test_returns_list(self):
"""Test returning history list."""
client = LDRClient()
client.logged_in = True
response = Mock()
response.status_code = 200
response.json.return_value = [{"query": "test"}]
client.session = Mock()
client.session.get.return_value = response
result = client.get_history()
assert result == [{"query": "test"}]
def test_handles_dict_response(self):
"""Test handling dict response with history key."""
client = LDRClient()
client.logged_in = True
response = Mock()
response.status_code = 200
response.json.return_value = {"history": [{"query": "test"}]}
client.session = Mock()
client.session.get.return_value = response
result = client.get_history()
assert result == [{"query": "test"}]
def test_handles_dict_response_with_items(self):
"""Test handling dict response with items key."""
client = LDRClient()
client.logged_in = True
response = Mock()
response.status_code = 200
response.json.return_value = {"items": [{"query": "test"}]}
client.session = Mock()
client.session.get.return_value = response
result = client.get_history()
assert result == [{"query": "test"}]
def test_raises_on_error(self):
"""Test raising error on failure."""
client = LDRClient()
client.logged_in = True
response = Mock()
response.status_code = 500
client.session = Mock()
client.session.get.return_value = response
with pytest.raises(RuntimeError, match="Failed to get history"):
client.get_history()
class TestLDRClientLogout:
"""Tests for LDRClient logout."""
def test_logout_clears_state(self):
"""Test that logout clears client state."""
client = LDRClient()
client.logged_in = True
client.username = "testuser"
client.csrf_token = "token"
client.session = Mock()
client.logout()
assert client.logged_in is False
assert client.username is None
assert client.csrf_token is None
def test_logout_posts_when_logged_in(self):
"""Test that logout posts to server when logged in."""
client = LDRClient()
client.logged_in = True
client.session = Mock()
client.logout()
client.session.post.assert_called_once()
def test_logout_skips_post_when_not_logged_in(self):
"""Test that logout doesn't post when not logged in."""
client = LDRClient()
client.logged_in = False
client.session = Mock()
client.logout()
client.session.post.assert_not_called()
class TestLDRClientContextManager:
"""Tests for LDRClient context manager."""
def test_enter_returns_client(self):
"""Test that __enter__ returns the client."""
client = LDRClient()
result = client.__enter__()
assert result is client
def test_exit_calls_logout(self):
"""Test that __exit__ calls logout."""
client = LDRClient()
client.session = Mock()
with patch.object(client, "logout") as mock_logout:
client.__exit__(None, None, None)
mock_logout.assert_called_once()
class TestLDRClientBenchmarks:
"""Tests for LDRClient benchmark methods."""
def test_submit_benchmark(self):
"""Test submitting benchmark results."""
client = LDRClient()
with patch(
"local_deep_research.api.client.Benchmark_results"
) as mock_class:
mock_benchmarks = Mock()
mock_benchmarks.add_result.return_value = True
mock_class.return_value = mock_benchmarks
result = client.submit_benchmark(
model="test-model",
hardware="test-hardware",
accuracy_focused=90.0,
accuracy_source=85.0,
avg_time_per_question=30.0,
context_window=4096,
temperature=0.7,
ldr_version="1.0.0",
date_tested="2024-01-01",
)
assert result is True
mock_benchmarks.add_result.assert_called_once()
def test_get_benchmarks_all(self):
"""Test getting all benchmarks."""
client = LDRClient()
with patch(
"local_deep_research.api.client.Benchmark_results"
) as mock_class:
mock_benchmarks = Mock()
mock_benchmarks.get_all.return_value = [{"model": "test"}]
mock_class.return_value = mock_benchmarks
result = client.get_benchmarks()
assert result == [{"model": "test"}]
mock_benchmarks.get_all.assert_called_once()
def test_get_benchmarks_best_only(self):
"""Test getting best benchmarks only."""
client = LDRClient()
with patch(
"local_deep_research.api.client.Benchmark_results"
) as mock_class:
mock_benchmarks = Mock()
mock_benchmarks.get_best.return_value = [{"model": "best"}]
mock_class.return_value = mock_benchmarks
result = client.get_benchmarks(best_only=True)
assert result == [{"model": "best"}]
mock_benchmarks.get_best.assert_called_once()
class TestQuickQuery:
"""Tests for quick_query function."""
def test_returns_summary(self):
"""Test that quick_query returns summary."""
with patch(
"local_deep_research.api.client.LDRClient"
) as mock_client_class:
mock_client = MagicMock()
mock_client.login.return_value = True
mock_client.quick_research.return_value = {
"summary": "Test summary"
}
mock_client_class.return_value.__enter__ = Mock(
return_value=mock_client
)
mock_client_class.return_value.__exit__ = Mock(return_value=False)
result = quick_query("user", "pass", "test query")
assert result == "Test summary"
def test_raises_on_login_failure(self):
"""Test raising error on login failure."""
with patch(
"local_deep_research.api.client.LDRClient"
) as mock_client_class:
mock_client = MagicMock()
mock_client.login.return_value = False
mock_client_class.return_value.__enter__ = Mock(
return_value=mock_client
)
mock_client_class.return_value.__exit__ = Mock(return_value=False)
with pytest.raises(RuntimeError, match="Login failed"):
quick_query("user", "wrongpass", "test query")
def test_uses_custom_base_url(self):
"""Test using custom base URL."""
with patch(
"local_deep_research.api.client.LDRClient"
) as mock_client_class:
mock_client = MagicMock()
mock_client.login.return_value = True
mock_client.quick_research.return_value = {"summary": "Test"}
mock_client_class.return_value.__enter__ = Mock(
return_value=mock_client
)
mock_client_class.return_value.__exit__ = Mock(return_value=False)
quick_query("user", "pass", "query", base_url="http://custom:8080")
mock_client_class.assert_called_once_with("http://custom:8080")
def test_returns_no_summary_available(self):
"""Test returning default when no summary."""
with patch(
"local_deep_research.api.client.LDRClient"
) as mock_client_class:
mock_client = MagicMock()
mock_client.login.return_value = True
mock_client.quick_research.return_value = {} # No summary
mock_client_class.return_value.__enter__ = Mock(
return_value=mock_client
)
mock_client_class.return_value.__exit__ = Mock(return_value=False)
result = quick_query("user", "pass", "query")
assert result == "No summary available"
+735
View File
@@ -0,0 +1,735 @@
"""
Extended Tests for API Client
Phase 20: API Client & Authentication - API Client Tests
Tests authentication, session management, and API operations.
"""
import pytest
from unittest.mock import patch, MagicMock
class TestAuthentication:
"""Tests for authentication functionality"""
@patch("local_deep_research.api.client.SafeSession")
def test_login_success(self, mock_session_cls):
"""Test successful login flow"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
# Mock login page response with CSRF token
mock_login_page = MagicMock()
mock_login_page.text = """
<form>
<input type="hidden" name="csrf_token" value="test_csrf_123"/>
</form>
"""
# Mock login POST response
mock_login_response = MagicMock()
mock_login_response.status_code = 200
# Mock CSRF token endpoint
mock_csrf_response = MagicMock()
mock_csrf_response.status_code = 200
mock_csrf_response.json.return_value = {"csrf_token": "api_csrf_456"}
mock_session.get.side_effect = [mock_login_page, mock_csrf_response]
mock_session.post.return_value = mock_login_response
client = LDRClient()
result = client.login("testuser", "testpass")
assert result is True
assert client.logged_in is True
assert client.username == "testuser"
assert client.csrf_token == "api_csrf_456"
@patch("local_deep_research.api.client.SafeSession")
def test_login_invalid_credentials(self, mock_session_cls):
"""Test login with invalid credentials"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
# Mock login page response
mock_login_page = MagicMock()
mock_login_page.text = '<input name="csrf_token" value="test"/>'
# Mock failed login
mock_login_response = MagicMock()
mock_login_response.status_code = 401
mock_session.get.return_value = mock_login_page
mock_session.post.return_value = mock_login_response
client = LDRClient()
result = client.login("baduser", "badpass")
assert result is False
assert client.logged_in is False
@patch("local_deep_research.api.client.SafeSession")
def test_login_csrf_token_extraction(self, mock_session_cls):
"""Test CSRF token extraction from login page"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
# HTML with various CSRF patterns
mock_login_page = MagicMock()
mock_login_page.text = """
<form method="POST">
<input type="hidden" name="csrf_token" value="extracted_token_123"/>
<input type="text" name="username"/>
</form>
"""
mock_session.get.side_effect = [
mock_login_page,
MagicMock(
status_code=200,
json=MagicMock(return_value={"csrf_token": "api_token"}),
),
]
mock_session.post.return_value = MagicMock(status_code=200)
client = LDRClient()
client.login("user", "pass")
# Verify the correct CSRF token was sent in login POST
call_args = mock_session.post.call_args
assert call_args[1]["data"]["csrf_token"] == "extracted_token_123"
@patch("local_deep_research.api.client.SafeSession")
def test_login_session_persistence(self, mock_session_cls):
"""Test session cookies persist after login"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_login_page = MagicMock()
mock_login_page.text = '<input name="csrf_token" value="test"/>'
mock_session.get.side_effect = [
mock_login_page,
MagicMock(
status_code=200,
json=MagicMock(return_value={"csrf_token": "token"}),
),
]
mock_session.post.return_value = MagicMock(status_code=200)
client = LDRClient()
client.login("user", "pass")
# Session should be used for subsequent requests
assert client.session is mock_session
@patch("local_deep_research.api.client.SafeSession")
def test_logout_session_cleanup(self, mock_session_cls):
"""Test logout cleans up session"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
client = LDRClient()
client.logged_in = True
client.csrf_token = "test_token"
client.username = "testuser"
client.logout()
assert client.logged_in is False
assert client.csrf_token is None
assert client.username is None
mock_session.close.assert_called_once()
@patch("local_deep_research.api.client.SafeSession")
def test_session_expiry_handling(self, mock_session_cls):
"""Test handling of expired session"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
# Simulate expired session response
mock_session.get.return_value = MagicMock(status_code=401)
client = LDRClient()
client.logged_in = True
with pytest.raises(RuntimeError):
client.get_settings()
@patch("local_deep_research.api.client.SafeSession")
def test_login_no_csrf_token_in_page(self, mock_session_cls):
"""Test login fails gracefully when no CSRF token found"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
# Page without CSRF token
mock_login_page = MagicMock()
mock_login_page.text = (
'<form><input type="text" name="username"/></form>'
)
mock_session.get.return_value = mock_login_page
client = LDRClient()
result = client.login("user", "pass")
assert result is False
@patch("local_deep_research.api.client.SafeSession")
def test_login_redirect_handling(self, mock_session_cls):
"""Test login handles redirects properly"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_login_page = MagicMock()
mock_login_page.text = '<input name="csrf_token" value="test"/>'
# 302 redirect after successful login
mock_login_response = MagicMock()
mock_login_response.status_code = 302
mock_csrf_response = MagicMock()
mock_csrf_response.status_code = 200
mock_csrf_response.json.return_value = {"csrf_token": "api_token"}
mock_session.get.side_effect = [mock_login_page, mock_csrf_response]
mock_session.post.return_value = mock_login_response
client = LDRClient()
result = client.login("user", "pass")
assert result is True
@patch("local_deep_research.api.client.SafeSession")
def test_login_html_parsing(self, mock_session_cls):
"""Test CSRF extraction from various HTML formats"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
# Test different HTML input formats
html_formats = [
'<input type="hidden" name="csrf_token" value="token1"/>',
'<input name="csrf_token" type="hidden" value="token2"/>',
'<input name="csrf_token" value="token3" />',
]
for html in html_formats:
mock_login_page = MagicMock()
mock_login_page.text = f"<form>{html}</form>"
mock_session.get.side_effect = [
mock_login_page,
MagicMock(
status_code=200,
json=MagicMock(return_value={"csrf_token": "api"}),
),
]
mock_session.post.return_value = MagicMock(status_code=200)
client = LDRClient()
result = client.login("user", "pass")
assert result is True
@patch("local_deep_research.api.client.SafeSession")
def test_login_error_extraction(self, mock_session_cls):
"""Test error message extraction on login failure"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_login_page = MagicMock()
mock_login_page.text = '<input name="csrf_token" value="test"/>'
mock_login_response = MagicMock()
mock_login_response.status_code = 403
mock_session.get.return_value = mock_login_page
mock_session.post.return_value = mock_login_response
client = LDRClient()
result = client.login("user", "pass")
assert result is False
class TestAPIOperations:
"""Tests for API operation methods"""
@patch("local_deep_research.api.client.SafeSession")
def test_api_get_request(self, mock_session_cls):
"""Test GET request to API"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_session.get.return_value = MagicMock(
status_code=200, json=MagicMock(return_value={"data": "test"})
)
client = LDRClient()
client.logged_in = True
result = client.get_settings()
assert result == {"data": "test"}
@patch("local_deep_research.api.client.SafeSession")
def test_api_post_request(self, mock_session_cls):
"""Test POST request to API"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_session.post.return_value = MagicMock(
status_code=200, json=MagicMock(return_value={"research_id": "123"})
)
client = LDRClient()
client.logged_in = True
client.csrf_token = "test_token"
result = client.quick_research("test query", wait_for_result=False)
assert result == {"research_id": "123"}
# Verify CSRF token was included
call_args = mock_session.post.call_args
assert call_args[1]["headers"]["X-CSRF-Token"] == "test_token"
@patch("local_deep_research.api.client.SafeSession")
def test_api_put_request(self, mock_session_cls):
"""Test PUT request to API"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_session.put.return_value = MagicMock(status_code=200)
client = LDRClient()
client.logged_in = True
client.csrf_token = "test_token"
result = client.update_setting("llm.model", "test-model")
assert result is True
mock_session.put.assert_called_once()
@patch("local_deep_research.api.client.SafeSession")
def test_api_error_handling(self, mock_session_cls):
"""Test API error handling"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_session.get.return_value = MagicMock(status_code=500)
client = LDRClient()
client.logged_in = True
with pytest.raises(RuntimeError):
client.get_settings()
@patch("local_deep_research.api.client.SafeSession")
@patch("time.sleep")
def test_api_timeout_handling(self, mock_sleep, mock_session_cls):
"""Test timeout handling in wait_for_research"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
# Always return in_progress status
mock_session.get.return_value = MagicMock(
status_code=200,
json=MagicMock(return_value={"status": "in_progress"}),
)
client = LDRClient()
client.logged_in = True
with pytest.raises(RuntimeError, match="timed out"):
client.wait_for_research("123", timeout=1)
@patch("local_deep_research.api.client.SafeSession")
def test_api_response_parsing(self, mock_session_cls):
"""Test API response JSON parsing"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
complex_response = {
"history": [
{"id": 1, "query": "test1"},
{"id": 2, "query": "test2"},
]
}
mock_session.get.return_value = MagicMock(
status_code=200, json=MagicMock(return_value=complex_response)
)
client = LDRClient()
client.logged_in = True
result = client.get_history()
assert len(result) == 2
assert result[0]["query"] == "test1"
@patch("local_deep_research.api.client.SafeSession")
def test_api_headers_with_csrf(self, mock_session_cls):
"""Test API headers include CSRF token"""
from local_deep_research.api.client import LDRClient
client = LDRClient()
client.csrf_token = "my_csrf_token"
headers = client._api_headers()
assert headers["X-CSRF-Token"] == "my_csrf_token"
@patch("local_deep_research.api.client.SafeSession")
def test_api_headers_without_csrf(self, mock_session_cls):
"""Test API headers when no CSRF token"""
from local_deep_research.api.client import LDRClient
client = LDRClient()
client.csrf_token = None
headers = client._api_headers()
assert headers == {}
@patch("local_deep_research.api.client.SafeSession")
def test_not_logged_in_raises_error(self, mock_session_cls):
"""Test methods raise error when not logged in"""
from local_deep_research.api.client import LDRClient
client = LDRClient()
client.logged_in = False
with pytest.raises(RuntimeError, match="Not logged in"):
client.get_settings()
with pytest.raises(RuntimeError, match="Not logged in"):
client.quick_research("test")
with pytest.raises(RuntimeError, match="Not logged in"):
client.get_history()
@patch("local_deep_research.api.client.SafeSession")
@patch("time.sleep")
def test_wait_for_research_success(self, mock_sleep, mock_session_cls):
"""Test successful research completion"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
# First call returns in_progress, second returns completed
mock_session.get.side_effect = [
MagicMock(
status_code=200,
json=MagicMock(return_value={"status": "in_progress"}),
),
MagicMock(
status_code=200,
json=MagicMock(return_value={"status": "completed"}),
),
MagicMock(
status_code=200,
json=MagicMock(return_value={"summary": "Research results"}),
),
]
client = LDRClient()
client.logged_in = True
result = client.wait_for_research("123", timeout=30)
assert result["summary"] == "Research results"
@patch("local_deep_research.api.client.SafeSession")
def test_wait_for_research_failure(self, mock_session_cls):
"""Test research failure handling"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_session.get.return_value = MagicMock(
status_code=200,
json=MagicMock(
return_value={"status": "failed", "error": "LLM error"}
),
)
client = LDRClient()
client.logged_in = True
with pytest.raises(RuntimeError, match="Research failed"):
client.wait_for_research("123")
class TestContextManager:
"""Tests for context manager functionality"""
@patch("local_deep_research.api.client.SafeSession")
def test_context_manager_enter(self, mock_session_cls):
"""Test context manager __enter__"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
with LDRClient() as client:
assert isinstance(client, LDRClient)
@patch("local_deep_research.api.client.SafeSession")
def test_context_manager_exit_logout(self, mock_session_cls):
"""Test context manager __exit__ calls logout"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
with LDRClient() as client:
client.logged_in = True
mock_session.close.assert_called_once()
class TestQuickQuery:
"""Tests for quick_query convenience function"""
@patch("local_deep_research.api.client.LDRClient")
def test_quick_query_success(self, mock_client_cls):
"""Test quick_query returns summary"""
from local_deep_research.api.client import quick_query
mock_client = MagicMock()
mock_client_cls.return_value.__enter__ = MagicMock(
return_value=mock_client
)
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
mock_client.login.return_value = True
mock_client.quick_research.return_value = {"summary": "Test summary"}
result = quick_query("user", "pass", "test query")
assert result == "Test summary"
@patch("local_deep_research.api.client.LDRClient")
def test_quick_query_login_failure(self, mock_client_cls):
"""Test quick_query raises on login failure"""
from local_deep_research.api.client import quick_query
mock_client = MagicMock()
mock_client_cls.return_value.__enter__ = MagicMock(
return_value=mock_client
)
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
mock_client.login.return_value = False
with pytest.raises(RuntimeError, match="Login failed"):
quick_query("user", "pass", "test")
class TestBenchmarkMethods:
"""Tests for benchmark-related methods"""
@patch("local_deep_research.api.client.SafeSession")
@patch("local_deep_research.api.client.Benchmark_results")
def test_submit_benchmark(self, mock_benchmark_cls, mock_session_cls):
"""Test benchmark submission"""
from local_deep_research.api.client import LDRClient
mock_benchmark = MagicMock()
mock_benchmark.add_result.return_value = True
mock_benchmark_cls.return_value = mock_benchmark
client = LDRClient()
result = client.submit_benchmark(
model="test-model",
hardware="test-hw",
accuracy_focused=85.0,
accuracy_source=80.0,
avg_time_per_question=30.0,
context_window=32000,
temperature=0.1,
ldr_version="0.6.0",
date_tested="2024-01-01",
)
assert result is True
mock_benchmark.add_result.assert_called_once()
@patch("local_deep_research.api.client.SafeSession")
@patch("local_deep_research.api.client.Benchmark_results")
def test_get_benchmarks_all(self, mock_benchmark_cls, mock_session_cls):
"""Test getting all benchmarks"""
from local_deep_research.api.client import LDRClient
mock_benchmark = MagicMock()
mock_benchmark.get_all.return_value = [{"model": "test"}]
mock_benchmark_cls.return_value = mock_benchmark
client = LDRClient()
result = client.get_benchmarks(best_only=False)
assert result == [{"model": "test"}]
mock_benchmark.get_all.assert_called_once()
@patch("local_deep_research.api.client.SafeSession")
@patch("local_deep_research.api.client.Benchmark_results")
def test_get_benchmarks_best_only(
self, mock_benchmark_cls, mock_session_cls
):
"""Test getting best benchmarks only"""
from local_deep_research.api.client import LDRClient
mock_benchmark = MagicMock()
mock_benchmark.get_best.return_value = [{"model": "best"}]
mock_benchmark_cls.return_value = mock_benchmark
client = LDRClient()
result = client.get_benchmarks(best_only=True)
assert result == [{"model": "best"}]
mock_benchmark.get_best.assert_called_once()
class TestHistoryHandling:
"""Tests for history retrieval"""
@patch("local_deep_research.api.client.SafeSession")
def test_get_history_dict_format(self, mock_session_cls):
"""Test history with dict response format"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_session.get.return_value = MagicMock(
status_code=200,
json=MagicMock(return_value={"history": [{"id": 1}]}),
)
client = LDRClient()
client.logged_in = True
result = client.get_history()
assert result == [{"id": 1}]
@patch("local_deep_research.api.client.SafeSession")
def test_get_history_items_format(self, mock_session_cls):
"""Test history with items key in response"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_session.get.return_value = MagicMock(
status_code=200, json=MagicMock(return_value={"items": [{"id": 2}]})
)
client = LDRClient()
client.logged_in = True
result = client.get_history()
assert result == [{"id": 2}]
@patch("local_deep_research.api.client.SafeSession")
def test_get_history_list_format(self, mock_session_cls):
"""Test history with list response format"""
from local_deep_research.api.client import LDRClient
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_session.get.return_value = MagicMock(
status_code=200, json=MagicMock(return_value=[{"id": 3}])
)
client = LDRClient()
client.logged_in = True
result = client.get_history()
assert result == [{"id": 3}]
class TestErrorResponseParsing:
"""Tests for error response JSON parsing (PR #2027).
PR #2027 changed bare `except:` to `except Exception:` when
parsing error JSON from failed API responses.
"""
def test_valid_json_error_parsed(self):
"""Valid JSON error response is parsed."""
import json
error_body = json.dumps([{"message": "Bad request"}])
try:
data = json.loads(error_body)
if isinstance(data, list) and len(data) > 0:
msg = data[0].get("message", "Unknown error")
else:
msg = str(data)
except Exception:
msg = error_body[:200]
assert msg == "Bad request"
def test_invalid_json_falls_back_to_text(self):
"""Invalid JSON falls back to raw text (caught by except Exception)."""
error_body = "Not JSON at all"
try:
import json
data = json.loads(error_body)
msg = str(data)
except Exception:
msg = error_body[:200]
assert msg == "Not JSON at all"
def test_empty_response_falls_back(self):
"""Empty response body falls back to text."""
error_body = ""
try:
import json
data = json.loads(error_body)
msg = str(data)
except Exception:
msg = error_body[:200]
assert msg == ""
+500
View File
@@ -0,0 +1,500 @@
"""
Tests for Research API endpoints.
Phase 32: API Endpoint Tests - Tests for research-related API functionality.
Tests research_functions.py API methods including quick_summary and deep_research.
"""
from unittest.mock import MagicMock, patch
import pytest
class TestInitSearchSystem:
"""Tests for _init_search_system function."""
@patch("local_deep_research.api.research_functions.get_llm")
@patch("local_deep_research.api.research_functions.AdvancedSearchSystem")
def test_init_search_system_basic(self, mock_system_class, mock_get_llm):
"""Test basic initialization of search system."""
from local_deep_research.api.research_functions import (
_init_search_system,
)
mock_llm = MagicMock()
mock_get_llm.return_value = mock_llm
mock_system = MagicMock()
mock_system_class.return_value = mock_system
result = _init_search_system()
mock_get_llm.assert_called_once()
mock_system_class.assert_called_once()
assert result == mock_system
@patch("local_deep_research.api.research_functions.get_llm")
@patch("local_deep_research.api.research_functions.AdvancedSearchSystem")
def test_init_search_system_with_model_name(
self, mock_system_class, mock_get_llm
):
"""Test initialization with custom model name."""
from local_deep_research.api.research_functions import (
_init_search_system,
)
mock_llm = MagicMock()
mock_get_llm.return_value = mock_llm
mock_system = MagicMock()
mock_system_class.return_value = mock_system
_init_search_system(model_name="gpt-4")
call_kwargs = mock_get_llm.call_args[1]
assert call_kwargs.get("model_name") == "gpt-4"
@patch("local_deep_research.api.research_functions.get_llm")
@patch("local_deep_research.api.research_functions.AdvancedSearchSystem")
def test_init_search_system_with_temperature(
self, mock_system_class, mock_get_llm
):
"""Test initialization with custom temperature."""
from local_deep_research.api.research_functions import (
_init_search_system,
)
mock_llm = MagicMock()
mock_get_llm.return_value = mock_llm
mock_system = MagicMock()
mock_system_class.return_value = mock_system
_init_search_system(temperature=0.5)
call_kwargs = mock_get_llm.call_args[1]
assert call_kwargs.get("temperature") == 0.5
@patch("local_deep_research.api.research_functions.get_llm")
@patch("local_deep_research.api.research_functions.AdvancedSearchSystem")
def test_init_search_system_with_provider(
self, mock_system_class, mock_get_llm
):
"""Test initialization with custom provider."""
from local_deep_research.api.research_functions import (
_init_search_system,
)
mock_llm = MagicMock()
mock_get_llm.return_value = mock_llm
mock_system = MagicMock()
mock_system_class.return_value = mock_system
_init_search_system(provider="anthropic")
call_kwargs = mock_get_llm.call_args[1]
assert call_kwargs.get("provider") == "anthropic"
@patch("local_deep_research.api.research_functions.get_llm")
@patch("local_deep_research.api.research_functions.AdvancedSearchSystem")
def test_init_search_system_with_iterations(
self, mock_system_class, mock_get_llm
):
"""Test initialization with custom iterations."""
from local_deep_research.api.research_functions import (
_init_search_system,
)
mock_llm = MagicMock()
mock_get_llm.return_value = mock_llm
mock_system = MagicMock()
mock_system_class.return_value = mock_system
result = _init_search_system(iterations=5)
assert result.max_iterations == 5
@patch("local_deep_research.api.research_functions.get_llm")
@patch("local_deep_research.api.research_functions.AdvancedSearchSystem")
def test_init_search_system_with_questions_per_iteration(
self, mock_system_class, mock_get_llm
):
"""Test initialization with custom questions per iteration."""
from local_deep_research.api.research_functions import (
_init_search_system,
)
mock_llm = MagicMock()
mock_get_llm.return_value = mock_llm
mock_system = MagicMock()
mock_system_class.return_value = mock_system
result = _init_search_system(questions_per_iteration=3)
assert result.questions_per_iteration == 3
@patch("local_deep_research.api.research_functions.get_llm")
@patch("local_deep_research.api.research_functions.AdvancedSearchSystem")
def test_init_search_system_with_progress_callback(
self, mock_system_class, mock_get_llm
):
"""Test initialization with progress callback."""
from local_deep_research.api.research_functions import (
_init_search_system,
)
mock_llm = MagicMock()
mock_get_llm.return_value = mock_llm
mock_system = MagicMock()
mock_system_class.return_value = mock_system
callback = MagicMock()
_init_search_system(progress_callback=callback)
mock_system.set_progress_callback.assert_called_once_with(callback)
@patch("local_deep_research.api.research_functions.get_llm")
@patch("local_deep_research.api.research_functions.AdvancedSearchSystem")
@patch("local_deep_research.api.research_functions.get_search")
def test_init_search_system_with_search_tool(
self, mock_get_search, mock_system_class, mock_get_llm
):
"""Test initialization with custom search tool."""
from local_deep_research.api.research_functions import (
_init_search_system,
)
mock_llm = MagicMock()
mock_get_llm.return_value = mock_llm
mock_system = MagicMock()
mock_system_class.return_value = mock_system
mock_search = MagicMock()
mock_get_search.return_value = mock_search
_init_search_system(search_tool="arxiv")
mock_get_search.assert_called_once()
call_args = mock_get_search.call_args[0]
assert call_args[0] == "arxiv"
@patch("local_deep_research.api.research_functions.get_llm")
@patch("local_deep_research.api.research_functions.AdvancedSearchSystem")
def test_init_search_system_with_search_strategy(
self, mock_system_class, mock_get_llm
):
"""Test initialization with custom search strategy."""
from local_deep_research.api.research_functions import (
_init_search_system,
)
mock_llm = MagicMock()
mock_get_llm.return_value = mock_llm
mock_system = MagicMock()
mock_system_class.return_value = mock_system
_init_search_system(search_strategy="modular")
call_kwargs = mock_system_class.call_args[1]
assert call_kwargs.get("strategy_name") == "modular"
@patch("local_deep_research.api.research_functions.get_llm")
@patch("local_deep_research.api.research_functions.AdvancedSearchSystem")
@patch(
"local_deep_research.web_search_engines.retriever_registry.retriever_registry"
)
def test_init_search_system_with_retrievers(
self, mock_registry, mock_system_class, mock_get_llm
):
"""Test initialization with custom retrievers."""
from local_deep_research.api.research_functions import (
_init_search_system,
)
mock_llm = MagicMock()
mock_get_llm.return_value = mock_llm
mock_system = MagicMock()
mock_system_class.return_value = mock_system
retrievers = {"custom": MagicMock()}
_init_search_system(retrievers=retrievers)
mock_registry.register_multiple.assert_called_once_with(retrievers)
@patch("local_deep_research.api.research_functions.get_llm")
@patch("local_deep_research.api.research_functions.AdvancedSearchSystem")
@patch("local_deep_research.llm.register_llm")
def test_init_search_system_with_llms(
self, mock_register_llm, mock_system_class, mock_get_llm
):
"""Test initialization with custom LLMs."""
from local_deep_research.api.research_functions import (
_init_search_system,
)
mock_llm = MagicMock()
mock_get_llm.return_value = mock_llm
mock_system = MagicMock()
mock_system_class.return_value = mock_system
custom_llm = MagicMock()
llms = {"custom_llm": custom_llm}
_init_search_system(llms=llms)
mock_register_llm.assert_called_once_with("custom_llm", custom_llm)
class TestQuickSummary:
"""Tests for quick_summary function."""
@patch("local_deep_research.api.research_functions._init_search_system")
def test_quick_summary_basic(self, mock_init_system):
"""Test basic quick summary."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {
"current_knowledge": "Summary content",
"iterations": 1,
"questions_by_iteration": {},
"all_links_of_system": [],
}
mock_init_system.return_value = mock_system
result = quick_summary("What is AI?")
assert "summary" in result or "current_knowledge" in result
mock_system.analyze_topic.assert_called_once()
@patch("local_deep_research.api.research_functions._init_search_system")
def test_quick_summary_with_provider(self, mock_init_system):
"""Test quick summary with custom provider in settings_snapshot."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {
"current_knowledge": "Summary",
"iterations": 1,
"questions_by_iteration": {},
"all_links_of_system": [],
}
mock_init_system.return_value = mock_system
quick_summary("Query", provider="anthropic")
# Provider is passed via settings_snapshot, not as a direct kwarg
call_kwargs = mock_init_system.call_args[1]
# Check that settings_snapshot was created and passed
assert "settings_snapshot" in call_kwargs
@patch("local_deep_research.api.research_functions._init_search_system")
def test_quick_summary_with_temperature(self, mock_init_system):
"""Test quick summary with custom temperature in settings_snapshot."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {
"current_knowledge": "Summary",
"iterations": 1,
"questions_by_iteration": {},
"all_links_of_system": [],
}
mock_init_system.return_value = mock_system
quick_summary("Query", temperature=0.3)
# Temperature is passed via settings_snapshot, not as a direct kwarg
call_kwargs = mock_init_system.call_args[1]
# Check that settings_snapshot was created and passed
assert "settings_snapshot" in call_kwargs
@patch("local_deep_research.api.research_functions._init_search_system")
@patch(
"local_deep_research.web_search_engines.retriever_registry.retriever_registry"
)
def test_quick_summary_with_retrievers(
self, mock_registry, mock_init_system
):
"""Test quick summary registers retrievers with registry."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {
"current_knowledge": "Summary",
"iterations": 1,
"questions_by_iteration": {},
"all_links_of_system": [],
}
mock_init_system.return_value = mock_system
retrievers = {"custom": MagicMock()}
quick_summary("Query", retrievers=retrievers)
# Retrievers are registered with the registry, not passed to _init_search_system
mock_registry.register_multiple.assert_called_once_with(retrievers)
@patch("local_deep_research.api.research_functions._init_search_system")
def test_quick_summary_with_research_id(self, mock_init_system):
"""Test quick summary with research ID tracking."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {
"current_knowledge": "Summary",
"iterations": 1,
"questions_by_iteration": {},
"all_links_of_system": [],
}
mock_init_system.return_value = mock_system
quick_summary("Query", research_id="test-123")
call_kwargs = mock_init_system.call_args[1]
assert call_kwargs.get("research_id") == "test-123"
@patch("local_deep_research.api.research_functions._init_search_system")
def test_quick_summary_search_original_query_default(
self, mock_init_system
):
"""Test quick summary search_original_query default is True."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {
"current_knowledge": "Summary",
"iterations": 1,
"questions_by_iteration": {},
"all_links_of_system": [],
}
mock_init_system.return_value = mock_system
quick_summary("Query")
call_kwargs = mock_init_system.call_args[1]
assert call_kwargs.get("search_original_query") is True
@patch("local_deep_research.api.research_functions._init_search_system")
def test_quick_summary_search_original_query_false(self, mock_init_system):
"""Test quick summary with search_original_query disabled."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {
"current_knowledge": "Summary",
"iterations": 1,
"questions_by_iteration": {},
"all_links_of_system": [],
}
mock_init_system.return_value = mock_system
quick_summary("Query", search_original_query=False)
call_kwargs = mock_init_system.call_args[1]
assert call_kwargs.get("search_original_query") is False
class TestResearchAPIValidation:
"""Tests for API input validation."""
@patch("local_deep_research.api.research_functions._init_search_system")
def test_quick_summary_empty_query(self, mock_init_system):
"""Test quick summary with empty query."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {
"current_knowledge": "",
"iterations": 0,
"questions_by_iteration": {},
"all_links_of_system": [],
}
mock_init_system.return_value = mock_system
# Should not raise, but may return empty results
result = quick_summary("")
assert result is not None
@patch("local_deep_research.api.research_functions._init_search_system")
def test_quick_summary_special_characters(self, mock_init_system):
"""Test quick summary with special characters."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {
"current_knowledge": "Result",
"iterations": 1,
"questions_by_iteration": {},
"all_links_of_system": [],
}
mock_init_system.return_value = mock_system
result = quick_summary("What about <script>alert('test')</script>?")
assert result is not None
@patch("local_deep_research.api.research_functions._init_search_system")
def test_quick_summary_unicode_query(self, mock_init_system):
"""Test quick summary with unicode characters."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {
"current_knowledge": "Result",
"iterations": 1,
"questions_by_iteration": {},
"all_links_of_system": [],
}
mock_init_system.return_value = mock_system
result = quick_summary("什么是人工智能?")
assert result is not None
class TestResearchAPIErrorHandling:
"""Tests for API error handling."""
@patch("local_deep_research.api.research_functions._init_search_system")
def test_quick_summary_system_error(self, mock_init_system):
"""Test quick summary handles system errors."""
from local_deep_research.api.research_functions import quick_summary
mock_init_system.side_effect = Exception("System error")
with pytest.raises(Exception):
quick_summary("Query")
@patch("local_deep_research.api.research_functions._init_search_system")
def test_quick_summary_analyze_error(self, mock_init_system):
"""Test quick summary handles analyze_topic errors."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.side_effect = Exception("Analysis error")
mock_init_system.return_value = mock_system
with pytest.raises(Exception):
quick_summary("Query")
class TestResearchAPIIntegration:
"""Integration tests for research API."""
@patch("local_deep_research.api.research_functions._init_search_system")
def test_full_research_workflow(self, mock_init_system):
"""Test complete research workflow."""
from local_deep_research.api.research_functions import quick_summary
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {
"current_knowledge": "Comprehensive research results about AI",
"iterations": 3,
"questions_by_iteration": {1: ["Q1", "Q2"], 2: ["Q3"], 3: ["Q4"]},
"all_links_of_system": [
{"url": "http://source1.com", "title": "Source 1"},
{"url": "http://source2.com", "title": "Source 2"},
],
}
mock_init_system.return_value = mock_system
result = quick_summary(
"What is artificial intelligence?",
provider="openai",
temperature=0.7,
)
assert result is not None
mock_system.analyze_topic.assert_called_once()
+636
View File
@@ -0,0 +1,636 @@
"""Tests for API research functions."""
from unittest.mock import MagicMock, patch
from local_deep_research.api.research_functions import (
_init_search_system,
quick_summary,
generate_report,
detailed_research,
analyze_documents,
)
class TestInitSearchSystem:
"""Tests for _init_search_system function."""
def test_returns_search_system(self, mock_get_llm, mock_get_search):
"""Test that function returns an AdvancedSearchSystem."""
with patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem"
) as mock_class:
mock_system = MagicMock()
mock_class.return_value = mock_system
result = _init_search_system()
assert result == mock_system
mock_class.assert_called_once()
def test_passes_custom_temperature(self, mock_get_llm, mock_get_search):
"""Test that custom temperature is passed to get_llm."""
with (
patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem"
),
patch(
"local_deep_research.api.research_functions.get_llm"
) as mock_llm,
):
_init_search_system(temperature=0.5)
mock_llm.assert_called_once()
assert mock_llm.call_args[1]["temperature"] == 0.5
def test_passes_model_name(self, mock_get_llm, mock_get_search):
"""Test that model_name is passed to get_llm."""
with (
patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem"
),
patch(
"local_deep_research.api.research_functions.get_llm"
) as mock_llm,
):
_init_search_system(model_name="gpt-4")
assert mock_llm.call_args[1]["model_name"] == "gpt-4"
def test_passes_provider(self, mock_get_llm, mock_get_search):
"""Test that provider is passed to get_llm."""
with (
patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem"
),
patch(
"local_deep_research.api.research_functions.get_llm"
) as mock_llm,
):
_init_search_system(provider="openai")
assert mock_llm.call_args[1]["provider"] == "openai"
def test_creates_search_engine(self, mock_get_llm, mock_get_search):
"""Test that search engine is created when search_tool specified."""
with (
patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem"
),
patch(
"local_deep_research.api.research_functions.get_search"
) as mock_search,
):
_init_search_system(search_tool="wikipedia")
mock_search.assert_called_once()
assert mock_search.call_args[0][0] == "wikipedia"
def test_sets_iterations(self, mock_get_llm, mock_get_search):
"""Test that max_iterations is set on system."""
with patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem"
) as mock_class:
mock_system = MagicMock()
mock_class.return_value = mock_system
_init_search_system(iterations=5)
assert mock_system.max_iterations == 5
def test_sets_questions_per_iteration(self, mock_get_llm, mock_get_search):
"""Test that questions_per_iteration is set on system."""
with patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem"
) as mock_class:
mock_system = MagicMock()
mock_class.return_value = mock_system
_init_search_system(questions_per_iteration=10)
assert mock_system.questions_per_iteration == 10
def test_sets_progress_callback(self, mock_get_llm, mock_get_search):
"""Test that progress callback is set."""
callback = MagicMock()
with patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem"
) as mock_class:
mock_system = MagicMock()
mock_class.return_value = mock_system
_init_search_system(progress_callback=callback)
mock_system.set_progress_callback.assert_called_once_with(callback)
def test_registers_retrievers(self, mock_get_llm, mock_get_search):
"""Test that custom retrievers are registered."""
retriever = MagicMock()
with (
patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem"
),
patch(
"local_deep_research.web_search_engines.retriever_registry.retriever_registry"
) as mock_registry,
):
_init_search_system(retrievers={"custom": retriever})
mock_registry.register_multiple.assert_called_once_with(
{"custom": retriever}
)
def test_registers_llms(self, mock_get_llm, mock_get_search):
"""Test that custom LLMs are registered."""
custom_llm = MagicMock()
with (
patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem"
),
patch("local_deep_research.llm.register_llm") as mock_register,
):
_init_search_system(llms={"custom_llm": custom_llm})
mock_register.assert_called_once_with("custom_llm", custom_llm)
def test_uses_settings_snapshot(
self, mock_get_llm, mock_get_search, sample_settings_snapshot
):
"""Test that settings_snapshot is passed through."""
with patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem"
) as mock_class:
_init_search_system(settings_snapshot=sample_settings_snapshot)
call_kwargs = mock_class.call_args[1]
assert call_kwargs["settings_snapshot"] == sample_settings_snapshot
class TestQuickSummary:
"""Tests for quick_summary function."""
def test_returns_dict(self, mock_get_llm, mock_advanced_search_system):
"""Test that function returns a dictionary."""
with patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
):
result = quick_summary("test query")
assert isinstance(result, dict)
def test_returns_summary_key(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that result contains 'summary' key."""
with patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
):
result = quick_summary("test query")
assert "summary" in result
def test_returns_findings_key(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that result contains 'findings' key."""
with patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
):
result = quick_summary("test query")
assert "findings" in result
def test_returns_iterations_key(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that result contains 'iterations' key."""
with patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
):
result = quick_summary("test query")
assert "iterations" in result
def test_returns_sources_key(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that result contains 'sources' key."""
with patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
):
result = quick_summary("test query")
assert "sources" in result
def test_generates_research_id_if_none(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that research_id is generated if not provided."""
with (
patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
),
patch(
"local_deep_research.api.research_functions.set_search_context"
) as mock_set_context,
):
quick_summary("test query")
call_args = mock_set_context.call_args[0][0]
assert "research_id" in call_args
# Should be a valid UUID
assert len(call_args["research_id"]) == 36
def test_uses_provided_research_id(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that provided research_id is used."""
with (
patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
),
patch(
"local_deep_research.api.research_functions.set_search_context"
) as mock_set_context,
):
quick_summary("test query", research_id="custom-id")
call_args = mock_set_context.call_args[0][0]
assert call_args["research_id"] == "custom-id"
def test_registers_custom_retrievers(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that custom retrievers are registered."""
retriever = MagicMock()
with (
patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
),
patch(
"local_deep_research.web_search_engines.retriever_registry.retriever_registry"
) as mock_registry,
):
quick_summary("test query", retrievers={"custom": retriever})
mock_registry.register_multiple.assert_called()
def test_calls_analyze_topic(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that analyze_topic is called with query."""
with patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
):
quick_summary("test query")
mock_advanced_search_system.analyze_topic.assert_called_once_with(
"test query"
)
def test_handles_empty_results(self, mock_get_llm):
"""Test handling when analyze_topic returns empty results."""
mock_system = MagicMock()
mock_system.analyze_topic.return_value = {}
with (
patch(
"local_deep_research.api.research_functions.AdvancedSearchSystem",
return_value=mock_system,
),
patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
),
):
result = quick_summary("test query")
assert (
result["summary"] == "Unable to generate summary for the query."
)
def test_creates_settings_snapshot_when_not_provided(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that settings snapshot is created if not provided."""
with patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
) as mock_create:
quick_summary("test query", provider="openai", temperature=0.5)
mock_create.assert_called_once()
def test_uses_provided_settings_snapshot(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that provided settings_snapshot is used."""
# Create a valid settings snapshot with required keys
custom_snapshot = {
# "library" is always available without extra engine config
# (searxng needs an instance URL before the factory accepts it).
"search.tool": {"value": "library"},
"custom": "settings",
}
with patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value=custom_snapshot,
) as mock_create:
quick_summary("test query", settings_snapshot=custom_snapshot)
# Should not call create_settings_snapshot when snapshot is provided
mock_create.assert_not_called()
class TestGenerateReport:
"""Tests for generate_report function."""
def test_returns_dict(
self, mock_get_llm, mock_advanced_search_system, mock_report_generator
):
"""Test that function returns a dictionary."""
with (
patch(
"local_deep_research.api.research_functions.IntegratedReportGenerator",
return_value=mock_report_generator,
),
patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
),
):
result = generate_report("test query")
assert isinstance(result, dict)
def test_returns_content_key(
self, mock_get_llm, mock_advanced_search_system, mock_report_generator
):
"""Test that result contains 'content' key."""
with (
patch(
"local_deep_research.api.research_functions.IntegratedReportGenerator",
return_value=mock_report_generator,
),
patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
),
):
result = generate_report("test query")
assert "content" in result
def test_calls_analyze_topic(
self, mock_get_llm, mock_advanced_search_system, mock_report_generator
):
"""Test that analyze_topic is called."""
with (
patch(
"local_deep_research.api.research_functions.IntegratedReportGenerator",
return_value=mock_report_generator,
),
patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
),
):
generate_report("test query")
mock_advanced_search_system.analyze_topic.assert_called_once_with(
"test query"
)
def test_calls_report_generator(
self, mock_get_llm, mock_advanced_search_system, mock_report_generator
):
"""Test that report generator is called."""
with (
patch(
"local_deep_research.api.research_functions.IntegratedReportGenerator",
return_value=mock_report_generator,
),
patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
),
):
generate_report("test query")
mock_report_generator.generate_report.assert_called_once()
def test_saves_to_file(
self,
mock_get_llm,
mock_advanced_search_system,
mock_report_generator,
tmp_path,
):
"""Test saving report to file."""
output_file = str(tmp_path / "report.md")
with (
patch(
"local_deep_research.api.research_functions.IntegratedReportGenerator",
return_value=mock_report_generator,
),
patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
),
patch(
"local_deep_research.security.file_write_verifier.write_file_verified"
) as mock_write,
):
result = generate_report("test query", output_file=output_file)
mock_write.assert_called_once()
assert result["file_path"] == output_file
def test_sets_progress_callback(
self, mock_get_llm, mock_advanced_search_system, mock_report_generator
):
"""Test that progress callback is set."""
callback = MagicMock()
with (
patch(
"local_deep_research.api.research_functions.IntegratedReportGenerator",
return_value=mock_report_generator,
),
patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
),
):
generate_report("test query", progress_callback=callback)
mock_advanced_search_system.set_progress_callback.assert_called_once_with(
callback
)
def test_searches_per_section(
self, mock_get_llm, mock_advanced_search_system, mock_report_generator
):
"""Test that searches_per_section is passed to generator."""
with (
patch(
"local_deep_research.api.research_functions.IntegratedReportGenerator"
) as mock_gen_class,
patch(
"local_deep_research.api.research_functions.create_settings_snapshot",
return_value={},
),
):
mock_gen_class.return_value = mock_report_generator
generate_report("test query", searches_per_section=5)
call_kwargs = mock_gen_class.call_args[1]
assert call_kwargs["searches_per_section"] == 5
class TestDetailedResearch:
"""Tests for detailed_research function."""
def test_returns_dict(self, mock_get_llm, mock_advanced_search_system):
"""Test that function returns a dictionary."""
result = detailed_research("test query")
assert isinstance(result, dict)
def test_returns_query(self, mock_get_llm, mock_advanced_search_system):
"""Test that result contains 'query' key."""
result = detailed_research("test query")
assert result["query"] == "test query"
def test_returns_research_id(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that result contains 'research_id' key."""
result = detailed_research("test query")
assert "research_id" in result
def test_uses_provided_research_id(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that provided research_id is used."""
result = detailed_research("test query", research_id="custom-id")
assert result["research_id"] == "custom-id"
def test_generates_research_id(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that research_id is generated if not provided."""
result = detailed_research("test query")
# Should be a valid UUID
assert len(result["research_id"]) == 36
def test_returns_summary(self, mock_get_llm, mock_advanced_search_system):
"""Test that result contains 'summary' key."""
result = detailed_research("test query")
assert "summary" in result
def test_returns_findings(self, mock_get_llm, mock_advanced_search_system):
"""Test that result contains 'findings' key."""
result = detailed_research("test query")
assert "findings" in result
def test_returns_metadata(self, mock_get_llm, mock_advanced_search_system):
"""Test that result contains 'metadata' key."""
result = detailed_research("test query")
assert "metadata" in result
assert "timestamp" in result["metadata"]
def test_calls_analyze_topic(
self, mock_get_llm, mock_advanced_search_system
):
"""Test that analyze_topic is called with query."""
detailed_research("test query")
mock_advanced_search_system.analyze_topic.assert_called_once_with(
"test query"
)
class TestAnalyzeDocuments:
"""Tests for analyze_documents function."""
def test_returns_dict(self, mock_get_llm, mock_get_search):
"""Test that function returns a dictionary."""
result = analyze_documents("test query", "test_collection")
assert isinstance(result, dict)
def test_returns_summary(self, mock_get_llm, mock_get_search):
"""Test that result contains 'summary' key."""
result = analyze_documents("test query", "test_collection")
assert "summary" in result
def test_returns_documents(self, mock_get_llm, mock_get_search):
"""Test that result contains 'documents' key."""
result = analyze_documents("test query", "test_collection")
assert "documents" in result
def test_collection_not_found(self, mock_get_llm):
"""Test handling when collection is not found."""
with patch(
"local_deep_research.api.research_functions.get_search",
return_value=None,
):
result = analyze_documents("test query", "nonexistent_collection")
assert "Error" in result["summary"]
assert result["documents"] == []
def test_no_results_found(self, mock_get_llm):
"""Test handling when no documents are found."""
mock_search = MagicMock()
mock_search.run.return_value = []
with patch(
"local_deep_research.api.research_functions.get_search",
return_value=mock_search,
):
result = analyze_documents("test query", "test_collection")
assert "No documents found" in result["summary"]
assert result["documents"] == []
def test_sets_max_results(self, mock_get_llm):
"""Test that max_results is set on search engine."""
mock_search = MagicMock()
mock_search.run.return_value = []
with patch(
"local_deep_research.api.research_functions.get_search",
return_value=mock_search,
):
analyze_documents("test query", "test_collection", max_results=50)
assert mock_search.max_results == 50
def test_uses_custom_temperature(self, mock_get_search):
"""Test that custom temperature is passed to get_llm."""
with patch(
"local_deep_research.api.research_functions.get_llm"
) as mock_llm:
# Create a mock LLM that returns a proper response
mock_llm_instance = MagicMock()
mock_llm_instance.invoke.return_value = MagicMock(
content="Summary text"
)
mock_llm.return_value = mock_llm_instance
analyze_documents("test query", "test_collection", temperature=0.3)
mock_llm.assert_called_once()
assert mock_llm.call_args[1]["temperature"] == 0.3
def test_saves_to_file(self, mock_get_llm, tmp_path):
"""Test saving analysis to file."""
output_file = str(tmp_path / "analysis.md")
mock_search = MagicMock()
mock_search.run.return_value = [
{
"title": "Doc 1",
"link": "https://example.com",
"content": "Content",
}
]
with (
patch(
"local_deep_research.api.research_functions.get_search",
return_value=mock_search,
),
patch(
"local_deep_research.security.file_write_verifier.write_file_verified"
) as mock_write,
):
result = analyze_documents(
"test query", "test_collection", output_file=output_file
)
mock_write.assert_called_once()
assert result["file_path"] == output_file
def test_returns_collection_name(self, mock_get_llm, mock_get_search):
"""Test that result includes collection name."""
result = analyze_documents("test query", "my_collection")
assert result["collection"] == "my_collection"
def test_returns_document_count(self, mock_get_llm, mock_get_search):
"""Test that result includes document count."""
result = analyze_documents("test query", "test_collection")
assert "document_count" in result
@@ -0,0 +1,46 @@
"""Contract tests for the programmatic SDK report entry point (#3665).
Mirrors the MCP ``generate_report`` contract test: the SDK
``generate_report`` returns the in-memory report generator's assembled
``content`` verbatim — including the ``## Sources`` block — without a DB
read or any stripping. Pins the SDK-keeps-assembled-shape invariant the
audit confirmed.
"""
from unittest.mock import MagicMock, patch
import local_deep_research.api.research_functions as rf
def test_generate_report_content_includes_sources():
assembled = (
"# Research Report\n\n## Introduction\n\nBody text.\n\n"
"## Sources\n\n[1] https://src.example\n\n"
"## Research Metrics\n\nSearch Iterations: 2\n"
)
fake_system = MagicMock()
fake_system.analyze_topic.return_value = {"findings": "x"}
fake_generator = MagicMock()
fake_generator.generate_report.return_value = {
"content": assembled,
"metadata": {"query": "q"},
}
# Mock the research pipeline so no real search/LLM work runs; the SDK must
# pass the generator's assembled content straight through.
with (
patch.object(rf, "_init_search_system", return_value=fake_system),
patch.object(
rf, "IntegratedReportGenerator", return_value=fake_generator
),
patch.object(rf, "_close_system"),
):
result = rf.generate_report(query="q", settings_snapshot={})
assert "content" in result
# Verbatim passthrough: the SDK returns the generator's assembled content
# unmodified. Full equality catches truncation/stripping, not just the
# absence of the ## Sources header.
assert result["content"] == assembled
+151
View File
@@ -0,0 +1,151 @@
"""
Tests for InMemorySettingsManager.get_settings_snapshot() and import_settings()
in api/settings_utils.py.
Tests cover:
- get_settings_snapshot: full format extraction, simplified format passthrough, mixed
- import_settings: overwrite=True, overwrite=False, delete_extra, type handling, empty
"""
from unittest.mock import patch, MagicMock
def _make_manager_with_settings(settings_dict):
"""Create an InMemorySettingsManager with pre-loaded settings (no disk I/O)."""
with patch(
"local_deep_research.api.settings_utils.InMemorySettingsManager.__init__",
lambda self: None,
):
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
mgr = InMemorySettingsManager()
mgr._settings = settings_dict.copy()
mgr._base_manager = MagicMock()
return mgr
class TestGetSettingsSnapshot:
"""Tests for get_settings_snapshot()."""
def test_full_format_extracts_value(self):
"""Dict-with-value entries → snapshot has just the value."""
mgr = _make_manager_with_settings(
{"key1": {"value": 42, "ui_element": "number"}}
)
snapshot = mgr.get_settings_snapshot()
assert snapshot["key1"] == 42
def test_simplified_format_passthrough(self):
"""Non-dict or dict-without-value entries pass through as-is."""
mgr = _make_manager_with_settings({"key1": "simple_value"})
snapshot = mgr.get_settings_snapshot()
assert snapshot["key1"] == "simple_value"
def test_mixed_formats(self):
"""Mix of full and simplified formats in one snapshot."""
mgr = _make_manager_with_settings(
{
"full_key": {"value": True, "ui_element": "checkbox"},
"simple_key": 100,
"dict_no_value": {"other": "data"},
}
)
snapshot = mgr.get_settings_snapshot()
assert snapshot["full_key"] is True
assert snapshot["simple_key"] == 100
assert snapshot["dict_no_value"] == {"other": "data"}
def test_empty_settings(self):
"""Empty settings → empty snapshot."""
mgr = _make_manager_with_settings({})
assert mgr.get_settings_snapshot() == {}
def test_none_value_in_full_format(self):
"""Full format with value=None → snapshot has None."""
mgr = _make_manager_with_settings(
{"key": {"value": None, "ui_element": "text"}}
)
snapshot = mgr.get_settings_snapshot()
assert snapshot["key"] is None
def test_snapshot_is_independent_copy(self):
"""Modifying snapshot doesn't affect internal settings."""
mgr = _make_manager_with_settings(
{"key": {"value": "original", "ui_element": "text"}}
)
snapshot = mgr.get_settings_snapshot()
snapshot["key"] = "modified"
# Internal still has original value
assert mgr._settings["key"]["value"] == "original"
def test_list_value(self):
"""Full format with list value → snapshot has the list."""
mgr = _make_manager_with_settings(
{"key": {"value": [1, 2, 3], "ui_element": "text"}}
)
snapshot = mgr.get_settings_snapshot()
assert snapshot["key"] == [1, 2, 3]
class TestImportSettings:
"""Tests for import_settings()."""
def test_import_overwrites_existing(self):
"""overwrite=True replaces existing settings."""
mgr = _make_manager_with_settings({"key1": "old_value"})
mgr.import_settings({"key1": "new_value"}, overwrite=True)
assert mgr._settings["key1"] == "new_value"
def test_import_no_overwrite_skips_existing(self):
"""overwrite=False skips keys that already exist."""
mgr = _make_manager_with_settings({"key1": "original"})
mgr.import_settings({"key1": "ignored", "key2": "new"}, overwrite=False)
assert mgr._settings["key1"] == "original"
assert mgr._settings["key2"] == "new"
def test_import_adds_new_keys(self):
"""New keys are always added regardless of overwrite flag."""
mgr = _make_manager_with_settings({})
mgr.import_settings({"new_key": "value"}, overwrite=False)
assert mgr._settings["new_key"] == "value"
def test_delete_extra_clears_first(self):
"""delete_extra=True clears existing settings before importing."""
mgr = _make_manager_with_settings({"old_key": "old_value"})
mgr.import_settings({"new_key": "new_value"}, delete_extra=True)
assert "old_key" not in mgr._settings
assert mgr._settings["new_key"] == "new_value"
def test_delete_extra_false_preserves(self):
"""delete_extra=False preserves existing settings."""
mgr = _make_manager_with_settings({"old_key": "old_value"})
mgr.import_settings({"new_key": "new_value"}, delete_extra=False)
assert "old_key" in mgr._settings
assert "new_key" in mgr._settings
def test_import_full_format_with_type_conversion(self):
"""Full format dicts get _get_typed_value applied."""
mgr = _make_manager_with_settings({})
mgr._get_typed_value = MagicMock(return_value=42)
mgr.import_settings(
{"key": {"value": "42", "ui_element": "number"}}, overwrite=True
)
mgr._get_typed_value.assert_called()
# Value should be the typed result
assert mgr._settings["key"]["value"] == 42
def test_import_simple_value_no_type_conversion(self):
"""Simple (non-dict) values are stored as-is without type conversion."""
mgr = _make_manager_with_settings({})
mgr._get_typed_value = MagicMock()
mgr.import_settings({"key": "plain_string"}, overwrite=True)
mgr._get_typed_value.assert_not_called()
assert mgr._settings["key"] == "plain_string"
def test_import_empty_dict(self):
"""Importing empty dict changes nothing."""
mgr = _make_manager_with_settings({"existing": "value"})
mgr.import_settings({})
assert mgr._settings["existing"] == "value"
+342
View File
@@ -0,0 +1,342 @@
"""Tests for API settings utilities."""
from unittest.mock import patch
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
get_default_settings_snapshot,
create_settings_snapshot,
extract_setting_value,
)
class TestInMemorySettingsManager:
"""Tests for InMemorySettingsManager class."""
def test_init_loads_defaults(self):
"""Test that initialization loads default settings."""
with patch.object(
InMemorySettingsManager, "_load_defaults"
) as mock_load:
InMemorySettingsManager()
mock_load.assert_called_once()
def test_get_setting_existing_key(self):
"""Test getting an existing setting."""
manager = InMemorySettingsManager()
# llm.provider should be in defaults
value = manager.get_setting("llm.provider")
assert value is not None
def test_get_setting_missing_key_returns_default(self):
"""Test getting a missing setting returns the default."""
manager = InMemorySettingsManager()
value = manager.get_setting("nonexistent.key", default="default_value")
assert value == "default_value"
def test_get_setting_none_default(self):
"""Test getting a missing setting with None default."""
manager = InMemorySettingsManager()
value = manager.get_setting("nonexistent.key")
assert value is None
def test_set_setting_existing_key(self):
"""Test setting an existing key."""
manager = InMemorySettingsManager()
result = manager.set_setting("llm.provider", "openai")
assert result is True
assert manager.get_setting("llm.provider") == "openai"
def test_set_setting_nonexistent_key(self):
"""Test setting a nonexistent key returns False."""
manager = InMemorySettingsManager()
result = manager.set_setting("nonexistent.key", "value")
assert result is False
def test_get_all_settings_returns_dict(self):
"""Test get_all_settings returns a dictionary."""
manager = InMemorySettingsManager()
settings = manager.get_all_settings()
assert isinstance(settings, dict)
assert len(settings) > 0
def test_get_all_settings_returns_copy(self):
"""Test get_all_settings returns a deep copy."""
manager = InMemorySettingsManager()
settings1 = manager.get_all_settings()
settings2 = manager.get_all_settings()
assert settings1 is not settings2
def test_create_or_update_setting_with_dict(self):
"""Test creating a setting from a dict."""
manager = InMemorySettingsManager()
setting = {
"key": "new.setting",
"value": "test_value",
"ui_element": "text",
}
result = manager.create_or_update_setting(setting)
assert result is not None
assert manager.get_setting("new.setting") == "test_value"
def test_create_or_update_setting_without_key(self):
"""Test creating a setting without key returns None."""
manager = InMemorySettingsManager()
result = manager.create_or_update_setting({"value": "test"})
assert result is None
def test_delete_setting_existing(self):
"""Test deleting an existing setting."""
manager = InMemorySettingsManager()
# First ensure it exists
manager.create_or_update_setting(
{"key": "temp.setting", "value": "temp", "ui_element": "text"}
)
assert manager.get_setting("temp.setting") == "temp"
result = manager.delete_setting("temp.setting")
assert result is True
assert manager.get_setting("temp.setting") is None
def test_delete_setting_nonexistent(self):
"""Test deleting a nonexistent setting returns False."""
manager = InMemorySettingsManager()
result = manager.delete_setting("nonexistent.key")
assert result is False
def test_import_settings_basic(self):
"""Test importing settings."""
manager = InMemorySettingsManager()
new_settings = {
"imported.setting": {
"value": "imported_value",
"ui_element": "text",
},
}
manager.import_settings(new_settings)
assert manager.get_setting("imported.setting") == "imported_value"
def test_import_settings_with_delete_extra(self):
"""Test importing with delete_extra clears existing settings."""
manager = InMemorySettingsManager()
original_count = len(manager.get_all_settings())
assert original_count > 0
manager.import_settings(
{"only.setting": {"value": "only", "ui_element": "text"}},
delete_extra=True,
)
settings = manager.get_all_settings()
assert len(settings) == 1
assert "only.setting" in settings
def test_import_settings_no_overwrite(self):
"""Test importing without overwriting existing settings."""
manager = InMemorySettingsManager()
manager.create_or_update_setting(
{"key": "existing", "value": "original", "ui_element": "text"}
)
manager.import_settings(
{"existing": {"value": "new_value", "ui_element": "text"}},
overwrite=False,
)
assert manager.get_setting("existing") == "original"
def test_get_typed_value_checkbox(self):
"""Test type conversion for checkbox."""
manager = InMemorySettingsManager()
setting_data = {"ui_element": "checkbox"}
assert manager._get_typed_value(setting_data, "true") is True
assert manager._get_typed_value(setting_data, "false") is False
assert manager._get_typed_value(setting_data, "1") is True
assert manager._get_typed_value(setting_data, "yes") is True
def test_get_typed_value_number(self):
"""Test type conversion for number."""
manager = InMemorySettingsManager()
setting_data = {"ui_element": "number"}
assert manager._get_typed_value(setting_data, "42") == 42.0
assert manager._get_typed_value(setting_data, "3.14") == 3.14
def test_get_typed_value_text(self):
"""Test type conversion for text."""
manager = InMemorySettingsManager()
setting_data = {"ui_element": "text"}
assert manager._get_typed_value(setting_data, 123) == "123"
assert manager._get_typed_value(setting_data, "hello") == "hello"
def test_get_typed_value_unknown_type(self):
"""Test type conversion for unknown ui_element returns as-is."""
manager = InMemorySettingsManager()
setting_data = {"ui_element": "unknown_type"}
result = manager._get_typed_value(setting_data, "value")
assert result == "value"
def test_load_from_defaults_file(self):
"""Test that load_from_defaults_file reloads defaults."""
manager = InMemorySettingsManager()
# Modify a setting
manager.set_setting("llm.provider", "modified")
assert manager.get_setting("llm.provider") == "modified"
# Reload defaults
manager.load_from_defaults_file()
# Should be back to default
# (exact value depends on defaults file)
def test_env_var_override(self, monkeypatch):
"""Test that environment variables override defaults."""
monkeypatch.setenv("LDR_LLM_PROVIDER", "env_provider")
InMemorySettingsManager()
# Check if env var was applied (depends on implementation)
class TestGetDefaultSettingsSnapshot:
"""Tests for get_default_settings_snapshot function."""
def test_returns_dict(self):
"""Test that function returns a dictionary."""
snapshot = get_default_settings_snapshot()
assert isinstance(snapshot, dict)
def test_contains_core_settings(self):
"""Test that snapshot contains core settings."""
snapshot = get_default_settings_snapshot()
# Check for some expected keys
assert "llm.provider" in snapshot
assert "llm.temperature" in snapshot or "llm.model" in snapshot
def test_settings_have_value_key(self):
"""Test that settings have a 'value' key."""
snapshot = get_default_settings_snapshot()
for key, setting in snapshot.items():
if isinstance(setting, dict):
assert "value" in setting, f"Setting {key} missing 'value' key"
class TestCreateSettingsSnapshot:
"""Tests for create_settings_snapshot function."""
def test_returns_dict(self):
"""Test that function returns a dictionary."""
snapshot = create_settings_snapshot()
assert isinstance(snapshot, dict)
def test_without_overrides(self):
"""Test creating snapshot without overrides uses defaults."""
snapshot = create_settings_snapshot()
assert len(snapshot) > 0
def test_with_overrides_dict(self):
"""Test creating snapshot with overrides dict."""
snapshot = create_settings_snapshot(
overrides={"llm.provider": "openai"}
)
assert snapshot["llm.provider"]["value"] == "openai"
def test_with_overrides_as_first_arg(self):
"""Test creating snapshot with overrides as first positional arg."""
snapshot = create_settings_snapshot({"llm.provider": "anthropic"})
assert snapshot["llm.provider"]["value"] == "anthropic"
def test_with_custom_base_settings(self):
"""Test creating snapshot with custom base settings."""
base = {"custom.setting": {"value": "base_value", "ui_element": "text"}}
snapshot = create_settings_snapshot(base_settings=base)
assert "custom.setting" in snapshot
assert snapshot["custom.setting"]["value"] == "base_value"
def test_override_unknown_key(self):
"""Test overriding with unknown key creates new entry."""
snapshot = create_settings_snapshot(
overrides={"new.unknown.key": "new_value"}
)
assert "new.unknown.key" in snapshot
assert snapshot["new.unknown.key"]["value"] == "new_value"
def test_provider_kwarg(self):
"""Test provider kwarg shortcut."""
snapshot = create_settings_snapshot(provider="openai")
assert snapshot["llm.provider"]["value"] == "openai"
def test_provider_with_api_key_kwarg(self):
"""Test provider and api_key kwargs together."""
snapshot = create_settings_snapshot(
provider="openai", api_key="sk-test-key"
)
assert snapshot["llm.provider"]["value"] == "openai"
assert "llm.openai.api_key" in snapshot
assert snapshot["llm.openai.api_key"]["value"] == "sk-test-key"
def test_temperature_kwarg(self):
"""Test temperature kwarg shortcut."""
snapshot = create_settings_snapshot(temperature=0.5)
assert snapshot["llm.temperature"]["value"] == 0.5
def test_max_search_results_kwarg(self):
"""Test max_search_results kwarg shortcut."""
snapshot = create_settings_snapshot(max_search_results=50)
assert snapshot["search.max_results"]["value"] == 50
def test_infer_ui_element_bool(self):
"""Test ui_element inference for boolean values."""
snapshot = create_settings_snapshot(overrides={"new.bool": True})
assert snapshot["new.bool"]["ui_element"] == "checkbox"
def test_infer_ui_element_number(self):
"""Test ui_element inference for numeric values."""
snapshot = create_settings_snapshot(overrides={"new.number": 42})
assert snapshot["new.number"]["ui_element"] == "number"
def test_infer_ui_element_dict(self):
"""Test ui_element inference for dict values."""
snapshot = create_settings_snapshot(
overrides={"new.dict": {"nested": "value"}}
)
assert snapshot["new.dict"]["ui_element"] == "json"
class TestExtractSettingValue:
"""Tests for extract_setting_value function."""
def test_extract_with_value_key(self):
"""Test extracting value from dict with 'value' key."""
snapshot = {"test.key": {"value": "test_value"}}
result = extract_setting_value(snapshot, "test.key")
assert result == "test_value"
def test_extract_direct_value(self):
"""Test extracting direct value (not dict)."""
snapshot = {"test.key": "direct_value"}
result = extract_setting_value(snapshot, "test.key")
assert result == "direct_value"
def test_extract_missing_key(self):
"""Test extracting missing key returns default."""
snapshot = {"other.key": {"value": "value"}}
result = extract_setting_value(
snapshot, "missing.key", default="default"
)
assert result == "default"
def test_extract_from_none_snapshot(self):
"""Test extracting from None snapshot returns default."""
result = extract_setting_value(None, "any.key", default="default")
assert result == "default"
def test_extract_default_none(self):
"""Test default value is None when not specified."""
snapshot = {}
result = extract_setting_value(snapshot, "missing.key")
assert result is None
def test_extract_nested_dict_value(self):
"""Test extracting value that is itself a dict."""
snapshot = {"complex.key": {"value": {"nested": "data"}}}
result = extract_setting_value(snapshot, "complex.key")
assert result == {"nested": "data"}
+619
View File
@@ -0,0 +1,619 @@
"""
Behavioral tests for settings_utils module.
Tests the settings utility functions used by the programmatic API.
"""
class TestExtractSettingValue:
"""Tests for extract_setting_value function."""
def test_extracts_value_from_dict_with_value_key(self):
"""Extracts value from setting dict with 'value' key."""
from local_deep_research.api.settings_utils import extract_setting_value
settings = {
"test.setting": {"value": "test_value", "ui_element": "text"}
}
result = extract_setting_value(settings, "test.setting")
assert result == "test_value"
def test_returns_setting_directly_if_not_dict(self):
"""Returns setting directly if not a dict."""
from local_deep_research.api.settings_utils import extract_setting_value
settings = {"test.setting": "direct_value"}
result = extract_setting_value(settings, "test.setting")
assert result == "direct_value"
def test_returns_default_for_missing_key(self):
"""Returns default value when key is missing."""
from local_deep_research.api.settings_utils import extract_setting_value
settings = {"other.setting": "value"}
result = extract_setting_value(
settings, "missing.key", default="default"
)
assert result == "default"
def test_returns_none_default_for_missing_key(self):
"""Returns None by default for missing key."""
from local_deep_research.api.settings_utils import extract_setting_value
settings = {"other.setting": "value"}
result = extract_setting_value(settings, "missing.key")
assert result is None
def test_returns_default_for_none_settings(self):
"""Returns default when settings is None."""
from local_deep_research.api.settings_utils import extract_setting_value
result = extract_setting_value(None, "any.key", default="fallback")
assert result == "fallback"
def test_extracts_integer_value(self):
"""Extracts integer value correctly."""
from local_deep_research.api.settings_utils import extract_setting_value
settings = {"count": {"value": 42}}
result = extract_setting_value(settings, "count")
assert result == 42
def test_extracts_boolean_value(self):
"""Extracts boolean value correctly."""
from local_deep_research.api.settings_utils import extract_setting_value
settings = {"enabled": {"value": True}}
result = extract_setting_value(settings, "enabled")
assert result is True
def test_extracts_dict_value(self):
"""Extracts dict value correctly."""
from local_deep_research.api.settings_utils import extract_setting_value
settings = {"config": {"value": {"key": "val"}}}
result = extract_setting_value(settings, "config")
assert result == {"key": "val"}
def test_extracts_list_value(self):
"""Extracts list value correctly."""
from local_deep_research.api.settings_utils import extract_setting_value
settings = {"items": {"value": [1, 2, 3]}}
result = extract_setting_value(settings, "items")
assert result == [1, 2, 3]
class TestExtractBoolSetting:
"""Tests for extract_bool_setting function."""
def test_extracts_true_from_boolean(self):
"""Extracts True from boolean True."""
from local_deep_research.api.settings_utils import extract_bool_setting
settings = {"enabled": {"value": True}}
result = extract_bool_setting(settings, "enabled")
assert result is True
def test_extracts_false_from_boolean(self):
"""Extracts False from boolean False."""
from local_deep_research.api.settings_utils import extract_bool_setting
settings = {"enabled": {"value": False}}
result = extract_bool_setting(settings, "enabled")
assert result is False
def test_converts_string_true(self):
"""Converts string 'true' to True."""
from local_deep_research.api.settings_utils import extract_bool_setting
settings = {"enabled": {"value": "true"}}
result = extract_bool_setting(settings, "enabled")
assert result is True
def test_converts_string_false(self):
"""Converts string 'false' to False."""
from local_deep_research.api.settings_utils import extract_bool_setting
settings = {"enabled": {"value": "false"}}
result = extract_bool_setting(settings, "enabled")
assert result is False
def test_converts_string_1(self):
"""Converts string '1' to True."""
from local_deep_research.api.settings_utils import extract_bool_setting
settings = {"enabled": {"value": "1"}}
result = extract_bool_setting(settings, "enabled")
assert result is True
def test_converts_string_yes(self):
"""Converts string 'yes' to True."""
from local_deep_research.api.settings_utils import extract_bool_setting
settings = {"enabled": {"value": "yes"}}
result = extract_bool_setting(settings, "enabled")
assert result is True
def test_returns_default_true_for_missing(self):
"""Returns default True for missing key."""
from local_deep_research.api.settings_utils import extract_bool_setting
settings = {}
result = extract_bool_setting(settings, "missing", default=True)
assert result is True
def test_returns_default_false_for_missing(self):
"""Returns default False for missing key."""
from local_deep_research.api.settings_utils import extract_bool_setting
settings = {}
result = extract_bool_setting(settings, "missing", default=False)
assert result is False
def test_returns_default_for_none_settings(self):
"""Returns default when settings is None."""
from local_deep_research.api.settings_utils import extract_bool_setting
result = extract_bool_setting(None, "any.key", default=True)
assert result is True
class TestCreateSettingsSnapshot:
"""Tests for create_settings_snapshot function."""
def test_returns_dict(self):
"""Returns a dictionary."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot()
assert isinstance(result, dict)
def test_applies_overrides_to_existing_settings(self):
"""Applies overrides to existing settings."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot(overrides={"llm.temperature": 0.5})
# If llm.temperature exists, it should have the override value
if "llm.temperature" in result:
if isinstance(result["llm.temperature"], dict):
assert result["llm.temperature"]["value"] == 0.5
else:
assert result["llm.temperature"] == 0.5
def test_creates_new_settings_for_unknown_keys(self):
"""Creates new settings for unknown keys."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot(
overrides={"custom.new_key": "new_value"}
)
assert "custom.new_key" in result
assert result["custom.new_key"]["value"] == "new_value"
def test_infers_checkbox_ui_element_for_bool(self):
"""Infers checkbox ui_element for boolean values."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot(overrides={"custom.flag": True})
assert result["custom.flag"]["ui_element"] == "checkbox"
def test_infers_number_ui_element_for_int(self):
"""Infers number ui_element for integer values."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot(overrides={"custom.count": 42})
assert result["custom.count"]["ui_element"] == "number"
def test_infers_number_ui_element_for_float(self):
"""Infers number ui_element for float values."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot(overrides={"custom.ratio": 0.75})
assert result["custom.ratio"]["ui_element"] == "number"
def test_infers_json_ui_element_for_dict(self):
"""Infers json ui_element for dict values."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot(overrides={"custom.config": {"a": 1}})
assert result["custom.config"]["ui_element"] == "json"
def test_infers_text_ui_element_for_string(self):
"""Infers text ui_element for string values."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot(overrides={"custom.name": "test"})
assert result["custom.name"]["ui_element"] == "text"
def test_provider_kwarg_sets_llm_provider(self):
"""Provider kwarg sets llm.provider."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot(provider="openai")
assert "llm.provider" in result
if isinstance(result["llm.provider"], dict):
assert result["llm.provider"]["value"] == "openai"
else:
assert result["llm.provider"] == "openai"
def test_api_key_kwarg_with_provider(self):
"""api_key kwarg with provider sets provider-specific key."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot(provider="openai", api_key="test-key")
api_key_setting = "llm.openai.api_key"
assert api_key_setting in result
if isinstance(result[api_key_setting], dict):
assert result[api_key_setting]["value"] == "test-key"
else:
assert result[api_key_setting] == "test-key"
def test_temperature_kwarg(self):
"""temperature kwarg sets llm.temperature."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot(temperature=0.8)
assert "llm.temperature" in result
if isinstance(result["llm.temperature"], dict):
assert result["llm.temperature"]["value"] == 0.8
else:
assert result["llm.temperature"] == 0.8
def test_max_search_results_kwarg(self):
"""max_search_results kwarg sets search.max_results."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
result = create_settings_snapshot(max_search_results=20)
assert "search.max_results" in result
if isinstance(result["search.max_results"], dict):
assert result["search.max_results"]["value"] == 20
else:
assert result["search.max_results"] == 20
def test_uses_provided_base_settings(self):
"""Uses provided base_settings instead of defaults."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
base = {"custom.base": {"value": "base_value"}}
result = create_settings_snapshot(base_settings=base)
assert "custom.base" in result
assert result["custom.base"]["value"] == "base_value"
def test_does_not_modify_base_settings(self):
"""Does not modify the provided base_settings."""
from local_deep_research.api.settings_utils import (
create_settings_snapshot,
)
base = {"setting": {"value": "original"}}
create_settings_snapshot(
base_settings=base, overrides={"setting": "modified"}
)
# Original should be unchanged
assert base["setting"]["value"] == "original"
class TestGetDefaultSettingsSnapshot:
"""Tests for get_default_settings_snapshot function."""
def test_returns_dict(self):
"""Returns a dictionary."""
from local_deep_research.api.settings_utils import (
get_default_settings_snapshot,
)
result = get_default_settings_snapshot()
assert isinstance(result, dict)
def test_contains_llm_settings(self):
"""Contains LLM-related settings."""
from local_deep_research.api.settings_utils import (
get_default_settings_snapshot,
)
result = get_default_settings_snapshot()
# Should have some llm.* settings
llm_keys = [k for k in result.keys() if k.startswith("llm.")]
assert len(llm_keys) > 0
def test_contains_search_settings(self):
"""Contains search-related settings."""
from local_deep_research.api.settings_utils import (
get_default_settings_snapshot,
)
result = get_default_settings_snapshot()
# Should have some search.* settings
search_keys = [k for k in result.keys() if k.startswith("search.")]
assert len(search_keys) > 0
def test_settings_have_value_key(self):
"""Settings have 'value' key."""
from local_deep_research.api.settings_utils import (
get_default_settings_snapshot,
)
result = get_default_settings_snapshot()
# Check a few settings
for key, setting in list(result.items())[:5]:
if isinstance(setting, dict):
assert "value" in setting
class TestInMemorySettingsManager:
"""Tests for InMemorySettingsManager class."""
def test_can_instantiate(self):
"""Can instantiate InMemorySettingsManager."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
assert manager is not None
def test_get_setting_returns_value(self):
"""get_setting returns setting value."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
# Get a setting that should exist
all_settings = manager.get_all_settings()
if all_settings:
first_key = next(iter(all_settings.keys()))
result = manager.get_setting(first_key)
# Should return something (not None if the setting exists)
assert result is not None or first_key in all_settings
def test_get_setting_returns_default_for_missing(self):
"""get_setting returns default for missing key."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
result = manager.get_setting("nonexistent.key", default="fallback")
assert result == "fallback"
def test_set_setting_updates_value(self):
"""set_setting updates an existing setting value."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
# Get any existing setting
all_settings = manager.get_all_settings()
if all_settings:
first_key = next(iter(all_settings.keys()))
manager.set_setting(first_key, "new_value")
# Value should be updated
new_value = manager.get_setting(first_key)
# Either it's "new_value" or type-converted
assert new_value is not None
def test_set_setting_returns_false_for_unknown_key(self):
"""set_setting returns False for unknown key."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
result = manager.set_setting("nonexistent.key", "value")
assert result is False
def test_get_all_settings_returns_dict(self):
"""get_all_settings returns a dictionary."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
result = manager.get_all_settings()
assert isinstance(result, dict)
def test_get_all_settings_returns_copy(self):
"""get_all_settings returns a deep copy."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
result1 = manager.get_all_settings()
result2 = manager.get_all_settings()
# Should be equal but not the same object
assert result1 == result2
assert result1 is not result2
def test_delete_setting_removes_key(self):
"""delete_setting removes a setting."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
# Add a custom setting first
manager.create_or_update_setting({"key": "test.delete", "value": "val"})
assert manager.delete_setting("test.delete") is True
assert manager.get_setting("test.delete") is None
def test_delete_setting_returns_false_for_unknown(self):
"""delete_setting returns False for unknown key."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
result = manager.delete_setting("nonexistent.key")
assert result is False
def test_create_or_update_setting(self):
"""create_or_update_setting adds or updates a setting."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
setting = {
"key": "test.new",
"value": "new_value",
"ui_element": "text",
}
result = manager.create_or_update_setting(setting)
assert result is not None
assert manager.get_setting("test.new") == "new_value"
def test_import_settings_with_overwrite(self):
"""import_settings overwrites existing settings."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
manager.create_or_update_setting({"key": "test.import", "value": "old"})
manager.import_settings(
{"test.import": {"value": "new", "ui_element": "text"}},
overwrite=True,
)
assert manager.get_setting("test.import") == "new"
def test_import_settings_without_overwrite(self):
"""import_settings respects overwrite=False."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
manager.create_or_update_setting(
{"key": "test.keep", "value": "original"}
)
manager.import_settings(
{"test.keep": {"value": "new", "ui_element": "text"}},
overwrite=False,
)
assert manager.get_setting("test.keep") == "original"
def test_import_settings_delete_extra(self):
"""import_settings with delete_extra clears existing."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
manager.import_settings(
{"only.setting": {"value": "val", "ui_element": "text"}},
delete_extra=True,
)
all_settings = manager.get_all_settings()
assert "only.setting" in all_settings
class TestUIElementTypeConversion:
"""Tests for UI element type conversion in InMemorySettingsManager."""
def test_checkbox_to_bool(self):
"""Checkbox ui_element converts to bool."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
manager.create_or_update_setting(
{"key": "test.bool", "value": "true", "ui_element": "checkbox"}
)
result = manager.get_setting("test.bool")
assert result is True
def test_number_to_float(self):
"""Number ui_element converts to float."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
manager.create_or_update_setting(
{"key": "test.num", "value": "42", "ui_element": "number"}
)
result = manager.get_setting("test.num")
assert result == 42.0
def test_text_to_str(self):
"""Text ui_element stays as string."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
manager.create_or_update_setting(
{"key": "test.str", "value": "hello", "ui_element": "text"}
)
result = manager.get_setting("test.str")
assert result == "hello"
def test_checkbox_string_false(self):
"""Checkbox ui_element handles 'false' string."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
manager.create_or_update_setting(
{"key": "test.false", "value": "false", "ui_element": "checkbox"}
)
result = manager.get_setting("test.false")
assert result is False
def test_checkbox_string_0(self):
"""Checkbox ui_element handles '0' string."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
manager.create_or_update_setting(
{"key": "test.zero", "value": "0", "ui_element": "checkbox"}
)
result = manager.get_setting("test.zero")
assert result is False
def test_checkbox_string_yes(self):
"""Checkbox ui_element handles 'yes' string."""
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
)
manager = InMemorySettingsManager()
manager.create_or_update_setting(
{"key": "test.yes", "value": "yes", "ui_element": "checkbox"}
)
result = manager.get_setting("test.yes")
assert result is True
+360
View File
@@ -0,0 +1,360 @@
"""
Gap tests for settings_utils: _get_typed_value edge cases,
get_bool_setting, get_settings_snapshot, extract_setting_value
corner cases, and extract_bool_setting with non-bool inputs.
These complement the existing test_settings_utils_typed_value.py and
test_settings_utils_behavior.py by covering paths that those files miss.
"""
import pytest
from local_deep_research.api.settings_utils import (
InMemorySettingsManager,
extract_bool_setting,
extract_setting_value,
)
# ---------------------------------------------------------------------------
# Fixture
# ---------------------------------------------------------------------------
@pytest.fixture
def manager():
"""Create an InMemorySettingsManager with defaults loaded."""
return InMemorySettingsManager()
# ---------------------------------------------------------------------------
# _get_typed_value — additional edge cases
# ---------------------------------------------------------------------------
class TestGetTypedValueTextarea:
"""textarea ui_element — should behave like text (str conversion)."""
def test_string_passthrough(self, manager):
result = manager._get_typed_value(
{"ui_element": "textarea"}, "hello\nworld"
)
assert result == "hello\nworld"
def test_int_converted_to_string(self, manager):
result = manager._get_typed_value({"ui_element": "textarea"}, 99)
assert result == "99"
assert isinstance(result, str)
class TestGetTypedValueConversionEdgeCases:
"""Conversion failure and boundary cases not covered elsewhere."""
def test_none_returns_none_regardless_of_ui_element(self, manager):
"""None short-circuits before any type dispatch."""
for ui in ("text", "checkbox", "number", "select", "json", "textarea"):
assert manager._get_typed_value({"ui_element": ui}, None) is None
def test_number_with_empty_string(self, manager):
"""Empty string cannot be parsed as number; returns original value."""
result = manager._get_typed_value({"ui_element": "number"}, "")
assert result == ""
def test_range_with_negative_float(self, manager):
result = manager._get_typed_value({"ui_element": "range"}, "-2.5")
assert result == -2.5
def test_checkbox_with_none_value(self, manager):
"""None should be returned directly (short-circuit), not converted."""
result = manager._get_typed_value({"ui_element": "checkbox"}, None)
assert result is None
def test_json_with_string_json_object(self, manager):
"""A JSON string should be parsed into a dict."""
result = manager._get_typed_value({"ui_element": "json"}, '{"a": 1}')
assert result == {"a": 1}
def test_json_with_empty_string(self, manager):
"""Empty string for json ui_element — should pass through as-is."""
result = manager._get_typed_value({"ui_element": "json"}, "")
# _parse_json_value returns empty strings as-is
assert result == ""
def test_multiselect_with_empty_list(self, manager):
result = manager._get_typed_value({"ui_element": "multiselect"}, [])
assert result == []
def test_multiselect_with_single_item_string(self, manager):
"""A plain string without commas should still be parsed."""
result = manager._get_typed_value({"ui_element": "multiselect"}, "solo")
# _parse_multiselect with a non-bracketed, non-comma string
# returns a single-element list
assert isinstance(result, list)
assert "solo" in result
class TestGetTypedValueMissingUiElement:
"""When setting_data has no ui_element key at all."""
def test_defaults_to_text_converts_to_str(self, manager):
"""Missing ui_element defaults to 'text', so str() is applied."""
result = manager._get_typed_value({}, 42)
assert result == "42"
assert isinstance(result, str)
# ---------------------------------------------------------------------------
# get_bool_setting — InMemorySettingsManager method
# ---------------------------------------------------------------------------
class TestGetBoolSetting:
"""InMemorySettingsManager.get_bool_setting wraps get_setting + to_bool."""
def test_true_bool_value(self, manager):
manager.create_or_update_setting(
{"key": "test.gbool", "value": True, "ui_element": "checkbox"}
)
assert manager.get_bool_setting("test.gbool") is True
def test_false_bool_value(self, manager):
manager.create_or_update_setting(
{"key": "test.gbool", "value": False, "ui_element": "checkbox"}
)
assert manager.get_bool_setting("test.gbool") is False
def test_string_true(self, manager):
manager.create_or_update_setting(
{"key": "test.gbool", "value": "true", "ui_element": "checkbox"}
)
assert manager.get_bool_setting("test.gbool") is True
def test_string_on(self, manager):
manager.create_or_update_setting(
{"key": "test.gbool", "value": "on", "ui_element": "checkbox"}
)
assert manager.get_bool_setting("test.gbool") is True
def test_missing_key_returns_default_true(self, manager):
assert manager.get_bool_setting("nonexistent.key", default=True) is True
def test_missing_key_returns_default_false(self, manager):
assert (
manager.get_bool_setting("nonexistent.key", default=False) is False
)
def test_integer_1_is_true(self, manager):
manager.create_or_update_setting(
{"key": "test.gbool", "value": 1, "ui_element": "checkbox"}
)
# parse_boolean treats any truthy value (including 1) as True
assert manager.get_bool_setting("test.gbool") is True
def test_integer_0_is_false(self, manager):
manager.create_or_update_setting(
{"key": "test.gbool", "value": 0, "ui_element": "checkbox"}
)
assert manager.get_bool_setting("test.gbool") is False
# ---------------------------------------------------------------------------
# get_settings_snapshot — simplified key→value dict
# ---------------------------------------------------------------------------
class TestGetSettingsSnapshot:
"""InMemorySettingsManager.get_settings_snapshot returns flat key→value."""
def test_returns_dict(self, manager):
snapshot = manager.get_settings_snapshot()
assert isinstance(snapshot, dict)
def test_values_are_extracted_from_setting_dicts(self, manager):
"""Each setting's 'value' field should be the snapshot value."""
manager.create_or_update_setting(
{"key": "snap.test", "value": "hello", "ui_element": "text"}
)
snapshot = manager.get_settings_snapshot()
assert snapshot["snap.test"] == "hello"
def test_non_dict_settings_passed_through(self, manager):
"""If a setting is not a dict (unlikely but possible), pass through."""
# Force a raw value into _settings
manager._settings["raw.val"] = "plain_string"
snapshot = manager.get_settings_snapshot()
assert snapshot["raw.val"] == "plain_string"
def test_snapshot_is_independent_copy(self, manager):
"""Modifying snapshot should not affect manager state."""
snapshot = manager.get_settings_snapshot()
snapshot["injected"] = "bad"
snapshot2 = manager.get_settings_snapshot()
assert "injected" not in snapshot2
# ---------------------------------------------------------------------------
# create_or_update_setting — edge cases
# ---------------------------------------------------------------------------
class TestCreateOrUpdateSettingEdgeCases:
def test_non_dict_returns_none(self, manager):
"""Passing a non-dict returns None."""
assert manager.create_or_update_setting("not_a_dict") is None
def test_dict_without_key_returns_none(self, manager):
assert manager.create_or_update_setting({"value": 42}) is None
def test_does_not_mutate_input_dict(self, manager):
"""The original dict should not be modified when value is typed."""
original = {
"key": "test.nomut",
"value": "42",
"ui_element": "number",
}
original_value = original["value"]
manager.create_or_update_setting(original)
# Original dict's value should remain unchanged
assert original["value"] == original_value
def test_setting_without_value_key(self, manager):
"""A dict with 'key' but no 'value' should still be stored."""
result = manager.create_or_update_setting(
{"key": "test.noval", "ui_element": "text"}
)
assert result is not None
assert "test.noval" in manager.get_all_settings()
# ---------------------------------------------------------------------------
# import_settings — type conversion path
# ---------------------------------------------------------------------------
class TestImportSettingsTypedValues:
"""import_settings applies _get_typed_value when value dicts are imported."""
def test_number_value_converted_on_import(self, manager):
manager.import_settings(
{"imp.num": {"value": "7", "ui_element": "number"}},
delete_extra=True,
)
# The stored value should be int 7 (whole-number float → int)
all_settings = manager.get_all_settings()
assert all_settings["imp.num"]["value"] == 7
def test_checkbox_value_converted_on_import(self, manager):
manager.import_settings(
{"imp.bool": {"value": "true", "ui_element": "checkbox"}},
delete_extra=True,
)
all_settings = manager.get_all_settings()
assert all_settings["imp.bool"]["value"] is True
def test_plain_value_stored_as_is(self, manager):
"""Non-dict values are stored without type conversion."""
manager.import_settings(
{"imp.raw": "just_a_string"},
delete_extra=True,
)
all_settings = manager.get_all_settings()
assert all_settings["imp.raw"] == "just_a_string"
# ---------------------------------------------------------------------------
# extract_setting_value — additional corner cases
# ---------------------------------------------------------------------------
class TestExtractSettingValueEdgeCases:
def test_dict_without_value_key_returns_whole_dict(self):
"""A dict that lacks 'value' is returned as-is (it's not a setting dict)."""
settings = {"key": {"other_field": "data"}}
result = extract_setting_value(settings, "key")
assert result == {"other_field": "data"}
def test_value_is_none_explicitly(self):
"""When value is explicitly None, it should be returned (not default)."""
settings = {"key": {"value": None}}
result = extract_setting_value(settings, "key", default="fallback")
assert result is None
def test_value_is_zero(self):
"""Falsy value 0 should be returned, not the default."""
settings = {"key": {"value": 0}}
result = extract_setting_value(settings, "key", default=99)
assert result == 0
def test_value_is_empty_string(self):
"""Falsy value '' should be returned, not the default."""
settings = {"key": {"value": ""}}
result = extract_setting_value(settings, "key", default="fallback")
assert result == ""
def test_value_is_false(self):
"""Falsy value False should be returned, not the default."""
settings = {"key": {"value": False}}
result = extract_setting_value(settings, "key", default=True)
assert result is False
def test_empty_snapshot(self):
result = extract_setting_value({}, "any", default="d")
assert result == "d"
# ---------------------------------------------------------------------------
# extract_bool_setting — additional edge cases
# ---------------------------------------------------------------------------
class TestExtractBoolSettingEdgeCases:
def test_integer_1_is_true(self):
settings = {"flag": {"value": 1}}
assert extract_bool_setting(settings, "flag") is True
def test_integer_0_is_false(self):
settings = {"flag": {"value": 0}}
assert extract_bool_setting(settings, "flag") is False
def test_none_value_uses_default_true(self):
settings = {"flag": {"value": None}}
assert extract_bool_setting(settings, "flag", default=True) is True
def test_none_value_uses_default_false(self):
settings = {"flag": {"value": None}}
assert extract_bool_setting(settings, "flag", default=False) is False
def test_empty_string_is_false(self):
"""Empty string is not in the truthy set, so it's False."""
settings = {"flag": {"value": ""}}
assert extract_bool_setting(settings, "flag") is False
def test_whitespace_string_true(self):
"""to_bool strips whitespace before comparison."""
settings = {"flag": {"value": " true "}}
assert extract_bool_setting(settings, "flag") is True
def test_case_insensitive_TRUE(self):
settings = {"flag": {"value": "TRUE"}}
assert extract_bool_setting(settings, "flag") is True
def test_plain_value_not_dict(self):
"""When value is stored directly (not in a dict with 'value' key)."""
settings = {"flag": "yes"}
assert extract_bool_setting(settings, "flag") is True
def test_none_snapshot_returns_default(self):
assert extract_bool_setting(None, "flag", default=True) is True
def test_string_enabled_is_true(self):
settings = {"flag": {"value": "enabled"}}
assert extract_bool_setting(settings, "flag") is True
def test_string_on_is_true(self):
settings = {"flag": {"value": "on"}}
assert extract_bool_setting(settings, "flag") is True
def test_arbitrary_string_is_false(self):
"""An unrecognized string should be False."""
settings = {"flag": {"value": "maybe"}}
assert extract_bool_setting(settings, "flag") is False
@@ -0,0 +1,161 @@
"""Tests for InMemorySettingsManager._get_typed_value.
This method dispatches value type conversion based on ui_element type.
Critical for ensuring settings are stored with correct types. Edge
cases around type conversion failures are untested.
"""
import pytest
from local_deep_research.api.settings_utils import InMemorySettingsManager
@pytest.fixture
def manager():
"""Create an InMemorySettingsManager instance."""
return InMemorySettingsManager()
class TestGetTypedValueText:
"""Text type conversion — string passthrough."""
def test_string_passthrough(self, manager):
result = manager._get_typed_value({"ui_element": "text"}, "hello")
assert result == "hello"
def test_int_converted_to_string(self, manager):
result = manager._get_typed_value({"ui_element": "text"}, 42)
assert result == "42"
class TestGetTypedValueCheckbox:
"""Checkbox type conversion — uses parse_boolean."""
def test_true_string(self, manager):
result = manager._get_typed_value({"ui_element": "checkbox"}, "true")
assert result is True
def test_false_string(self, manager):
result = manager._get_typed_value({"ui_element": "checkbox"}, "false")
assert result is False
def test_bool_passthrough(self, manager):
result = manager._get_typed_value({"ui_element": "checkbox"}, True)
assert result is True
def test_zero_is_false(self, manager):
result = manager._get_typed_value({"ui_element": "checkbox"}, 0)
assert result is False
class TestGetTypedValueNumber:
"""Number type conversion — uses _parse_number."""
def test_integer_string(self, manager):
result = manager._get_typed_value({"ui_element": "number"}, "42")
assert result == 42
assert isinstance(result, int)
def test_float_string(self, manager):
result = manager._get_typed_value({"ui_element": "number"}, "3.14")
assert result == 3.14
assert isinstance(result, float)
def test_int_passthrough(self, manager):
result = manager._get_typed_value({"ui_element": "number"}, 42)
assert result == 42
def test_float_whole_number_becomes_int(self, manager):
result = manager._get_typed_value({"ui_element": "number"}, "5.0")
assert result == 5
assert isinstance(result, int)
class TestGetTypedValueRange:
"""Range type (slider) — same behavior as number."""
def test_range_like_number(self, manager):
result = manager._get_typed_value({"ui_element": "range"}, "0.7")
assert result == 0.7
class TestGetTypedValueSelect:
"""Select type — string passthrough."""
def test_string_passthrough(self, manager):
result = manager._get_typed_value({"ui_element": "select"}, "option_a")
assert result == "option_a"
class TestGetTypedValueMultiselect:
"""Multiselect type — uses _parse_multiselect."""
def test_list_passthrough(self, manager):
result = manager._get_typed_value(
{"ui_element": "multiselect"}, ["a", "b"]
)
assert result == ["a", "b"]
def test_json_string_parsed(self, manager):
result = manager._get_typed_value(
{"ui_element": "multiselect"}, '["x","y"]'
)
assert result == ["x", "y"]
def test_comma_separated_parsed(self, manager):
result = manager._get_typed_value(
{"ui_element": "multiselect"}, "a,b,c"
)
assert result == ["a", "b", "c"]
class TestGetTypedValueJSON:
"""JSON type — value passed through as-is."""
def test_dict_passthrough(self, manager):
val = {"key": "value"}
result = manager._get_typed_value({"ui_element": "json"}, val)
assert result == val
def test_list_passthrough(self, manager):
val = [1, 2, 3]
result = manager._get_typed_value({"ui_element": "json"}, val)
assert result == val
class TestGetTypedValuePassword:
"""Password type — string passthrough."""
def test_password_string(self, manager):
result = manager._get_typed_value(
{"ui_element": "password"}, "secret123"
)
assert result == "secret123"
class TestGetTypedValueUnknownType:
"""Unknown ui_element types — value returned as-is."""
def test_unknown_type_returns_value(self, manager):
result = manager._get_typed_value(
{"ui_element": "custom_widget"}, "val"
)
assert result == "val"
def test_missing_ui_element_defaults_to_text(self, manager):
result = manager._get_typed_value({}, "val")
assert result == "val"
class TestGetTypedValueConversionFailure:
"""Type conversion failures — value returned as-is."""
def test_non_numeric_for_number(self, manager):
result = manager._get_typed_value(
{"ui_element": "number"}, "not_a_number"
)
assert result == "not_a_number"
def test_none_for_number(self, manager):
result = manager._get_typed_value({"ui_element": "number"}, None)
assert result is None