659 lines
25 KiB
Python
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"])
|