""" Copyright 2024-2026 ChatterMate Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ import pytest from unittest.mock import Mock, patch, AsyncMock, MagicMock from uuid import uuid4 from app.services.workflow_execution import WorkflowExecutionService from app.models.workflow_node import WorkflowNode, NodeType, ExitCondition from app.agents.chat_agent import ChatResponse from app.models.workflow import Workflow, WorkflowStatus @pytest.fixture def mock_db(): return Mock() @pytest.fixture def workflow_service(mock_db): return WorkflowExecutionService(mock_db) @pytest.fixture def sample_workflow(): workflow = MagicMock(spec=Workflow) workflow.id = uuid4() workflow.name = "Test Workflow" workflow.nodes = [] # List to store nodes workflow.connections = [] # List to store connections workflow.status = WorkflowStatus.PUBLISHED return workflow class TestWorkflowExecutionEndChat: @pytest.mark.asyncio async def test_llm_node_end_chat_with_rating_config_true(self, workflow_service, sample_workflow): """Test LLM node end chat with ask_for_rating config set to True""" # Create LLM node with continuous execution and ask_for_rating: True node = Mock(spec=WorkflowNode) node.id = uuid4() node.node_type = NodeType.LLM config_dict = { "system_prompt": "You are a helpful assistant.", "exit_condition": ExitCondition.CONTINUOUS_EXECUTION, "ask_for_rating": True, "model_config": { "temperature": 0.7, "max_tokens": 150 } } node.config = MagicMock() node.config.__getitem__.side_effect = config_dict.__getitem__ node.config.get.side_effect = config_dict.get # Mock chat agent and its response mock_chat_agent = AsyncMock() mock_response = Mock(spec=ChatResponse) mock_response.message = "Thank you for chatting with us. Goodbye!" mock_response.transfer_to_human = False mock_response.end_chat = True mock_response.request_rating = False # This should be updated by _handle_end_chat mock_response.transfer_reason = None mock_response.transfer_description = None mock_response.success = True # Add success attribute # Mock the updated response after _handle_end_chat mock_updated_response = Mock(spec=ChatResponse) mock_updated_response.message = "Thank you for chatting with us. Goodbye!\n\nThank you for chatting with us! Would you please take a moment to rate your experience? Your feedback helps us improve our service." mock_updated_response.transfer_to_human = False mock_updated_response.end_chat = True mock_updated_response.request_rating = True # Updated by _handle_end_chat mock_updated_response.transfer_reason = None mock_updated_response.transfer_description = None mock_updated_response.success = True # Add success attribute # Set up async method return values mock_chat_agent._get_llm_response_only = AsyncMock(return_value=mock_response) mock_chat_agent._handle_end_chat = AsyncMock(return_value=mock_updated_response) # Mock _find_next_node next_node_id = uuid4() with patch('app.services.workflow_execution.ChatAgent') as mock_chat_agent_class: mock_chat_agent_class.create_async = AsyncMock(return_value=mock_chat_agent) with patch.object(workflow_service.session_repo, 'get_workflow_history', return_value=[]), \ patch.object(workflow_service, '_find_next_node', return_value=next_node_id): result = await workflow_service._execute_llm_node( node=node, workflow=sample_workflow, workflow_state={}, user_message="Goodbye", api_key="test-key", model_name="gpt-4", model_type="openai", base_url=None, org_id="test-org", agent_id="test-agent", customer_id="test-customer", session_id="test-session" ) # Verify _handle_end_chat was NOT called (it's handled in workflow_chat service) mock_chat_agent._handle_end_chat.assert_not_called() # Verify the result has the correct rating setting assert result.success is True assert result.end_chat is True assert result.request_rating is True assert result.next_node_id == next_node_id @pytest.mark.asyncio async def test_llm_node_end_chat_with_rating_config_false(self, workflow_service, sample_workflow): """Test LLM node end chat with ask_for_rating config set to False""" # Create LLM node with continuous execution and ask_for_rating: False node = Mock(spec=WorkflowNode) node.id = uuid4() node.node_type = NodeType.LLM config_dict = { "system_prompt": "You are a helpful assistant.", "exit_condition": ExitCondition.CONTINUOUS_EXECUTION, "ask_for_rating": False, "model_config": { "temperature": 0.7, "max_tokens": 150 } } node.config = MagicMock() node.config.__getitem__.side_effect = config_dict.__getitem__ node.config.get.side_effect = config_dict.get node.config.__contains__ = lambda *args: args[1] in config_dict # Fix lambda to handle multiple args # Mock chat agent and its response mock_chat_agent = AsyncMock() mock_response = Mock(spec=ChatResponse) mock_response.message = "Thank you for chatting with us. Goodbye!" mock_response.transfer_to_human = False mock_response.end_chat = True mock_response.request_rating = True # This should be updated by _handle_end_chat mock_response.transfer_reason = None mock_response.transfer_description = None mock_response.success = True # Add success attribute # Mock the updated response after _handle_end_chat mock_updated_response = Mock(spec=ChatResponse) mock_updated_response.message = "Thank you for chatting with us. Goodbye!" mock_updated_response.transfer_to_human = False mock_updated_response.end_chat = True mock_updated_response.request_rating = False # Updated by _handle_end_chat to False mock_updated_response.transfer_reason = None mock_updated_response.transfer_description = None mock_updated_response.success = True # Add success attribute # Set up async method return values mock_chat_agent._get_llm_response_only = AsyncMock(return_value=mock_response) mock_chat_agent._handle_end_chat = AsyncMock(return_value=mock_updated_response) # Mock _find_next_node next_node_id = uuid4() with patch('app.services.workflow_execution.ChatAgent') as mock_chat_agent_class: mock_chat_agent_class.create_async = AsyncMock(return_value=mock_chat_agent) with patch.object(workflow_service.session_repo, 'get_workflow_history', return_value=[]), \ patch.object(workflow_service, '_find_next_node', return_value=next_node_id): result = await workflow_service._execute_llm_node( node=node, workflow=sample_workflow, workflow_state={}, user_message="Goodbye", api_key="test-key", model_name="gpt-4", model_type="openai", base_url=None, org_id="test-org", agent_id="test-agent", customer_id="test-customer", session_id="test-session" ) # Verify _handle_end_chat was NOT called (it's handled in workflow_chat service) mock_chat_agent._handle_end_chat.assert_not_called() # Verify the result has the correct rating setting assert result.success is True assert result.end_chat is True assert result.request_rating is False assert result.next_node_id == next_node_id @pytest.mark.asyncio async def test_llm_node_end_chat_without_rating_config(self, workflow_service, sample_workflow): """Test LLM node end chat without ask_for_rating config (should default to True)""" # Create LLM node with continuous execution but no ask_for_rating config node = Mock(spec=WorkflowNode) node.id = uuid4() node.node_type = NodeType.LLM config_dict = { "system_prompt": "You are a helpful assistant.", "exit_condition": ExitCondition.CONTINUOUS_EXECUTION, "model_config": { "temperature": 0.7, "max_tokens": 150 } } node.config = MagicMock() node.config.__getitem__.side_effect = config_dict.__getitem__ node.config.get.side_effect = config_dict.get # Mock chat agent and its response mock_chat_agent = AsyncMock() mock_response = Mock(spec=ChatResponse) mock_response.message = "Thank you for chatting with us. Goodbye!" mock_response.transfer_to_human = False mock_response.end_chat = True mock_response.request_rating = False mock_response.transfer_reason = None mock_response.transfer_description = None mock_response.success = True # Add success attribute # Mock the updated response after _handle_end_chat mock_updated_response = Mock(spec=ChatResponse) mock_updated_response.message = "Thank you for chatting with us. Goodbye!\n\nThank you for chatting with us! Would you please take a moment to rate your experience? Your feedback helps us improve our service." mock_updated_response.transfer_to_human = False mock_updated_response.end_chat = True mock_updated_response.request_rating = True # Should default to True mock_updated_response.transfer_reason = None mock_updated_response.transfer_description = None mock_updated_response.success = True # Add success attribute # Set up async method return values mock_chat_agent._get_llm_response_only = AsyncMock(return_value=mock_response) mock_chat_agent._handle_end_chat = AsyncMock(return_value=mock_updated_response) # Mock _find_next_node next_node_id = uuid4() with patch('app.services.workflow_execution.ChatAgent') as mock_chat_agent_class: mock_chat_agent_class.create_async = AsyncMock(return_value=mock_chat_agent) with patch.object(workflow_service.session_repo, 'get_workflow_history', return_value=[]), \ patch.object(workflow_service, '_find_next_node', return_value=next_node_id): result = await workflow_service._execute_llm_node( node=node, workflow=sample_workflow, workflow_state={}, user_message="Goodbye", api_key="test-key", model_name="gpt-4", model_type="openai", base_url=None, org_id="test-org", agent_id="test-agent", customer_id="test-customer", session_id="test-session" ) # Verify _handle_end_chat was NOT called (it's handled in workflow_chat service) mock_chat_agent._handle_end_chat.assert_not_called() # Verify the result has the correct rating setting (default True) assert result.success is True assert result.end_chat is True assert result.request_rating is True assert result.next_node_id == next_node_id @pytest.mark.asyncio async def test_llm_node_end_chat_single_execution_no_config_override(self, workflow_service, sample_workflow): """Test LLM node end chat with single execution (should not use config override)""" # Create LLM node with single execution node = Mock(spec=WorkflowNode) node.id = uuid4() node.node_type = NodeType.LLM config_dict = { "system_prompt": "You are a helpful assistant.", "exit_condition": ExitCondition.SINGLE_EXECUTION, "ask_for_rating": False, # This should not be used for single execution "model_config": { "temperature": 0.7, "max_tokens": 150 } } node.config = MagicMock() node.config.__getitem__.side_effect = config_dict.__getitem__ node.config.get.side_effect = config_dict.get # Mock chat agent and its response mock_chat_agent = AsyncMock() mock_response = Mock(spec=ChatResponse) mock_response.message = "Thank you for chatting with us. Goodbye!" mock_response.transfer_to_human = False mock_response.end_chat = True mock_response.request_rating = False mock_response.transfer_reason = None mock_response.transfer_description = None mock_response.success = True # Add success attribute # Mock the updated response after _handle_end_chat (using agent's default) mock_updated_response = Mock(spec=ChatResponse) mock_updated_response.message = "Thank you for chatting with us. Goodbye!" mock_updated_response.transfer_to_human = False mock_updated_response.end_chat = True mock_updated_response.request_rating = False # Uses agent's default, not config mock_updated_response.transfer_reason = None mock_updated_response.transfer_description = None mock_updated_response.success = True # Add success attribute # Set up async method return values mock_chat_agent._get_llm_response_only = AsyncMock(return_value=mock_response) mock_chat_agent._handle_end_chat = AsyncMock(return_value=mock_updated_response) # Mock _find_next_node next_node_id = uuid4() with patch('app.services.workflow_execution.ChatAgent') as mock_chat_agent_class: mock_chat_agent_class.create_async = AsyncMock(return_value=mock_chat_agent) with patch.object(workflow_service.session_repo, 'get_workflow_history', return_value=[]), \ patch.object(workflow_service, '_find_next_node', return_value=next_node_id): result = await workflow_service._execute_llm_node( node=node, workflow=sample_workflow, workflow_state={}, user_message="Goodbye", api_key="test-key", model_name="gpt-4", model_type="openai", base_url=None, org_id="test-org", agent_id="test-agent", customer_id="test-customer", session_id="test-session" ) # Verify _handle_end_chat was NOT called (it's handled in workflow_chat service) mock_chat_agent._handle_end_chat.assert_not_called() # Verify the result (single execution doesn't override rating) assert result.success is True assert result.end_chat is True assert result.request_rating is False assert result.next_node_id == next_node_id @pytest.mark.asyncio async def test_llm_node_no_end_chat_no_handle_end_chat_call(self, workflow_service, sample_workflow): """Test LLM node without end chat (should not call _handle_end_chat)""" # Create LLM node node = Mock(spec=WorkflowNode) node.id = uuid4() node.node_type = NodeType.LLM config_dict = { "system_prompt": "You are a helpful assistant.", "exit_condition": ExitCondition.CONTINUOUS_EXECUTION, "model_config": { "temperature": 0.7, "max_tokens": 150 } } node.config = MagicMock() node.config.__getitem__.side_effect = config_dict.__getitem__ node.config.get.side_effect = config_dict.get # Mock chat agent and its response (no end chat) mock_chat_agent = AsyncMock() mock_response = Mock(spec=ChatResponse) mock_response.message = "How can I help you today?" mock_response.transfer_to_human = False mock_response.end_chat = False # No end chat mock_response.request_rating = False mock_response.transfer_reason = None mock_response.transfer_description = None mock_response.success = True # Add success attribute # Set up async method return values mock_chat_agent._get_llm_response_only = AsyncMock(return_value=mock_response) # Mock _find_next_node next_node_id = uuid4() with patch('app.services.workflow_execution.ChatAgent') as mock_chat_agent_class: mock_chat_agent_class.create_async = AsyncMock(return_value=mock_chat_agent) with patch.object(workflow_service.session_repo, 'get_workflow_history', return_value=[]), \ patch.object(workflow_service, '_find_next_node', return_value=next_node_id): result = await workflow_service._execute_llm_node( node=node, workflow=sample_workflow, workflow_state={}, user_message="Hello", api_key="test-key", model_name="gpt-4", model_type="openai", base_url=None, org_id="test-org", agent_id="test-agent", customer_id="test-customer", session_id="test-session" ) # Verify _handle_end_chat was NOT called mock_chat_agent._handle_end_chat.assert_not_called() # Verify the result assert result.success is True assert result.end_chat is False assert result.request_rating is False assert result.next_node_id is None