Files
journal_organizer/tests/integration/test_backward_compatibility.py
T
windyboy f7e54692a9 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
2025-12-31 17:55:10 +08:00

659 lines
25 KiB
Python

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