| @@ -0,0 +1,739 @@ | |||
| import pytest | |||
| from faker import Faker | |||
| from core.variables.segments import StringSegment | |||
| from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID | |||
| from models import App, Workflow | |||
| from models.enums import DraftVariableType | |||
| from models.workflow import WorkflowDraftVariable | |||
| from services.workflow_draft_variable_service import ( | |||
| UpdateNotSupportedError, | |||
| WorkflowDraftVariableService, | |||
| ) | |||
| class TestWorkflowDraftVariableService: | |||
| """ | |||
| Comprehensive integration tests for WorkflowDraftVariableService using testcontainers. | |||
| This test class covers all major functionality of the WorkflowDraftVariableService: | |||
| - CRUD operations for workflow draft variables (Create, Read, Update, Delete) | |||
| - Variable listing and filtering by type (conversation, system, node) | |||
| - Variable updates and resets with proper validation | |||
| - Variable deletion operations at different scopes | |||
| - Special functionality like prefill and conversation ID retrieval | |||
| - Error handling for various edge cases and invalid operations | |||
| All tests use the testcontainers infrastructure to ensure proper database isolation | |||
| and realistic testing environment with actual database interactions. | |||
| """ | |||
| @pytest.fixture | |||
| def mock_external_service_dependencies(self): | |||
| """ | |||
| Mock setup for external service dependencies. | |||
| WorkflowDraftVariableService doesn't have external dependencies that need mocking, | |||
| so this fixture returns an empty dictionary to maintain consistency with other test classes. | |||
| This ensures the test structure remains consistent across different service test files. | |||
| """ | |||
| # WorkflowDraftVariableService doesn't have external dependencies that need mocking | |||
| return {} | |||
| def _create_test_app(self, db_session_with_containers, mock_external_service_dependencies, fake=None): | |||
| """ | |||
| Helper method to create a test app with realistic data for testing. | |||
| This method creates a complete App instance with all required fields populated | |||
| using Faker for generating realistic test data. The app is configured for | |||
| workflow mode to support workflow draft variable testing. | |||
| Args: | |||
| db_session_with_containers: Database session from testcontainers infrastructure | |||
| mock_external_service_dependencies: Mock dependencies (unused in this service) | |||
| fake: Faker instance for generating test data, creates new instance if not provided | |||
| Returns: | |||
| App: Created test app instance with all required fields populated | |||
| """ | |||
| fake = fake or Faker() | |||
| app = App() | |||
| app.id = fake.uuid4() | |||
| app.tenant_id = fake.uuid4() | |||
| app.name = fake.company() | |||
| app.description = fake.text() | |||
| app.mode = "workflow" | |||
| app.icon_type = "emoji" | |||
| app.icon = "🤖" | |||
| app.icon_background = "#FFEAD5" | |||
| app.enable_site = True | |||
| app.enable_api = True | |||
| app.created_by = fake.uuid4() | |||
| app.updated_by = app.created_by | |||
| from extensions.ext_database import db | |||
| db.session.add(app) | |||
| db.session.commit() | |||
| return app | |||
| def _create_test_workflow(self, db_session_with_containers, app, fake=None): | |||
| """ | |||
| Helper method to create a test workflow associated with an app. | |||
| This method creates a Workflow instance using the proper factory method | |||
| to ensure all required fields are set correctly. The workflow is configured | |||
| as a draft version with basic graph structure for testing workflow variables. | |||
| Args: | |||
| db_session_with_containers: Database session from testcontainers infrastructure | |||
| app: The app to associate the workflow with | |||
| fake: Faker instance for generating test data, creates new instance if not provided | |||
| Returns: | |||
| Workflow: Created test workflow instance with proper configuration | |||
| """ | |||
| fake = fake or Faker() | |||
| workflow = Workflow.new( | |||
| tenant_id=app.tenant_id, | |||
| app_id=app.id, | |||
| type="workflow", | |||
| version="draft", | |||
| graph='{"nodes": [], "edges": []}', | |||
| features="{}", | |||
| created_by=app.created_by, | |||
| environment_variables=[], | |||
| conversation_variables=[], | |||
| ) | |||
| from extensions.ext_database import db | |||
| db.session.add(workflow) | |||
| db.session.commit() | |||
| return workflow | |||
| def _create_test_variable( | |||
| self, db_session_with_containers, app_id, node_id, name, value, variable_type="conversation", fake=None | |||
| ): | |||
| """ | |||
| Helper method to create a test workflow draft variable with proper configuration. | |||
| This method creates different types of variables (conversation, system, node) using | |||
| the appropriate factory methods to ensure proper initialization. Each variable type | |||
| has specific requirements and this method handles the creation logic for all types. | |||
| Args: | |||
| db_session_with_containers: Database session from testcontainers infrastructure | |||
| app_id: ID of the app to associate the variable with | |||
| node_id: ID of the node (or special constants like CONVERSATION_VARIABLE_NODE_ID) | |||
| name: Name of the variable for identification | |||
| value: StringSegment value for the variable content | |||
| variable_type: Type of variable ("conversation", "system", "node") determining creation method | |||
| fake: Faker instance for generating test data, creates new instance if not provided | |||
| Returns: | |||
| WorkflowDraftVariable: Created test variable instance with proper type configuration | |||
| """ | |||
| fake = fake or Faker() | |||
| if variable_type == "conversation": | |||
| # Create conversation variable using the appropriate factory method | |||
| variable = WorkflowDraftVariable.new_conversation_variable( | |||
| app_id=app_id, | |||
| name=name, | |||
| value=value, | |||
| description=fake.text(max_nb_chars=20), | |||
| ) | |||
| elif variable_type == "system": | |||
| # Create system variable with editable flag and execution context | |||
| variable = WorkflowDraftVariable.new_sys_variable( | |||
| app_id=app_id, | |||
| name=name, | |||
| value=value, | |||
| node_execution_id=fake.uuid4(), | |||
| editable=True, | |||
| ) | |||
| else: # node variable | |||
| # Create node variable with visibility and editability settings | |||
| variable = WorkflowDraftVariable.new_node_variable( | |||
| app_id=app_id, | |||
| node_id=node_id, | |||
| name=name, | |||
| value=value, | |||
| node_execution_id=fake.uuid4(), | |||
| visible=True, | |||
| editable=True, | |||
| ) | |||
| from extensions.ext_database import db | |||
| db.session.add(variable) | |||
| db.session.commit() | |||
| return variable | |||
| def test_get_variable_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test getting a single variable by ID successfully. | |||
| This test verifies that the service can retrieve a specific variable | |||
| by its ID and that the returned variable contains the correct data. | |||
| It ensures the basic CRUD read operation works correctly for workflow draft variables. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| test_value = StringSegment(value=fake.word()) | |||
| variable = self._create_test_variable( | |||
| db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "test_var", test_value, fake=fake | |||
| ) | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| retrieved_variable = service.get_variable(variable.id) | |||
| assert retrieved_variable is not None | |||
| assert retrieved_variable.id == variable.id | |||
| assert retrieved_variable.name == "test_var" | |||
| assert retrieved_variable.app_id == app.id | |||
| assert retrieved_variable.get_value().value == test_value.value | |||
| def test_get_variable_not_found(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test getting a variable that doesn't exist. | |||
| This test verifies that the service returns None when trying to | |||
| retrieve a variable with a non-existent ID. This ensures proper | |||
| handling of missing data scenarios. | |||
| """ | |||
| fake = Faker() | |||
| non_existent_id = fake.uuid4() | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| retrieved_variable = service.get_variable(non_existent_id) | |||
| assert retrieved_variable is None | |||
| def test_get_draft_variables_by_selectors_success( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test getting variables by selectors successfully. | |||
| This test verifies that the service can retrieve multiple variables | |||
| using selector pairs (node_id, variable_name) and returns the correct | |||
| variables for each selector. This is useful for bulk variable retrieval | |||
| operations in workflow execution contexts. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| var1_value = StringSegment(value=fake.word()) | |||
| var2_value = StringSegment(value=fake.word()) | |||
| var3_value = StringSegment(value=fake.word()) | |||
| var1 = self._create_test_variable( | |||
| db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "var1", var1_value, fake=fake | |||
| ) | |||
| var2 = self._create_test_variable( | |||
| db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "var2", var2_value, fake=fake | |||
| ) | |||
| var3 = self._create_test_variable( | |||
| db_session_with_containers, app.id, "test_node_1", "var3", var3_value, "node", fake=fake | |||
| ) | |||
| selectors = [ | |||
| [CONVERSATION_VARIABLE_NODE_ID, "var1"], | |||
| [CONVERSATION_VARIABLE_NODE_ID, "var2"], | |||
| ["test_node_1", "var3"], | |||
| ] | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| retrieved_variables = service.get_draft_variables_by_selectors(app.id, selectors) | |||
| assert len(retrieved_variables) == 3 | |||
| var_names = [var.name for var in retrieved_variables] | |||
| assert "var1" in var_names | |||
| assert "var2" in var_names | |||
| assert "var3" in var_names | |||
| for var in retrieved_variables: | |||
| if var.name == "var1": | |||
| assert var.get_value().value == var1_value.value | |||
| elif var.name == "var2": | |||
| assert var.get_value().value == var2_value.value | |||
| elif var.name == "var3": | |||
| assert var.get_value().value == var3_value.value | |||
| def test_list_variables_without_values_success( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test listing variables without values successfully with pagination. | |||
| This test verifies that the service can list variables with pagination | |||
| and that the returned variables don't include their values (for performance). | |||
| This is important for scenarios where only variable metadata is needed | |||
| without loading the actual content. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| for i in range(5): | |||
| test_value = StringSegment(value=fake.numerify("value##")) | |||
| self._create_test_variable( | |||
| db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, fake.word(), test_value, fake=fake | |||
| ) | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| result = service.list_variables_without_values(app.id, page=1, limit=3) | |||
| assert result.total == 5 | |||
| assert len(result.variables) == 3 | |||
| assert result.variables[0].created_at >= result.variables[1].created_at | |||
| assert result.variables[1].created_at >= result.variables[2].created_at | |||
| for var in result.variables: | |||
| assert var.name is not None | |||
| assert var.app_id == app.id | |||
| def test_list_node_variables_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test listing variables for a specific node successfully. | |||
| This test verifies that the service can filter and return only | |||
| variables associated with a specific node ID. This is crucial for | |||
| workflow execution where variables need to be scoped to specific nodes. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| node_id = fake.word() | |||
| var1_value = StringSegment(value=fake.word()) | |||
| var2_value = StringSegment(value=fake.word()) | |||
| var3_value = StringSegment(value=fake.word()) | |||
| self._create_test_variable(db_session_with_containers, app.id, node_id, "var1", var1_value, "node", fake=fake) | |||
| self._create_test_variable(db_session_with_containers, app.id, node_id, "var2", var3_value, "node", fake=fake) | |||
| self._create_test_variable( | |||
| db_session_with_containers, app.id, "other_node", "var3", var2_value, "node", fake=fake | |||
| ) | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| result = service.list_node_variables(app.id, node_id) | |||
| assert len(result.variables) == 2 | |||
| for var in result.variables: | |||
| assert var.node_id == node_id | |||
| assert var.app_id == app.id | |||
| var_names = [var.name for var in result.variables] | |||
| assert "var1" in var_names | |||
| assert "var2" in var_names | |||
| assert "var3" not in var_names | |||
| def test_list_conversation_variables_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test listing conversation variables successfully. | |||
| This test verifies that the service can filter and return only | |||
| conversation variables, excluding system and node variables. | |||
| Conversation variables are user-facing variables that can be | |||
| modified during conversation flows. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| conv_var1_value = StringSegment(value=fake.word()) | |||
| conv_var2_value = StringSegment(value=fake.word()) | |||
| conv_var1 = self._create_test_variable( | |||
| db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "conv_var1", conv_var1_value, fake=fake | |||
| ) | |||
| conv_var2 = self._create_test_variable( | |||
| db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "conv_var2", conv_var2_value, fake=fake | |||
| ) | |||
| sys_var_value = StringSegment(value=fake.word()) | |||
| self._create_test_variable( | |||
| db_session_with_containers, app.id, SYSTEM_VARIABLE_NODE_ID, "sys_var", sys_var_value, "system", fake=fake | |||
| ) | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| result = service.list_conversation_variables(app.id) | |||
| assert len(result.variables) == 2 | |||
| for var in result.variables: | |||
| assert var.node_id == CONVERSATION_VARIABLE_NODE_ID | |||
| assert var.app_id == app.id | |||
| assert var.get_variable_type() == DraftVariableType.CONVERSATION | |||
| var_names = [var.name for var in result.variables] | |||
| assert "conv_var1" in var_names | |||
| assert "conv_var2" in var_names | |||
| assert "sys_var" not in var_names | |||
| def test_update_variable_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test updating a variable's name and value successfully. | |||
| This test verifies that the service can update both the name and value | |||
| of an editable variable and that the changes are persisted correctly. | |||
| It also checks that the last_edited_at timestamp is updated appropriately. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| original_value = StringSegment(value=fake.word()) | |||
| new_value = StringSegment(value=fake.word()) | |||
| variable = self._create_test_variable( | |||
| db_session_with_containers, | |||
| app.id, | |||
| CONVERSATION_VARIABLE_NODE_ID, | |||
| "original_name", | |||
| original_value, | |||
| fake=fake, | |||
| ) | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| updated_variable = service.update_variable(variable, name="new_name", value=new_value) | |||
| assert updated_variable.name == "new_name" | |||
| assert updated_variable.get_value().value == new_value.value | |||
| assert updated_variable.last_edited_at is not None | |||
| from extensions.ext_database import db | |||
| db.session.refresh(variable) | |||
| assert variable.name == "new_name" | |||
| assert variable.get_value().value == new_value.value | |||
| assert variable.last_edited_at is not None | |||
| def test_update_variable_not_editable(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test that updating a non-editable variable raises an exception. | |||
| This test verifies that the service properly prevents updates to | |||
| variables that are not marked as editable. This is important for | |||
| maintaining data integrity and preventing unauthorized modifications | |||
| to system-controlled variables. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| original_value = StringSegment(value=fake.word()) | |||
| new_value = StringSegment(value=fake.word()) | |||
| variable = WorkflowDraftVariable.new_sys_variable( | |||
| app_id=app.id, | |||
| name=fake.word(), # This is typically not editable | |||
| value=original_value, | |||
| node_execution_id=fake.uuid4(), | |||
| editable=False, # Set as non-editable | |||
| ) | |||
| from extensions.ext_database import db | |||
| db.session.add(variable) | |||
| db.session.commit() | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| with pytest.raises(UpdateNotSupportedError) as exc_info: | |||
| service.update_variable(variable, name="new_name", value=new_value) | |||
| assert "variable not support updating" in str(exc_info.value) | |||
| assert variable.id in str(exc_info.value) | |||
| def test_reset_conversation_variable_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test resetting conversation variable successfully. | |||
| This test verifies that the service can reset a conversation variable | |||
| to its default value and clear the last_edited_at timestamp. | |||
| This functionality is useful for reverting user modifications | |||
| back to the original workflow configuration. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| workflow = self._create_test_workflow(db_session_with_containers, app, fake=fake) | |||
| from core.variables.variables import StringVariable | |||
| conv_var = StringVariable( | |||
| id=fake.uuid4(), | |||
| name="test_conv_var", | |||
| value="default_value", | |||
| selector=[CONVERSATION_VARIABLE_NODE_ID, "test_conv_var"], | |||
| ) | |||
| workflow.conversation_variables = [conv_var] | |||
| from extensions.ext_database import db | |||
| db.session.commit() | |||
| modified_value = StringSegment(value=fake.word()) | |||
| variable = self._create_test_variable( | |||
| db_session_with_containers, | |||
| app.id, | |||
| CONVERSATION_VARIABLE_NODE_ID, | |||
| "test_conv_var", | |||
| modified_value, | |||
| fake=fake, | |||
| ) | |||
| variable.last_edited_at = fake.date_time() | |||
| db.session.commit() | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| reset_variable = service.reset_variable(workflow, variable) | |||
| assert reset_variable is not None | |||
| assert reset_variable.get_value().value == "default_value" | |||
| assert reset_variable.last_edited_at is None | |||
| db.session.refresh(variable) | |||
| assert variable.get_value().value == "default_value" | |||
| assert variable.last_edited_at is None | |||
| def test_delete_variable_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test deleting a single variable successfully. | |||
| This test verifies that the service can delete a specific variable | |||
| and that it's properly removed from the database. It ensures that | |||
| the deletion operation is atomic and complete. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| test_value = StringSegment(value=fake.word()) | |||
| variable = self._create_test_variable( | |||
| db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "test_var", test_value, fake=fake | |||
| ) | |||
| from extensions.ext_database import db | |||
| assert db.session.query(WorkflowDraftVariable).filter_by(id=variable.id).first() is not None | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| service.delete_variable(variable) | |||
| assert db.session.query(WorkflowDraftVariable).filter_by(id=variable.id).first() is None | |||
| def test_delete_workflow_variables_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test deleting all variables for a workflow successfully. | |||
| This test verifies that the service can delete all variables | |||
| associated with a specific app/workflow. This is useful for | |||
| cleanup operations when workflows are deleted or reset. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| for i in range(3): | |||
| test_value = StringSegment(value=fake.numerify("value##")) | |||
| self._create_test_variable( | |||
| db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, fake.word(), test_value, fake=fake | |||
| ) | |||
| other_app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| other_value = StringSegment(value=fake.word()) | |||
| self._create_test_variable( | |||
| db_session_with_containers, other_app.id, CONVERSATION_VARIABLE_NODE_ID, fake.word(), other_value, fake=fake | |||
| ) | |||
| from extensions.ext_database import db | |||
| app_variables = db.session.query(WorkflowDraftVariable).filter_by(app_id=app.id).all() | |||
| other_app_variables = db.session.query(WorkflowDraftVariable).filter_by(app_id=other_app.id).all() | |||
| assert len(app_variables) == 3 | |||
| assert len(other_app_variables) == 1 | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| service.delete_workflow_variables(app.id) | |||
| app_variables_after = db.session.query(WorkflowDraftVariable).filter_by(app_id=app.id).all() | |||
| other_app_variables_after = db.session.query(WorkflowDraftVariable).filter_by(app_id=other_app.id).all() | |||
| assert len(app_variables_after) == 0 | |||
| assert len(other_app_variables_after) == 1 | |||
| def test_delete_node_variables_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test deleting all variables for a specific node successfully. | |||
| This test verifies that the service can delete all variables | |||
| associated with a specific node while preserving variables | |||
| for other nodes and conversation variables. This is important | |||
| for node-specific cleanup operations in workflow management. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| node_id = fake.word() | |||
| for i in range(2): | |||
| test_value = StringSegment(value=fake.numerify("node_value##")) | |||
| self._create_test_variable( | |||
| db_session_with_containers, app.id, node_id, fake.word(), test_value, "node", fake=fake | |||
| ) | |||
| other_node_value = StringSegment(value=fake.word()) | |||
| self._create_test_variable( | |||
| db_session_with_containers, app.id, "other_node", fake.word(), other_node_value, "node", fake=fake | |||
| ) | |||
| conv_value = StringSegment(value=fake.word()) | |||
| self._create_test_variable( | |||
| db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, fake.word(), conv_value, fake=fake | |||
| ) | |||
| from extensions.ext_database import db | |||
| target_node_variables = db.session.query(WorkflowDraftVariable).filter_by(app_id=app.id, node_id=node_id).all() | |||
| other_node_variables = ( | |||
| db.session.query(WorkflowDraftVariable).filter_by(app_id=app.id, node_id="other_node").all() | |||
| ) | |||
| conv_variables = ( | |||
| db.session.query(WorkflowDraftVariable) | |||
| .filter_by(app_id=app.id, node_id=CONVERSATION_VARIABLE_NODE_ID) | |||
| .all() | |||
| ) | |||
| assert len(target_node_variables) == 2 | |||
| assert len(other_node_variables) == 1 | |||
| assert len(conv_variables) == 1 | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| service.delete_node_variables(app.id, node_id) | |||
| target_node_variables_after = ( | |||
| db.session.query(WorkflowDraftVariable).filter_by(app_id=app.id, node_id=node_id).all() | |||
| ) | |||
| other_node_variables_after = ( | |||
| db.session.query(WorkflowDraftVariable).filter_by(app_id=app.id, node_id="other_node").all() | |||
| ) | |||
| conv_variables_after = ( | |||
| db.session.query(WorkflowDraftVariable) | |||
| .filter_by(app_id=app.id, node_id=CONVERSATION_VARIABLE_NODE_ID) | |||
| .all() | |||
| ) | |||
| assert len(target_node_variables_after) == 0 | |||
| assert len(other_node_variables_after) == 1 | |||
| assert len(conv_variables_after) == 1 | |||
| def test_prefill_conversation_variable_default_values_success( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test prefill conversation variable default values successfully. | |||
| This test verifies that the service can automatically create | |||
| conversation variables with default values based on the workflow | |||
| configuration when none exist. This is important for initializing | |||
| workflow variables with proper defaults from the workflow definition. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| workflow = self._create_test_workflow(db_session_with_containers, app, fake=fake) | |||
| from core.variables.variables import StringVariable | |||
| conv_var1 = StringVariable( | |||
| id=fake.uuid4(), | |||
| name="conv_var1", | |||
| value="default_value1", | |||
| selector=[CONVERSATION_VARIABLE_NODE_ID, "conv_var1"], | |||
| ) | |||
| conv_var2 = StringVariable( | |||
| id=fake.uuid4(), | |||
| name="conv_var2", | |||
| value="default_value2", | |||
| selector=[CONVERSATION_VARIABLE_NODE_ID, "conv_var2"], | |||
| ) | |||
| workflow.conversation_variables = [conv_var1, conv_var2] | |||
| from extensions.ext_database import db | |||
| db.session.commit() | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| service.prefill_conversation_variable_default_values(workflow) | |||
| draft_variables = ( | |||
| db.session.query(WorkflowDraftVariable) | |||
| .filter_by(app_id=app.id, node_id=CONVERSATION_VARIABLE_NODE_ID) | |||
| .all() | |||
| ) | |||
| assert len(draft_variables) == 2 | |||
| var_names = [var.name for var in draft_variables] | |||
| assert "conv_var1" in var_names | |||
| assert "conv_var2" in var_names | |||
| for var in draft_variables: | |||
| assert var.app_id == app.id | |||
| assert var.node_id == CONVERSATION_VARIABLE_NODE_ID | |||
| assert var.editable is True | |||
| assert var.get_variable_type() == DraftVariableType.CONVERSATION | |||
| def test_get_conversation_id_from_draft_variable_success( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test getting conversation ID from draft variable successfully. | |||
| This test verifies that the service can extract the conversation ID | |||
| from a system variable named "conversation_id". This is important | |||
| for maintaining conversation context across workflow executions. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| conversation_id = fake.uuid4() | |||
| conv_id_value = StringSegment(value=conversation_id) | |||
| self._create_test_variable( | |||
| db_session_with_containers, | |||
| app.id, | |||
| SYSTEM_VARIABLE_NODE_ID, | |||
| "conversation_id", | |||
| conv_id_value, | |||
| "system", | |||
| fake=fake, | |||
| ) | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| retrieved_conv_id = service._get_conversation_id_from_draft_variable(app.id) | |||
| assert retrieved_conv_id == conversation_id | |||
| def test_get_conversation_id_from_draft_variable_not_found( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test getting conversation ID when it doesn't exist. | |||
| This test verifies that the service returns None when no | |||
| conversation_id variable exists for the app. This ensures | |||
| proper handling of missing conversation context scenarios. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| retrieved_conv_id = service._get_conversation_id_from_draft_variable(app.id) | |||
| assert retrieved_conv_id is None | |||
| def test_list_system_variables_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test listing system variables successfully. | |||
| This test verifies that the service can filter and return only | |||
| system variables, excluding conversation and node variables. | |||
| System variables are internal variables used by the workflow | |||
| engine for maintaining state and context. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| sys_var1_value = StringSegment(value=fake.word()) | |||
| sys_var2_value = StringSegment(value=fake.word()) | |||
| sys_var1 = self._create_test_variable( | |||
| db_session_with_containers, app.id, SYSTEM_VARIABLE_NODE_ID, "sys_var1", sys_var1_value, "system", fake=fake | |||
| ) | |||
| sys_var2 = self._create_test_variable( | |||
| db_session_with_containers, app.id, SYSTEM_VARIABLE_NODE_ID, "sys_var2", sys_var2_value, "system", fake=fake | |||
| ) | |||
| conv_var_value = StringSegment(value=fake.word()) | |||
| self._create_test_variable( | |||
| db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "conv_var", conv_var_value, fake=fake | |||
| ) | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| result = service.list_system_variables(app.id) | |||
| assert len(result.variables) == 2 | |||
| for var in result.variables: | |||
| assert var.node_id == SYSTEM_VARIABLE_NODE_ID | |||
| assert var.app_id == app.id | |||
| assert var.get_variable_type() == DraftVariableType.SYS | |||
| var_names = [var.name for var in result.variables] | |||
| assert "sys_var1" in var_names | |||
| assert "sys_var2" in var_names | |||
| assert "conv_var" not in var_names | |||
| def test_get_variable_by_name_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test getting variables by name successfully for different types. | |||
| This test verifies that the service can retrieve variables by name | |||
| for different variable types (conversation, system, node). This | |||
| functionality is important for variable lookup operations during | |||
| workflow execution and user interactions. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| test_value = StringSegment(value=fake.word()) | |||
| conv_var = self._create_test_variable( | |||
| db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "test_conv_var", test_value, fake=fake | |||
| ) | |||
| sys_var = self._create_test_variable( | |||
| db_session_with_containers, app.id, SYSTEM_VARIABLE_NODE_ID, "test_sys_var", test_value, "system", fake=fake | |||
| ) | |||
| node_var = self._create_test_variable( | |||
| db_session_with_containers, app.id, "test_node", "test_node_var", test_value, "node", fake=fake | |||
| ) | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| retrieved_conv_var = service.get_conversation_variable(app.id, "test_conv_var") | |||
| assert retrieved_conv_var is not None | |||
| assert retrieved_conv_var.name == "test_conv_var" | |||
| assert retrieved_conv_var.node_id == CONVERSATION_VARIABLE_NODE_ID | |||
| retrieved_sys_var = service.get_system_variable(app.id, "test_sys_var") | |||
| assert retrieved_sys_var is not None | |||
| assert retrieved_sys_var.name == "test_sys_var" | |||
| assert retrieved_sys_var.node_id == SYSTEM_VARIABLE_NODE_ID | |||
| retrieved_node_var = service.get_node_variable(app.id, "test_node", "test_node_var") | |||
| assert retrieved_node_var is not None | |||
| assert retrieved_node_var.name == "test_node_var" | |||
| assert retrieved_node_var.node_id == "test_node" | |||
| def test_get_variable_by_name_not_found(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test getting variables by name when they don't exist. | |||
| This test verifies that the service returns None when trying to | |||
| retrieve variables by name that don't exist. This ensures proper | |||
| handling of missing variable scenarios for all variable types. | |||
| """ | |||
| fake = Faker() | |||
| app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake) | |||
| service = WorkflowDraftVariableService(db_session_with_containers) | |||
| retrieved_conv_var = service.get_conversation_variable(app.id, "non_existent_conv_var") | |||
| assert retrieved_conv_var is None | |||
| retrieved_sys_var = service.get_system_variable(app.id, "non_existent_sys_var") | |||
| assert retrieved_sys_var is None | |||
| retrieved_node_var = service.get_node_variable(app.id, "test_node", "non_existent_node_var") | |||
| assert retrieved_node_var is None | |||