From ba449890a9e52b28c62de1553b78de09da0b692c Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Wed, 22 Jul 2026 13:53:00 +0900 Subject: [PATCH] test: use sqlite3 session in test_app_runner_conversation_variables (#38687) --- .../test_app_runner_conversation_variables.py | 521 +++--------------- 1 file changed, 91 insertions(+), 430 deletions(-) diff --git a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py index 1970e5c1522..1f592ddec82 100644 --- a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py +++ b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py @@ -1,459 +1,120 @@ -"""Test conversation variable handling in AdvancedChatAppRunner.""" +"""SQLite-backed conversation-variable synchronization tests for AdvancedChatAppRunner.""" -from unittest.mock import MagicMock, patch -from uuid import uuid4 +from unittest.mock import MagicMock +import pytest +from sqlalchemy import select from sqlalchemy.orm import Session +from core.app.apps.advanced_chat import app_runner as app_runner_module from core.app.apps.advanced_chat.app_runner import AdvancedChatAppRunner -from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom from factories import variable_factory from graphon.variables import SegmentType -from models import ConversationVariable, Workflow +from models import ConversationVariable -MINIMAL_GRAPH = { - "nodes": [ +APP_ID = "11111111-1111-1111-1111-111111111111" +CONVERSATION_ID = "22222222-2222-2222-2222-222222222222" +OTHER_CONVERSATION_ID = "22222222-2222-2222-2222-222222222223" +VAR_1_ID = "33333333-3333-3333-3333-333333333333" +VAR_2_ID = "33333333-3333-3333-3333-333333333334" + + +def _variable(variable_id: str, name: str, value: str): + return variable_factory.build_conversation_variable_from_mapping( { - "id": "start", - "data": { - "type": "start", - "title": "Start", - }, + "id": variable_id, + "name": name, + "value_type": SegmentType.STRING, + "value": value, } - ], - "edges": [], -} + ) -def _patch_create_session(mock_session: MagicMock): - session_context = MagicMock() - session_context.__enter__.return_value = mock_session - session_context.__exit__.return_value = False - mock_session.begin.return_value.__enter__.return_value = mock_session - mock_session.begin.return_value.__exit__.return_value = False - return patch("core.app.apps.advanced_chat.app_runner.create_session", return_value=session_context) +def _runner(workflow_variables: list[object]) -> AdvancedChatAppRunner: + workflow = MagicMock() + workflow.conversation_variables = workflow_variables + conversation = MagicMock(app_id=APP_ID, id=CONVERSATION_ID) + return AdvancedChatAppRunner( + application_generate_entity=MagicMock(), + queue_manager=MagicMock(), + conversation=conversation, + message=MagicMock(), + dialogue_count=1, + variable_loader=MagicMock(), + workflow=workflow, + system_user_id="44444444-4444-4444-4444-444444444444", + app=MagicMock(), + workflow_execution_repository=MagicMock(), + workflow_node_execution_repository=MagicMock(), + ) -class TestAdvancedChatAppRunnerConversationVariables: - """Test that AdvancedChatAppRunner correctly handles conversation variables.""" +def _bind_runner_sessions(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None: + engine = sqlite_session.get_bind() + monkeypatch.setattr( + app_runner_module, + "create_session", + lambda: Session(engine, expire_on_commit=False), + ) - def test_missing_conversation_variables_are_added(self): - """Test that new conversation variables added to workflow are created for existing conversations.""" - # Setup - app_id = str(uuid4()) - conversation_id = str(uuid4()) - workflow_id = str(uuid4()) - # Create workflow with two conversation variables - workflow_vars = [ - variable_factory.build_conversation_variable_from_mapping( - { - "id": "var1", - "name": "existing_var", - "value_type": SegmentType.STRING, - "value": "default1", - } - ), - variable_factory.build_conversation_variable_from_mapping( - { - "id": "var2", - "name": "new_var", - "value_type": SegmentType.STRING, - "value": "default2", - } - ), - ] - - # Mock workflow with conversation variables - mock_workflow = MagicMock(spec=Workflow) - mock_workflow.conversation_variables = workflow_vars - mock_workflow.tenant_id = str(uuid4()) - mock_workflow.app_id = app_id - mock_workflow.id = workflow_id - mock_workflow.type = "chat" - mock_workflow.graph_dict = MINIMAL_GRAPH - mock_workflow.environment_variables = [] - - # Create existing conversation variable (only var1 exists in DB) - existing_db_var = MagicMock(spec=ConversationVariable) - existing_db_var.id = "var1" - existing_db_var.app_id = app_id - existing_db_var.conversation_id = conversation_id - existing_db_var.to_variable = MagicMock(return_value=workflow_vars[0]) - - # Mock conversation and message - mock_conversation = MagicMock() - mock_conversation.app_id = app_id - mock_conversation.id = conversation_id - - mock_message = MagicMock() - mock_message.id = str(uuid4()) - - # Mock app config - mock_app_config = MagicMock() - mock_app_config.app_id = app_id - mock_app_config.workflow_id = workflow_id - mock_app_config.tenant_id = str(uuid4()) - - # Mock app generate entity - mock_app_generate_entity = MagicMock(spec=AdvancedChatAppGenerateEntity) - mock_app_generate_entity.app_config = mock_app_config - mock_app_generate_entity.inputs = {} - mock_app_generate_entity.query = "test query" - mock_app_generate_entity.files = [] - mock_app_generate_entity.user_id = str(uuid4()) - mock_app_generate_entity.invoke_from = InvokeFrom.SERVICE_API - mock_app_generate_entity.workflow_run_id = str(uuid4()) - mock_app_generate_entity.task_id = str(uuid4()) - mock_app_generate_entity.call_depth = 0 - mock_app_generate_entity.single_iteration_run = None - mock_app_generate_entity.single_loop_run = None - mock_app_generate_entity.extras = {} - mock_app_generate_entity.trace_manager = None - - # Create runner - runner = AdvancedChatAppRunner( - application_generate_entity=mock_app_generate_entity, - queue_manager=MagicMock(), - conversation=mock_conversation, - message=mock_message, - dialogue_count=1, - variable_loader=MagicMock(), - workflow=mock_workflow, - system_user_id=str(uuid4()), - app=MagicMock(), - workflow_execution_repository=MagicMock(), - workflow_node_execution_repository=MagicMock(), +def _persist_variable(session: Session, *, variable: object, conversation_id: str = CONVERSATION_ID) -> None: + session.add( + ConversationVariable.from_variable( + app_id=APP_ID, + conversation_id=conversation_id, + variable=variable, ) + ) + session.commit() - # Mock database session - mock_session = MagicMock(spec=Session) - # First query returns only existing variable - mock_scalars_result = MagicMock() - mock_scalars_result.all.return_value = [existing_db_var] - mock_session.scalars.return_value = mock_scalars_result +@pytest.mark.parametrize("sqlite_session", [(ConversationVariable,)], indirect=True) +def test_missing_conversation_variables_are_added(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None: + existing_variable = _variable(VAR_1_ID, "existing_var", "default1") + new_variable = _variable(VAR_2_ID, "new_var", "default2") + _persist_variable(sqlite_session, variable=existing_variable) + _persist_variable(sqlite_session, variable=new_variable, conversation_id=OTHER_CONVERSATION_ID) + _bind_runner_sessions(monkeypatch, sqlite_session) - # Track what gets added to session - added_items = [] + variables = _runner([existing_variable, new_variable])._initialize_conversation_variables() - def track_add_all(items): - added_items.extend(items) + assert [variable.id for variable in variables] == [VAR_1_ID, VAR_2_ID] + persisted = sqlite_session.scalars( + select(ConversationVariable) + .where(ConversationVariable.conversation_id == CONVERSATION_ID) + .order_by(ConversationVariable.id) + ).all() + assert [variable.id for variable in persisted] == [VAR_1_ID, VAR_2_ID] - mock_session.add_all.side_effect = track_add_all - # Patch the necessary components - with ( - _patch_create_session(mock_session), - patch("core.app.apps.advanced_chat.app_runner.select") as mock_select, - patch.object(runner, "_init_graph") as mock_init_graph, - patch.object( - runner, - "handle_input_moderation", - return_value=(False, mock_app_generate_entity.inputs, mock_app_generate_entity.query), - ), - patch.object(runner, "handle_annotation_reply", return_value=False), - patch("core.app.apps.advanced_chat.app_runner.WorkflowEntry") as mock_workflow_entry_class, - patch("core.app.apps.advanced_chat.app_runner.GraphRuntimeState") as mock_graph_runtime_state_class, - patch("core.app.apps.advanced_chat.app_runner.redis_client") as mock_redis_client, - patch("core.app.apps.advanced_chat.app_runner.RedisChannel") as mock_redis_channel_class, - ): - # Mock GraphRuntimeState to accept the variable pool - mock_graph_runtime_state_class.return_value = MagicMock() +@pytest.mark.parametrize("sqlite_session", [(ConversationVariable,)], indirect=True) +def test_no_variables_creates_all(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None: + workflow_variables = [ + _variable(VAR_1_ID, "var1", "default1"), + _variable(VAR_2_ID, "var2", "default2"), + ] + _bind_runner_sessions(monkeypatch, sqlite_session) - # Mock graph initialization - mock_init_graph.return_value = MagicMock() + variables = _runner(workflow_variables)._initialize_conversation_variables() - # Mock workflow entry - mock_workflow_entry = MagicMock() - mock_workflow_entry.run.return_value = iter([]) # Empty generator - mock_workflow_entry_class.return_value = mock_workflow_entry + assert [variable.id for variable in variables] == [VAR_1_ID, VAR_2_ID] + persisted = sqlite_session.scalars(select(ConversationVariable).order_by(ConversationVariable.id)).all() + assert [variable.id for variable in persisted] == [VAR_1_ID, VAR_2_ID] - # Run the method - runner.run() - # Verify that the missing variable was added - assert len(added_items) == 1, "Should have added exactly one missing variable" +@pytest.mark.parametrize("sqlite_session", [(ConversationVariable,)], indirect=True) +def test_all_variables_exist_no_changes(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None: + workflow_variables = [ + _variable(VAR_1_ID, "var1", "default1"), + _variable(VAR_2_ID, "var2", "default2"), + ] + for variable in workflow_variables: + _persist_variable(sqlite_session, variable=variable) + _bind_runner_sessions(monkeypatch, sqlite_session) - # Check that the added item is the missing variable (var2) - added_var = added_items[0] - assert hasattr(added_var, "id"), "Added item should be a ConversationVariable" - # Note: Since we're mocking ConversationVariable.from_variable, - # we can't directly check the id, but we can verify add_all was called - assert mock_session.add_all.called, "Session add_all should have been called" + variables = _runner(workflow_variables)._initialize_conversation_variables() - def test_no_variables_creates_all(self): - """Test that all conversation variables are created when none exist in DB.""" - # Setup - app_id = str(uuid4()) - conversation_id = str(uuid4()) - workflow_id = str(uuid4()) - - # Create workflow with conversation variables - workflow_vars = [ - variable_factory.build_conversation_variable_from_mapping( - { - "id": "var1", - "name": "var1", - "value_type": SegmentType.STRING, - "value": "default1", - } - ), - variable_factory.build_conversation_variable_from_mapping( - { - "id": "var2", - "name": "var2", - "value_type": SegmentType.STRING, - "value": "default2", - } - ), - ] - - # Mock workflow - mock_workflow = MagicMock(spec=Workflow) - mock_workflow.conversation_variables = workflow_vars - mock_workflow.tenant_id = str(uuid4()) - mock_workflow.app_id = app_id - mock_workflow.id = workflow_id - mock_workflow.type = "chat" - mock_workflow.graph_dict = MINIMAL_GRAPH - mock_workflow.environment_variables = [] - - # Mock conversation and message - mock_conversation = MagicMock() - mock_conversation.app_id = app_id - mock_conversation.id = conversation_id - - mock_message = MagicMock() - mock_message.id = str(uuid4()) - - # Mock app config - mock_app_config = MagicMock() - mock_app_config.app_id = app_id - mock_app_config.workflow_id = workflow_id - mock_app_config.tenant_id = str(uuid4()) - - # Mock app generate entity - mock_app_generate_entity = MagicMock(spec=AdvancedChatAppGenerateEntity) - mock_app_generate_entity.app_config = mock_app_config - mock_app_generate_entity.inputs = {} - mock_app_generate_entity.query = "test query" - mock_app_generate_entity.files = [] - mock_app_generate_entity.user_id = str(uuid4()) - mock_app_generate_entity.invoke_from = InvokeFrom.SERVICE_API - mock_app_generate_entity.workflow_run_id = str(uuid4()) - mock_app_generate_entity.task_id = str(uuid4()) - mock_app_generate_entity.call_depth = 0 - mock_app_generate_entity.single_iteration_run = None - mock_app_generate_entity.single_loop_run = None - mock_app_generate_entity.extras = {} - mock_app_generate_entity.trace_manager = None - - # Create runner - runner = AdvancedChatAppRunner( - application_generate_entity=mock_app_generate_entity, - queue_manager=MagicMock(), - conversation=mock_conversation, - message=mock_message, - dialogue_count=1, - variable_loader=MagicMock(), - workflow=mock_workflow, - system_user_id=str(uuid4()), - app=MagicMock(), - workflow_execution_repository=MagicMock(), - workflow_node_execution_repository=MagicMock(), - ) - - # Mock database session - mock_session = MagicMock(spec=Session) - - # Query returns empty list (no existing variables) - mock_scalars_result = MagicMock() - mock_scalars_result.all.return_value = [] - mock_session.scalars.return_value = mock_scalars_result - - # Track what gets added to session - added_items = [] - - def track_add_all(items): - added_items.extend(items) - - mock_session.add_all.side_effect = track_add_all - - # Patch the necessary components - with ( - _patch_create_session(mock_session), - patch("core.app.apps.advanced_chat.app_runner.select") as mock_select, - patch.object(runner, "_init_graph") as mock_init_graph, - patch.object( - runner, - "handle_input_moderation", - return_value=(False, mock_app_generate_entity.inputs, mock_app_generate_entity.query), - ), - patch.object(runner, "handle_annotation_reply", return_value=False), - patch("core.app.apps.advanced_chat.app_runner.WorkflowEntry") as mock_workflow_entry_class, - patch("core.app.apps.advanced_chat.app_runner.GraphRuntimeState") as mock_graph_runtime_state_class, - patch("core.app.apps.advanced_chat.app_runner.ConversationVariable") as mock_conv_var_class, - patch("core.app.apps.advanced_chat.app_runner.redis_client") as mock_redis_client, - patch("core.app.apps.advanced_chat.app_runner.RedisChannel") as mock_redis_channel_class, - ): - # Mock ConversationVariable.from_variable to return mock objects - mock_conv_vars = [] - for var in workflow_vars: - mock_cv = MagicMock() - mock_cv.id = var.id - mock_cv.to_variable.return_value = var - mock_conv_vars.append(mock_cv) - - mock_conv_var_class.from_variable.side_effect = mock_conv_vars - - # Mock GraphRuntimeState to accept the variable pool - mock_graph_runtime_state_class.return_value = MagicMock() - - # Mock graph initialization - mock_init_graph.return_value = MagicMock() - - # Mock workflow entry - mock_workflow_entry = MagicMock() - mock_workflow_entry.run.return_value = iter([]) # Empty generator - mock_workflow_entry_class.return_value = mock_workflow_entry - - # Run the method - runner.run() - - # Verify that all variables were created - assert len(added_items) == 2, "Should have added both variables" - assert mock_session.add_all.called, "Session add_all should have been called" - - def test_all_variables_exist_no_changes(self): - """Test that no changes are made when all variables already exist in DB.""" - # Setup - app_id = str(uuid4()) - conversation_id = str(uuid4()) - workflow_id = str(uuid4()) - - # Create workflow with conversation variables - workflow_vars = [ - variable_factory.build_conversation_variable_from_mapping( - { - "id": "var1", - "name": "var1", - "value_type": SegmentType.STRING, - "value": "default1", - } - ), - variable_factory.build_conversation_variable_from_mapping( - { - "id": "var2", - "name": "var2", - "value_type": SegmentType.STRING, - "value": "default2", - } - ), - ] - - # Mock workflow - mock_workflow = MagicMock(spec=Workflow) - mock_workflow.conversation_variables = workflow_vars - mock_workflow.tenant_id = str(uuid4()) - mock_workflow.app_id = app_id - mock_workflow.id = workflow_id - mock_workflow.type = "chat" - mock_workflow.graph_dict = MINIMAL_GRAPH - mock_workflow.environment_variables = [] - - # Create existing conversation variables (both exist in DB) - existing_db_vars = [] - for var in workflow_vars: - db_var = MagicMock(spec=ConversationVariable) - db_var.id = var.id - db_var.app_id = app_id - db_var.conversation_id = conversation_id - db_var.to_variable = MagicMock(return_value=var) - existing_db_vars.append(db_var) - - # Mock conversation and message - mock_conversation = MagicMock() - mock_conversation.app_id = app_id - mock_conversation.id = conversation_id - - mock_message = MagicMock() - mock_message.id = str(uuid4()) - - # Mock app config - mock_app_config = MagicMock() - mock_app_config.app_id = app_id - mock_app_config.workflow_id = workflow_id - mock_app_config.tenant_id = str(uuid4()) - - # Mock app generate entity - mock_app_generate_entity = MagicMock(spec=AdvancedChatAppGenerateEntity) - mock_app_generate_entity.app_config = mock_app_config - mock_app_generate_entity.inputs = {} - mock_app_generate_entity.query = "test query" - mock_app_generate_entity.files = [] - mock_app_generate_entity.user_id = str(uuid4()) - mock_app_generate_entity.invoke_from = InvokeFrom.SERVICE_API - mock_app_generate_entity.workflow_run_id = str(uuid4()) - mock_app_generate_entity.task_id = str(uuid4()) - mock_app_generate_entity.call_depth = 0 - mock_app_generate_entity.single_iteration_run = None - mock_app_generate_entity.single_loop_run = None - mock_app_generate_entity.extras = {} - mock_app_generate_entity.trace_manager = None - - # Create runner - runner = AdvancedChatAppRunner( - application_generate_entity=mock_app_generate_entity, - queue_manager=MagicMock(), - conversation=mock_conversation, - message=mock_message, - dialogue_count=1, - variable_loader=MagicMock(), - workflow=mock_workflow, - system_user_id=str(uuid4()), - app=MagicMock(), - workflow_execution_repository=MagicMock(), - workflow_node_execution_repository=MagicMock(), - ) - - # Mock database session - mock_session = MagicMock(spec=Session) - - # Query returns all existing variables - mock_scalars_result = MagicMock() - mock_scalars_result.all.return_value = existing_db_vars - mock_session.scalars.return_value = mock_scalars_result - - # Patch the necessary components - with ( - _patch_create_session(mock_session), - patch("core.app.apps.advanced_chat.app_runner.select") as mock_select, - patch.object(runner, "_init_graph") as mock_init_graph, - patch.object( - runner, - "handle_input_moderation", - return_value=(False, mock_app_generate_entity.inputs, mock_app_generate_entity.query), - ), - patch.object(runner, "handle_annotation_reply", return_value=False), - patch("core.app.apps.advanced_chat.app_runner.WorkflowEntry") as mock_workflow_entry_class, - patch("core.app.apps.advanced_chat.app_runner.GraphRuntimeState") as mock_graph_runtime_state_class, - patch("core.app.apps.advanced_chat.app_runner.redis_client") as mock_redis_client, - patch("core.app.apps.advanced_chat.app_runner.RedisChannel") as mock_redis_channel_class, - ): - # Mock GraphRuntimeState to accept the variable pool - mock_graph_runtime_state_class.return_value = MagicMock() - - # Mock graph initialization - mock_init_graph.return_value = MagicMock() - - # Mock workflow entry - mock_workflow_entry = MagicMock() - mock_workflow_entry.run.return_value = iter([]) # Empty generator - mock_workflow_entry_class.return_value = mock_workflow_entry - - # Run the method - runner.run() - - # Verify that no variables were added - assert not mock_session.add_all.called, "Session add_all should not have been called" + assert [variable.id for variable in variables] == [VAR_1_ID, VAR_2_ID] + persisted = sqlite_session.scalars(select(ConversationVariable)).all() + assert len(persisted) == 2