first commit
This commit is contained in:
314
api/tests/test_action_prompt_loader.py
Normal file
314
api/tests/test_action_prompt_loader.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""
|
||||
Tests for ActionPromptLoader service
|
||||
|
||||
Tests loading from YAML, filtering by tier and location,
|
||||
and error handling.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from app.services.action_prompt_loader import (
|
||||
ActionPromptLoader,
|
||||
ActionPromptLoaderError,
|
||||
ActionPromptNotFoundError,
|
||||
)
|
||||
from app.models.action_prompt import LocationType
|
||||
from app.ai.model_selector import UserTier
|
||||
|
||||
|
||||
class TestActionPromptLoader:
|
||||
"""Tests for ActionPromptLoader service."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_singleton(self):
|
||||
"""Reset singleton before each test."""
|
||||
ActionPromptLoader.reset_instance()
|
||||
yield
|
||||
ActionPromptLoader.reset_instance()
|
||||
|
||||
@pytest.fixture
|
||||
def sample_yaml(self):
|
||||
"""Create a sample YAML file for testing."""
|
||||
content = """
|
||||
action_prompts:
|
||||
- prompt_id: test_free
|
||||
category: explore
|
||||
display_text: Free Action
|
||||
description: Available to all
|
||||
tier_required: free
|
||||
context_filter: [town, tavern]
|
||||
dm_prompt_template: Test template
|
||||
|
||||
- prompt_id: test_premium
|
||||
category: gather_info
|
||||
display_text: Premium Action
|
||||
description: Premium only
|
||||
tier_required: premium
|
||||
context_filter: [any]
|
||||
dm_prompt_template: Premium template
|
||||
|
||||
- prompt_id: test_elite
|
||||
category: special
|
||||
display_text: Elite Action
|
||||
description: Elite only
|
||||
tier_required: elite
|
||||
context_filter: [library]
|
||||
dm_prompt_template: Elite template
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||
f.write(content)
|
||||
filepath = f.name
|
||||
|
||||
yield filepath
|
||||
os.unlink(filepath)
|
||||
|
||||
@pytest.fixture
|
||||
def loader(self, sample_yaml):
|
||||
"""Create a loader with sample data."""
|
||||
loader = ActionPromptLoader()
|
||||
loader.load_from_yaml(sample_yaml)
|
||||
return loader
|
||||
|
||||
# Loading tests
|
||||
|
||||
def test_load_from_yaml(self, sample_yaml):
|
||||
"""Test loading prompts from YAML file."""
|
||||
loader = ActionPromptLoader()
|
||||
count = loader.load_from_yaml(sample_yaml)
|
||||
|
||||
assert count == 3
|
||||
assert loader.is_loaded()
|
||||
|
||||
def test_load_file_not_found(self):
|
||||
"""Test error when file doesn't exist."""
|
||||
loader = ActionPromptLoader()
|
||||
|
||||
with pytest.raises(ActionPromptLoaderError) as exc_info:
|
||||
loader.load_from_yaml("/nonexistent/path.yaml")
|
||||
|
||||
assert "not found" in str(exc_info.value)
|
||||
|
||||
def test_load_invalid_yaml(self):
|
||||
"""Test error when YAML is malformed."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||
f.write("invalid: yaml: content: [")
|
||||
filepath = f.name
|
||||
|
||||
try:
|
||||
loader = ActionPromptLoader()
|
||||
with pytest.raises(ActionPromptLoaderError) as exc_info:
|
||||
loader.load_from_yaml(filepath)
|
||||
|
||||
assert "Invalid YAML" in str(exc_info.value)
|
||||
finally:
|
||||
os.unlink(filepath)
|
||||
|
||||
def test_load_missing_key(self):
|
||||
"""Test error when action_prompts key is missing."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||
f.write("other_key: value")
|
||||
filepath = f.name
|
||||
|
||||
try:
|
||||
loader = ActionPromptLoader()
|
||||
with pytest.raises(ActionPromptLoaderError) as exc_info:
|
||||
loader.load_from_yaml(filepath)
|
||||
|
||||
assert "Missing 'action_prompts'" in str(exc_info.value)
|
||||
finally:
|
||||
os.unlink(filepath)
|
||||
|
||||
# Get methods tests
|
||||
|
||||
def test_get_all_actions(self, loader):
|
||||
"""Test getting all loaded actions."""
|
||||
actions = loader.get_all_actions()
|
||||
|
||||
assert len(actions) == 3
|
||||
prompt_ids = [a.prompt_id for a in actions]
|
||||
assert "test_free" in prompt_ids
|
||||
assert "test_premium" in prompt_ids
|
||||
assert "test_elite" in prompt_ids
|
||||
|
||||
def test_get_action_by_id(self, loader):
|
||||
"""Test getting action by ID."""
|
||||
action = loader.get_action_by_id("test_free")
|
||||
|
||||
assert action.prompt_id == "test_free"
|
||||
assert action.display_text == "Free Action"
|
||||
|
||||
def test_get_action_by_id_not_found(self, loader):
|
||||
"""Test error when action ID not found."""
|
||||
with pytest.raises(ActionPromptNotFoundError):
|
||||
loader.get_action_by_id("nonexistent")
|
||||
|
||||
# Filtering tests
|
||||
|
||||
def test_get_available_actions_free_tier(self, loader):
|
||||
"""Test filtering for free tier user."""
|
||||
actions = loader.get_available_actions(UserTier.FREE, LocationType.TOWN)
|
||||
|
||||
assert len(actions) == 1
|
||||
assert actions[0].prompt_id == "test_free"
|
||||
|
||||
def test_get_available_actions_premium_tier(self, loader):
|
||||
"""Test filtering for premium tier user."""
|
||||
actions = loader.get_available_actions(UserTier.PREMIUM, LocationType.TOWN)
|
||||
|
||||
# Premium gets: test_free (town) + test_premium (any)
|
||||
assert len(actions) == 2
|
||||
prompt_ids = [a.prompt_id for a in actions]
|
||||
assert "test_free" in prompt_ids
|
||||
assert "test_premium" in prompt_ids
|
||||
|
||||
def test_get_available_actions_elite_tier(self, loader):
|
||||
"""Test filtering for elite tier user."""
|
||||
actions = loader.get_available_actions(UserTier.ELITE, LocationType.LIBRARY)
|
||||
|
||||
# Elite in library gets: test_premium (any) + test_elite (library)
|
||||
assert len(actions) == 2
|
||||
prompt_ids = [a.prompt_id for a in actions]
|
||||
assert "test_premium" in prompt_ids
|
||||
assert "test_elite" in prompt_ids
|
||||
|
||||
def test_get_available_actions_location_filter(self, loader):
|
||||
"""Test that location filtering works correctly."""
|
||||
# In town, no elite actions available
|
||||
actions = loader.get_available_actions(UserTier.ELITE, LocationType.TOWN)
|
||||
prompt_ids = [a.prompt_id for a in actions]
|
||||
|
||||
assert "test_elite" not in prompt_ids # Only in library
|
||||
|
||||
def test_get_actions_by_tier(self, loader):
|
||||
"""Test getting actions by tier without location filter."""
|
||||
free_actions = loader.get_actions_by_tier(UserTier.FREE)
|
||||
premium_actions = loader.get_actions_by_tier(UserTier.PREMIUM)
|
||||
elite_actions = loader.get_actions_by_tier(UserTier.ELITE)
|
||||
|
||||
assert len(free_actions) == 1
|
||||
assert len(premium_actions) == 2
|
||||
assert len(elite_actions) == 3
|
||||
|
||||
def test_get_actions_by_category(self, loader):
|
||||
"""Test getting actions by category."""
|
||||
explore_actions = loader.get_actions_by_category("explore")
|
||||
special_actions = loader.get_actions_by_category("special")
|
||||
|
||||
assert len(explore_actions) == 1
|
||||
assert explore_actions[0].prompt_id == "test_free"
|
||||
|
||||
assert len(special_actions) == 1
|
||||
assert special_actions[0].prompt_id == "test_elite"
|
||||
|
||||
def test_get_locked_actions(self, loader):
|
||||
"""Test getting locked actions for upgrade prompts."""
|
||||
# Free user in library sees elite action as locked
|
||||
locked = loader.get_locked_actions(UserTier.FREE, LocationType.LIBRARY)
|
||||
|
||||
# test_premium (any) and test_elite (library) are locked for free
|
||||
assert len(locked) == 2
|
||||
prompt_ids = [a.prompt_id for a in locked]
|
||||
assert "test_premium" in prompt_ids
|
||||
assert "test_elite" in prompt_ids
|
||||
|
||||
# Singleton and reload tests
|
||||
|
||||
def test_singleton_pattern(self, sample_yaml):
|
||||
"""Test that loader is singleton."""
|
||||
loader1 = ActionPromptLoader()
|
||||
loader1.load_from_yaml(sample_yaml)
|
||||
|
||||
loader2 = ActionPromptLoader()
|
||||
|
||||
assert loader1 is loader2
|
||||
assert loader2.is_loaded()
|
||||
|
||||
def test_reload(self, sample_yaml):
|
||||
"""Test reloading prompts."""
|
||||
loader = ActionPromptLoader()
|
||||
loader.load_from_yaml(sample_yaml)
|
||||
|
||||
# Modify and reload
|
||||
count = loader.reload(sample_yaml)
|
||||
assert count == 3
|
||||
|
||||
def test_get_prompt_count(self, loader):
|
||||
"""Test getting prompt count."""
|
||||
assert loader.get_prompt_count() == 3
|
||||
|
||||
|
||||
class TestActionPromptLoaderIntegration:
|
||||
"""Integration tests with actual YAML file."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_singleton(self):
|
||||
"""Reset singleton before each test."""
|
||||
ActionPromptLoader.reset_instance()
|
||||
yield
|
||||
ActionPromptLoader.reset_instance()
|
||||
|
||||
def test_load_actual_yaml(self):
|
||||
"""Test loading the actual action_prompts.yaml file."""
|
||||
loader = ActionPromptLoader()
|
||||
filepath = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'..', 'app', 'data', 'action_prompts.yaml'
|
||||
)
|
||||
|
||||
if os.path.exists(filepath):
|
||||
count = loader.load_from_yaml(filepath)
|
||||
|
||||
# Should have 10 actions
|
||||
assert count == 10
|
||||
|
||||
# Verify tier distribution
|
||||
free_actions = loader.get_actions_by_tier(UserTier.FREE)
|
||||
premium_actions = loader.get_actions_by_tier(UserTier.PREMIUM)
|
||||
elite_actions = loader.get_actions_by_tier(UserTier.ELITE)
|
||||
|
||||
assert len(free_actions) == 4 # Only free tier
|
||||
assert len(premium_actions) == 7 # Free + premium
|
||||
assert len(elite_actions) == 10 # All
|
||||
|
||||
def test_free_tier_town_actions(self):
|
||||
"""Test free tier actions in town location."""
|
||||
loader = ActionPromptLoader()
|
||||
filepath = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'..', 'app', 'data', 'action_prompts.yaml'
|
||||
)
|
||||
|
||||
if os.path.exists(filepath):
|
||||
loader.load_from_yaml(filepath)
|
||||
|
||||
actions = loader.get_available_actions(UserTier.FREE, LocationType.TOWN)
|
||||
|
||||
# Free user in town should have:
|
||||
# - ask_locals (town/tavern)
|
||||
# - search_supplies (any)
|
||||
# - rest_recover (town/tavern/safe_area)
|
||||
prompt_ids = [a.prompt_id for a in actions]
|
||||
assert "ask_locals" in prompt_ids
|
||||
assert "search_supplies" in prompt_ids
|
||||
assert "rest_recover" in prompt_ids
|
||||
|
||||
def test_premium_tier_wilderness_actions(self):
|
||||
"""Test premium tier actions in wilderness location."""
|
||||
loader = ActionPromptLoader()
|
||||
filepath = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'..', 'app', 'data', 'action_prompts.yaml'
|
||||
)
|
||||
|
||||
if os.path.exists(filepath):
|
||||
loader.load_from_yaml(filepath)
|
||||
|
||||
actions = loader.get_available_actions(UserTier.PREMIUM, LocationType.WILDERNESS)
|
||||
|
||||
prompt_ids = [a.prompt_id for a in actions]
|
||||
# Should include wilderness actions
|
||||
assert "explore_area" in prompt_ids
|
||||
assert "make_camp" in prompt_ids
|
||||
assert "search_supplies" in prompt_ids
|
||||
Reference in New Issue
Block a user