Files
journal_organizer/test_startup_validation.py
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

484 lines
18 KiB
Python

#!/usr/bin/env python3
"""
Application startup and basic functionality validation script.
Tests both v1.0 CLI and v2.0 conversational interfaces.
"""
import asyncio
import json
import logging
import os
import sys
import tempfile
import traceback
from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple
# Add the current directory to Python path for imports
sys.path.insert(0, str(Path(__file__).parent))
# Configure logging for validation
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("StartupValidation")
class ValidationResult:
"""Container for validation test results"""
def __init__(self, test_name: str):
self.test_name = test_name
self.success = False
self.message = ""
self.details: Dict[str, Any] = {}
self.error: Optional[Exception] = None
def set_success(self, message: str, details: Optional[Dict[str, Any]] = None):
self.success = True
self.message = message
self.details = details or {}
def set_failure(self, message: str, error: Optional[Exception] = None, details: Optional[Dict[str, Any]] = None):
self.success = False
self.message = message
self.error = error
self.details = details or {}
def __str__(self) -> str:
status = "✅ PASS" if self.success else "❌ FAIL"
return f"{status} {self.test_name}: {self.message}"
class StartupValidator:
"""Validates application startup and basic functionality"""
def __init__(self):
self.results: List[ValidationResult] = []
self.temp_config_path: Optional[Path] = None
def create_test_config(self) -> Path:
"""Create a temporary test configuration file"""
config_content = {
'obsidian': {
'vault_path': '/tmp/test_vault',
'rest_api': {
'url': 'https://localhost:27123',
'api_key': 'test-api-key',
'verify_ssl': False
}
},
'claude': {
'api_key': '${ANTHROPIC_API_KEY}',
'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'
},
'logging': {
'level': 'INFO',
'file': 'logs/journal_organizer.log'
}
}
# Create temporary config file
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
json.dump(config_content, temp_file, indent=2)
temp_file.close()
self.temp_config_path = Path(temp_file.name)
return self.temp_config_path
def cleanup_test_config(self):
"""Clean up temporary test configuration"""
if self.temp_config_path and self.temp_config_path.exists():
self.temp_config_path.unlink()
async def test_import_main_modules(self) -> ValidationResult:
"""Test importing main application modules"""
result = ValidationResult("Import Main Modules")
try:
# Test importing core modules
modules_tested = []
# Test core modules
try:
import main
modules_tested.append("main")
except ImportError as e:
logger.warning(f"Could not import main: {e}")
try:
import chat_main
modules_tested.append("chat_main")
except ImportError as e:
logger.warning(f"Could not import chat_main: {e}")
try:
import agent_core
modules_tested.append("agent_core")
except ImportError as e:
logger.warning(f"Could not import agent_core: {e}")
try:
import config
modules_tested.append("config")
except ImportError as e:
logger.warning(f"Could not import config: {e}")
# Test command modules
try:
from commands import organize_command
modules_tested.append("commands.organize_command")
except ImportError as e:
logger.warning(f"Could not import commands.organize_command: {e}")
# Test skill modules
try:
from skills import obsidian_skill
modules_tested.append("skills.obsidian_skill")
except ImportError as e:
logger.warning(f"Could not import skills.obsidian_skill: {e}")
try:
from skills import claude_skill
modules_tested.append("skills.claude_skill")
except ImportError as e:
logger.warning(f"Could not import skills.claude_skill: {e}")
# Test conversation modules
try:
from conversation import conversational_agent
modules_tested.append("conversation.conversational_agent")
except ImportError as e:
logger.warning(f"Could not import conversation.conversational_agent: {e}")
try:
from conversation import conversation_state
modules_tested.append("conversation.conversation_state")
except ImportError as e:
logger.warning(f"Could not import conversation.conversation_state: {e}")
if len(modules_tested) >= 4: # At least core modules should work
result.set_success(f"Successfully imported {len(modules_tested)} modules", {
"modules_tested": modules_tested,
"total_attempted": 9
})
else:
result.set_failure(f"Only imported {len(modules_tested)} out of 9 modules", details={
"modules_tested": modules_tested
})
except Exception as e:
result.set_failure(f"Failed to import modules: {str(e)}", e)
return result
async def test_agent_initialization(self) -> ValidationResult:
"""Test basic agent initialization"""
result = ValidationResult("Agent Initialization")
try:
# Try to import and test agent initialization
try:
from main import JournalOrganizerAgent
except ImportError:
# If relative import fails, skip this test
result.set_failure("Could not import JournalOrganizerAgent - likely due to package structure")
return result
# Create test config
config_path = self.create_test_config()
# Initialize agent
agent = JournalOrganizerAgent(str(config_path))
# Test basic properties
assert hasattr(agent, 'config'), "Agent should have config attribute"
assert hasattr(agent, 'agent'), "Agent should have agent attribute"
assert hasattr(agent, 'logger'), "Agent should have logger attribute"
# Test command listing
commands = agent.list_commands()
assert isinstance(commands, list), "Commands should be a list"
assert len(commands) > 0, "Should have at least one command"
result.set_success("Agent initialized successfully", {
"available_commands": commands,
"config_loaded": bool(agent.config)
})
except Exception as e:
result.set_failure(f"Agent initialization failed: {str(e)}", e)
return result
async def test_configuration_loading(self) -> ValidationResult:
"""Test configuration loading with various scenarios"""
result = ValidationResult("Configuration Loading")
try:
from main import JournalOrganizerAgent
test_results = {}
# Test 1: Load from specific config file
config_path = self.create_test_config()
agent1 = JournalOrganizerAgent(str(config_path))
test_results["specific_config"] = bool(agent1.config)
# Test 2: Load with no config file (should use defaults)
agent2 = JournalOrganizerAgent(None)
test_results["default_config"] = isinstance(agent2.config, dict)
# Test 3: Load with non-existent config file (should use defaults)
agent3 = JournalOrganizerAgent("/non/existent/config.yaml")
test_results["fallback_config"] = isinstance(agent3.config, dict)
result.set_success("Configuration loading works correctly", test_results)
except Exception as e:
result.set_failure(f"Configuration loading failed: {str(e)}", e)
return result
async def test_command_registration(self) -> ValidationResult:
"""Test command registration and basic info retrieval"""
result = ValidationResult("Command Registration")
try:
from main import JournalOrganizerAgent
config_path = self.create_test_config()
agent = JournalOrganizerAgent(str(config_path))
# Test command listing
commands = agent.list_commands()
assert "organize" in commands, "Should have 'organize' command"
# Test command info retrieval
organize_info = agent.get_command_info("organize")
assert isinstance(organize_info, dict), "Command info should be a dict"
assert "name" in organize_info, "Command info should have name"
# Test all commands info
all_info = agent.get_all_commands_info()
assert isinstance(all_info, dict), "All commands info should be a dict"
result.set_success("Command registration working correctly", {
"registered_commands": commands,
"organize_command_info": bool(organize_info),
"all_commands_info": bool(all_info)
})
except Exception as e:
result.set_failure(f"Command registration failed: {str(e)}", e)
return result
async def test_conversational_agent_init(self) -> ValidationResult:
"""Test conversational agent initialization"""
result = ValidationResult("Conversational Agent Initialization")
try:
from main import JournalOrganizerAgent
from conversation import ConversationalAgent
config_path = self.create_test_config()
journal_agent = JournalOrganizerAgent(str(config_path))
# Initialize conversational agent
conv_agent = ConversationalAgent(journal_agent.agent, journal_agent.config)
# Test basic properties
assert hasattr(conv_agent, 'agent'), "Should have agent attribute"
assert hasattr(conv_agent, 'config'), "Should have config attribute"
# Test initialization
welcome_msg = await conv_agent.initialize()
assert isinstance(welcome_msg, str), "Welcome message should be a string"
assert len(welcome_msg) > 0, "Welcome message should not be empty"
result.set_success("Conversational agent initialized successfully", {
"welcome_message_length": len(welcome_msg),
"has_required_attributes": True
})
except Exception as e:
result.set_failure(f"Conversational agent initialization failed: {str(e)}", e)
return result
async def test_dependency_management(self) -> ValidationResult:
"""Test dependency management and graceful degradation"""
result = ValidationResult("Dependency Management")
try:
from dependency_manager import get_dependency_manager
dep_manager = get_dependency_manager()
# Test dependency status
status_report = dep_manager.get_dependency_status_report()
assert isinstance(status_report, str), "Status report should be a string"
# Test missing dependencies check
missing_deps = dep_manager.get_missing_dependencies()
assert isinstance(missing_deps, list), "Missing deps should be a list"
# Test module retrieval (should work even if module is missing)
yaml_module = dep_manager.get_module('yaml')
# yaml_module could be None if not installed, which is fine
result.set_success("Dependency management working correctly", {
"status_report_generated": bool(status_report),
"missing_dependencies_count": len(missing_deps),
"yaml_module_available": yaml_module is not None
})
except Exception as e:
result.set_failure(f"Dependency management failed: {str(e)}", e)
return result
async def test_error_handling_framework(self) -> ValidationResult:
"""Test error handling framework"""
result = ValidationResult("Error Handling Framework")
try:
from error_handling import (
JournalOrganizerError, ConfigurationError,
APIError, ValidationError, ErrorHandler
)
# Test custom exceptions
test_exceptions = [
JournalOrganizerError("test"),
ConfigurationError("test config error"),
APIError("test api error"),
ValidationError("test validation error")
]
for exc in test_exceptions:
assert isinstance(exc, Exception), f"{type(exc).__name__} should be an Exception"
assert str(exc), f"{type(exc).__name__} should have string representation"
# Test ErrorHandler
import logging
logger = logging.getLogger("test")
error_handler = ErrorHandler(logger)
assert hasattr(error_handler, 'handle_api_error'), "Should have handle_api_error method"
assert hasattr(error_handler, 'handle_validation_error'), "Should have handle_validation_error method"
result.set_success("Error handling framework working correctly", {
"custom_exceptions_count": len(test_exceptions),
"error_handler_methods": ["handle_api_error", "handle_validation_error"]
})
except Exception as e:
result.set_failure(f"Error handling framework test failed: {str(e)}", e)
return result
async def run_all_tests(self) -> List[ValidationResult]:
"""Run all validation tests"""
logger.info("🚀 Starting application startup validation...")
test_methods = [
self.test_import_main_modules,
self.test_agent_initialization,
self.test_configuration_loading,
self.test_command_registration,
self.test_conversational_agent_init,
self.test_dependency_management,
self.test_error_handling_framework
]
for test_method in test_methods:
try:
logger.info(f"Running {test_method.__name__}...")
result = await test_method()
self.results.append(result)
logger.info(str(result))
except Exception as e:
error_result = ValidationResult(test_method.__name__)
error_result.set_failure(f"Test execution failed: {str(e)}", e)
self.results.append(error_result)
logger.error(str(error_result))
# Cleanup
self.cleanup_test_config()
return self.results
def generate_report(self) -> str:
"""Generate a comprehensive validation report"""
total_tests = len(self.results)
passed_tests = sum(1 for r in self.results if r.success)
failed_tests = total_tests - passed_tests
report = f"""
{'='*60}
APPLICATION STARTUP VALIDATION REPORT
{'='*60}
Summary:
Total Tests: {total_tests}
Passed: {passed_tests}
Failed: {failed_tests}
Success Rate: {(passed_tests/total_tests*100):.1f}%
Test Results:
"""
for result in self.results:
report += f"\n{str(result)}"
if result.details:
for key, value in result.details.items():
report += f"\n - {key}: {value}"
if not result.success and result.error:
report += f"\n - Error: {str(result.error)}"
report += f"\n\n{'='*60}\n"
return report
async def main():
"""Main validation function"""
validator = StartupValidator()
try:
results = await validator.run_all_tests()
report = validator.generate_report()
print(report)
# Return appropriate exit code
failed_count = sum(1 for r in results if not r.success)
return 0 if failed_count == 0 else 1
except Exception as e:
logger.error(f"Validation failed with unexpected error: {str(e)}")
traceback.print_exc()
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)