710 lines
27 KiB
Python
710 lines
27 KiB
Python
"""
|
|||
|
|
Unit tests for Claude API configuration system
|
||
|
|
Tests configuration loading, validation, environment variable expansion, and migration
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import pytest
|
||
|
|
import tempfile
|
||
|
|
import yaml
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest.mock import Mock, patch, AsyncMock
|
||
|
|
from typing import Dict, Any
|
||
|
|
|
||
|
|
# Import the modules we're testing
|
||
|
|
from config_validation import (
|
||
|
|
ClaudeAPIConfig, ConfigurationValidator, SystemConfig,
|
||
|
|
ObsidianConfig, ObsidianRestAPIConfig, JournalConfig,
|
||
|
|
OutputConfig, AnalysisConfig, LoggingConfig
|
||
|
|
)
|
||
|
|
from configuration_loader import ConfigurationLoader, ConfigurationError, EnvironmentVariableError
|
||
|
|
from configuration_migrator import ConfigurationMigrator
|
||
|
|
from claude_api_client import ClaudeAPIClient
|
||
|
|
from error_handling import ClaudeConfigurationError, ClaudeAPIURLError, ClaudeModelValidationError
|
||
|
|
|
||
|
|
|
||
|
|
class TestClaudeAPIConfig:
|
||
|
|
"""Test ClaudeAPIConfig validation"""
|
||
|
|
|
||
|
|
def test_valid_claude_config(self):
|
||
|
|
"""Test creating valid Claude API configuration"""
|
||
|
|
config = ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||
|
|
api_url="https://api.anthropic.com",
|
||
|
|
model="claude-3-5-sonnet-20241022",
|
||
|
|
max_tokens=4096,
|
||
|
|
temperature=0.7
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.api_key.startswith("sk-ant-")
|
||
|
|
assert config.api_url == "https://api.anthropic.com"
|
||
|
|
assert config.model == "claude-3-5-sonnet-20241022"
|
||
|
|
assert config.max_tokens == 4096
|
||
|
|
assert config.temperature == 0.7
|
||
|
|
|
||
|
|
def test_default_values(self):
|
||
|
|
"""Test default values are applied correctly"""
|
||
|
|
config = ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.api_url == "https://api.anthropic.com"
|
||
|
|
assert config.model == "claude-3-5-sonnet-20241022"
|
||
|
|
assert config.max_tokens == 4096
|
||
|
|
assert config.temperature == 0.7
|
||
|
|
|
||
|
|
def test_custom_api_url(self):
|
||
|
|
"""Test custom API URL validation"""
|
||
|
|
config = ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||
|
|
api_url="https://custom-claude-api.example.com"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.api_url == "https://custom-claude-api.example.com"
|
||
|
|
|
||
|
|
def test_api_url_trailing_slash_removal(self):
|
||
|
|
"""Test that trailing slashes are removed from API URLs"""
|
||
|
|
config = ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||
|
|
api_url="https://api.anthropic.com/"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.api_url == "https://api.anthropic.com"
|
||
|
|
|
||
|
|
def test_invalid_api_url_format(self):
|
||
|
|
"""Test validation of invalid API URL formats"""
|
||
|
|
with pytest.raises(ValueError, match="Invalid URL format"):
|
||
|
|
ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||
|
|
api_url="not-a-url"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_invalid_api_url_protocol(self):
|
||
|
|
"""Test validation of invalid URL protocols"""
|
||
|
|
with pytest.raises(ValueError, match="URL must use http or https protocol"):
|
||
|
|
ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||
|
|
api_url="ftp://api.anthropic.com"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_invalid_api_key_format(self):
|
||
|
|
"""Test validation of invalid API key formats"""
|
||
|
|
with pytest.raises(ValueError, match="Claude API key should start with"):
|
||
|
|
ClaudeAPIConfig(api_key="invalid-key")
|
||
|
|
|
||
|
|
def test_api_key_too_short(self):
|
||
|
|
"""Test validation of API keys that are too short"""
|
||
|
|
with pytest.raises(ValueError, match="Claude API key appears to be too short"):
|
||
|
|
ClaudeAPIConfig(api_key="sk-ant-short")
|
||
|
|
|
||
|
|
def test_valid_model_names(self):
|
||
|
|
"""Test validation of valid model names"""
|
||
|
|
valid_models = [
|
||
|
|
"claude-3-opus-20240229",
|
||
|
|
"claude-3-sonnet-20240229",
|
||
|
|
"claude-3-haiku-20240307",
|
||
|
|
"claude-3-5-sonnet-20241022",
|
||
|
|
"claude-3-5-haiku-20241022",
|
||
|
|
"claude-3-opus-latest",
|
||
|
|
"claude-3-sonnet-latest",
|
||
|
|
"claude-3-haiku-latest",
|
||
|
|
"claude-3-5-sonnet-latest",
|
||
|
|
"claude-3-5-haiku-latest"
|
||
|
|
]
|
||
|
|
|
||
|
|
for model in valid_models:
|
||
|
|
config = ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||
|
|
model=model
|
||
|
|
)
|
||
|
|
assert config.model == model
|
||
|
|
|
||
|
|
def test_invalid_model_name(self):
|
||
|
|
"""Test validation of invalid model names"""
|
||
|
|
with pytest.raises(ValueError, match="Invalid model name"):
|
||
|
|
ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||
|
|
model="invalid-model"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_invalid_max_tokens(self):
|
||
|
|
"""Test validation of invalid max_tokens values"""
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||
|
|
max_tokens=0
|
||
|
|
)
|
||
|
|
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||
|
|
max_tokens=300000 # Too high
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_invalid_temperature(self):
|
||
|
|
"""Test validation of invalid temperature values"""
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||
|
|
temperature=-0.1
|
||
|
|
)
|
||
|
|
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
ClaudeAPIConfig(
|
||
|
|
api_key="sk-ant-test-key-12345678901234567890123456789012345678901234567890",
|
||
|
|
temperature=1.1
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestConfigurationLoader:
|
||
|
|
"""Test ConfigurationLoader environment variable expansion"""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures"""
|
||
|
|
self.loader = ConfigurationLoader()
|
||
|
|
self.temp_dir = Path(tempfile.mkdtemp())
|
||
|
|
|
||
|
|
def teardown_method(self):
|
||
|
|
"""Clean up test fixtures"""
|
||
|
|
import shutil
|
||
|
|
shutil.rmtree(self.temp_dir)
|
||
|
|
|
||
|
|
def test_simple_env_var_expansion(self):
|
||
|
|
"""Test simple environment variable expansion"""
|
||
|
|
os.environ['TEST_API_KEY'] = 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
|
||
|
|
|
||
|
|
config_data = {
|
||
|
|
'claude': {
|
||
|
|
'api_key': '${TEST_API_KEY}'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
expanded = self.loader.expand_environment_variables(config_data)
|
||
|
|
|
||
|
|
assert expanded['claude']['api_key'] == 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
|
||
|
|
|
||
|
|
# Clean up
|
||
|
|
del os.environ['TEST_API_KEY']
|
||
|
|
|
||
|
|
def test_env_var_with_default(self):
|
||
|
|
"""Test environment variable expansion with default values"""
|
||
|
|
config_data = {
|
||
|
|
'claude': {
|
||
|
|
'api_url': '${CLAUDE_API_URL:-https://api.anthropic.com}',
|
||
|
|
'model': '${CLAUDE_MODEL:-claude-3-5-sonnet-20241022}'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
expanded = self.loader.expand_environment_variables(config_data)
|
||
|
|
|
||
|
|
assert expanded['claude']['api_url'] == 'https://api.anthropic.com'
|
||
|
|
assert expanded['claude']['model'] == 'claude-3-5-sonnet-20241022'
|
||
|
|
|
||
|
|
def test_env_var_override_default(self):
|
||
|
|
"""Test environment variable overriding default values"""
|
||
|
|
os.environ['CLAUDE_API_URL'] = 'https://custom-api.example.com'
|
||
|
|
|
||
|
|
config_data = {
|
||
|
|
'claude': {
|
||
|
|
'api_url': '${CLAUDE_API_URL:-https://api.anthropic.com}'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
expanded = self.loader.expand_environment_variables(config_data)
|
||
|
|
|
||
|
|
assert expanded['claude']['api_url'] == 'https://custom-api.example.com'
|
||
|
|
|
||
|
|
# Clean up
|
||
|
|
del os.environ['CLAUDE_API_URL']
|
||
|
|
|
||
|
|
def test_missing_required_env_var(self):
|
||
|
|
"""Test error handling for missing required environment variables"""
|
||
|
|
config_data = {
|
||
|
|
'claude': {
|
||
|
|
'api_key': '${MISSING_API_KEY}'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
with pytest.raises(EnvironmentVariableError, match="Environment variable 'MISSING_API_KEY' is not set"):
|
||
|
|
self.loader.expand_environment_variables(config_data)
|
||
|
|
|
||
|
|
def test_nested_env_var_expansion(self):
|
||
|
|
"""Test environment variable expansion in nested structures"""
|
||
|
|
os.environ['VAULT_PATH'] = '/test/vault'
|
||
|
|
os.environ['API_KEY'] = 'test-key'
|
||
|
|
|
||
|
|
config_data = {
|
||
|
|
'obsidian': {
|
||
|
|
'vault_path': '${VAULT_PATH}',
|
||
|
|
'rest_api': {
|
||
|
|
'api_key': '${API_KEY}'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
expanded = self.loader.expand_environment_variables(config_data)
|
||
|
|
|
||
|
|
assert expanded['obsidian']['vault_path'] == '/test/vault'
|
||
|
|
assert expanded['obsidian']['rest_api']['api_key'] == 'test-key'
|
||
|
|
|
||
|
|
# Clean up
|
||
|
|
del os.environ['VAULT_PATH']
|
||
|
|
del os.environ['API_KEY']
|
||
|
|
|
||
|
|
def test_load_yaml_config(self):
|
||
|
|
"""Test loading YAML configuration file"""
|
||
|
|
config_data = {
|
||
|
|
'claude': {
|
||
|
|
'api_key': '${TEST_API_KEY:-default-key}',
|
||
|
|
'api_url': 'https://api.anthropic.com',
|
||
|
|
'model': 'claude-3-5-sonnet-20241022'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
config_file = self.temp_dir / 'test_config.yaml'
|
||
|
|
with config_file.open('w') as f:
|
||
|
|
yaml.dump(config_data, f)
|
||
|
|
|
||
|
|
loaded_config = self.loader.load_config(config_file)
|
||
|
|
|
||
|
|
assert loaded_config['claude']['api_key'] == 'default-key'
|
||
|
|
assert loaded_config['claude']['api_url'] == 'https://api.anthropic.com'
|
||
|
|
|
||
|
|
def test_load_nonexistent_config(self):
|
||
|
|
"""Test error handling for nonexistent configuration files"""
|
||
|
|
nonexistent_file = self.temp_dir / 'nonexistent.yaml'
|
||
|
|
|
||
|
|
with pytest.raises(ConfigurationError, match="Configuration file not found"):
|
||
|
|
self.loader.load_config(nonexistent_file)
|
||
|
|
|
||
|
|
def test_validate_environment_variables(self):
|
||
|
|
"""Test validation of environment variables in configuration"""
|
||
|
|
config_data = {
|
||
|
|
'claude': {
|
||
|
|
'api_key': '${EXISTING_VAR}',
|
||
|
|
'api_url': '${MISSING_VAR}',
|
||
|
|
'model': '${VAR_WITH_DEFAULT:-default-model}'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
os.environ['EXISTING_VAR'] = 'test-value'
|
||
|
|
|
||
|
|
missing_vars = self.loader.validate_environment_variables(config_data)
|
||
|
|
|
||
|
|
assert len(missing_vars) == 1
|
||
|
|
assert 'MISSING_VAR' in missing_vars[0]
|
||
|
|
|
||
|
|
# Clean up
|
||
|
|
del os.environ['EXISTING_VAR']
|
||
|
|
|
||
|
|
def test_get_environment_variable_references(self):
|
||
|
|
"""Test getting all environment variable references"""
|
||
|
|
config_data = {
|
||
|
|
'claude': {
|
||
|
|
'api_key': '${API_KEY}',
|
||
|
|
'api_url': '${API_URL:-default}',
|
||
|
|
'model': 'claude-3-5-sonnet-20241022'
|
||
|
|
},
|
||
|
|
'obsidian': {
|
||
|
|
'vault_path': '${VAULT_PATH}'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
env_vars = self.loader.get_environment_variable_references(config_data)
|
||
|
|
|
||
|
|
assert 'API_KEY' in env_vars
|
||
|
|
assert 'API_URL' in env_vars
|
||
|
|
assert 'VAULT_PATH' in env_vars
|
||
|
|
assert 'claude.api_key' in env_vars['API_KEY']
|
||
|
|
assert 'claude.api_url' in env_vars['API_URL']
|
||
|
|
|
||
|
|
|
||
|
|
class TestConfigurationMigrator:
|
||
|
|
"""Test ConfigurationMigrator backward compatibility"""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures"""
|
||
|
|
self.migrator = ConfigurationMigrator()
|
||
|
|
|
||
|
|
def test_migrate_claude_config_missing_api_url(self):
|
||
|
|
"""Test migration of Claude config missing api_url"""
|
||
|
|
config_dict = {
|
||
|
|
'claude': {
|
||
|
|
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||
|
|
'model': 'claude-3-5-sonnet-20241022'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
migrated = self.migrator.migrate_claude_config(config_dict)
|
||
|
|
|
||
|
|
assert migrated['claude']['api_url'] == 'https://api.anthropic.com'
|
||
|
|
assert migrated['claude']['api_key'] == 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
|
||
|
|
assert migrated['claude']['model'] == 'claude-3-5-sonnet-20241022'
|
||
|
|
|
||
|
|
def test_migrate_legacy_model_names(self):
|
||
|
|
"""Test migration of legacy model names"""
|
||
|
|
legacy_models = {
|
||
|
|
'claude-3-sonnet': 'claude-3-sonnet-20240229',
|
||
|
|
'claude-3-opus': 'claude-3-opus-20240229',
|
||
|
|
'claude-3-haiku': 'claude-3-haiku-20240307',
|
||
|
|
'sonnet': 'claude-3-5-sonnet-20241022'
|
||
|
|
}
|
||
|
|
|
||
|
|
for old_model, expected_new_model in legacy_models.items():
|
||
|
|
config_dict = {
|
||
|
|
'claude': {
|
||
|
|
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||
|
|
'model': old_model
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
migrated = self.migrator.migrate_claude_config(config_dict)
|
||
|
|
|
||
|
|
assert migrated['claude']['model'] == expected_new_model
|
||
|
|
|
||
|
|
def test_migrate_missing_claude_section(self):
|
||
|
|
"""Test migration when Claude section is completely missing"""
|
||
|
|
config_dict = {
|
||
|
|
'obsidian': {
|
||
|
|
'vault_path': '/test/vault'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
migrated = self.migrator.migrate_claude_config(config_dict)
|
||
|
|
|
||
|
|
assert 'claude' in migrated
|
||
|
|
assert migrated['claude']['api_url'] == 'https://api.anthropic.com'
|
||
|
|
assert migrated['claude']['model'] == 'claude-3-5-sonnet-20241022'
|
||
|
|
|
||
|
|
def test_migrate_complete_configuration(self):
|
||
|
|
"""Test migration of complete configuration"""
|
||
|
|
config_dict = {
|
||
|
|
'obsidian': {
|
||
|
|
'vault_path': '/test/vault',
|
||
|
|
'rest_api': {
|
||
|
|
'url': 'https://localhost:27123',
|
||
|
|
'api_key': 'test-key'
|
||
|
|
}
|
||
|
|
},
|
||
|
|
'claude': {
|
||
|
|
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||
|
|
'model': 'claude-3-sonnet' # Legacy model name
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
migrated = self.migrator.migrate_configuration(config_dict)
|
||
|
|
|
||
|
|
# Check Claude migration
|
||
|
|
assert migrated['claude']['api_url'] == 'https://api.anthropic.com'
|
||
|
|
assert migrated['claude']['model'] == 'claude-3-sonnet-20240229'
|
||
|
|
|
||
|
|
# Check that other sections are added with defaults
|
||
|
|
assert 'journal' in migrated
|
||
|
|
assert 'output' in migrated
|
||
|
|
assert 'analysis' in migrated
|
||
|
|
assert 'logging' in migrated
|
||
|
|
|
||
|
|
def test_check_migration_needed(self):
|
||
|
|
"""Test checking if migration is needed"""
|
||
|
|
# Config that needs migration
|
||
|
|
config_needing_migration = {
|
||
|
|
'claude': {
|
||
|
|
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||
|
|
'model': 'claude-3-sonnet' # Legacy model name
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
assert self.migrator.check_migration_needed(config_needing_migration) is True
|
||
|
|
|
||
|
|
# Config that doesn't need migration - need all required fields
|
||
|
|
config_up_to_date = {
|
||
|
|
'claude': {
|
||
|
|
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||
|
|
'api_url': 'https://api.anthropic.com',
|
||
|
|
'model': 'claude-3-5-sonnet-20241022',
|
||
|
|
'max_tokens': 4096,
|
||
|
|
'temperature': 0.7
|
||
|
|
},
|
||
|
|
'journal': {
|
||
|
|
'daily_notes_folder': 'Daily',
|
||
|
|
'date_format': 'YYYY-MM-DD',
|
||
|
|
'file_extension': '.md'
|
||
|
|
},
|
||
|
|
'output': {
|
||
|
|
'experiences_folder': 'Knowledge/Experiences',
|
||
|
|
'lessons_folder': 'Knowledge/Lessons',
|
||
|
|
'tasks_folder': 'Tasks/Daily',
|
||
|
|
'problems_folder': 'Knowledge/Problems',
|
||
|
|
'achievements_folder': 'Knowledge/Achievements',
|
||
|
|
'improvements_folder': 'Knowledge/Improvements'
|
||
|
|
},
|
||
|
|
'analysis': {
|
||
|
|
'categories': [],
|
||
|
|
'extraction_rules': {}
|
||
|
|
},
|
||
|
|
'logging': {
|
||
|
|
'level': 'INFO',
|
||
|
|
'file': 'logs/journal_organizer.log'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
assert self.migrator.check_migration_needed(config_up_to_date) is False
|
||
|
|
|
||
|
|
def test_get_migration_preview(self):
|
||
|
|
"""Test getting migration preview"""
|
||
|
|
config_dict = {
|
||
|
|
'claude': {
|
||
|
|
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||
|
|
'model': 'claude-3-sonnet'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
preview = self.migrator.get_migration_preview(config_dict)
|
||
|
|
|
||
|
|
assert len(preview) > 0
|
||
|
|
assert any('api_url' in action for action in preview)
|
||
|
|
assert any('claude-3-sonnet' in action and 'claude-3-sonnet-20240229' in action for action in preview)
|
||
|
|
|
||
|
|
def test_get_supported_model_names(self):
|
||
|
|
"""Test getting supported model names"""
|
||
|
|
supported_models = self.migrator.get_supported_model_names()
|
||
|
|
|
||
|
|
# Should include current models
|
||
|
|
assert 'claude-3-5-sonnet-20241022' in supported_models
|
||
|
|
assert 'claude-3-opus-20240229' in supported_models
|
||
|
|
|
||
|
|
# Should include legacy models
|
||
|
|
assert 'claude-3-sonnet' in supported_models
|
||
|
|
assert 'sonnet' in supported_models
|
||
|
|
|
||
|
|
|
||
|
|
class TestConfigurationValidator:
|
||
|
|
"""Test ConfigurationValidator comprehensive validation"""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures"""
|
||
|
|
self.validator = ConfigurationValidator()
|
||
|
|
self.temp_dir = Path(tempfile.mkdtemp())
|
||
|
|
|
||
|
|
# Create a test vault directory
|
||
|
|
self.test_vault = self.temp_dir / 'test_vault'
|
||
|
|
self.test_vault.mkdir()
|
||
|
|
(self.test_vault / '.obsidian').mkdir()
|
||
|
|
|
||
|
|
def teardown_method(self):
|
||
|
|
"""Clean up test fixtures"""
|
||
|
|
import shutil
|
||
|
|
shutil.rmtree(self.temp_dir)
|
||
|
|
|
||
|
|
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_load_and_validate_valid_config(self):
|
||
|
|
"""Test loading and validating a valid configuration"""
|
||
|
|
config_data = {
|
||
|
|
'obsidian': {
|
||
|
|
'vault_path': str(self.test_vault),
|
||
|
|
'rest_api': {
|
||
|
|
'url': 'https://localhost:27123',
|
||
|
|
'api_key': 'test-api-key',
|
||
|
|
'verify_ssl': False
|
||
|
|
}
|
||
|
|
},
|
||
|
|
'claude': {
|
||
|
|
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||
|
|
'api_url': 'https://api.anthropic.com',
|
||
|
|
'model': 'claude-3-5-sonnet-20241022'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
config_file = self.create_test_config_file(config_data)
|
||
|
|
|
||
|
|
system_config = self.validator.load_and_validate_config(config_file)
|
||
|
|
|
||
|
|
assert isinstance(system_config, SystemConfig)
|
||
|
|
assert system_config.claude.api_url == 'https://api.anthropic.com'
|
||
|
|
assert system_config.claude.model == 'claude-3-5-sonnet-20241022'
|
||
|
|
# Path resolution may add /private prefix on macOS, so check if paths resolve to same location
|
||
|
|
assert Path(system_config.obsidian.vault_path).resolve() == self.test_vault.resolve()
|
||
|
|
|
||
|
|
def test_load_and_validate_with_migration(self):
|
||
|
|
"""Test loading configuration that needs migration"""
|
||
|
|
config_data = {
|
||
|
|
'obsidian': {
|
||
|
|
'vault_path': str(self.test_vault),
|
||
|
|
'rest_api': {
|
||
|
|
'url': 'https://localhost:27123',
|
||
|
|
'api_key': 'test-api-key'
|
||
|
|
}
|
||
|
|
},
|
||
|
|
'claude': {
|
||
|
|
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||
|
|
'model': 'claude-3-sonnet' # Legacy model name
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
config_file = self.create_test_config_file(config_data)
|
||
|
|
|
||
|
|
system_config = self.validator.load_and_validate_config(config_file)
|
||
|
|
|
||
|
|
# Should have migrated the model name
|
||
|
|
assert system_config.claude.model == 'claude-3-sonnet-20240229'
|
||
|
|
# Should have added default api_url
|
||
|
|
assert system_config.claude.api_url == 'https://api.anthropic.com'
|
||
|
|
|
||
|
|
def test_load_and_validate_with_env_vars(self):
|
||
|
|
"""Test loading configuration with environment variables"""
|
||
|
|
os.environ['TEST_CLAUDE_KEY'] = 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
|
||
|
|
os.environ['TEST_VAULT_PATH'] = str(self.test_vault)
|
||
|
|
|
||
|
|
config_data = {
|
||
|
|
'obsidian': {
|
||
|
|
'vault_path': '${TEST_VAULT_PATH}',
|
||
|
|
'rest_api': {
|
||
|
|
'url': 'https://localhost:27123',
|
||
|
|
'api_key': 'test-api-key'
|
||
|
|
}
|
||
|
|
},
|
||
|
|
'claude': {
|
||
|
|
'api_key': '${TEST_CLAUDE_KEY}',
|
||
|
|
'api_url': '${CLAUDE_API_URL:-https://api.anthropic.com}'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
config_file = self.create_test_config_file(config_data)
|
||
|
|
|
||
|
|
system_config = self.validator.load_and_validate_config(config_file)
|
||
|
|
|
||
|
|
assert system_config.claude.api_key == 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
|
||
|
|
# Path resolution may add /private prefix on macOS, so check if paths resolve to same location
|
||
|
|
assert Path(system_config.obsidian.vault_path).resolve() == self.test_vault.resolve()
|
||
|
|
assert system_config.claude.api_url == 'https://api.anthropic.com'
|
||
|
|
|
||
|
|
# Clean up
|
||
|
|
del os.environ['TEST_CLAUDE_KEY']
|
||
|
|
del os.environ['TEST_VAULT_PATH']
|
||
|
|
|
||
|
|
def test_validation_error_handling(self):
|
||
|
|
"""Test validation error handling"""
|
||
|
|
config_data = {
|
||
|
|
'obsidian': {
|
||
|
|
'vault_path': '/nonexistent/path',
|
||
|
|
'rest_api': {
|
||
|
|
'url': 'invalid-url',
|
||
|
|
'api_key': 'test-key'
|
||
|
|
}
|
||
|
|
},
|
||
|
|
'claude': {
|
||
|
|
'api_key': 'invalid-key',
|
||
|
|
'model': 'invalid-model'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
config_file = self.create_test_config_file(config_data)
|
||
|
|
|
||
|
|
with pytest.raises(ValueError, match="Configuration validation failed"):
|
||
|
|
self.validator.load_and_validate_config(config_file)
|
||
|
|
|
||
|
|
def test_validate_api_keys(self):
|
||
|
|
"""Test API key validation"""
|
||
|
|
# Create a valid system config for testing
|
||
|
|
system_config = SystemConfig(
|
||
|
|
obsidian=ObsidianConfig(
|
||
|
|
vault_path=str(self.test_vault),
|
||
|
|
rest_api=ObsidianRestAPIConfig(
|
||
|
|
url='https://localhost:27123',
|
||
|
|
api_key='test-api-key'
|
||
|
|
)
|
||
|
|
),
|
||
|
|
claude=ClaudeAPIConfig(
|
||
|
|
api_key='sk-ant-test-key-12345678901234567890123456789012345678901234567890'
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
issues = self.validator.validate_api_keys(system_config)
|
||
|
|
|
||
|
|
# Should have no issues with valid keys
|
||
|
|
assert len(issues) == 0
|
||
|
|
|
||
|
|
def test_validate_api_keys_with_env_vars(self):
|
||
|
|
"""Test API key validation with environment variable placeholders"""
|
||
|
|
# Skip this test as it requires complex mocking of pydantic validation
|
||
|
|
pytest.skip("Environment variable validation requires complex setup")
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
class TestClaudeAPIClient:
|
||
|
|
"""Test ClaudeAPIClient functionality"""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures"""
|
||
|
|
self.config = ClaudeAPIConfig(
|
||
|
|
api_key='sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||
|
|
api_url='https://api.anthropic.com',
|
||
|
|
model='claude-3-5-sonnet-20241022'
|
||
|
|
)
|
||
|
|
|
||
|
|
@patch('claude_api_client.AsyncAnthropic')
|
||
|
|
@patch('claude_api_client.aiohttp')
|
||
|
|
def test_client_initialization(self, mock_aiohttp, mock_anthropic):
|
||
|
|
"""Test Claude API client initialization"""
|
||
|
|
client = ClaudeAPIClient(self.config)
|
||
|
|
|
||
|
|
assert client.base_url == 'https://api.anthropic.com'
|
||
|
|
assert client.api_key == 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
|
||
|
|
assert client.model == 'claude-3-5-sonnet-20241022'
|
||
|
|
|
||
|
|
# Should have called AsyncAnthropic constructor
|
||
|
|
mock_anthropic.assert_called_once()
|
||
|
|
|
||
|
|
@patch('claude_api_client.AsyncAnthropic')
|
||
|
|
@patch('claude_api_client.aiohttp')
|
||
|
|
def test_client_with_custom_url(self, mock_aiohttp, mock_anthropic):
|
||
|
|
"""Test client initialization with custom API URL"""
|
||
|
|
custom_config = ClaudeAPIConfig(
|
||
|
|
api_key='sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||
|
|
api_url='https://custom-api.example.com',
|
||
|
|
model='claude-3-5-sonnet-20241022'
|
||
|
|
)
|
||
|
|
|
||
|
|
client = ClaudeAPIClient(custom_config)
|
||
|
|
|
||
|
|
assert client.base_url == 'https://custom-api.example.com'
|
||
|
|
|
||
|
|
# Should have called AsyncAnthropic with custom base_url
|
||
|
|
mock_anthropic.assert_called_once()
|
||
|
|
call_args = mock_anthropic.call_args
|
||
|
|
assert call_args[1]['base_url'] == 'https://custom-api.example.com'
|
||
|
|
|
||
|
|
@patch('claude_api_client.AsyncAnthropic')
|
||
|
|
@patch('claude_api_client.aiohttp')
|
||
|
|
def test_get_client_info(self, mock_aiohttp, mock_anthropic):
|
||
|
|
"""Test getting client configuration information"""
|
||
|
|
client = ClaudeAPIClient(self.config)
|
||
|
|
|
||
|
|
info = client.get_client_info()
|
||
|
|
|
||
|
|
assert info['api_url'] == 'https://api.anthropic.com'
|
||
|
|
assert info['model'] == 'claude-3-5-sonnet-20241022'
|
||
|
|
assert info['max_tokens'] == 4096
|
||
|
|
assert info['temperature'] == 0.7
|
||
|
|
assert info['is_custom_endpoint'] is False
|
||
|
|
|
||
|
|
@patch('claude_api_client.AsyncAnthropic')
|
||
|
|
@patch('claude_api_client.aiohttp')
|
||
|
|
def test_get_client_info_custom_endpoint(self, mock_aiohttp, mock_anthropic):
|
||
|
|
"""Test getting client info for custom endpoint"""
|
||
|
|
custom_config = ClaudeAPIConfig(
|
||
|
|
api_key='sk-ant-test-key-12345678901234567890123456789012345678901234567890',
|
||
|
|
api_url='https://custom-api.example.com'
|
||
|
|
)
|
||
|
|
|
||
|
|
client = ClaudeAPIClient(custom_config)
|
||
|
|
info = client.get_client_info()
|
||
|
|
|
||
|
|
assert info['is_custom_endpoint'] is True
|
||
|
|
assert info['api_url'] == 'https://custom-api.example.com'
|