#!/usr/bin/env python3 """ Verification script for Task 8.24: Session Persistence Checkpoint. Tests the SessionService against real Appwrite database to verify: 1. Solo session creation and storage 2. Session retrieval and ownership validation 3. Conversation history persistence 4. Game state tracking (location, quests, events) 5. Session lifecycle (create, update, end) Usage: python scripts/verify_session_persistence.py Requirements: - Appwrite configured in .env - game_sessions collection created in Appwrite - At least one character created for testing """ import sys import os # Add project root to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from dotenv import load_dotenv load_dotenv() from app.services.session_service import ( SessionService, SessionNotFound, SessionLimitExceeded, SessionValidationError, get_session_service, ) from app.services.character_service import get_character_service, CharacterNotFound from app.models.enums import SessionStatus, SessionType, LocationType from app.utils.logging import get_logger logger = get_logger(__file__) def print_header(text: str): """Print a section header.""" print(f"\n{'='*60}") print(f" {text}") print(f"{'='*60}\n") def print_result(test_name: str, passed: bool, details: str = ""): """Print test result.""" status = "✅ PASS" if passed else "❌ FAIL" print(f"{status}: {test_name}") if details: print(f" {details}") def verify_session_creation(service: SessionService, user_id: str, character_id: str) -> str: """ Test 1: Create a solo session and verify it's stored correctly. Returns session_id if successful. """ print_header("Test 1: Solo Session Creation") try: # Create session session = service.create_solo_session( user_id=user_id, character_id=character_id, starting_location="Test Town", starting_location_type=LocationType.TOWN ) # Verify fields checks = [ (session.session_type == SessionType.SOLO, "session_type is SOLO"), (session.solo_character_id == character_id, "solo_character_id matches"), (session.user_id == user_id, "user_id matches"), (session.turn_number == 0, "turn_number is 0"), (session.status == SessionStatus.ACTIVE, "status is ACTIVE"), (session.game_state.current_location == "Test Town", "current_location set"), (session.game_state.location_type == LocationType.TOWN, "location_type set"), ("Test Town" in session.game_state.discovered_locations, "location in discovered"), ] all_passed = True for passed, desc in checks: print_result(desc, passed) if not passed: all_passed = False if all_passed: print(f"\n Session ID: {session.session_id}") return session.session_id else: return None except Exception as e: print_result("Session creation", False, str(e)) return None def verify_session_retrieval(service: SessionService, session_id: str, user_id: str) -> bool: """ Test 2: Load session from database and verify data integrity. """ print_header("Test 2: Session Retrieval") try: # Load session session = service.get_session(session_id, user_id) checks = [ (session.session_id == session_id, "session_id matches"), (session.user_id == user_id, "user_id matches"), (session.session_type == SessionType.SOLO, "session_type preserved"), (session.status == SessionStatus.ACTIVE, "status preserved"), ] all_passed = True for passed, desc in checks: print_result(desc, passed) if not passed: all_passed = False # Test ownership validation try: service.get_session(session_id, "wrong_user_id") print_result("Ownership validation", False, "Should have raised SessionNotFound") all_passed = False except SessionNotFound: print_result("Ownership validation (wrong user rejected)", True) return all_passed except Exception as e: print_result("Session retrieval", False, str(e)) return False def verify_conversation_history(service: SessionService, session_id: str) -> bool: """ Test 3: Add conversation entries and verify persistence. """ print_header("Test 3: Conversation History") try: # Add first entry service.add_conversation_entry( session_id=session_id, character_id="char_test", character_name="Test Hero", action="I explore the town", dm_response="You find a bustling marketplace..." ) # Add second entry with quest service.add_conversation_entry( session_id=session_id, character_id="char_test", character_name="Test Hero", action="Talk to the merchant", dm_response="The merchant offers you a quest...", quest_offered={"quest_id": "test_quest", "name": "Test Quest"} ) # Retrieve and verify session = service.get_session(session_id) checks = [ (session.turn_number == 2, f"turn_number is 2 (got {session.turn_number})"), (len(session.conversation_history) == 2, f"2 entries in history (got {len(session.conversation_history)})"), (session.conversation_history[0].action == "I explore the town", "first action preserved"), (session.conversation_history[1].quest_offered is not None, "quest_offered preserved"), (session.conversation_history[0].timestamp != "", "timestamp auto-generated"), ] all_passed = True for passed, desc in checks: print_result(desc, passed) if not passed: all_passed = False # Test get_recent_history recent = service.get_recent_history(session_id, num_turns=1) check = len(recent) == 1 and recent[0].turn == 2 print_result("get_recent_history returns last entry", check) if not check: all_passed = False return all_passed except Exception as e: print_result("Conversation history", False, str(e)) return False def verify_game_state_tracking(service: SessionService, session_id: str) -> bool: """ Test 4: Test location, quest, and event tracking. """ print_header("Test 4: Game State Tracking") try: all_passed = True # Update location service.update_location( session_id=session_id, new_location="Dark Forest", location_type=LocationType.WILDERNESS ) session = service.get_session(session_id) check = session.game_state.current_location == "Dark Forest" print_result("Location updated", check, f"Got: {session.game_state.current_location}") if not check: all_passed = False check = session.game_state.location_type == LocationType.WILDERNESS print_result("Location type updated", check) if not check: all_passed = False check = "Dark Forest" in session.game_state.discovered_locations print_result("New location added to discovered", check) if not check: all_passed = False # Add quest service.add_active_quest(session_id, "quest_1") session = service.get_session(session_id) check = "quest_1" in session.game_state.active_quests print_result("Quest added to active_quests", check) if not check: all_passed = False # Add second quest service.add_active_quest(session_id, "quest_2") session = service.get_session(session_id) check = len(session.game_state.active_quests) == 2 print_result("Second quest added", check) if not check: all_passed = False # Try to add third quest (should fail) try: service.add_active_quest(session_id, "quest_3") print_result("Max quest limit enforced", False, "Should have raised error") all_passed = False except SessionValidationError: print_result("Max quest limit enforced (2/2)", True) # Remove quest service.remove_active_quest(session_id, "quest_1") session = service.get_session(session_id) check = "quest_1" not in session.game_state.active_quests print_result("Quest removed", check) if not check: all_passed = False # Add world event service.add_world_event(session_id, {"type": "storm", "description": "A storm approaches"}) session = service.get_session(session_id) check = len(session.game_state.world_events) == 1 print_result("World event added", check) if not check: all_passed = False check = "timestamp" in session.game_state.world_events[0] print_result("Event timestamp auto-added", check) if not check: all_passed = False return all_passed except Exception as e: print_result("Game state tracking", False, str(e)) return False def verify_session_lifecycle(service: SessionService, session_id: str, user_id: str) -> bool: """ Test 5: Test session ending and status changes. """ print_header("Test 5: Session Lifecycle") try: all_passed = True # End session session = service.end_session(session_id, user_id) check = session.status == SessionStatus.COMPLETED print_result("Session status set to COMPLETED", check, f"Got: {session.status}") if not check: all_passed = False # Verify persisted session = service.get_session(session_id) check = session.status == SessionStatus.COMPLETED print_result("Completed status persisted", check) if not check: all_passed = False # Verify it's not counted as active count = service.count_user_sessions(user_id, active_only=True) # Note: This might include other sessions, so just check it works print_result(f"count_user_sessions works (active: {count})", True) return all_passed except Exception as e: print_result("Session lifecycle", False, str(e)) return False def verify_error_handling(service: SessionService) -> bool: """ Test 6: Test error handling for invalid operations. """ print_header("Test 6: Error Handling") all_passed = True # Invalid session ID try: service.get_session("invalid_session_id_12345") print_result("Invalid session ID raises error", False) all_passed = False except SessionNotFound: print_result("Invalid session ID raises SessionNotFound", True) except Exception as e: print_result("Invalid session ID raises error", False, str(e)) all_passed = False return all_passed def main(): """Run all verification tests.""" print("\n" + "="*60) print(" Task 8.24: Session Persistence Verification") print("="*60) # Check environment if not os.getenv('APPWRITE_ENDPOINT'): print("\n❌ ERROR: Appwrite not configured. Set APPWRITE_* in .env") return False # Get test user and character print("\nSetup: Finding test character...") char_service = get_character_service() # Try to find an existing character for testing # You may need to adjust this based on your test data test_user_id = os.getenv('TEST_USER_ID', '') test_character_id = os.getenv('TEST_CHARACTER_ID', '') if not test_user_id or not test_character_id: print("\n⚠️ No TEST_USER_ID or TEST_CHARACTER_ID in .env") print(" Will attempt to use mock IDs for basic testing") print(" For full integration test, set these environment variables") # Use mock approach - only tests that don't need real DB print("\n" + "="*60) print(" Running Unit Test Verification Only") print("="*60) # Run the pytest tests instead import subprocess result = subprocess.run( ["python", "-m", "pytest", "tests/test_session_service.py", "-v", "--tb=short"], cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) return result.returncode == 0 # Initialize service service = get_session_service() # Run tests results = [] # Test 1: Create session session_id = verify_session_creation(service, test_user_id, test_character_id) results.append(("Session Creation", session_id is not None)) if session_id: # Test 2: Retrieve session results.append(("Session Retrieval", verify_session_retrieval(service, session_id, test_user_id))) # Test 3: Conversation history results.append(("Conversation History", verify_conversation_history(service, session_id))) # Test 4: Game state tracking results.append(("Game State Tracking", verify_game_state_tracking(service, session_id))) # Test 5: Session lifecycle results.append(("Session Lifecycle", verify_session_lifecycle(service, session_id, test_user_id))) # Test 6: Error handling results.append(("Error Handling", verify_error_handling(service))) # Summary print_header("Verification Summary") passed = sum(1 for _, p in results if p) total = len(results) for name, result in results: status = "✅" if result else "❌" print(f" {status} {name}") print(f"\n Total: {passed}/{total} tests passed") if passed == total: print("\n✅ All session persistence tests PASSED!") return True else: print("\n❌ Some tests FAILED. Check output above for details.") return False if __name__ == "__main__": success = main() sys.exit(0 if success else 1)