""" 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