first commit
This commit is contained in:
390
api/tests/test_session_model.py
Normal file
390
api/tests/test_session_model.py
Normal file
@@ -0,0 +1,390 @@
|
||||
"""
|
||||
Tests for the GameSession model with solo play support.
|
||||
|
||||
Tests cover:
|
||||
- Solo session creation and serialization
|
||||
- Multiplayer session creation and serialization
|
||||
- ConversationEntry with timestamps
|
||||
- GameState with location_type
|
||||
- Session type detection
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
|
||||
from app.models.session import (
|
||||
GameSession,
|
||||
GameState,
|
||||
ConversationEntry,
|
||||
SessionConfig,
|
||||
)
|
||||
from app.models.enums import (
|
||||
SessionStatus,
|
||||
SessionType,
|
||||
LocationType,
|
||||
)
|
||||
|
||||
|
||||
class TestGameState:
|
||||
"""Tests for GameState dataclass."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test GameState has correct defaults."""
|
||||
state = GameState()
|
||||
assert state.current_location == "Crossroads Village"
|
||||
assert state.location_type == LocationType.TOWN
|
||||
assert state.discovered_locations == []
|
||||
assert state.active_quests == []
|
||||
assert state.world_events == []
|
||||
|
||||
def test_to_dict_serializes_location_type(self):
|
||||
"""Test location_type is serialized as string."""
|
||||
state = GameState(
|
||||
current_location="The Rusty Anchor",
|
||||
location_type=LocationType.TAVERN,
|
||||
)
|
||||
data = state.to_dict()
|
||||
assert data["current_location"] == "The Rusty Anchor"
|
||||
assert data["location_type"] == "tavern"
|
||||
|
||||
def test_from_dict_deserializes_location_type(self):
|
||||
"""Test location_type is deserialized from string."""
|
||||
data = {
|
||||
"current_location": "Dark Forest",
|
||||
"location_type": "wilderness",
|
||||
"discovered_locations": ["Town A"],
|
||||
"active_quests": ["quest_1"],
|
||||
"world_events": [],
|
||||
}
|
||||
state = GameState.from_dict(data)
|
||||
assert state.current_location == "Dark Forest"
|
||||
assert state.location_type == LocationType.WILDERNESS
|
||||
assert state.discovered_locations == ["Town A"]
|
||||
|
||||
def test_roundtrip_serialization(self):
|
||||
"""Test GameState serializes and deserializes correctly."""
|
||||
state = GameState(
|
||||
current_location="Ancient Library",
|
||||
location_type=LocationType.LIBRARY,
|
||||
discovered_locations=["Town", "Forest"],
|
||||
active_quests=["quest_1", "quest_2"],
|
||||
world_events=[{"type": "festival"}],
|
||||
)
|
||||
data = state.to_dict()
|
||||
restored = GameState.from_dict(data)
|
||||
|
||||
assert restored.current_location == state.current_location
|
||||
assert restored.location_type == state.location_type
|
||||
assert restored.discovered_locations == state.discovered_locations
|
||||
assert restored.active_quests == state.active_quests
|
||||
|
||||
|
||||
class TestConversationEntry:
|
||||
"""Tests for ConversationEntry dataclass."""
|
||||
|
||||
def test_auto_timestamp(self):
|
||||
"""Test timestamp is auto-generated."""
|
||||
entry = ConversationEntry(
|
||||
turn=1,
|
||||
character_id="char_123",
|
||||
character_name="Hero",
|
||||
action="I explore",
|
||||
dm_response="You find a chest",
|
||||
)
|
||||
assert entry.timestamp
|
||||
assert entry.timestamp.endswith("Z")
|
||||
|
||||
def test_provided_timestamp_preserved(self):
|
||||
"""Test provided timestamp is not overwritten."""
|
||||
ts = "2025-11-21T10:30:00Z"
|
||||
entry = ConversationEntry(
|
||||
turn=1,
|
||||
character_id="char_123",
|
||||
character_name="Hero",
|
||||
action="I explore",
|
||||
dm_response="You find a chest",
|
||||
timestamp=ts,
|
||||
)
|
||||
assert entry.timestamp == ts
|
||||
|
||||
def test_to_dict_with_quest_offered(self):
|
||||
"""Test serialization includes quest_offered when present."""
|
||||
entry = ConversationEntry(
|
||||
turn=5,
|
||||
character_id="char_123",
|
||||
character_name="Hero",
|
||||
action="Talk to elder",
|
||||
dm_response="The elder offers you a quest",
|
||||
quest_offered={
|
||||
"quest_id": "quest_goblin_cave",
|
||||
"quest_name": "Clear the Goblin Cave",
|
||||
},
|
||||
)
|
||||
data = entry.to_dict()
|
||||
assert "quest_offered" in data
|
||||
assert data["quest_offered"]["quest_id"] == "quest_goblin_cave"
|
||||
|
||||
def test_to_dict_without_quest_offered(self):
|
||||
"""Test serialization omits quest_offered when None."""
|
||||
entry = ConversationEntry(
|
||||
turn=1,
|
||||
character_id="char_123",
|
||||
character_name="Hero",
|
||||
action="I explore",
|
||||
dm_response="You find nothing",
|
||||
)
|
||||
data = entry.to_dict()
|
||||
assert "quest_offered" not in data
|
||||
|
||||
def test_from_dict_roundtrip(self):
|
||||
"""Test ConversationEntry roundtrip serialization."""
|
||||
entry = ConversationEntry(
|
||||
turn=3,
|
||||
character_id="char_456",
|
||||
character_name="Wizard",
|
||||
action="Cast fireball",
|
||||
dm_response="The spell illuminates the cave",
|
||||
combat_log=[{"action": "attack", "damage": 15}],
|
||||
quest_offered={"quest_id": "q1"},
|
||||
)
|
||||
data = entry.to_dict()
|
||||
restored = ConversationEntry.from_dict(data)
|
||||
|
||||
assert restored.turn == entry.turn
|
||||
assert restored.character_id == entry.character_id
|
||||
assert restored.action == entry.action
|
||||
assert restored.dm_response == entry.dm_response
|
||||
assert restored.timestamp == entry.timestamp
|
||||
assert restored.combat_log == entry.combat_log
|
||||
assert restored.quest_offered == entry.quest_offered
|
||||
|
||||
|
||||
class TestGameSessionSolo:
|
||||
"""Tests for solo GameSession functionality."""
|
||||
|
||||
def test_create_solo_session(self):
|
||||
"""Test creating a solo session."""
|
||||
session = GameSession(
|
||||
session_id="sess_123",
|
||||
session_type=SessionType.SOLO,
|
||||
solo_character_id="char_456",
|
||||
user_id="user_789",
|
||||
)
|
||||
assert session.session_type == SessionType.SOLO
|
||||
assert session.solo_character_id == "char_456"
|
||||
assert session.user_id == "user_789"
|
||||
assert session.is_solo() is True
|
||||
|
||||
def test_is_solo_method(self):
|
||||
"""Test is_solo returns correct values."""
|
||||
solo = GameSession(
|
||||
session_id="s1",
|
||||
session_type=SessionType.SOLO,
|
||||
solo_character_id="c1",
|
||||
)
|
||||
multi = GameSession(
|
||||
session_id="s2",
|
||||
session_type=SessionType.MULTIPLAYER,
|
||||
party_member_ids=["c1", "c2"],
|
||||
)
|
||||
assert solo.is_solo() is True
|
||||
assert multi.is_solo() is False
|
||||
|
||||
def test_get_character_id_solo(self):
|
||||
"""Test get_character_id returns solo_character_id for solo sessions."""
|
||||
session = GameSession(
|
||||
session_id="sess_123",
|
||||
session_type=SessionType.SOLO,
|
||||
solo_character_id="char_456",
|
||||
)
|
||||
assert session.get_character_id() == "char_456"
|
||||
|
||||
def test_get_character_id_multiplayer(self):
|
||||
"""Test get_character_id returns current turn character for multiplayer."""
|
||||
session = GameSession(
|
||||
session_id="sess_123",
|
||||
session_type=SessionType.MULTIPLAYER,
|
||||
party_member_ids=["c1", "c2", "c3"],
|
||||
turn_order=["c2", "c1", "c3"],
|
||||
current_turn=1,
|
||||
)
|
||||
assert session.get_character_id() == "c1"
|
||||
|
||||
def test_to_dict_includes_new_fields(self):
|
||||
"""Test to_dict includes session_type, solo_character_id, user_id."""
|
||||
session = GameSession(
|
||||
session_id="sess_123",
|
||||
session_type=SessionType.SOLO,
|
||||
solo_character_id="char_456",
|
||||
user_id="user_789",
|
||||
)
|
||||
data = session.to_dict()
|
||||
|
||||
assert data["session_id"] == "sess_123"
|
||||
assert data["session_type"] == "solo"
|
||||
assert data["solo_character_id"] == "char_456"
|
||||
assert data["user_id"] == "user_789"
|
||||
|
||||
def test_from_dict_solo_session(self):
|
||||
"""Test from_dict correctly deserializes solo session."""
|
||||
data = {
|
||||
"session_id": "sess_123",
|
||||
"session_type": "solo",
|
||||
"solo_character_id": "char_456",
|
||||
"user_id": "user_789",
|
||||
"party_member_ids": [],
|
||||
"turn_number": 5,
|
||||
"game_state": {
|
||||
"current_location": "Town",
|
||||
"location_type": "town",
|
||||
},
|
||||
}
|
||||
session = GameSession.from_dict(data)
|
||||
|
||||
assert session.session_id == "sess_123"
|
||||
assert session.session_type == SessionType.SOLO
|
||||
assert session.solo_character_id == "char_456"
|
||||
assert session.user_id == "user_789"
|
||||
assert session.turn_number == 5
|
||||
assert session.game_state.location_type == LocationType.TOWN
|
||||
|
||||
def test_roundtrip_serialization(self):
|
||||
"""Test complete roundtrip of solo session."""
|
||||
session = GameSession(
|
||||
session_id="sess_test",
|
||||
session_type=SessionType.SOLO,
|
||||
solo_character_id="char_hero",
|
||||
user_id="user_player",
|
||||
turn_number=10,
|
||||
game_state=GameState(
|
||||
current_location="Dark Dungeon",
|
||||
location_type=LocationType.DUNGEON,
|
||||
active_quests=["quest_1"],
|
||||
),
|
||||
conversation_history=[
|
||||
ConversationEntry(
|
||||
turn=1,
|
||||
character_id="char_hero",
|
||||
character_name="Hero",
|
||||
action="Enter dungeon",
|
||||
dm_response="The darkness swallows you...",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
data = session.to_dict()
|
||||
restored = GameSession.from_dict(data)
|
||||
|
||||
assert restored.session_id == session.session_id
|
||||
assert restored.session_type == session.session_type
|
||||
assert restored.solo_character_id == session.solo_character_id
|
||||
assert restored.user_id == session.user_id
|
||||
assert restored.turn_number == session.turn_number
|
||||
assert restored.game_state.current_location == session.game_state.current_location
|
||||
assert restored.game_state.location_type == session.game_state.location_type
|
||||
assert len(restored.conversation_history) == 1
|
||||
assert restored.conversation_history[0].action == "Enter dungeon"
|
||||
|
||||
def test_repr_solo(self):
|
||||
"""Test __repr__ for solo session."""
|
||||
session = GameSession(
|
||||
session_id="sess_123",
|
||||
session_type=SessionType.SOLO,
|
||||
solo_character_id="char_456",
|
||||
turn_number=5,
|
||||
)
|
||||
repr_str = repr(session)
|
||||
assert "type=solo" in repr_str
|
||||
assert "char=char_456" in repr_str
|
||||
assert "turn=5" in repr_str
|
||||
|
||||
def test_repr_multiplayer(self):
|
||||
"""Test __repr__ for multiplayer session."""
|
||||
session = GameSession(
|
||||
session_id="sess_123",
|
||||
session_type=SessionType.MULTIPLAYER,
|
||||
party_member_ids=["c1", "c2", "c3"],
|
||||
turn_number=10,
|
||||
)
|
||||
repr_str = repr(session)
|
||||
assert "type=multiplayer" in repr_str
|
||||
assert "party=3" in repr_str
|
||||
assert "turn=10" in repr_str
|
||||
|
||||
|
||||
class TestGameSessionBackwardsCompatibility:
|
||||
"""Tests for backwards compatibility with existing sessions."""
|
||||
|
||||
def test_default_session_type_is_solo(self):
|
||||
"""Test new sessions default to solo type."""
|
||||
session = GameSession(session_id="test")
|
||||
assert session.session_type == SessionType.SOLO
|
||||
|
||||
def test_from_dict_without_session_type(self):
|
||||
"""Test from_dict handles missing session_type (defaults to solo)."""
|
||||
data = {
|
||||
"session_id": "old_session",
|
||||
"party_member_ids": ["c1"],
|
||||
}
|
||||
session = GameSession.from_dict(data)
|
||||
assert session.session_type == SessionType.SOLO
|
||||
|
||||
def test_from_dict_without_location_type(self):
|
||||
"""Test from_dict handles missing location_type in game_state."""
|
||||
data = {
|
||||
"session_id": "old_session",
|
||||
"game_state": {
|
||||
"current_location": "Old Town",
|
||||
},
|
||||
}
|
||||
session = GameSession.from_dict(data)
|
||||
assert session.game_state.location_type == LocationType.TOWN
|
||||
|
||||
def test_existing_methods_still_work(self):
|
||||
"""Test existing session methods work with new fields."""
|
||||
session = GameSession(
|
||||
session_id="test",
|
||||
session_type=SessionType.SOLO,
|
||||
solo_character_id="char_1",
|
||||
)
|
||||
|
||||
# Test existing methods
|
||||
assert session.is_in_combat() is False
|
||||
session.update_activity()
|
||||
assert session.last_activity
|
||||
|
||||
# Add conversation entry
|
||||
entry = ConversationEntry(
|
||||
turn=1,
|
||||
character_id="char_1",
|
||||
character_name="Hero",
|
||||
action="test",
|
||||
dm_response="response",
|
||||
)
|
||||
session.add_conversation_entry(entry)
|
||||
assert len(session.conversation_history) == 1
|
||||
|
||||
|
||||
class TestLocationTypeEnum:
|
||||
"""Tests for LocationType enum."""
|
||||
|
||||
def test_all_location_types_defined(self):
|
||||
"""Test all expected location types exist."""
|
||||
expected = ["town", "tavern", "wilderness", "dungeon", "ruins", "library", "safe_area"]
|
||||
actual = [lt.value for lt in LocationType]
|
||||
assert sorted(actual) == sorted(expected)
|
||||
|
||||
def test_location_type_from_string(self):
|
||||
"""Test LocationType can be created from string."""
|
||||
assert LocationType("town") == LocationType.TOWN
|
||||
assert LocationType("wilderness") == LocationType.WILDERNESS
|
||||
assert LocationType("dungeon") == LocationType.DUNGEON
|
||||
|
||||
|
||||
class TestSessionTypeEnum:
|
||||
"""Tests for SessionType enum."""
|
||||
|
||||
def test_session_types_defined(self):
|
||||
"""Test session types are defined correctly."""
|
||||
assert SessionType.SOLO.value == "solo"
|
||||
assert SessionType.MULTIPLAYER.value == "multiplayer"
|
||||
Reference in New Issue
Block a user