npc-clients-python/tests/test_npc_features.py

384 lines
13 KiB
Python

"""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