first commit
This commit is contained in:
428
api/scripts/verify_session_persistence.py
Executable file
428
api/scripts/verify_session_persistence.py
Executable file
@@ -0,0 +1,428 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user