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
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:
@@ -0,0 +1 @@
|
||||
"""Tests for API module."""
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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 == ""
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user