7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
316 lines
12 KiB
Python
316 lines
12 KiB
Python
"""Tests for API settings validation and error handling."""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from local_deep_research.api.settings_utils import (
|
|
InMemorySettingsManager,
|
|
create_settings_snapshot,
|
|
)
|
|
from local_deep_research.api import quick_summary
|
|
|
|
|
|
class TestSettingsValidation:
|
|
"""Test validation of settings values."""
|
|
|
|
def test_temperature_validation(self):
|
|
"""Test temperature value validation."""
|
|
# Valid temperature values
|
|
valid_temps = [0.0, 0.5, 1.0, 0.7, 0.999]
|
|
for temp in valid_temps:
|
|
snapshot = create_settings_snapshot(temperature=temp)
|
|
assert snapshot["llm.temperature"]["value"] == temp
|
|
|
|
# Invalid temperatures should be handled gracefully
|
|
# (actual validation would happen in the LLM layer)
|
|
invalid_temps = [-0.1, 1.1, 2.0, "not_a_number"]
|
|
for temp in invalid_temps:
|
|
snapshot = create_settings_snapshot(temperature=temp)
|
|
# Should accept the value (validation happens later)
|
|
assert snapshot["llm.temperature"]["value"] == temp
|
|
|
|
def test_api_key_validation(self):
|
|
"""Test API key handling and validation."""
|
|
# Test empty/None API keys
|
|
snapshot1 = create_settings_snapshot(provider="openai", api_key="")
|
|
assert snapshot1["llm.openai.api_key"]["value"] == ""
|
|
|
|
snapshot2 = create_settings_snapshot(provider="anthropic", api_key=None)
|
|
assert snapshot2["llm.anthropic.api_key"]["value"] is None
|
|
|
|
# Test special characters in API keys
|
|
special_key = "sk-proj-$p3c!@l_ch@rs_123"
|
|
snapshot3 = create_settings_snapshot(
|
|
provider="openai", api_key=special_key
|
|
)
|
|
assert snapshot3["llm.openai.api_key"]["value"] == special_key
|
|
|
|
def test_provider_name_normalization(self):
|
|
"""Test that provider names are handled correctly."""
|
|
# Test various provider name formats
|
|
providers = [
|
|
("openai", "openai"),
|
|
("OpenAI", "OpenAI"), # Case preserved
|
|
("OPENAI", "OPENAI"),
|
|
("anthropic", "anthropic"),
|
|
("Anthropic", "Anthropic"),
|
|
("ollama", "ollama"),
|
|
("custom_provider", "custom_provider"),
|
|
]
|
|
|
|
for input_provider, expected in providers:
|
|
snapshot = create_settings_snapshot(provider=input_provider)
|
|
assert snapshot["llm.provider"]["value"] == expected
|
|
|
|
def test_search_results_validation(self):
|
|
"""Test search results count validation."""
|
|
# Valid counts
|
|
valid_counts = [1, 5, 10, 20, 50, 100]
|
|
for count in valid_counts:
|
|
snapshot = create_settings_snapshot(max_search_results=count)
|
|
assert snapshot["search.max_results"]["value"] == count
|
|
|
|
# Edge cases (actual validation in search layer)
|
|
edge_counts = [0, -1, 1000, "ten"]
|
|
for count in edge_counts:
|
|
snapshot = create_settings_snapshot(max_search_results=count)
|
|
assert snapshot["search.max_results"]["value"] == count
|
|
|
|
|
|
class TestAPIErrorHandling:
|
|
"""Test error handling in API functions with settings."""
|
|
|
|
@patch("local_deep_research.api.research_functions._init_search_system")
|
|
def test_quick_summary_with_invalid_provider(self, mock_init):
|
|
"""Test quick_summary with invalid provider."""
|
|
mock_init.side_effect = ValueError("Invalid provider: invalid_provider")
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
quick_summary(
|
|
"test query", provider="invalid_provider", api_key="some-key"
|
|
)
|
|
|
|
assert "Invalid provider" in str(exc_info.value)
|
|
|
|
@patch("local_deep_research.api.research_functions._init_search_system")
|
|
def test_quick_summary_with_missing_api_key(self, mock_init):
|
|
"""Test behavior when API key is missing."""
|
|
# Mock the search system to simulate missing API key error
|
|
mock_system = MagicMock()
|
|
mock_system.analyze_topic.side_effect = Exception(
|
|
"API key required for provider"
|
|
)
|
|
mock_init.return_value = mock_system
|
|
|
|
# Should raise an error about missing API key
|
|
with pytest.raises(Exception) as exc_info:
|
|
result = quick_summary(
|
|
"test query",
|
|
provider="openai",
|
|
# No API key provided
|
|
)
|
|
# Force evaluation
|
|
if hasattr(result, "__next__"):
|
|
next(result)
|
|
|
|
assert "API key required" in str(exc_info.value)
|
|
|
|
def test_settings_with_circular_references(self):
|
|
"""Test handling of circular references in settings."""
|
|
# Create settings with potential circular structure
|
|
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: rewrite to either assert successful deepcopy or expected exception type).
|
|
circular_dict = {"a": {}}
|
|
circular_dict["a"]["b"] = circular_dict # Circular reference
|
|
|
|
# Should handle gracefully (no infinite loop)
|
|
# Note: actual deep copy in create_settings_snapshot prevents this
|
|
try:
|
|
_ = create_settings_snapshot(
|
|
base_settings={"custom": circular_dict}
|
|
)
|
|
# If we get here, the circular reference was handled
|
|
assert True
|
|
except Exception as e:
|
|
# Some exception is acceptable as long as it's not a RecursionError
|
|
assert not isinstance(e, RecursionError)
|
|
|
|
|
|
class TestEnvironmentVariableIntegration:
|
|
"""Test environment variable integration with settings."""
|
|
|
|
def test_environment_variable_type_conversion(self, monkeypatch):
|
|
"""Test that env vars are converted to correct types."""
|
|
# Set various types via env vars
|
|
monkeypatch.setenv("LDR_LLM_TEMPERATURE", "0.7")
|
|
monkeypatch.setenv("LDR_SEARCH_MAX_RESULTS", "15")
|
|
monkeypatch.setenv("LDR_SEARCH_SAFE_SEARCH", "true")
|
|
monkeypatch.setenv("LDR_LLM_PROVIDER", "anthropic")
|
|
|
|
manager = InMemorySettingsManager()
|
|
|
|
# Check type conversions
|
|
temp = manager.get_setting("llm.temperature")
|
|
assert isinstance(temp, float)
|
|
assert temp == 0.7
|
|
|
|
# Note: The current implementation may not convert all types
|
|
# This test documents expected behavior
|
|
|
|
def test_invalid_environment_variable_values(self, monkeypatch):
|
|
"""Test handling of invalid env var values."""
|
|
# Set invalid values
|
|
monkeypatch.setenv("LDR_LLM_TEMPERATURE", "not_a_number")
|
|
monkeypatch.setenv("LDR_SEARCH_MAX_RESULTS", "abc")
|
|
|
|
manager = InMemorySettingsManager()
|
|
|
|
# Should handle gracefully - either use default or store as string
|
|
temp = manager.get_setting("llm.temperature")
|
|
assert temp is not None # Should have some value
|
|
|
|
def test_environment_variable_precedence(self, monkeypatch):
|
|
"""Test precedence of env vars vs explicit settings."""
|
|
monkeypatch.setenv("LDR_LLM_PROVIDER", "env_provider")
|
|
|
|
# Env var should affect default snapshot
|
|
default_snapshot = create_settings_snapshot()
|
|
assert default_snapshot["llm.provider"]["value"] == "env_provider"
|
|
|
|
# But explicit setting should override
|
|
explicit_snapshot = create_settings_snapshot(
|
|
provider="explicit_provider"
|
|
)
|
|
assert explicit_snapshot["llm.provider"]["value"] == "explicit_provider"
|
|
|
|
# And overrides should also take precedence
|
|
override_snapshot = create_settings_snapshot(
|
|
overrides={"llm.provider": "override_provider"}
|
|
)
|
|
assert override_snapshot["llm.provider"]["value"] == "override_provider"
|
|
|
|
|
|
class TestSettingsUsabilityPatterns:
|
|
"""Test common usage patterns for settings."""
|
|
|
|
def test_minimal_configuration(self):
|
|
"""Test that minimal configuration works correctly."""
|
|
# Just provider and API key
|
|
snapshot = create_settings_snapshot(
|
|
provider="openai", api_key="sk-test"
|
|
)
|
|
|
|
# Should have sensible defaults for everything else
|
|
assert snapshot["llm.provider"]["value"] == "openai"
|
|
assert snapshot["llm.openai.api_key"]["value"] == "sk-test"
|
|
assert "llm.temperature" in snapshot # Should have default
|
|
assert "search.max_results" in snapshot # Should have default
|
|
|
|
def test_common_research_presets(self):
|
|
"""Test common research configuration presets."""
|
|
# Fast research preset
|
|
fast_preset = create_settings_snapshot(
|
|
provider="anthropic",
|
|
api_key="test-key",
|
|
temperature=0.7,
|
|
overrides={
|
|
"research.max_iterations": 1,
|
|
"search.max_results": 5,
|
|
"llm.max_tokens": 1000,
|
|
},
|
|
)
|
|
|
|
# Thorough research preset
|
|
thorough_preset = create_settings_snapshot(
|
|
provider="openai",
|
|
api_key="test-key",
|
|
temperature=0.3,
|
|
overrides={
|
|
"research.max_iterations": 5,
|
|
"search.max_results": 20,
|
|
"llm.max_tokens": 4000,
|
|
"search.engines.arxiv.enabled": True,
|
|
"search.engines.wikipedia.enabled": True,
|
|
},
|
|
)
|
|
|
|
# Verify presets have expected values
|
|
assert fast_preset["research.max_iterations"]["value"] == 1
|
|
assert fast_preset["search.max_results"]["value"] == 5
|
|
|
|
assert thorough_preset["research.max_iterations"]["value"] == 5
|
|
assert thorough_preset["search.engines.arxiv.enabled"]["value"] is True
|
|
|
|
def test_provider_switching(self):
|
|
"""Test switching between providers easily."""
|
|
base_config = {
|
|
"research.max_iterations": 3,
|
|
"search.max_results": 10,
|
|
}
|
|
|
|
# Create snapshots for different providers
|
|
openai_snapshot = create_settings_snapshot(
|
|
provider="openai", api_key="openai-key", overrides=base_config
|
|
)
|
|
|
|
anthropic_snapshot = create_settings_snapshot(
|
|
provider="anthropic", api_key="anthropic-key", overrides=base_config
|
|
)
|
|
|
|
ollama_snapshot = create_settings_snapshot(
|
|
provider="ollama",
|
|
overrides={
|
|
**base_config,
|
|
"llm.ollama.base_url": "http://localhost:11434",
|
|
},
|
|
)
|
|
|
|
# Verify each has correct provider config
|
|
assert openai_snapshot["llm.provider"]["value"] == "openai"
|
|
assert openai_snapshot["llm.openai.api_key"]["value"] == "openai-key"
|
|
|
|
assert anthropic_snapshot["llm.provider"]["value"] == "anthropic"
|
|
assert (
|
|
anthropic_snapshot["llm.anthropic.api_key"]["value"]
|
|
== "anthropic-key"
|
|
)
|
|
|
|
assert ollama_snapshot["llm.provider"]["value"] == "ollama"
|
|
assert (
|
|
ollama_snapshot["llm.ollama.base_url"]["value"]
|
|
== "http://localhost:11434"
|
|
)
|
|
|
|
# All should have same base config
|
|
for snapshot in [openai_snapshot, anthropic_snapshot, ollama_snapshot]:
|
|
assert snapshot["research.max_iterations"]["value"] == 3
|
|
assert snapshot["search.max_results"]["value"] == 10
|
|
|
|
|
|
class TestSettingsBackwardCompatibility:
|
|
"""Test backward compatibility of settings."""
|
|
|
|
def test_legacy_setting_names(self):
|
|
"""Test that legacy setting names still work."""
|
|
# If we rename settings in the future, we should handle old names
|
|
# This is a placeholder for future compatibility tests
|
|
snapshot = create_settings_snapshot()
|
|
|
|
# Current settings should exist
|
|
assert "llm.provider" in snapshot
|
|
assert "search.max_results" in snapshot
|
|
|
|
def test_settings_migration(self):
|
|
"""Test migration of old settings format to new format."""
|
|
# Example: if we change setting structure in the future
|
|
# This is a placeholder for future compatibility tests
|
|
|
|
# Future: converter function would migrate to new format
|
|
# For now, just verify current format works
|
|
snapshot = create_settings_snapshot(
|
|
provider="openai", temperature=0.7, max_search_results=10
|
|
)
|
|
|
|
assert snapshot["llm.provider"]["value"] == "openai"
|
|
assert snapshot["llm.temperature"]["value"] == 0.7
|
|
assert snapshot["search.max_results"]["value"] == 10
|