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