Files

659 lines
25 KiB
Python
Raw Permalink Normal View History

"""
Backward compatibility validation tests
Tests that existing configuration files continue to work, no breaking changes to existing functionality,
and migration messages are appropriate.
Requirements: 3.1, 3.2, 3.3, 3.4
"""
import os
import pytest
import tempfile
import yaml
from pathlib import Path
from typing import Dict, Any
from unittest.mock import patch, Mock
# Import the modules we're testing
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from config import Config
from config_validation import SystemConfig, ConfigurationValidator
from configuration_loader import ConfigurationLoader
from configuration_migrator import ConfigurationMigrator
class TestLegacyConfigurationFiles:
"""Test that existing configuration files continue to work"""
def setup_method(self):
"""Set up test fixtures"""
self.temp_dir = Path(tempfile.mkdtemp())
self.test_vault = self.temp_dir / 'test_vault'
self.test_vault.mkdir()
(self.test_vault / '.obsidian').mkdir()
# Store original environment variables
self.original_env = {}
if 'ANTHROPIC_API_KEY' in os.environ:
self.original_env['ANTHROPIC_API_KEY'] = os.environ['ANTHROPIC_API_KEY']
# Set a test API key for validation
os.environ['ANTHROPIC_API_KEY'] = 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
def teardown_method(self):
"""Clean up test fixtures"""
import shutil
shutil.rmtree(self.temp_dir)
# Restore original environment variables
if 'ANTHROPIC_API_KEY' in self.original_env:
os.environ['ANTHROPIC_API_KEY'] = self.original_env['ANTHROPIC_API_KEY']
elif 'ANTHROPIC_API_KEY' in os.environ:
del os.environ['ANTHROPIC_API_KEY']
def create_legacy_config_file(self, config_data: Dict[str, Any]) -> Path:
"""Create a legacy configuration file"""
config_file = self.temp_dir / 'legacy_config.yaml'
with config_file.open('w') as f:
yaml.dump(config_data, f)
return config_file
def test_minimal_legacy_config_loads(self):
"""Test that minimal legacy configuration loads successfully"""
# This represents a very basic legacy config that users might have
legacy_config = {
'obsidian': {
'vault_path': str(self.test_vault),
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key'
},
'claude': {
'api_key': '${ANTHROPIC_API_KEY}',
'model': 'claude-3-sonnet' # Legacy model name
}
}
config_file = self.create_legacy_config_file(legacy_config)
# Should load without errors
config = Config(str(config_file))
# Verify legacy values are preserved and migrated appropriately
assert config.claude.api_key == 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
assert config.claude.model == 'claude-3-sonnet-20240229' # Should be migrated
assert config.claude.api_url == 'https://api.anthropic.com' # Should get default
assert Path(config.obsidian.vault_path).resolve() == self.test_vault.resolve()
def test_legacy_config_without_claude_section(self):
"""Test legacy config that doesn't have Claude section at all"""
legacy_config = {
'obsidian': {
'vault_path': str(self.test_vault),
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key'
}
}
config_file = self.create_legacy_config_file(legacy_config)
# Should load and add Claude section with defaults
config = Config(str(config_file))
# Should have Claude config with defaults
assert config.claude.api_key == 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
assert config.claude.model == 'claude-3-5-sonnet-20241022'
assert config.claude.api_url == 'https://api.anthropic.com'
def test_legacy_config_with_old_structure(self):
"""Test legacy config with old obsidian structure"""
legacy_config = {
'obsidian': {
'vault_path': str(self.test_vault),
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key',
'verify_ssl': False
},
'claude': {
'api_key': '${ANTHROPIC_API_KEY}',
'model': 'claude-3-opus', # Legacy model name
'max_tokens': 2048,
'temperature': 0.5
},
'journal': {
'daily_notes_folder': 'Journal/Daily',
'date_format': 'YYYY-MM-DD'
}
}
config_file = self.create_legacy_config_file(legacy_config)
# Should load and migrate appropriately
config = Config(str(config_file))
# Verify all legacy values are preserved
assert config.claude.model == 'claude-3-opus-20240229' # Migrated
assert config.claude.max_tokens == 2048 # Preserved
assert config.claude.temperature == 0.5 # Preserved
assert config.journal.daily_notes_folder == 'Journal/Daily' # Preserved
assert config.obsidian.verify_ssl is False # Preserved
def test_legacy_config_with_missing_sections(self):
"""Test legacy config with missing sections gets defaults"""
legacy_config = {
'obsidian': {
'vault_path': str(self.test_vault),
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key'
},
'claude': {
'api_key': '${ANTHROPIC_API_KEY}'
}
# Missing journal, output, analysis, logging sections
}
config_file = self.create_legacy_config_file(legacy_config)
# Should load and add missing sections with defaults
config = Config(str(config_file))
# Should have all sections with defaults
assert config.journal.daily_notes_folder == 'Daily'
assert config.journal.date_format == 'YYYY-MM-DD'
assert config.output.experiences_folder == 'Knowledge/Experiences'
assert config.analysis.categories == []
assert config.logging.level == 'INFO'
def test_legacy_model_name_variations(self):
"""Test various legacy model name formats are migrated correctly"""
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',
'opus': 'claude-3-opus-20240229',
'haiku': 'claude-3-haiku-20240307'
}
for legacy_model, expected_model in legacy_models.items():
legacy_config = {
'obsidian': {
'vault_path': str(self.test_vault),
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key'
},
'claude': {
'api_key': '${ANTHROPIC_API_KEY}',
'model': legacy_model
}
}
config_file = self.create_legacy_config_file(legacy_config)
config = Config(str(config_file))
assert config.claude.model == expected_model, f"Legacy model '{legacy_model}' should migrate to '{expected_model}'"
class TestNoBreakingChanges:
"""Test that no breaking changes exist in existing functionality"""
def setup_method(self):
"""Set up test fixtures"""
self.temp_dir = Path(tempfile.mkdtemp())
self.test_vault = self.temp_dir / 'test_vault'
self.test_vault.mkdir()
(self.test_vault / '.obsidian').mkdir()
# Set test API key
os.environ['ANTHROPIC_API_KEY'] = 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
def teardown_method(self):
"""Clean up test fixtures"""
import shutil
shutil.rmtree(self.temp_dir)
if 'ANTHROPIC_API_KEY' in os.environ:
del os.environ['ANTHROPIC_API_KEY']
def create_config_file(self, config_data: Dict[str, Any]) -> Path:
"""Create a configuration file"""
config_file = self.temp_dir / 'config.yaml'
with config_file.open('w') as f:
yaml.dump(config_data, f)
return config_file
def test_config_class_interface_unchanged(self):
"""Test that Config class interface remains unchanged"""
config_data = {
'obsidian': {
'vault_path': str(self.test_vault),
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key'
},
'claude': {
'api_key': '${ANTHROPIC_API_KEY}',
'model': 'claude-3-5-sonnet-20241022'
}
}
config_file = self.create_config_file(config_data)
config = Config(str(config_file))
# Test that all expected attributes exist and work as before
assert hasattr(config, 'obsidian')
assert hasattr(config, 'claude')
assert hasattr(config, 'journal')
assert hasattr(config, 'output')
assert hasattr(config, 'analysis')
assert hasattr(config, 'logging')
# Test that methods still work
assert callable(config.get_daily_note_path)
assert callable(config.to_dict)
# Test method functionality
daily_path = config.get_daily_note_path('2024-01-01')
assert '2024-01-01' in daily_path
config_dict = config.to_dict()
assert isinstance(config_dict, dict)
assert 'obsidian' in config_dict
assert 'claude' in config_dict
def test_config_attribute_access_unchanged(self):
"""Test that config attribute access patterns remain unchanged"""
config_data = {
'obsidian': {
'vault_path': str(self.test_vault),
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key'
},
'claude': {
'api_key': '${ANTHROPIC_API_KEY}',
'model': 'claude-3-5-sonnet-20241022',
'max_tokens': 4096,
'temperature': 0.7
}
}
config_file = self.create_config_file(config_data)
config = Config(str(config_file))
# Test that all legacy attribute access patterns still work
assert config.obsidian.vault_path == str(self.test_vault)
assert config.obsidian.rest_api_url == 'https://localhost:27123'
assert config.obsidian.rest_api_key == 'test-key'
assert config.claude.api_key == 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
assert config.claude.model == 'claude-3-5-sonnet-20241022'
assert config.claude.max_tokens == 4096
assert config.claude.temperature == 0.7
# Test new attributes are accessible
assert hasattr(config.claude, 'api_url')
assert config.claude.api_url == 'https://api.anthropic.com'
def test_config_validation_behavior_unchanged(self):
"""Test that config validation behavior remains the same for valid configs"""
valid_config = {
'obsidian': {
'vault_path': str(self.test_vault),
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key'
},
'claude': {
'api_key': '${ANTHROPIC_API_KEY}',
'model': 'claude-3-5-sonnet-20241022'
}
}
config_file = self.create_config_file(valid_config)
# Should load without errors (same as before)
config = Config(str(config_file))
assert config is not None
# Should still validate the same way
errors = config.get_validation_errors()
assert isinstance(errors, list)
def test_config_error_handling_unchanged(self):
"""Test that config error handling behavior remains unchanged"""
# Test with invalid vault path (should still raise appropriate error)
invalid_config = {
'obsidian': {
'vault_path': '/nonexistent/path',
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key'
},
'claude': {
'api_key': '${ANTHROPIC_API_KEY}',
'model': 'claude-3-5-sonnet-20241022'
}
}
config_file = self.create_config_file(invalid_config)
# Should still handle errors the same way (warnings, not exceptions for path issues)
config = Config(str(config_file))
errors = config.get_validation_errors()
assert len(errors) > 0 # Should have path validation warnings
class TestMigrationMessages:
"""Test that migration messages are appropriate and helpful"""
def setup_method(self):
"""Set up test fixtures"""
self.migrator = ConfigurationMigrator()
self.temp_dir = Path(tempfile.mkdtemp())
# Set test API key
os.environ['ANTHROPIC_API_KEY'] = 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
def teardown_method(self):
"""Clean up test fixtures"""
import shutil
shutil.rmtree(self.temp_dir)
if 'ANTHROPIC_API_KEY' in os.environ:
del os.environ['ANTHROPIC_API_KEY']
def test_migration_needed_detection_accurate(self):
"""Test that migration detection is accurate"""
# 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
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_migration_preview_messages_helpful(self):
"""Test that migration preview messages are helpful and informative"""
config_dict = {
'claude': {
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
'model': 'claude-3-sonnet' # Legacy model name
}
}
preview = self.migrator.get_migration_preview(config_dict)
assert len(preview) > 0
# Should have informative messages about what will be changed
preview_text = '\n'.join(preview)
assert 'api_url' in preview_text.lower()
assert 'claude-3-sonnet' in preview_text
assert 'claude-3-sonnet-20240229' in preview_text
# Messages should be user-friendly
assert any('will be added' in msg or 'will be migrated' in msg or 'will be updated' in msg for msg in preview)
def test_migration_messages_include_rationale(self):
"""Test that migration messages include rationale for changes"""
config_dict = {
'claude': {
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
# Missing api_url and using default model
}
}
preview = self.migrator.get_migration_preview(config_dict)
# Should explain why changes are being made
preview_text = '\n'.join(preview).lower()
assert 'default' in preview_text or 'missing' in preview_text
def test_migration_preserves_user_values(self):
"""Test that migration preserves user-specified values"""
config_dict = {
'claude': {
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
'model': 'claude-3-sonnet', # Legacy model name
'max_tokens': 8192, # User-specified value
'temperature': 0.3 # User-specified value
}
}
migrated = self.migrator.migrate_claude_config(config_dict)
# Should preserve user values
assert migrated['claude']['max_tokens'] == 8192
assert migrated['claude']['temperature'] == 0.3
# Should migrate legacy values
assert migrated['claude']['model'] == 'claude-3-sonnet-20240229'
# Should add missing defaults
assert migrated['claude']['api_url'] == 'https://api.anthropic.com'
def test_migration_handles_partial_configs(self):
"""Test that migration handles partial configurations gracefully"""
partial_configs = [
# Only obsidian config
{
'obsidian': {
'vault_path': '/test/vault',
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key'
}
},
# Only claude config
{
'claude': {
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
}
},
# Empty config
{}
]
for partial_config in partial_configs:
# Should not raise errors
migrated = self.migrator.migrate_configuration(partial_config)
# Should have all required sections
assert 'claude' in migrated
assert 'journal' in migrated
assert 'output' in migrated
assert 'analysis' in migrated
assert 'logging' in migrated
# Claude section should have all required fields
assert 'api_key' in migrated['claude']
assert 'api_url' in migrated['claude']
assert 'model' in migrated['claude']
def test_supported_model_names_comprehensive(self):
"""Test that supported model names list is comprehensive"""
supported_models = self.migrator.get_supported_model_names()
# Should include current models
current_models = [
'claude-3-5-sonnet-20241022',
'claude-3-5-haiku-20241022',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
'claude-3-haiku-20240307'
]
for model in current_models:
assert model in supported_models, f"Current model '{model}' should be in supported list"
# Should include legacy models
legacy_models = [
'claude-3-sonnet',
'claude-3-opus',
'claude-3-haiku',
'sonnet',
'opus',
'haiku'
]
for model in legacy_models:
assert model in supported_models, f"Legacy model '{model}' should be in supported list"
@patch('builtins.print')
def test_migration_logging_appropriate(self, mock_print):
"""Test that migration produces appropriate logging messages"""
config_dict = {
'claude': {
'api_key': 'sk-ant-test-key-12345678901234567890123456789012345678901234567890',
'model': 'claude-3-sonnet' # Legacy model name
}
}
# Perform migration
migrated = self.migrator.migrate_claude_config(config_dict)
# Should have logged the migration
assert mock_print.called
# Check that the log message is informative
log_calls = [call[0][0] for call in mock_print.call_args_list]
log_text = ' '.join(log_calls)
assert 'claude-3-sonnet' in log_text
assert 'claude-3-sonnet-20240229' in log_text
assert 'migrated' in log_text.lower() or 'updated' in log_text.lower()
class TestConfigurationIntegration:
"""Test full configuration integration with backward compatibility"""
def setup_method(self):
"""Set up test fixtures"""
self.temp_dir = Path(tempfile.mkdtemp())
self.test_vault = self.temp_dir / 'test_vault'
self.test_vault.mkdir()
(self.test_vault / '.obsidian').mkdir()
# Set test API key
os.environ['ANTHROPIC_API_KEY'] = 'sk-ant-test-key-12345678901234567890123456789012345678901234567890'
def teardown_method(self):
"""Clean up test fixtures"""
import shutil
shutil.rmtree(self.temp_dir)
if 'ANTHROPIC_API_KEY' in os.environ:
del os.environ['ANTHROPIC_API_KEY']
def create_config_file(self, config_data: Dict[str, Any]) -> Path:
"""Create a configuration file"""
config_file = self.temp_dir / 'config.yaml'
with config_file.open('w') as f:
yaml.dump(config_data, f)
return config_file
def test_end_to_end_legacy_config_loading(self):
"""Test end-to-end loading of legacy configuration"""
# Simulate a real legacy config file that a user might have
legacy_config = {
'obsidian': {
'vault_path': str(self.test_vault),
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key',
'verify_ssl': False
},
'claude': {
'api_key': '${ANTHROPIC_API_KEY}',
'model': 'sonnet', # Very legacy model name
'max_tokens': 2048
},
'journal': {
'daily_notes_folder': 'Daily Notes',
'date_format': 'YYYY-MM-DD'
}
}
config_file = self.create_config_file(legacy_config)
# Load through the full Config class (end-to-end test)
config = Config(str(config_file))
# Verify everything works as expected
assert config.claude.model == 'claude-3-5-sonnet-20241022' # Migrated from 'sonnet'
assert config.claude.api_url == 'https://api.anthropic.com' # Added default
assert config.claude.max_tokens == 2048 # Preserved user value
assert config.journal.daily_notes_folder == 'Daily Notes' # Preserved
# Test that methods still work
daily_path = config.get_daily_note_path('2024-01-01')
assert 'Daily Notes' in daily_path
assert '2024-01-01' in daily_path
# Test serialization still works
config_dict = config.to_dict()
assert config_dict['claude']['model'] == 'claude-3-5-sonnet-20241022'
assert 'api_key' not in config_dict['claude'] # Should be sanitized
def test_config_reload_preserves_migration(self):
"""Test that config reload preserves migration results"""
legacy_config = {
'obsidian': {
'vault_path': str(self.test_vault),
'rest_api_url': 'https://localhost:27123',
'rest_api_key': 'test-key'
},
'claude': {
'api_key': '${ANTHROPIC_API_KEY}',
'model': 'claude-3-opus' # Legacy model name
}
}
config_file = self.create_config_file(legacy_config)
config = Config(str(config_file))
# Verify initial migration
assert config.claude.model == 'claude-3-opus-20240229'
# Reload config
config.reload_config()
# Should still have migrated values
assert config.claude.model == 'claude-3-opus-20240229'
assert config.claude.api_url == 'https://api.anthropic.com'
if __name__ == "__main__":
# Run tests with pytest
pytest.main([__file__, "-v"])