384 lines
13 KiB
Python
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
|