Initial project setup: Obsidian intelligent journal organizer
- Add core agent architecture with Command + Skill pattern - Implement Claude API integration for content analysis - Add Obsidian REST API integration for vault operations - Create conversational interface (v2.0) with natural language processing - Add comprehensive configuration management and validation - Include project documentation and developer guides - Set up testing framework with unit, integration, and property tests - Add Kiro specs for Claude API configuration and code quality improvements - Configure project steering files for development guidelines
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
"""
|
||||
Integration tests for real Claude API endpoints
|
||||
Tests validation with default Anthropic API, proxy server configurations, and different model selections
|
||||
Requirements: 1.3, 2.4
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import asyncio
|
||||
import tempfile
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
# Import the modules we're testing
|
||||
from config_validation import ClaudeAPIConfig
|
||||
from claude_api_client import ClaudeAPIClient
|
||||
from error_handling import APIError
|
||||
|
||||
|
||||
class TestRealAPIEndpoints:
|
||||
"""Test with real API endpoints - requires valid API key"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures"""
|
||||
self.temp_dir = Path(tempfile.mkdtemp())
|
||||
|
||||
# Check if we have a real API key for testing
|
||||
self.api_key = os.getenv('ANTHROPIC_API_KEY')
|
||||
self.has_real_api_key = (
|
||||
self.api_key and
|
||||
self.api_key.startswith('sk-ant-') and
|
||||
len(self.api_key) > 50
|
||||
)
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test fixtures"""
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.getenv('ANTHROPIC_API_KEY'),
|
||||
reason="Requires ANTHROPIC_API_KEY environment variable for real API testing"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_anthropic_api_validation(self):
|
||||
"""Test validation with default Anthropic API"""
|
||||
if not self.has_real_api_key:
|
||||
pytest.skip("Requires valid ANTHROPIC_API_KEY for real API testing")
|
||||
|
||||
config = ClaudeAPIConfig(
|
||||
api_key=self.api_key,
|
||||
api_url="https://api.anthropic.com",
|
||||
model="claude-3-5-sonnet-20241022"
|
||||
)
|
||||
|
||||
client = ClaudeAPIClient(config)
|
||||
|
||||
# Test connection validation
|
||||
is_valid = await client.validate_connection()
|
||||
assert is_valid is True
|
||||
|
||||
# Test model availability
|
||||
is_model_valid = await client.validate_model_availability()
|
||||
assert is_model_valid is True
|
||||
|
||||
# Test comprehensive connectivity
|
||||
results = await client.test_api_connectivity()
|
||||
assert results['overall_status'] == 'success'
|
||||
assert results['connection_test']['status'] == 'success'
|
||||
assert results['model_test']['status'] == 'success'
|
||||
assert results['authentication_test']['status'] == 'success'
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.getenv('ANTHROPIC_API_KEY'),
|
||||
reason="Requires ANTHROPIC_API_KEY environment variable for real API testing"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_different_model_selections(self):
|
||||
"""Test different Claude model selections work correctly"""
|
||||
if not self.has_real_api_key:
|
||||
pytest.skip("Requires valid ANTHROPIC_API_KEY for real API testing")
|
||||
|
||||
# Test different models that should be available
|
||||
models_to_test = [
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-haiku-20240307",
|
||||
# Note: claude-3-opus may not be available in all regions/accounts
|
||||
]
|
||||
|
||||
for model in models_to_test:
|
||||
config = ClaudeAPIConfig(
|
||||
api_key=self.api_key,
|
||||
api_url="https://api.anthropic.com",
|
||||
model=model
|
||||
)
|
||||
|
||||
client = ClaudeAPIClient(config)
|
||||
|
||||
try:
|
||||
# Test that the model is available
|
||||
is_valid = await client.validate_model_availability()
|
||||
assert is_valid is True, f"Model {model} should be available"
|
||||
|
||||
# Test a simple API call with the model
|
||||
response = await client.create_message([
|
||||
{"role": "user", "content": "Hello"}
|
||||
])
|
||||
|
||||
assert response is not None
|
||||
assert hasattr(response, 'content')
|
||||
assert len(response.content) > 0
|
||||
|
||||
except APIError as e:
|
||||
# Some models might not be available in all regions/accounts
|
||||
if "model" in str(e).lower() and "not found" in str(e).lower():
|
||||
pytest.skip(f"Model {model} not available in this account/region")
|
||||
else:
|
||||
raise
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_api_key_handling(self):
|
||||
"""Test handling of invalid API keys"""
|
||||
config = ClaudeAPIConfig(
|
||||
api_key="sk-ant-invalid-key-12345678901234567890123456789012345678901234567890",
|
||||
api_url="https://api.anthropic.com",
|
||||
model="claude-3-5-sonnet-20241022"
|
||||
)
|
||||
|
||||
client = ClaudeAPIClient(config)
|
||||
|
||||
with pytest.raises(APIError, match="Invalid API key or unauthorized access"):
|
||||
await client.validate_connection()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_api_url_handling(self):
|
||||
"""Test handling of invalid API URLs"""
|
||||
config = ClaudeAPIConfig(
|
||||
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||||
api_url="https://nonexistent-api.example.com",
|
||||
model="claude-3-5-sonnet-20241022"
|
||||
)
|
||||
|
||||
client = ClaudeAPIClient(config)
|
||||
|
||||
with pytest.raises(APIError, match="Connection error"):
|
||||
await client.validate_connection()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_model_handling(self):
|
||||
"""Test handling of invalid model names"""
|
||||
if not self.has_real_api_key:
|
||||
pytest.skip("Requires valid ANTHROPIC_API_KEY for real API testing")
|
||||
|
||||
config = ClaudeAPIConfig(
|
||||
api_key=self.api_key,
|
||||
api_url="https://api.anthropic.com",
|
||||
model="claude-nonexistent-model"
|
||||
)
|
||||
|
||||
client = ClaudeAPIClient(config)
|
||||
|
||||
with pytest.raises(APIError, match="Model.*not found"):
|
||||
await client.validate_model_availability()
|
||||
|
||||
|
||||
class TestProxyServerConfigurations:
|
||||
"""Test proxy server configurations"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures"""
|
||||
self.temp_dir = Path(tempfile.mkdtemp())
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test fixtures"""
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_localhost_proxy_configuration(self):
|
||||
"""Test configuration for localhost proxy servers"""
|
||||
config = ClaudeAPIConfig(
|
||||
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||||
api_url="https://localhost:8080",
|
||||
model="claude-3-5-sonnet-20241022"
|
||||
)
|
||||
|
||||
client = ClaudeAPIClient(config)
|
||||
|
||||
# Test that SSL context is configured for localhost
|
||||
async with client.create_http_session() as session:
|
||||
# Should not raise SSL errors for localhost
|
||||
assert session is not None
|
||||
|
||||
# Verify SSL context is configured for localhost
|
||||
connector = session.connector
|
||||
assert connector.ssl is not None
|
||||
# For localhost, SSL verification should be disabled
|
||||
assert not connector.ssl.check_hostname
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_proxy_url_configuration(self):
|
||||
"""Test configuration for custom proxy URLs"""
|
||||
proxy_urls = [
|
||||
"https://proxy.example.com:8080",
|
||||
"https://claude-proxy.internal:443",
|
||||
"http://localhost:3128"
|
||||
]
|
||||
|
||||
for proxy_url in proxy_urls:
|
||||
config = ClaudeAPIConfig(
|
||||
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||||
api_url=proxy_url,
|
||||
model="claude-3-5-sonnet-20241022"
|
||||
)
|
||||
|
||||
client = ClaudeAPIClient(config)
|
||||
|
||||
# Test client initialization
|
||||
assert client.base_url == proxy_url
|
||||
|
||||
# Test client info
|
||||
info = client.get_client_info()
|
||||
assert info['api_url'] == proxy_url
|
||||
assert info['is_custom_endpoint'] is True
|
||||
|
||||
def test_proxy_configuration_validation(self):
|
||||
"""Test validation of proxy server configurations"""
|
||||
# Valid proxy configurations
|
||||
valid_configs = [
|
||||
{
|
||||
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||||
'api_url': 'https://proxy.example.com:8080',
|
||||
'model': 'claude-3-5-sonnet-20241022'
|
||||
},
|
||||
{
|
||||
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||||
'api_url': 'http://localhost:3128',
|
||||
'model': 'claude-3-5-sonnet-20241022'
|
||||
}
|
||||
]
|
||||
|
||||
for config_data in valid_configs:
|
||||
config = ClaudeAPIConfig(**config_data)
|
||||
assert config.api_url == config_data['api_url']
|
||||
|
||||
# Invalid proxy configurations
|
||||
invalid_configs = [
|
||||
{
|
||||
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||||
'api_url': 'ftp://proxy.example.com:8080', # Invalid protocol
|
||||
'model': 'claude-3-5-sonnet-20241022'
|
||||
},
|
||||
{
|
||||
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||||
'api_url': 'not-a-url', # Invalid URL format
|
||||
'model': 'claude-3-5-sonnet-20241022'
|
||||
}
|
||||
]
|
||||
|
||||
for config_data in invalid_configs:
|
||||
with pytest.raises(ValueError):
|
||||
ClaudeAPIConfig(**config_data)
|
||||
|
||||
|
||||
class TestConfigurationValidationClass:
|
||||
"""Test configuration validation class methods"""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.getenv('ANTHROPIC_API_KEY'),
|
||||
reason="Requires ANTHROPIC_API_KEY environment variable for real API testing"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_configuration_quick_test(self):
|
||||
"""Test quick configuration validation"""
|
||||
api_key = os.getenv('ANTHROPIC_API_KEY')
|
||||
if not api_key or not api_key.startswith('sk-ant-'):
|
||||
pytest.skip("Requires valid ANTHROPIC_API_KEY for real API testing")
|
||||
|
||||
config = ClaudeAPIConfig(
|
||||
api_key=api_key,
|
||||
api_url="https://api.anthropic.com",
|
||||
model="claude-3-5-sonnet-20241022"
|
||||
)
|
||||
|
||||
results = await ClaudeAPIClient.validate_configuration(config, quick_test=True)
|
||||
|
||||
assert results['config_valid'] is True
|
||||
assert results['connection_valid'] is True
|
||||
assert results['model_valid'] is True
|
||||
assert len(results['errors']) == 0
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.getenv('ANTHROPIC_API_KEY'),
|
||||
reason="Requires ANTHROPIC_API_KEY environment variable for real API testing"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_configuration_comprehensive_test(self):
|
||||
"""Test comprehensive configuration validation"""
|
||||
api_key = os.getenv('ANTHROPIC_API_KEY')
|
||||
if not api_key or not api_key.startswith('sk-ant-'):
|
||||
pytest.skip("Requires valid ANTHROPIC_API_KEY for real API testing")
|
||||
|
||||
config = ClaudeAPIConfig(
|
||||
api_key=api_key,
|
||||
api_url="https://api.anthropic.com",
|
||||
model="claude-3-5-sonnet-20241022"
|
||||
)
|
||||
|
||||
results = await ClaudeAPIClient.validate_configuration(config, quick_test=False)
|
||||
|
||||
assert results['config_valid'] is True
|
||||
assert results['connection_valid'] is True
|
||||
assert results['model_valid'] is True
|
||||
assert len(results['errors']) == 0
|
||||
|
||||
# Should have detailed test results
|
||||
assert 'detailed_tests' in results
|
||||
detailed = results['detailed_tests']
|
||||
assert detailed['overall_status'] == 'success'
|
||||
assert detailed['connection_test']['status'] == 'success'
|
||||
assert detailed['model_test']['status'] == 'success'
|
||||
assert detailed['authentication_test']['status'] == 'success'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_configuration_invalid_key(self):
|
||||
"""Test configuration validation with invalid API key"""
|
||||
config = ClaudeAPIConfig(
|
||||
api_key="sk-ant-invalid-key-12345678901234567890123456789012345678901234567890",
|
||||
api_url="https://api.anthropic.com",
|
||||
model="claude-3-5-sonnet-20241022"
|
||||
)
|
||||
|
||||
results = await ClaudeAPIClient.validate_configuration(config, quick_test=True)
|
||||
|
||||
assert results['config_valid'] is True # Config format is valid
|
||||
assert results['connection_valid'] is False # But connection fails
|
||||
assert len(results['errors']) > 0
|
||||
assert any('Invalid API key' in error for error in results['errors'])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_configuration_custom_endpoint(self):
|
||||
"""Test configuration validation with custom endpoint"""
|
||||
config = ClaudeAPIConfig(
|
||||
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||||
api_url="https://custom-claude-api.example.com",
|
||||
model="claude-3-5-sonnet-20241022"
|
||||
)
|
||||
|
||||
results = await ClaudeAPIClient.validate_configuration(config, quick_test=True)
|
||||
|
||||
assert results['config_valid'] is True
|
||||
assert results['config_info']['is_custom_endpoint'] is True
|
||||
assert results['config_info']['api_url'] == "https://custom-claude-api.example.com"
|
||||
|
||||
# Should have warning about custom endpoint
|
||||
assert len(results['warnings']) > 0
|
||||
assert any('custom API endpoint' in warning for warning in results['warnings'])
|
||||
|
||||
|
||||
class TestEnvironmentVariableScenarios:
|
||||
"""Test environment variable scenarios in real configurations"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures"""
|
||||
self.temp_dir = Path(tempfile.mkdtemp())
|
||||
|
||||
# Store original environment variables
|
||||
self.original_env = {}
|
||||
test_vars = ['TEST_CLAUDE_API_KEY', 'TEST_CLAUDE_API_URL', 'TEST_CLAUDE_MODEL']
|
||||
for var in test_vars:
|
||||
if var in os.environ:
|
||||
self.original_env[var] = os.environ[var]
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up test fixtures"""
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
# Clean up test environment variables
|
||||
test_vars = ['TEST_CLAUDE_API_KEY', 'TEST_CLAUDE_API_URL', 'TEST_CLAUDE_MODEL']
|
||||
for var in test_vars:
|
||||
if var in os.environ:
|
||||
del os.environ[var]
|
||||
|
||||
# Restore original environment variables
|
||||
for var, value in self.original_env.items():
|
||||
os.environ[var] = value
|
||||
|
||||
def create_test_config_file(self, config_data: Dict[str, Any]) -> Path:
|
||||
"""Create a test configuration file"""
|
||||
config_file = self.temp_dir / 'test_config.yaml'
|
||||
with config_file.open('w') as f:
|
||||
yaml.dump(config_data, f)
|
||||
return config_file
|
||||
|
||||
def test_environment_variable_configuration_loading(self):
|
||||
"""Test loading configuration with environment variables"""
|
||||
# Set up environment variables
|
||||
os.environ['TEST_CLAUDE_API_KEY'] = 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
|
||||
os.environ['TEST_CLAUDE_API_URL'] = 'https://custom-api.example.com'
|
||||
os.environ['TEST_CLAUDE_MODEL'] = 'claude-3-haiku-20240307'
|
||||
|
||||
config_data = {
|
||||
'claude': {
|
||||
'api_key': '${TEST_CLAUDE_API_KEY}',
|
||||
'api_url': '${TEST_CLAUDE_API_URL}',
|
||||
'model': '${TEST_CLAUDE_MODEL}'
|
||||
}
|
||||
}
|
||||
|
||||
config_file = self.create_test_config_file(config_data)
|
||||
|
||||
# Load configuration through the configuration loader
|
||||
from configuration_loader import ConfigurationLoader
|
||||
loader = ConfigurationLoader()
|
||||
loaded_config = loader.load_config(config_file)
|
||||
|
||||
# Validate the loaded configuration
|
||||
claude_config = ClaudeAPIConfig(**loaded_config['claude'])
|
||||
|
||||
assert claude_config.api_key == 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
|
||||
assert claude_config.api_url == 'https://custom-api.example.com'
|
||||
assert claude_config.model == 'claude-3-haiku-20240307'
|
||||
|
||||
def test_environment_variable_defaults_in_configuration(self):
|
||||
"""Test environment variable defaults in configuration"""
|
||||
config_data = {
|
||||
'claude': {
|
||||
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||||
'api_url': '${CLAUDE_API_URL:-https://api.anthropic.com}',
|
||||
'model': '${CLAUDE_MODEL:-claude-3-5-sonnet-20241022}',
|
||||
'max_tokens': '${CLAUDE_MAX_TOKENS:-4096}',
|
||||
'temperature': '${CLAUDE_TEMPERATURE:-0.7}'
|
||||
}
|
||||
}
|
||||
|
||||
config_file = self.create_test_config_file(config_data)
|
||||
|
||||
# Load configuration through the configuration loader
|
||||
from configuration_loader import ConfigurationLoader
|
||||
loader = ConfigurationLoader()
|
||||
loaded_config = loader.load_config(config_file)
|
||||
|
||||
# Should use defaults since environment variables are not set
|
||||
claude_config = ClaudeAPIConfig(**loaded_config['claude'])
|
||||
|
||||
assert claude_config.api_url == 'https://api.anthropic.com'
|
||||
assert claude_config.model == 'claude-3-5-sonnet-20241022'
|
||||
assert claude_config.max_tokens == 4096
|
||||
assert claude_config.temperature == 0.7
|
||||
|
||||
def test_mixed_environment_and_direct_configuration(self):
|
||||
"""Test mixed configuration (some values from files, some from environment)"""
|
||||
# Set only some environment variables
|
||||
os.environ['TEST_CLAUDE_API_KEY'] = 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
|
||||
os.environ['TEST_CLAUDE_API_URL'] = 'https://proxy.example.com'
|
||||
|
||||
config_data = {
|
||||
'claude': {
|
||||
'api_key': '${TEST_CLAUDE_API_KEY}',
|
||||
'api_url': '${TEST_CLAUDE_API_URL}',
|
||||
'model': 'claude-3-5-sonnet-20241022', # Direct value
|
||||
'max_tokens': 8192, # Direct value
|
||||
'temperature': '${CLAUDE_TEMPERATURE:-0.5}' # Default value
|
||||
}
|
||||
}
|
||||
|
||||
config_file = self.create_test_config_file(config_data)
|
||||
|
||||
# Load configuration through the configuration loader
|
||||
from configuration_loader import ConfigurationLoader
|
||||
loader = ConfigurationLoader()
|
||||
loaded_config = loader.load_config(config_file)
|
||||
|
||||
claude_config = ClaudeAPIConfig(**loaded_config['claude'])
|
||||
|
||||
# Environment variables should be expanded
|
||||
assert claude_config.api_key == 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
|
||||
assert claude_config.api_url == 'https://proxy.example.com'
|
||||
|
||||
# Direct values should be preserved
|
||||
assert claude_config.model == 'claude-3-5-sonnet-20241022'
|
||||
assert claude_config.max_tokens == 8192
|
||||
|
||||
# Default should be used
|
||||
assert claude_config.temperature == 0.5
|
||||
|
||||
def test_missing_required_environment_variable(self):
|
||||
"""Test error handling for missing required environment variables"""
|
||||
config_data = {
|
||||
'claude': {
|
||||
'api_key': '${MISSING_API_KEY}', # Required but not set
|
||||
'api_url': 'https://api.anthropic.com',
|
||||
'model': 'claude-3-5-sonnet-20241022'
|
||||
}
|
||||
}
|
||||
|
||||
config_file = self.create_test_config_file(config_data)
|
||||
|
||||
# Should raise error for missing required environment variable
|
||||
from configuration_loader import ConfigurationLoader, EnvironmentVariableError
|
||||
loader = ConfigurationLoader()
|
||||
|
||||
with pytest.raises(EnvironmentVariableError, match="Environment variable 'MISSING_API_KEY' is not set"):
|
||||
loader.load_config(config_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run tests with pytest
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user