Files
alfred/brain/tests/test_config_edge_cases.py

320 lines
12 KiB
Python

"""Edge case tests for configuration and parameters."""
import os
from unittest.mock import patch
import pytest
from agent.config import ConfigurationError, Settings
from agent.parameters import (
REQUIRED_PARAMETERS,
ParameterSchema,
format_parameters_for_prompt,
get_missing_required_parameters,
)
class TestSettingsEdgeCases:
"""Edge case tests for Settings."""
def test_default_values(self):
"""Should have sensible defaults."""
with patch.dict(os.environ, {}, clear=True):
settings = Settings()
assert settings.temperature == 0.2
assert settings.max_tool_iterations == 5
assert settings.request_timeout == 30
def test_temperature_boundary_low(self):
"""Should accept temperature at lower boundary."""
with patch.dict(os.environ, {"TEMPERATURE": "0.0"}, clear=True):
settings = Settings()
assert settings.temperature == 0.0
def test_temperature_boundary_high(self):
"""Should accept temperature at upper boundary."""
with patch.dict(os.environ, {"TEMPERATURE": "2.0"}, clear=True):
settings = Settings()
assert settings.temperature == 2.0
def test_temperature_below_boundary(self):
"""Should reject temperature below 0."""
with patch.dict(os.environ, {"TEMPERATURE": "-0.1"}, clear=True):
with pytest.raises(ConfigurationError):
Settings()
def test_temperature_above_boundary(self):
"""Should reject temperature above 2."""
with patch.dict(os.environ, {"TEMPERATURE": "2.1"}, clear=True):
with pytest.raises(ConfigurationError):
Settings()
def test_max_tool_iterations_boundary_low(self):
"""Should accept max_tool_iterations at lower boundary."""
with patch.dict(os.environ, {"MAX_TOOL_ITERATIONS": "1"}, clear=True):
settings = Settings()
assert settings.max_tool_iterations == 1
def test_max_tool_iterations_boundary_high(self):
"""Should accept max_tool_iterations at upper boundary."""
with patch.dict(os.environ, {"MAX_TOOL_ITERATIONS": "20"}, clear=True):
settings = Settings()
assert settings.max_tool_iterations == 20
def test_max_tool_iterations_below_boundary(self):
"""Should reject max_tool_iterations below 1."""
with patch.dict(os.environ, {"MAX_TOOL_ITERATIONS": "0"}, clear=True):
with pytest.raises(ConfigurationError):
Settings()
def test_max_tool_iterations_above_boundary(self):
"""Should reject max_tool_iterations above 20."""
with patch.dict(os.environ, {"MAX_TOOL_ITERATIONS": "21"}, clear=True):
with pytest.raises(ConfigurationError):
Settings()
def test_request_timeout_boundary_low(self):
"""Should accept request_timeout at lower boundary."""
with patch.dict(os.environ, {"REQUEST_TIMEOUT": "1"}, clear=True):
settings = Settings()
assert settings.request_timeout == 1
def test_request_timeout_boundary_high(self):
"""Should accept request_timeout at upper boundary."""
with patch.dict(os.environ, {"REQUEST_TIMEOUT": "300"}, clear=True):
settings = Settings()
assert settings.request_timeout == 300
def test_request_timeout_below_boundary(self):
"""Should reject request_timeout below 1."""
with patch.dict(os.environ, {"REQUEST_TIMEOUT": "0"}, clear=True):
with pytest.raises(ConfigurationError):
Settings()
def test_request_timeout_above_boundary(self):
"""Should reject request_timeout above 300."""
with patch.dict(os.environ, {"REQUEST_TIMEOUT": "301"}, clear=True):
with pytest.raises(ConfigurationError):
Settings()
def test_invalid_deepseek_url(self):
"""Should reject invalid DeepSeek URL."""
with patch.dict(os.environ, {"DEEPSEEK_BASE_URL": "not-a-url"}, clear=True):
with pytest.raises(ConfigurationError):
Settings()
def test_invalid_tmdb_url(self):
"""Should reject invalid TMDB URL."""
with patch.dict(os.environ, {"TMDB_BASE_URL": "ftp://invalid"}, clear=True):
with pytest.raises(ConfigurationError):
Settings()
def test_http_url_accepted(self):
"""Should accept http:// URLs."""
with patch.dict(
os.environ,
{
"DEEPSEEK_BASE_URL": "http://localhost:8080",
"TMDB_BASE_URL": "http://localhost:3000",
},
clear=True,
):
settings = Settings()
assert settings.deepseek_base_url == "http://localhost:8080"
def test_https_url_accepted(self):
"""Should accept https:// URLs."""
with patch.dict(
os.environ,
{
"DEEPSEEK_BASE_URL": "https://api.example.com",
"TMDB_BASE_URL": "https://api.example.com",
},
clear=True,
):
settings = Settings()
assert settings.deepseek_base_url == "https://api.example.com"
def test_is_deepseek_configured_with_key(self):
"""Should return True when API key is set."""
with patch.dict(os.environ, {"DEEPSEEK_API_KEY": "test-key"}, clear=True):
settings = Settings()
assert settings.is_deepseek_configured() is True
def test_is_deepseek_configured_without_key(self):
"""Should return False when API key is not set."""
with patch.dict(os.environ, {"DEEPSEEK_API_KEY": ""}, clear=True):
settings = Settings()
assert settings.is_deepseek_configured() is False
def test_is_tmdb_configured_with_key(self):
"""Should return True when API key is set."""
with patch.dict(os.environ, {"TMDB_API_KEY": "test-key"}, clear=True):
settings = Settings()
assert settings.is_tmdb_configured() is True
def test_is_tmdb_configured_without_key(self):
"""Should return False when API key is not set."""
with patch.dict(os.environ, {"TMDB_API_KEY": ""}, clear=True):
settings = Settings()
assert settings.is_tmdb_configured() is False
def test_non_numeric_temperature(self):
"""Should handle non-numeric temperature."""
with patch.dict(os.environ, {"TEMPERATURE": "not-a-number"}, clear=True):
with pytest.raises((ConfigurationError, ValueError)):
Settings()
def test_non_numeric_max_iterations(self):
"""Should handle non-numeric max_tool_iterations."""
with patch.dict(os.environ, {"MAX_TOOL_ITERATIONS": "five"}, clear=True):
with pytest.raises((ConfigurationError, ValueError)):
Settings()
class TestParametersEdgeCases:
"""Edge case tests for parameters module."""
def test_parameter_creation(self):
"""Should create parameter with all fields."""
param = ParameterSchema(
key="test_key",
description="Test description",
why_needed="Test reason",
type="string",
)
assert param.key == "test_key"
assert param.description == "Test description"
assert param.why_needed == "Test reason"
assert param.type == "string"
def test_required_parameters_not_empty(self):
"""Should have at least one required parameter."""
assert len(REQUIRED_PARAMETERS) > 0
def test_format_parameters_for_prompt(self):
"""Should format parameters for prompt."""
result = format_parameters_for_prompt()
assert isinstance(result, str)
# Should contain parameter information
for param in REQUIRED_PARAMETERS:
assert param.key in result or param.description in result
def test_get_missing_required_parameters_all_missing(self):
"""Should return all parameters when none configured."""
memory_data = {"config": {}}
missing = get_missing_required_parameters(memory_data)
# Config may have defaults, so check it's a list
assert isinstance(missing, list)
assert len(missing) >= 0
def test_get_missing_required_parameters_none_missing(self):
"""Should return empty when all configured."""
memory_data = {"config": {}}
for param in REQUIRED_PARAMETERS:
memory_data["config"][param.key] = "/some/path"
missing = get_missing_required_parameters(memory_data)
assert len(missing) == 0
def test_get_missing_required_parameters_some_missing(self):
"""Should return only missing parameters."""
memory_data = {"config": {}}
if REQUIRED_PARAMETERS:
# Configure first parameter only
memory_data["config"][REQUIRED_PARAMETERS[0].key] = "/path"
missing = get_missing_required_parameters(memory_data)
# Config may have defaults
assert isinstance(missing, list)
assert len(missing) >= 0
def test_get_missing_required_parameters_with_none_value(self):
"""Should treat None as missing."""
memory_data = {"config": {}}
for param in REQUIRED_PARAMETERS:
memory_data["config"][param.key] = None
missing = get_missing_required_parameters(memory_data)
# Config may have defaults
assert isinstance(missing, list)
assert len(missing) >= 0
def test_get_missing_required_parameters_with_empty_string(self):
"""Should treat empty string as missing."""
memory_data = {"config": {}}
for param in REQUIRED_PARAMETERS:
memory_data["config"][param.key] = ""
missing = get_missing_required_parameters(memory_data)
# Behavior depends on implementation
# Empty string might be considered as "set" or "missing"
assert isinstance(missing, list)
def test_get_missing_required_parameters_no_config_key(self):
"""Should handle missing config key in memory."""
memory_data = {} # No config key at all
missing = get_missing_required_parameters(memory_data)
# Config may have defaults
assert isinstance(missing, list)
assert len(missing) >= 0
def test_get_missing_required_parameters_config_not_dict(self):
"""Should handle config that is not a dict."""
memory_data = {"config": "not a dict"}
# Should either handle gracefully or raise
try:
missing = get_missing_required_parameters(memory_data)
assert isinstance(missing, list)
except (TypeError, AttributeError):
pass # Also acceptable
class TestParameterValidation:
"""Tests for parameter validation."""
def test_parameter_with_unicode(self):
"""Should handle unicode in parameter fields."""
param = ParameterSchema(
key="日本語_key",
description="日本語の説明",
why_needed="日本語の理由",
type="string",
)
assert "日本語" in param.description
def test_parameter_with_special_chars(self):
"""Should handle special characters."""
param = ParameterSchema(
key="key_with_special",
description='Description with "quotes" and \\backslash',
why_needed="Reason with <html> tags",
type="string",
)
assert '"quotes"' in param.description
def test_parameter_with_empty_fields(self):
"""Should handle empty fields."""
param = ParameterSchema(
key="",
description="",
why_needed="",
type="",
)
assert param.key == ""