- 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
512 lines
20 KiB
Python
512 lines
20 KiB
Python
"""
|
|
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"]) |