""" 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"])