""" Test suite for session prompt adoption workflow. This module tests the session prompt adoption workflow including: - Promoting session overrides to permanent files - Validation before adoption - Backup and rollback capabilities - Error handling for adoption process **Feature: prompt-optimization, Task 11.5: Add session prompt adoption workflow** **Validates: Requirements 9.5** """ import pytest import sys import tempfile import os from pathlib import Path from unittest.mock import patch, mock_open, MagicMock from datetime import datetime # Add src to path for imports sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'src')) from config.prompt_management.prompt_controller import PromptController class TestSessionPromptAdoption: """Test session prompt adoption workflow functionality.""" def setup_method(self): """Set up test environment.""" self.controller = PromptController() # Clear any existing state self.controller._prompt_cache.clear() self.controller._session_overrides.clear() def test_promote_session_to_file_success(self): """Test successful promotion of session override to file.""" agent_type = 'spiritual_monitor' session_id = 'test_promotion_session' override_content = """ Updated spiritual monitor prompt for testing promotion workflow. This content should be promoted to the permanent file. Respond with JSON: {"state": "green|yellow|red", "confidence": 0.0-1.0} """ # Set session override success = self.controller.set_session_override(agent_type, override_content, session_id) assert success, "Session override should be set successfully" # Mock file operations to avoid actual file changes with patch('pathlib.Path.exists', return_value=True), \ patch('pathlib.Path.rename') as mock_rename, \ patch('builtins.open', mock_open()) as mock_file: # Promote session to file result = self.controller.promote_session_to_file(agent_type, session_id) # Should succeed assert result, "Promotion should succeed" # Verify backup was created mock_rename.assert_called_once() # Verify file was written mock_file.assert_called_once() handle = mock_file() handle.write.assert_called_once_with(override_content) def test_promote_session_to_file_with_backup(self): """Test that promotion creates proper backup of existing file.""" agent_type = 'triage_question' session_id = 'backup_test_session' override_content = "New triage question prompt content" # Set session override self.controller.set_session_override(agent_type, override_content, session_id) # Mock file operations - test that backup logic is invoked with patch('pathlib.Path.exists', return_value=True), \ patch('pathlib.Path.rename') as mock_rename, \ patch('pathlib.Path.with_suffix') as mock_with_suffix, \ patch('builtins.open', mock_open()): # Setup mock backup path mock_backup_path = MagicMock() mock_with_suffix.return_value = mock_backup_path result = self.controller.promote_session_to_file(agent_type, session_id) # Should succeed assert result, "Promotion with backup should succeed" # Verify backup path was created with timestamp assert mock_with_suffix.called, "Backup path should be created" if mock_with_suffix.called: backup_call_args = str(mock_with_suffix.call_args[0][0]) assert '.backup.' in backup_call_args assert datetime.now().strftime('%Y%m%d') in backup_call_args def test_promote_session_to_file_no_override(self): """Test promotion when no session override exists.""" agent_type = 'spiritual_monitor' session_id = 'no_override_session' # Try to promote without setting override result = self.controller.promote_session_to_file(agent_type, session_id) # Should fail gracefully assert not result, "Promotion should fail when no override exists" def test_promote_session_to_file_clears_override(self): """Test that promotion clears the session override.""" agent_type = 'triage_evaluator' session_id = 'clear_override_session' override_content = "Content to be promoted and cleared" # Set session override self.controller.set_session_override(agent_type, override_content, session_id) # Verify override exists assert self.controller._has_session_override(agent_type, session_id) # Mock file operations with patch('pathlib.Path.exists', return_value=False), \ patch('builtins.open', mock_open()): result = self.controller.promote_session_to_file(agent_type, session_id) # Should succeed assert result, "Promotion should succeed" # Verify override was cleared assert not self.controller._has_session_override(agent_type, session_id) def test_promote_session_to_file_clears_cache(self): """Test that promotion clears the prompt cache.""" agent_type = 'spiritual_monitor' session_id = 'cache_clear_session' override_content = "Content for cache clearing test" # Set session override and load config to populate cache self.controller.set_session_override(agent_type, override_content, session_id) config = self.controller.get_prompt(agent_type, session_id=session_id) # Verify cache is populated cache_key = f"{agent_type}_{session_id}" assert cache_key in self.controller._prompt_cache # Mock file operations with patch('pathlib.Path.exists', return_value=False), \ patch('builtins.open', mock_open()): result = self.controller.promote_session_to_file(agent_type, session_id) # Should succeed assert result, "Promotion should succeed" # Verify cache was cleared assert len(self.controller._prompt_cache) == 0 def test_promote_session_to_file_error_handling(self): """Test error handling during promotion process.""" agent_type = 'spiritual_monitor' session_id = 'error_test_session' override_content = "Content for error testing" # Set session override self.controller.set_session_override(agent_type, override_content, session_id) # Mock file operation that raises exception with patch('pathlib.Path.exists', return_value=True), \ patch('pathlib.Path.rename', side_effect=OSError("Permission denied")): result = self.controller.promote_session_to_file(agent_type, session_id) # Should fail gracefully assert not result, "Promotion should fail gracefully on file errors" # Session override should still exist (not cleared on failure) assert self.controller._has_session_override(agent_type, session_id) def test_promote_session_validation_before_adoption(self): """Test validation of session content before promotion.""" agent_type = 'spiritual_monitor' session_id = 'validation_test_session' # Test with empty content (should still work but might be flagged) empty_content = "" self.controller.set_session_override(agent_type, empty_content, session_id) with patch('pathlib.Path.exists', return_value=False), \ patch('builtins.open', mock_open()): result = self.controller.promote_session_to_file(agent_type, session_id) # Should still succeed (validation is informational, not blocking) assert result, "Promotion should succeed even with empty content" def test_promote_session_with_placeholders(self): """Test promotion of session content containing placeholders.""" agent_type = 'spiritual_monitor' session_id = 'placeholder_test_session' override_content = """ You are a spiritual distress classifier. Use these indicators: {{SHARED_INDICATORS}} Apply these rules: {{SHARED_RULES}} """ # Set session override with placeholders self.controller.set_session_override(agent_type, override_content, session_id) # Mock file operations with patch('pathlib.Path.exists', return_value=False), \ patch('builtins.open', mock_open()) as mock_file: result = self.controller.promote_session_to_file(agent_type, session_id) # Should succeed assert result, "Promotion should succeed with placeholder content" # Verify original content (with placeholders) was written to file handle = mock_file() written_content = handle.write.call_args[0][0] assert "{{SHARED_INDICATORS}}" in written_content assert "{{SHARED_RULES}}" in written_content def test_multiple_session_promotions(self): """Test promoting multiple different sessions.""" sessions_data = [ ('spiritual_monitor', 'session_1', 'Content for session 1'), ('triage_question', 'session_2', 'Content for session 2'), ('triage_evaluator', 'session_3', 'Content for session 3') ] # Set all session overrides for agent_type, session_id, content in sessions_data: success = self.controller.set_session_override(agent_type, content, session_id) assert success, f"Should set override for {agent_type} session {session_id}" # Promote all sessions with patch('pathlib.Path.exists', return_value=False), \ patch('builtins.open', mock_open()): for agent_type, session_id, content in sessions_data: result = self.controller.promote_session_to_file(agent_type, session_id) assert result, f"Should promote {agent_type} session {session_id}" # Verify override was cleared assert not self.controller._has_session_override(agent_type, session_id) def test_promote_session_rollback_capability(self): """Test that backup files enable rollback capability.""" agent_type = 'spiritual_monitor' session_id = 'rollback_test_session' override_content = "New content that might need rollback" # Set session override self.controller.set_session_override(agent_type, override_content, session_id) # Mock file operations to test backup creation with patch('pathlib.Path.exists', return_value=True), \ patch('pathlib.Path.rename') as mock_rename, \ patch('pathlib.Path.with_suffix') as mock_with_suffix, \ patch('builtins.open', mock_open()): # Setup mock backup path mock_backup_path = MagicMock() mock_with_suffix.return_value = mock_backup_path result = self.controller.promote_session_to_file(agent_type, session_id) # Should succeed assert result, "Promotion should succeed" # Verify backup operations were called assert mock_with_suffix.called, "Backup path should be created" assert mock_rename.called, "File should be renamed for backup" # Verify backup path has correct format if mock_with_suffix.called: backup_call_args = str(mock_with_suffix.call_args[0][0]) assert '.backup.' in backup_call_args, "Backup should have .backup. in filename" class TestSessionPromptAdoptionIntegration: """Test integration aspects of session prompt adoption.""" def setup_method(self): """Set up test environment.""" self.controller = PromptController() self.controller._prompt_cache.clear() self.controller._session_overrides.clear() def test_adoption_workflow_end_to_end(self): """Test complete adoption workflow from session to file.""" agent_type = 'spiritual_monitor' session_id = 'end_to_end_session' # Step 1: Get original prompt original_config = self.controller.get_prompt(agent_type) original_content = original_config.base_prompt # Step 2: Set session override override_content = "Modified prompt content for end-to-end test" success = self.controller.set_session_override(agent_type, override_content, session_id) assert success # Step 3: Verify session override is active session_config = self.controller.get_prompt(agent_type, session_id=session_id) assert override_content in session_config.base_prompt # Step 4: Promote to file with patch('pathlib.Path.exists', return_value=True), \ patch('pathlib.Path.rename'), \ patch('builtins.open', mock_open()): promotion_result = self.controller.promote_session_to_file(agent_type, session_id) assert promotion_result # Step 5: Verify session override was cleared assert not self.controller._has_session_override(agent_type, session_id) # Step 6: Verify cache was cleared assert len(self.controller._prompt_cache) == 0 def test_adoption_preserves_shared_components(self): """Test that adoption preserves shared component integration.""" agent_type = 'spiritual_monitor' session_id = 'shared_components_session' # Create override with shared component placeholders override_content = """ Enhanced spiritual monitor with shared components: {{SHARED_INDICATORS}} {{SHARED_RULES}} """ # Set session override self.controller.set_session_override(agent_type, override_content, session_id) # Get session config to verify placeholder replacement works session_config = self.controller.get_prompt(agent_type, session_id=session_id) assert '{{SHARED_INDICATORS}}' not in session_config.base_prompt assert '{{SHARED_RULES}}' not in session_config.base_prompt # Promote to file (should preserve original placeholders) with patch('pathlib.Path.exists', return_value=False), \ patch('builtins.open', mock_open()) as mock_file: result = self.controller.promote_session_to_file(agent_type, session_id) assert result # Verify original content with placeholders was written written_content = mock_file().write.call_args[0][0] assert '{{SHARED_INDICATORS}}' in written_content assert '{{SHARED_RULES}}' in written_content if __name__ == '__main__': pytest.main([__file__, '-v'])