322 lines
11 KiB
Python
322 lines
11 KiB
Python
"""
|
|
Unit tests for prompt templates module.
|
|
"""
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
from app.ai import (
|
|
PromptTemplates,
|
|
PromptTemplateError,
|
|
get_prompt_templates,
|
|
render_prompt,
|
|
)
|
|
|
|
|
|
class TestPromptTemplates:
|
|
"""Tests for PromptTemplates class."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
self.templates = PromptTemplates()
|
|
|
|
def test_initialization(self):
|
|
"""Test PromptTemplates initializes correctly."""
|
|
assert self.templates is not None
|
|
assert self.templates.env is not None
|
|
|
|
def test_template_directory_exists(self):
|
|
"""Test that template directory is created."""
|
|
assert self.templates.template_dir.exists()
|
|
|
|
def test_get_template_names(self):
|
|
"""Test listing available templates."""
|
|
names = self.templates.get_template_names()
|
|
assert isinstance(names, list)
|
|
# Should have our 4 core templates
|
|
assert 'story_action.j2' in names
|
|
assert 'combat_action.j2' in names
|
|
assert 'quest_offering.j2' in names
|
|
assert 'npc_dialogue.j2' in names
|
|
|
|
def test_render_string_simple(self):
|
|
"""Test rendering a simple template string."""
|
|
result = self.templates.render_string(
|
|
"Hello, {{ name }}!",
|
|
name="Player"
|
|
)
|
|
assert result == "Hello, Player!"
|
|
|
|
def test_render_string_with_filter(self):
|
|
"""Test rendering with custom filter."""
|
|
result = self.templates.render_string(
|
|
"{{ items | format_inventory }}",
|
|
items=[
|
|
{"name": "Sword", "quantity": 1},
|
|
{"name": "Potion", "quantity": 3}
|
|
]
|
|
)
|
|
assert "Sword" in result
|
|
assert "Potion (x3)" in result
|
|
|
|
def test_render_story_action_template(self):
|
|
"""Test rendering story_action template."""
|
|
result = self.templates.render(
|
|
"story_action.j2",
|
|
character={
|
|
"name": "Aldric",
|
|
"level": 5,
|
|
"player_class": "Warrior",
|
|
"current_hp": 45,
|
|
"max_hp": 50,
|
|
"stats": {"strength": 16, "dexterity": 12},
|
|
"skills": [],
|
|
"effects": []
|
|
},
|
|
game_state={
|
|
"current_location": "The Rusty Anchor",
|
|
"location_type": "TAVERN",
|
|
"active_quests": [],
|
|
"discovered_locations": []
|
|
},
|
|
action="I look around the tavern for anyone suspicious"
|
|
)
|
|
|
|
assert "Aldric" in result
|
|
assert "Warrior" in result
|
|
assert "Rusty Anchor" in result
|
|
assert "suspicious" in result
|
|
|
|
def test_render_combat_action_template(self):
|
|
"""Test rendering combat_action template."""
|
|
result = self.templates.render(
|
|
"combat_action.j2",
|
|
character={
|
|
"name": "Aldric",
|
|
"level": 5,
|
|
"player_class": "Warrior",
|
|
"current_hp": 45,
|
|
"max_hp": 50,
|
|
"effects": []
|
|
},
|
|
combat_state={
|
|
"round_number": 2,
|
|
"current_turn": "Player",
|
|
"enemies": [
|
|
{
|
|
"name": "Goblin",
|
|
"current_hp": 8,
|
|
"max_hp": 15,
|
|
"effects": []
|
|
}
|
|
]
|
|
},
|
|
action="swings their sword at the Goblin",
|
|
action_result={
|
|
"hit": True,
|
|
"damage": 7,
|
|
"effects_applied": []
|
|
},
|
|
is_critical=False,
|
|
is_finishing_blow=False
|
|
)
|
|
|
|
assert "Aldric" in result
|
|
assert "Goblin" in result
|
|
assert "sword" in result
|
|
|
|
def test_render_quest_offering_template(self):
|
|
"""Test rendering quest_offering template."""
|
|
result = self.templates.render(
|
|
"quest_offering.j2",
|
|
character={
|
|
"name": "Aldric",
|
|
"level": 3,
|
|
"player_class": "Warrior",
|
|
"completed_quests": []
|
|
},
|
|
game_context={
|
|
"current_location": "Village Square",
|
|
"location_type": "TOWN",
|
|
"active_quests": [],
|
|
"world_events": []
|
|
},
|
|
eligible_quests=[
|
|
{
|
|
"quest_id": "quest_goblin_cave",
|
|
"name": "Clear the Goblin Cave",
|
|
"difficulty": "EASY",
|
|
"quest_giver": "Village Elder",
|
|
"description": "Goblins have been raiding farms",
|
|
"narrative_hooks": [
|
|
"Farmers complaining about lost livestock"
|
|
]
|
|
}
|
|
],
|
|
recent_actions=["Talked to locals"]
|
|
)
|
|
|
|
assert "quest_goblin_cave" in result
|
|
assert "Clear the Goblin Cave" in result
|
|
assert "Village Elder" in result
|
|
|
|
def test_render_npc_dialogue_template(self):
|
|
"""Test rendering npc_dialogue template."""
|
|
result = self.templates.render(
|
|
"npc_dialogue.j2",
|
|
character={
|
|
"name": "Aldric",
|
|
"level": 5,
|
|
"player_class": "Warrior"
|
|
},
|
|
npc={
|
|
"name": "Grizzled Bartender",
|
|
"role": "Tavern Owner",
|
|
"personality": "Gruff but kind",
|
|
"speaking_style": "Short sentences, common slang"
|
|
},
|
|
conversation_topic="What's the latest news around here?",
|
|
game_state={
|
|
"current_location": "The Rusty Anchor",
|
|
"time_of_day": "Evening",
|
|
"active_quests": []
|
|
}
|
|
)
|
|
|
|
assert "Grizzled Bartender" in result
|
|
assert "Aldric" in result
|
|
assert "news" in result
|
|
|
|
def test_format_inventory_filter_empty(self):
|
|
"""Test format_inventory filter with empty list."""
|
|
result = PromptTemplates._format_inventory([])
|
|
assert result == "Empty inventory"
|
|
|
|
def test_format_inventory_filter_single(self):
|
|
"""Test format_inventory filter with single item."""
|
|
result = PromptTemplates._format_inventory([
|
|
{"name": "Sword", "quantity": 1}
|
|
])
|
|
assert result == "Sword"
|
|
|
|
def test_format_inventory_filter_multiple(self):
|
|
"""Test format_inventory filter with multiple items."""
|
|
result = PromptTemplates._format_inventory([
|
|
{"name": "Sword", "quantity": 1},
|
|
{"name": "Shield", "quantity": 1},
|
|
{"name": "Potion", "quantity": 5}
|
|
])
|
|
assert "Sword" in result
|
|
assert "Shield" in result
|
|
assert "Potion (x5)" in result
|
|
|
|
def test_format_inventory_filter_truncation(self):
|
|
"""Test format_inventory filter truncates long lists."""
|
|
items = [{"name": f"Item{i}", "quantity": 1} for i in range(15)]
|
|
result = PromptTemplates._format_inventory(items, max_items=10)
|
|
assert "and 5 more items" in result
|
|
|
|
def test_format_stats_filter(self):
|
|
"""Test format_stats filter."""
|
|
result = PromptTemplates._format_stats({
|
|
"strength": 16,
|
|
"dexterity": 14
|
|
})
|
|
assert "Strength: 16" in result
|
|
assert "Dexterity: 14" in result
|
|
|
|
def test_format_stats_filter_empty(self):
|
|
"""Test format_stats filter with empty dict."""
|
|
result = PromptTemplates._format_stats({})
|
|
assert result == "No stats available"
|
|
|
|
def test_format_skills_filter(self):
|
|
"""Test format_skills filter."""
|
|
result = PromptTemplates._format_skills([
|
|
{"name": "Sword Mastery", "level": 3},
|
|
{"name": "Shield Block", "level": 2}
|
|
])
|
|
assert "Sword Mastery (Lv.3)" in result
|
|
assert "Shield Block (Lv.2)" in result
|
|
|
|
def test_format_skills_filter_empty(self):
|
|
"""Test format_skills filter with empty list."""
|
|
result = PromptTemplates._format_skills([])
|
|
assert result == "No skills"
|
|
|
|
def test_format_effects_filter(self):
|
|
"""Test format_effects filter."""
|
|
result = PromptTemplates._format_effects([
|
|
{"name": "Blessed", "remaining_turns": 3},
|
|
{"name": "Strength Buff"}
|
|
])
|
|
assert "Blessed (3 turns)" in result
|
|
assert "Strength Buff" in result
|
|
|
|
def test_format_effects_filter_empty(self):
|
|
"""Test format_effects filter with empty list."""
|
|
result = PromptTemplates._format_effects([])
|
|
assert result == "No active effects"
|
|
|
|
def test_truncate_text_filter_short(self):
|
|
"""Test truncate_text filter with short text."""
|
|
result = PromptTemplates._truncate_text("Hello", 100)
|
|
assert result == "Hello"
|
|
|
|
def test_truncate_text_filter_long(self):
|
|
"""Test truncate_text filter with long text."""
|
|
long_text = "A" * 150
|
|
result = PromptTemplates._truncate_text(long_text, 100)
|
|
assert len(result) == 100
|
|
assert result.endswith("...")
|
|
|
|
def test_format_gold_filter(self):
|
|
"""Test format_gold filter."""
|
|
assert PromptTemplates._format_gold(1000) == "1,000 gold"
|
|
assert PromptTemplates._format_gold(1000000) == "1,000,000 gold"
|
|
assert PromptTemplates._format_gold(50) == "50 gold"
|
|
|
|
def test_invalid_template_raises_error(self):
|
|
"""Test that invalid template raises PromptTemplateError."""
|
|
with pytest.raises(PromptTemplateError):
|
|
self.templates.render("nonexistent_template.j2")
|
|
|
|
def test_invalid_template_string_raises_error(self):
|
|
"""Test that invalid template string raises PromptTemplateError."""
|
|
with pytest.raises(PromptTemplateError):
|
|
self.templates.render_string("{{ invalid syntax")
|
|
|
|
|
|
class TestPromptTemplateConvenienceFunctions:
|
|
"""Tests for module-level convenience functions."""
|
|
|
|
def test_get_prompt_templates_singleton(self):
|
|
"""Test get_prompt_templates returns singleton."""
|
|
templates1 = get_prompt_templates()
|
|
templates2 = get_prompt_templates()
|
|
assert templates1 is templates2
|
|
|
|
def test_render_prompt_function(self):
|
|
"""Test render_prompt convenience function."""
|
|
result = render_prompt(
|
|
"story_action.j2",
|
|
character={
|
|
"name": "Test",
|
|
"level": 1,
|
|
"player_class": "Warrior",
|
|
"current_hp": 10,
|
|
"max_hp": 10,
|
|
"stats": {},
|
|
"skills": [],
|
|
"effects": []
|
|
},
|
|
game_state={
|
|
"current_location": "Test Location",
|
|
"location_type": "TOWN",
|
|
"active_quests": []
|
|
},
|
|
action="test action"
|
|
)
|
|
assert "Test" in result
|
|
assert "test action" in result
|