319 lines
12 KiB
Python
319 lines
12 KiB
Python
"""Edge case tests for configuration and parameters."""
|
|
|
|
import os
|
|
import pytest
|
|
from unittest.mock import patch
|
|
|
|
from alfred.agent.config import ConfigurationError, Settings
|
|
from alfred.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 == ""
|