"""Unit tests for NPC chat history and serialization features.""" import json import pytest from unittest.mock import Mock, AsyncMock, patch from gumyum_npc_client import ( NPC, SpawnedData, GumYumClient, NPCChatProxy, ChatCompletion, ) from gumyum_npc_sync import NPC as SyncNPC, GumYumClient as SyncGumYumClient class TestNPCChatHistory: """Test chat history tracking features.""" def setup_method(self): """Set up test NPC.""" self.spawned_data = SpawnedData( location="market", mood="friendly", stress_level=3 ) self.test_npc = NPC( npc_id=123456789, name="Test Merchant", profession="merchant", personality_type=7, universe_id="test-universe", seed=42, spawned=self.spawned_data, cached=False, ) # Create mock client self.mock_client = Mock(spec=GumYumClient) self.test_npc.client = self.mock_client def test_chat_history_starts_empty(self): """Test that chat history starts empty.""" assert len(self.test_npc.chat_history) == 0 def test_chat_history_tracking(self): """Test manual chat history tracking.""" # Manually add messages self.test_npc.chat_history.append({"role": "user", "content": "Hello there!"}) self.test_npc.chat_history.append( {"role": "assistant", "content": "Greetings, traveler!"} ) assert len(self.test_npc.chat_history) == 2 assert self.test_npc.chat_history[0]["role"] == "user" assert self.test_npc.chat_history[1]["content"] == "Greetings, traveler!" def test_clear_history(self): """Test clearing chat history.""" # Add some messages self.test_npc.chat_history.extend( [ {"role": "user", "content": "Test 1"}, {"role": "assistant", "content": "Response 1"}, ] ) # Clear history self.test_npc.clear_history() assert len(self.test_npc.chat_history) == 0 @pytest.mark.asyncio async def test_chat_history_auto_tracking(self): """Test automatic history tracking during chat.""" # Create mock response mock_message = Mock() mock_message.role = "assistant" mock_message.content = "I sell the finest weapons!" mock_choice = Mock() mock_choice.message = mock_message mock_response = Mock(spec=ChatCompletion) mock_response.choices = [mock_choice] # Configure mock client to return our response self.mock_client.chat = AsyncMock() self.mock_client.chat.completions = AsyncMock(return_value=mock_response) # Call completions messages = [{"role": "user", "content": "What do you sell?"}] response = await self.test_npc.chat.completions(messages, stream=False) # Check history was updated assert len(self.test_npc.chat_history) == 2 assert self.test_npc.chat_history[0]["content"] == "What do you sell?" assert self.test_npc.chat_history[1]["content"] == "I sell the finest weapons!" @pytest.mark.asyncio async def test_chat_with_history(self): """Test chat_with_history method.""" # Pre-populate history self.test_npc.chat_history.extend( [ {"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Welcome!"}, ] ) # Mock completions to capture the messages sent captured_messages = None async def mock_completions(messages, **kwargs): nonlocal captured_messages captured_messages = messages return Mock() # Replace completions method self.test_npc.chat.completions = mock_completions # Call chat_with_history await self.test_npc.chat.chat_with_history("Tell me more") # Verify it included history assert captured_messages is not None assert len(captured_messages) == 3 assert captured_messages[0]["content"] == "Hello" assert captured_messages[1]["content"] == "Welcome!" assert captured_messages[2]["content"] == "Tell me more" class TestNPCSerialization: """Test NPC serialization features.""" def setup_method(self): """Set up test NPC with chat history.""" self.spawned_data = SpawnedData(location="castle", mood="stern", stress_level=5) self.test_npc = NPC( npc_id=987654321, name="Guard Captain", profession="guard", personality_type=1, universe_id="kingdom", seed=100, spawned=self.spawned_data, cached=True, cache_url="https://example.com/cache", ) # Add chat history self.test_npc.chat_history.extend( [ {"role": "user", "content": "Who goes there?"}, {"role": "assistant", "content": "State your business!"}, ] ) self.mock_client = Mock(spec=GumYumClient) self.test_npc.client = self.mock_client def test_chat_history_to_json(self): """Test converting chat history to JSON.""" json_str = self.test_npc.chat_history_to_json() assert json_str is not None # Parse it back parsed = json.loads(json_str) assert len(parsed) == 2 assert parsed[0]["content"] == "Who goes there?" def test_to_dict(self): """Test converting NPC to dictionary.""" npc_dict = self.test_npc.to_dict() # Check all fields are present assert npc_dict["npc_id"] == 987654321 assert npc_dict["name"] == "Guard Captain" assert npc_dict["profession"] == "guard" assert npc_dict["personality_type"] == 1 assert npc_dict["universe_id"] == "kingdom" assert npc_dict["seed"] == 100 assert npc_dict["cached"] == True assert npc_dict["cache_url"] == "https://example.com/cache" # Check spawned data assert npc_dict["spawned"]["location"] == "castle" assert npc_dict["spawned"]["mood"] == "stern" # Check chat history assert len(npc_dict["chat_history"]) == 2 assert npc_dict["chat_history"][0]["content"] == "Who goes there?" def test_to_json(self): """Test converting NPC to JSON.""" json_str = self.test_npc.to_json() assert json_str is not None # Parse it back parsed = json.loads(json_str) assert parsed["name"] == "Guard Captain" assert len(parsed["chat_history"]) == 2 def test_from_dict(self): """Test creating NPC from dictionary.""" test_data = { "npc_id": 111222333, "name": "Wise Elder", "profession": "sage", "personality_type": 5, "universe_id": "fantasy", "seed": 777, "spawned": { "location": "temple", "mood": "contemplative", "stress_level": 2, }, "cached": False, "chat_history": [ {"role": "user", "content": "What wisdom do you have?"}, {"role": "assistant", "content": "Patience is the key..."}, ], } loaded_npc = NPC.from_dict(test_data, self.mock_client) assert loaded_npc.npc_id == 111222333 assert loaded_npc.name == "Wise Elder" assert loaded_npc.profession == "sage" assert loaded_npc.universe_id == "fantasy" assert loaded_npc.client == self.mock_client # Check spawned data assert loaded_npc.spawned.location == "temple" assert loaded_npc.spawned.mood == "contemplative" # Check chat history assert len(loaded_npc.chat_history) == 2 assert loaded_npc.chat_history[0]["content"] == "What wisdom do you have?" def test_from_json(self): """Test creating NPC from JSON.""" json_data = { "npc_id": 444555666, "name": "Blacksmith", "profession": "blacksmith", "personality_type": 8, "universe_id": "medieval", "seed": 200, "spawned": {"location": "forge", "mood": "focused", "stress_level": 4}, "cached": False, "chat_history": [], } json_str = json.dumps(json_data) loaded_npc = NPC.from_json(json_str, self.mock_client) assert loaded_npc.name == "Blacksmith" assert loaded_npc.spawned.location == "forge" assert len(loaded_npc.chat_history) == 0 def test_from_json_invalid(self): """Test handling invalid JSON.""" invalid_json = "{ invalid json [" with pytest.raises(ValueError) as exc_info: NPC.from_json(invalid_json, self.mock_client) assert "Failed to parse NPC JSON" in str(exc_info.value) def test_round_trip_serialization(self): """Test converting to JSON and back preserves data.""" # Convert to JSON json_str = self.test_npc.to_json() # Load back restored_npc = NPC.from_json(json_str, self.mock_client) # Compare assert restored_npc.npc_id == self.test_npc.npc_id assert restored_npc.name == self.test_npc.name assert restored_npc.profession == self.test_npc.profession assert len(restored_npc.chat_history) == len(self.test_npc.chat_history) assert ( restored_npc.chat_history[0]["content"] == self.test_npc.chat_history[0]["content"] ) # Check client was set assert restored_npc.client == self.mock_client class TestSyncNPCFeatures: """Test sync version of NPC features.""" def setup_method(self): """Set up sync test NPC.""" self.spawned_data = SpawnedData(location="tavern", mood="jolly", stress_level=1) self.test_npc = SyncNPC( npc_id=555666777, name="Innkeeper", profession="innkeeper", personality_type=2, universe_id="fantasy-town", seed=300, spawned=self.spawned_data, cached=False, ) # Create mock sync client self.mock_client = Mock(spec=SyncGumYumClient) self.test_npc.client = self.mock_client def test_sync_chat_history_tracking(self): """Test sync NPC history tracking.""" # Create mock response mock_message = Mock() mock_message.role = "assistant" mock_message.content = "Welcome to my inn!" mock_choice = Mock() mock_choice.message = mock_message mock_response = Mock() mock_response.choices = [mock_choice] # Configure mock client self.mock_client.chat = Mock() self.mock_client.chat.completions = Mock(return_value=mock_response) # Call completions messages = [{"role": "user", "content": "Hello innkeeper!"}] response = self.test_npc.chat.completions(messages, stream=False) # Check history assert len(self.test_npc.chat_history) == 2 assert self.test_npc.chat_history[0]["content"] == "Hello innkeeper!" assert self.test_npc.chat_history[1]["content"] == "Welcome to my inn!" def test_sync_chat_with_history(self): """Test sync chat_with_history.""" # Pre-populate history self.test_npc.chat_history.extend( [ {"role": "user", "content": "Any rooms available?"}, {"role": "assistant", "content": "Yes, we have several!"}, ] ) # Mock completions captured_messages = None def mock_completions(messages, **kwargs): nonlocal captured_messages captured_messages = messages return Mock() self.test_npc.chat.completions = mock_completions # Call chat_with_history self.test_npc.chat.chat_with_history("How much for a night?") # Verify history was included assert len(captured_messages) == 3 assert captured_messages[2]["content"] == "How much for a night?" def test_sync_serialization(self): """Test sync NPC serialization.""" # Add history self.test_npc.chat_history.append({"role": "user", "content": "Test message"}) # Test to_dict npc_dict = self.test_npc.to_dict() assert npc_dict["name"] == "Innkeeper" assert len(npc_dict["chat_history"]) == 1 # Test round trip json_str = self.test_npc.to_json() restored = SyncNPC.from_json(json_str, self.mock_client) assert restored.name == self.test_npc.name assert len(restored.chat_history) == 1