Files
Code_of_Conquest/api/tests/test_action_prompt.py
2025-11-24 23:10:55 -06:00

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