"""Edge case tests for configuration and parameters.""" import os from unittest.mock import patch import pytest from alfred.agent.parameters import ( REQUIRED_PARAMETERS, ParameterSchema, format_parameters_for_prompt, get_missing_required_parameters, ) from alfred.settings import ConfigurationError, Settings 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.llm_temperature == 0.2 assert settings.max_tool_iterations == 10 assert settings.request_timeout == 30 def test_temperature_boundary_low(self): """Should accept temperature at lower boundary.""" with patch.dict(os.environ, {"LLM_TEMPERATURE": "0.0"}, clear=True): settings = Settings() assert settings.llm_temperature == 0.0 def test_temperature_boundary_high(self): """Should accept temperature at upper boundary.""" with patch.dict(os.environ, {"LLM_TEMPERATURE": "2.0"}, clear=True): settings = Settings() assert settings.llm_temperature == 2.0 def test_temperature_below_boundary(self): """Should reject temperature below 0.""" with patch.dict(os.environ, {"LLM_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, {"LLM_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, {"LLM_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 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 == ""