first commit
This commit is contained in:
311
api/tests/test_action_prompt.py
Normal file
311
api/tests/test_action_prompt.py
Normal file
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
Tests for ActionPrompt model
|
||||
|
||||
Tests the action prompt availability logic, tier filtering,
|
||||
location filtering, and serialization.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models.action_prompt import (
|
||||
ActionPrompt,
|
||||
ActionCategory,
|
||||
LocationType,
|
||||
)
|
||||
from app.ai.model_selector import UserTier
|
||||
|
||||
|
||||
class TestActionPrompt:
|
||||
"""Tests for ActionPrompt dataclass."""
|
||||
|
||||
@pytest.fixture
|
||||
def free_action(self):
|
||||
"""Create a free tier action available in towns."""
|
||||
return ActionPrompt(
|
||||
prompt_id="ask_locals",
|
||||
category=ActionCategory.ASK_QUESTION,
|
||||
display_text="Ask locals for information",
|
||||
description="Talk to NPCs to learn about quests and rumors",
|
||||
tier_required=UserTier.FREE,
|
||||
context_filter=[LocationType.TOWN, LocationType.TAVERN],
|
||||
dm_prompt_template="The player asks locals about {{ topic }}.",
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def premium_action(self):
|
||||
"""Create a premium tier action available anywhere."""
|
||||
return ActionPrompt(
|
||||
prompt_id="investigate",
|
||||
category=ActionCategory.GATHER_INFO,
|
||||
display_text="Investigate suspicious activity",
|
||||
description="Look for clues and hidden details",
|
||||
tier_required=UserTier.PREMIUM,
|
||||
context_filter=[LocationType.ANY],
|
||||
dm_prompt_template="The player investigates the area.",
|
||||
icon="magnifying_glass",
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def elite_action(self):
|
||||
"""Create an elite tier action for libraries."""
|
||||
return ActionPrompt(
|
||||
prompt_id="consult_texts",
|
||||
category=ActionCategory.SPECIAL,
|
||||
display_text="Consult ancient texts",
|
||||
description="Study rare manuscripts for hidden knowledge",
|
||||
tier_required=UserTier.ELITE,
|
||||
context_filter=[LocationType.LIBRARY, LocationType.TOWN],
|
||||
dm_prompt_template="The player studies ancient texts.",
|
||||
cooldown_turns=3,
|
||||
)
|
||||
|
||||
# Availability tests
|
||||
|
||||
def test_free_action_available_to_free_user(self, free_action):
|
||||
"""Free action should be available to free tier users."""
|
||||
assert free_action.is_available(UserTier.FREE, LocationType.TOWN) is True
|
||||
|
||||
def test_free_action_available_to_premium_user(self, free_action):
|
||||
"""Free action should be available to higher tier users."""
|
||||
assert free_action.is_available(UserTier.PREMIUM, LocationType.TOWN) is True
|
||||
assert free_action.is_available(UserTier.ELITE, LocationType.TOWN) is True
|
||||
|
||||
def test_premium_action_not_available_to_free_user(self, premium_action):
|
||||
"""Premium action should not be available to free tier users."""
|
||||
assert premium_action.is_available(UserTier.FREE, LocationType.TOWN) is False
|
||||
|
||||
def test_premium_action_available_to_premium_user(self, premium_action):
|
||||
"""Premium action should be available to premium tier users."""
|
||||
assert premium_action.is_available(UserTier.PREMIUM, LocationType.TOWN) is True
|
||||
|
||||
def test_elite_action_not_available_to_premium_user(self, elite_action):
|
||||
"""Elite action should not be available to premium tier users."""
|
||||
assert elite_action.is_available(UserTier.PREMIUM, LocationType.LIBRARY) is False
|
||||
|
||||
def test_elite_action_available_to_elite_user(self, elite_action):
|
||||
"""Elite action should be available to elite tier users."""
|
||||
assert elite_action.is_available(UserTier.ELITE, LocationType.LIBRARY) is True
|
||||
|
||||
# Location filtering tests
|
||||
|
||||
def test_action_available_in_matching_location(self, free_action):
|
||||
"""Action should be available in matching locations."""
|
||||
assert free_action.is_available(UserTier.FREE, LocationType.TOWN) is True
|
||||
assert free_action.is_available(UserTier.FREE, LocationType.TAVERN) is True
|
||||
|
||||
def test_action_not_available_in_non_matching_location(self, free_action):
|
||||
"""Action should not be available in non-matching locations."""
|
||||
assert free_action.is_available(UserTier.FREE, LocationType.WILDERNESS) is False
|
||||
assert free_action.is_available(UserTier.FREE, LocationType.DUNGEON) is False
|
||||
|
||||
def test_any_location_matches_all(self, premium_action):
|
||||
"""Action with ANY location should be available everywhere."""
|
||||
assert premium_action.is_available(UserTier.PREMIUM, LocationType.TOWN) is True
|
||||
assert premium_action.is_available(UserTier.PREMIUM, LocationType.WILDERNESS) is True
|
||||
assert premium_action.is_available(UserTier.PREMIUM, LocationType.DUNGEON) is True
|
||||
assert premium_action.is_available(UserTier.PREMIUM, LocationType.LIBRARY) is True
|
||||
|
||||
def test_both_tier_and_location_must_match(self, free_action):
|
||||
"""Both tier and location requirements must be met."""
|
||||
# Wrong location, right tier
|
||||
assert free_action.is_available(UserTier.ELITE, LocationType.DUNGEON) is False
|
||||
|
||||
# Lock status tests
|
||||
|
||||
def test_is_locked_for_lower_tier(self, premium_action):
|
||||
"""Action should be locked for lower tier users."""
|
||||
assert premium_action.is_locked(UserTier.FREE) is True
|
||||
assert premium_action.is_locked(UserTier.BASIC) is True
|
||||
|
||||
def test_is_not_locked_for_sufficient_tier(self, premium_action):
|
||||
"""Action should not be locked for sufficient tier users."""
|
||||
assert premium_action.is_locked(UserTier.PREMIUM) is False
|
||||
assert premium_action.is_locked(UserTier.ELITE) is False
|
||||
|
||||
def test_get_lock_reason_returns_message(self, premium_action):
|
||||
"""Lock reason should explain tier requirement."""
|
||||
reason = premium_action.get_lock_reason(UserTier.FREE)
|
||||
assert reason is not None
|
||||
assert "Premium" in reason
|
||||
|
||||
def test_get_lock_reason_returns_none_when_unlocked(self, premium_action):
|
||||
"""Lock reason should be None when action is unlocked."""
|
||||
reason = premium_action.get_lock_reason(UserTier.PREMIUM)
|
||||
assert reason is None
|
||||
|
||||
# Tier hierarchy tests
|
||||
|
||||
def test_tier_hierarchy_free_to_elite(self):
|
||||
"""Test full tier hierarchy from FREE to ELITE."""
|
||||
action = ActionPrompt(
|
||||
prompt_id="test",
|
||||
category=ActionCategory.EXPLORE,
|
||||
display_text="Test",
|
||||
description="Test action",
|
||||
tier_required=UserTier.BASIC,
|
||||
context_filter=[LocationType.ANY],
|
||||
dm_prompt_template="Test",
|
||||
)
|
||||
|
||||
# FREE < BASIC (should fail)
|
||||
assert action.is_available(UserTier.FREE, LocationType.TOWN) is False
|
||||
|
||||
# BASIC >= BASIC (should pass)
|
||||
assert action.is_available(UserTier.BASIC, LocationType.TOWN) is True
|
||||
|
||||
# PREMIUM > BASIC (should pass)
|
||||
assert action.is_available(UserTier.PREMIUM, LocationType.TOWN) is True
|
||||
|
||||
# ELITE > BASIC (should pass)
|
||||
assert action.is_available(UserTier.ELITE, LocationType.TOWN) is True
|
||||
|
||||
# Serialization tests
|
||||
|
||||
def test_to_dict(self, free_action):
|
||||
"""Test serialization to dictionary."""
|
||||
data = free_action.to_dict()
|
||||
|
||||
assert data["prompt_id"] == "ask_locals"
|
||||
assert data["category"] == "ask_question"
|
||||
assert data["display_text"] == "Ask locals for information"
|
||||
assert data["tier_required"] == "free"
|
||||
assert data["context_filter"] == ["town", "tavern"]
|
||||
assert "dm_prompt_template" in data
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test deserialization from dictionary."""
|
||||
data = {
|
||||
"prompt_id": "explore_area",
|
||||
"category": "explore",
|
||||
"display_text": "Explore the area",
|
||||
"description": "Look around for points of interest",
|
||||
"tier_required": "free",
|
||||
"context_filter": ["wilderness", "dungeon"],
|
||||
"dm_prompt_template": "The player explores {{ location }}.",
|
||||
"icon": "compass",
|
||||
"cooldown_turns": 2,
|
||||
}
|
||||
|
||||
action = ActionPrompt.from_dict(data)
|
||||
|
||||
assert action.prompt_id == "explore_area"
|
||||
assert action.category == ActionCategory.EXPLORE
|
||||
assert action.tier_required == UserTier.FREE
|
||||
assert LocationType.WILDERNESS in action.context_filter
|
||||
assert action.icon == "compass"
|
||||
assert action.cooldown_turns == 2
|
||||
|
||||
def test_round_trip_serialization(self, free_action):
|
||||
"""Test that to_dict and from_dict are inverse operations."""
|
||||
data = free_action.to_dict()
|
||||
restored = ActionPrompt.from_dict(data)
|
||||
|
||||
assert restored.prompt_id == free_action.prompt_id
|
||||
assert restored.category == free_action.category
|
||||
assert restored.display_text == free_action.display_text
|
||||
assert restored.tier_required == free_action.tier_required
|
||||
assert restored.context_filter == free_action.context_filter
|
||||
|
||||
def test_from_dict_invalid_category(self):
|
||||
"""Test error handling for invalid category."""
|
||||
data = {
|
||||
"prompt_id": "test",
|
||||
"category": "invalid_category",
|
||||
"display_text": "Test",
|
||||
"description": "Test",
|
||||
"tier_required": "free",
|
||||
"context_filter": ["any"],
|
||||
"dm_prompt_template": "Test",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
ActionPrompt.from_dict(data)
|
||||
|
||||
assert "Invalid action category" in str(exc_info.value)
|
||||
|
||||
def test_from_dict_invalid_tier(self):
|
||||
"""Test error handling for invalid tier."""
|
||||
data = {
|
||||
"prompt_id": "test",
|
||||
"category": "explore",
|
||||
"display_text": "Test",
|
||||
"description": "Test",
|
||||
"tier_required": "super_premium",
|
||||
"context_filter": ["any"],
|
||||
"dm_prompt_template": "Test",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
ActionPrompt.from_dict(data)
|
||||
|
||||
assert "Invalid user tier" in str(exc_info.value)
|
||||
|
||||
def test_from_dict_invalid_location(self):
|
||||
"""Test error handling for invalid location type."""
|
||||
data = {
|
||||
"prompt_id": "test",
|
||||
"category": "explore",
|
||||
"display_text": "Test",
|
||||
"description": "Test",
|
||||
"tier_required": "free",
|
||||
"context_filter": ["invalid_location"],
|
||||
"dm_prompt_template": "Test",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
ActionPrompt.from_dict(data)
|
||||
|
||||
assert "Invalid location type" in str(exc_info.value)
|
||||
|
||||
# Optional fields tests
|
||||
|
||||
def test_optional_icon(self, free_action, premium_action):
|
||||
"""Test that icon is optional."""
|
||||
assert free_action.icon is None
|
||||
assert premium_action.icon == "magnifying_glass"
|
||||
|
||||
def test_default_cooldown(self, free_action, elite_action):
|
||||
"""Test default and custom cooldown values."""
|
||||
assert free_action.cooldown_turns == 0
|
||||
assert elite_action.cooldown_turns == 3
|
||||
|
||||
# Repr test
|
||||
|
||||
def test_repr(self, free_action):
|
||||
"""Test string representation."""
|
||||
repr_str = repr(free_action)
|
||||
assert "ask_locals" in repr_str
|
||||
assert "ask_question" in repr_str
|
||||
assert "free" in repr_str
|
||||
|
||||
|
||||
class TestActionCategory:
|
||||
"""Tests for ActionCategory enum."""
|
||||
|
||||
def test_all_categories_defined(self):
|
||||
"""Verify all expected categories exist."""
|
||||
categories = [cat.value for cat in ActionCategory]
|
||||
|
||||
assert "ask_question" in categories
|
||||
assert "travel" in categories
|
||||
assert "gather_info" in categories
|
||||
assert "rest" in categories
|
||||
assert "interact" in categories
|
||||
assert "explore" in categories
|
||||
assert "special" in categories
|
||||
|
||||
|
||||
class TestLocationType:
|
||||
"""Tests for LocationType enum."""
|
||||
|
||||
def test_all_location_types_defined(self):
|
||||
"""Verify all expected location types exist."""
|
||||
locations = [loc.value for loc in LocationType]
|
||||
|
||||
assert "town" in locations
|
||||
assert "tavern" in locations
|
||||
assert "wilderness" in locations
|
||||
assert "dungeon" in locations
|
||||
assert "safe_area" in locations
|
||||
assert "library" in locations
|
||||
assert "any" in locations
|
||||
Reference in New Issue
Block a user