Files
learningcircuit--local-deep…/tests/test_api_settings_validation.py
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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