312 lines
12 KiB
Python
312 lines
12 KiB
Python
"""
|
|
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
|