| @@ -0,0 +1,713 @@ | |||
| import json | |||
| import uuid | |||
| from datetime import UTC, datetime, timedelta | |||
| from unittest.mock import patch | |||
| import pytest | |||
| from faker import Faker | |||
| from models.enums import CreatorUserRole | |||
| from models.model import ( | |||
| Message, | |||
| ) | |||
| from models.workflow import WorkflowRun | |||
| from services.account_service import AccountService, TenantService | |||
| from services.app_service import AppService | |||
| from services.workflow_run_service import WorkflowRunService | |||
| class TestWorkflowRunService: | |||
| """Integration tests for WorkflowRunService using testcontainers.""" | |||
| @pytest.fixture | |||
| def mock_external_service_dependencies(self): | |||
| """Mock setup for external service dependencies.""" | |||
| with ( | |||
| patch("services.app_service.FeatureService") as mock_feature_service, | |||
| patch("services.app_service.EnterpriseService") as mock_enterprise_service, | |||
| patch("services.app_service.ModelManager") as mock_model_manager, | |||
| patch("services.account_service.FeatureService") as mock_account_feature_service, | |||
| ): | |||
| # Setup default mock returns for app service | |||
| mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False | |||
| mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None | |||
| mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None | |||
| # Setup default mock returns for account service | |||
| mock_account_feature_service.get_system_features.return_value.is_allow_register = True | |||
| # Mock ModelManager for model configuration | |||
| mock_model_instance = mock_model_manager.return_value | |||
| mock_model_instance.get_default_model_instance.return_value = None | |||
| mock_model_instance.get_default_provider_model_name.return_value = ("openai", "gpt-3.5-turbo") | |||
| yield { | |||
| "feature_service": mock_feature_service, | |||
| "enterprise_service": mock_enterprise_service, | |||
| "model_manager": mock_model_manager, | |||
| "account_feature_service": mock_account_feature_service, | |||
| } | |||
| def _create_test_app_and_account(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Helper method to create a test app and account for testing. | |||
| Args: | |||
| db_session_with_containers: Database session from testcontainers infrastructure | |||
| mock_external_service_dependencies: Mock dependencies | |||
| Returns: | |||
| tuple: (app, account) - Created app and account instances | |||
| """ | |||
| fake = Faker() | |||
| # Setup mocks for account creation | |||
| mock_external_service_dependencies[ | |||
| "account_feature_service" | |||
| ].get_system_features.return_value.is_allow_register = True | |||
| # Create account and tenant | |||
| account = AccountService.create_account( | |||
| email=fake.email(), | |||
| name=fake.name(), | |||
| interface_language="en-US", | |||
| password=fake.password(length=12), | |||
| ) | |||
| TenantService.create_owner_tenant_if_not_exist(account, name=fake.company()) | |||
| tenant = account.current_tenant | |||
| # Create app with realistic data | |||
| app_args = { | |||
| "name": fake.company(), | |||
| "description": fake.text(max_nb_chars=100), | |||
| "mode": "chat", | |||
| "icon_type": "emoji", | |||
| "icon": "🤖", | |||
| "icon_background": "#FF6B6B", | |||
| "api_rph": 100, | |||
| "api_rpm": 10, | |||
| } | |||
| app_service = AppService() | |||
| app = app_service.create_app(tenant.id, app_args, account) | |||
| return app, account | |||
| def _create_test_workflow_run( | |||
| self, db_session_with_containers, app, account, triggered_from="debugging", offset_minutes=0 | |||
| ): | |||
| """ | |||
| Helper method to create a test workflow run for testing. | |||
| Args: | |||
| db_session_with_containers: Database session from testcontainers infrastructure | |||
| app: App instance | |||
| account: Account instance | |||
| triggered_from: Trigger source for workflow run | |||
| Returns: | |||
| WorkflowRun: Created workflow run instance | |||
| """ | |||
| fake = Faker() | |||
| from extensions.ext_database import db | |||
| # Create workflow run with offset timestamp | |||
| base_time = datetime.now(UTC) | |||
| created_time = base_time - timedelta(minutes=offset_minutes) | |||
| workflow_run = WorkflowRun( | |||
| tenant_id=app.tenant_id, | |||
| app_id=app.id, | |||
| workflow_id=str(uuid.uuid4()), | |||
| type="chat", | |||
| triggered_from=triggered_from, | |||
| version="1.0.0", | |||
| graph=json.dumps({"nodes": [], "edges": []}), | |||
| inputs=json.dumps({"input": "test"}), | |||
| status="succeeded", | |||
| outputs=json.dumps({"output": "test result"}), | |||
| elapsed_time=1.5, | |||
| total_tokens=100, | |||
| total_steps=3, | |||
| created_by_role=CreatorUserRole.ACCOUNT.value, | |||
| created_by=account.id, | |||
| created_at=created_time, | |||
| finished_at=created_time, | |||
| ) | |||
| db.session.add(workflow_run) | |||
| db.session.commit() | |||
| return workflow_run | |||
| def _create_test_message(self, db_session_with_containers, app, account, workflow_run): | |||
| """ | |||
| Helper method to create a test message for testing. | |||
| Args: | |||
| db_session_with_containers: Database session from testcontainers infrastructure | |||
| app: App instance | |||
| account: Account instance | |||
| workflow_run: WorkflowRun instance | |||
| Returns: | |||
| Message: Created message instance | |||
| """ | |||
| fake = Faker() | |||
| from extensions.ext_database import db | |||
| # Create conversation first (required for message) | |||
| from models.model import Conversation | |||
| conversation = Conversation( | |||
| app_id=app.id, | |||
| name=fake.sentence(), | |||
| inputs={}, | |||
| status="normal", | |||
| mode="chat", | |||
| from_source=CreatorUserRole.ACCOUNT.value, | |||
| from_account_id=account.id, | |||
| ) | |||
| db.session.add(conversation) | |||
| db.session.commit() | |||
| # Create message | |||
| message = Message() | |||
| message.app_id = app.id | |||
| message.conversation_id = conversation.id | |||
| message.query = fake.text(max_nb_chars=100) | |||
| message.message = {"type": "text", "content": fake.text(max_nb_chars=100)} | |||
| message.answer = fake.text(max_nb_chars=200) | |||
| message.message_tokens = 50 | |||
| message.answer_tokens = 100 | |||
| message.message_unit_price = 0.001 | |||
| message.answer_unit_price = 0.002 | |||
| message.message_price_unit = 0.001 | |||
| message.answer_price_unit = 0.001 | |||
| message.currency = "USD" | |||
| message.status = "normal" | |||
| message.from_source = CreatorUserRole.ACCOUNT.value | |||
| message.from_account_id = account.id | |||
| message.workflow_run_id = workflow_run.id | |||
| message.inputs = {"input": "test input"} | |||
| db.session.add(message) | |||
| db.session.commit() | |||
| return message | |||
| def test_get_paginate_workflow_runs_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test successful pagination of workflow runs with debugging trigger. | |||
| This test verifies: | |||
| - Proper pagination of workflow runs | |||
| - Correct filtering by triggered_from | |||
| - Proper limit and last_id handling | |||
| - Repository method calls | |||
| """ | |||
| # Arrange: Create test data | |||
| fake = Faker() | |||
| app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies) | |||
| # Create multiple workflow runs | |||
| workflow_runs = [] | |||
| for i in range(5): | |||
| workflow_run = self._create_test_workflow_run(db_session_with_containers, app, account, "debugging") | |||
| workflow_runs.append(workflow_run) | |||
| # Act: Execute the method under test | |||
| workflow_run_service = WorkflowRunService() | |||
| args = {"limit": 3, "last_id": None} | |||
| result = workflow_run_service.get_paginate_workflow_runs(app, args) | |||
| # Assert: Verify the expected outcomes | |||
| assert result is not None | |||
| assert hasattr(result, "data") | |||
| assert len(result.data) == 3 # Should return 3 items due to limit | |||
| # Verify pagination properties | |||
| assert hasattr(result, "has_more") | |||
| assert hasattr(result, "limit") | |||
| # Verify all returned items are debugging runs | |||
| for workflow_run in result.data: | |||
| assert workflow_run.triggered_from == "debugging" | |||
| assert workflow_run.app_id == app.id | |||
| assert workflow_run.tenant_id == app.tenant_id | |||
| def test_get_paginate_workflow_runs_with_last_id( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test pagination of workflow runs with last_id parameter. | |||
| This test verifies: | |||
| - Proper pagination with last_id parameter | |||
| - Correct handling of pagination state | |||
| - Repository method calls with proper parameters | |||
| """ | |||
| # Arrange: Create test data | |||
| fake = Faker() | |||
| app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies) | |||
| # Create multiple workflow runs with different timestamps | |||
| workflow_runs = [] | |||
| for i in range(5): | |||
| workflow_run = self._create_test_workflow_run( | |||
| db_session_with_containers, app, account, "debugging", offset_minutes=i | |||
| ) | |||
| workflow_runs.append(workflow_run) | |||
| # Act: Execute the method under test with last_id | |||
| workflow_run_service = WorkflowRunService() | |||
| args = {"limit": 2, "last_id": workflow_runs[1].id} | |||
| result = workflow_run_service.get_paginate_workflow_runs(app, args) | |||
| # Assert: Verify the expected outcomes | |||
| assert result is not None | |||
| assert hasattr(result, "data") | |||
| assert len(result.data) == 2 # Should return 2 items due to limit | |||
| # Verify pagination properties | |||
| assert hasattr(result, "has_more") | |||
| assert hasattr(result, "limit") | |||
| # Verify all returned items are debugging runs | |||
| for workflow_run in result.data: | |||
| assert workflow_run.triggered_from == "debugging" | |||
| assert workflow_run.app_id == app.id | |||
| assert workflow_run.tenant_id == app.tenant_id | |||
| def test_get_paginate_workflow_runs_default_limit( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test pagination of workflow runs with default limit. | |||
| This test verifies: | |||
| - Default limit of 20 when not specified | |||
| - Proper handling of missing limit parameter | |||
| - Repository method calls with default values | |||
| """ | |||
| # Arrange: Create test data | |||
| fake = Faker() | |||
| app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies) | |||
| # Create workflow runs | |||
| workflow_run = self._create_test_workflow_run(db_session_with_containers, app, account, "debugging") | |||
| # Act: Execute the method under test without limit | |||
| workflow_run_service = WorkflowRunService() | |||
| args = {} # No limit specified | |||
| result = workflow_run_service.get_paginate_workflow_runs(app, args) | |||
| # Assert: Verify the expected outcomes | |||
| assert result is not None | |||
| assert hasattr(result, "data") | |||
| # Verify pagination properties | |||
| assert hasattr(result, "has_more") | |||
| assert hasattr(result, "limit") | |||
| # Verify the returned workflow run | |||
| if result.data: | |||
| workflow_run_result = result.data[0] | |||
| assert workflow_run_result.triggered_from == "debugging" | |||
| assert workflow_run_result.app_id == app.id | |||
| assert workflow_run_result.tenant_id == app.tenant_id | |||
| def test_get_paginate_advanced_chat_workflow_runs_success( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test successful pagination of advanced chat workflow runs with message information. | |||
| This test verifies: | |||
| - Proper pagination of advanced chat workflow runs | |||
| - Correct filtering by triggered_from | |||
| - Message information enrichment | |||
| - WorkflowWithMessage wrapper functionality | |||
| """ | |||
| # Arrange: Create test data | |||
| fake = Faker() | |||
| app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies) | |||
| # Create workflow runs with messages | |||
| workflow_runs = [] | |||
| for i in range(3): | |||
| workflow_run = self._create_test_workflow_run( | |||
| db_session_with_containers, app, account, "debugging", offset_minutes=i | |||
| ) | |||
| message = self._create_test_message(db_session_with_containers, app, account, workflow_run) | |||
| workflow_runs.append(workflow_run) | |||
| # Act: Execute the method under test | |||
| workflow_run_service = WorkflowRunService() | |||
| args = {"limit": 2, "last_id": None} | |||
| result = workflow_run_service.get_paginate_advanced_chat_workflow_runs(app, args) | |||
| # Assert: Verify the expected outcomes | |||
| assert result is not None | |||
| assert hasattr(result, "data") | |||
| assert len(result.data) == 2 # Should return 2 items due to limit | |||
| # Verify pagination properties | |||
| assert hasattr(result, "has_more") | |||
| assert hasattr(result, "limit") | |||
| # Verify all returned items have message information | |||
| for workflow_run in result.data: | |||
| assert hasattr(workflow_run, "message_id") | |||
| assert hasattr(workflow_run, "conversation_id") | |||
| assert workflow_run.app_id == app.id | |||
| assert workflow_run.tenant_id == app.tenant_id | |||
| def test_get_workflow_run_success(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test successful retrieval of workflow run by ID. | |||
| This test verifies: | |||
| - Proper workflow run retrieval by ID | |||
| - Correct tenant and app isolation | |||
| - Repository method calls with proper parameters | |||
| """ | |||
| # Arrange: Create test data | |||
| fake = Faker() | |||
| app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies) | |||
| # Create workflow run | |||
| workflow_run = self._create_test_workflow_run(db_session_with_containers, app, account, "debugging") | |||
| # Act: Execute the method under test | |||
| workflow_run_service = WorkflowRunService() | |||
| result = workflow_run_service.get_workflow_run(app, workflow_run.id) | |||
| # Assert: Verify the expected outcomes | |||
| assert result is not None | |||
| assert result.id == workflow_run.id | |||
| assert result.tenant_id == app.tenant_id | |||
| assert result.app_id == app.id | |||
| assert result.triggered_from == "debugging" | |||
| assert result.status == "succeeded" | |||
| assert result.type == "chat" | |||
| assert result.version == "1.0.0" | |||
| def test_get_workflow_run_not_found(self, db_session_with_containers, mock_external_service_dependencies): | |||
| """ | |||
| Test workflow run retrieval when run ID does not exist. | |||
| This test verifies: | |||
| - Proper handling of non-existent workflow run IDs | |||
| - Repository method calls with proper parameters | |||
| - Return value for missing records | |||
| """ | |||
| # Arrange: Create test data | |||
| fake = Faker() | |||
| app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies) | |||
| # Use a non-existent UUID | |||
| non_existent_id = str(uuid.uuid4()) | |||
| # Act: Execute the method under test | |||
| workflow_run_service = WorkflowRunService() | |||
| result = workflow_run_service.get_workflow_run(app, non_existent_id) | |||
| # Assert: Verify the expected outcomes | |||
| assert result is None | |||
| def test_get_workflow_run_node_executions_success( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test successful retrieval of workflow run node executions. | |||
| This test verifies: | |||
| - Proper node execution retrieval for workflow run | |||
| - Correct tenant and app isolation | |||
| - Repository method calls with proper parameters | |||
| - Context setup for plugin tool providers | |||
| """ | |||
| # Arrange: Create test data | |||
| fake = Faker() | |||
| app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies) | |||
| # Create workflow run | |||
| workflow_run = self._create_test_workflow_run(db_session_with_containers, app, account, "debugging") | |||
| # Create node executions | |||
| from extensions.ext_database import db | |||
| from models.workflow import WorkflowNodeExecutionModel | |||
| node_executions = [] | |||
| for i in range(3): | |||
| node_execution = WorkflowNodeExecutionModel( | |||
| tenant_id=app.tenant_id, | |||
| app_id=app.id, | |||
| workflow_id=workflow_run.workflow_id, | |||
| triggered_from="workflow-run", | |||
| workflow_run_id=workflow_run.id, | |||
| index=i, | |||
| node_id=f"node_{i}", | |||
| node_type="llm" if i == 0 else "tool", | |||
| title=f"Node {i}", | |||
| inputs=json.dumps({"input": f"test_input_{i}"}), | |||
| process_data=json.dumps({"process": f"test_process_{i}"}), | |||
| status="succeeded", | |||
| elapsed_time=0.5, | |||
| execution_metadata=json.dumps({"tokens": 50}), | |||
| created_by_role=CreatorUserRole.ACCOUNT.value, | |||
| created_by=account.id, | |||
| created_at=datetime.now(UTC), | |||
| ) | |||
| db.session.add(node_execution) | |||
| node_executions.append(node_execution) | |||
| db.session.commit() | |||
| # Act: Execute the method under test | |||
| workflow_run_service = WorkflowRunService() | |||
| result = workflow_run_service.get_workflow_run_node_executions(app, workflow_run.id, account) | |||
| # Assert: Verify the expected outcomes | |||
| assert result is not None | |||
| assert len(result) == 3 | |||
| # Verify node execution properties | |||
| for node_execution in result: | |||
| assert node_execution.tenant_id == app.tenant_id | |||
| assert node_execution.app_id == app.id | |||
| assert node_execution.workflow_run_id == workflow_run.id | |||
| assert node_execution.index in [0, 1, 2] # Check that index is one of the expected values | |||
| assert node_execution.node_id.startswith("node_") # Check that node_id starts with "node_" | |||
| assert node_execution.status == "succeeded" | |||
| def test_get_workflow_run_node_executions_empty( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test getting node executions for a workflow run with no executions. | |||
| This test verifies: | |||
| - Empty result when no node executions exist | |||
| - Proper handling of empty data | |||
| - No errors when querying non-existent executions | |||
| """ | |||
| # Arrange: Setup test data | |||
| account_service = AccountService() | |||
| tenant_service = TenantService() | |||
| app_service = AppService() | |||
| workflow_run_service = WorkflowRunService() | |||
| # Create account and tenant | |||
| account = account_service.create_account( | |||
| email="test@example.com", | |||
| name="Test User", | |||
| password="password123", | |||
| interface_language="en-US", | |||
| ) | |||
| TenantService.create_owner_tenant_if_not_exist(account, name="test_tenant") | |||
| tenant = account.current_tenant | |||
| # Create app | |||
| app_args = { | |||
| "name": "Test App", | |||
| "mode": "chat", | |||
| "icon_type": "emoji", | |||
| "icon": "🚀", | |||
| "icon_background": "#4ECDC4", | |||
| } | |||
| app = app_service.create_app(tenant.id, app_args, account) | |||
| # Create workflow run without node executions | |||
| workflow_run = self._create_test_workflow_run(db_session_with_containers, app, account, "debugging") | |||
| # Act: Get node executions | |||
| result = workflow_run_service.get_workflow_run_node_executions( | |||
| app_model=app, | |||
| run_id=workflow_run.id, | |||
| user=account, | |||
| ) | |||
| # Assert: Verify empty result | |||
| assert result is not None | |||
| assert len(result) == 0 | |||
| def test_get_workflow_run_node_executions_invalid_workflow_run_id( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test getting node executions with invalid workflow run ID. | |||
| This test verifies: | |||
| - Proper handling of invalid workflow run ID | |||
| - Empty result when workflow run doesn't exist | |||
| - No errors when querying with invalid ID | |||
| """ | |||
| # Arrange: Setup test data | |||
| account_service = AccountService() | |||
| tenant_service = TenantService() | |||
| app_service = AppService() | |||
| workflow_run_service = WorkflowRunService() | |||
| # Create account and tenant | |||
| account = account_service.create_account( | |||
| email="test@example.com", | |||
| name="Test User", | |||
| password="password123", | |||
| interface_language="en-US", | |||
| ) | |||
| TenantService.create_owner_tenant_if_not_exist(account, name="test_tenant") | |||
| tenant = account.current_tenant | |||
| # Create app | |||
| app_args = { | |||
| "name": "Test App", | |||
| "mode": "chat", | |||
| "icon_type": "emoji", | |||
| "icon": "🚀", | |||
| "icon_background": "#4ECDC4", | |||
| } | |||
| app = app_service.create_app(tenant.id, app_args, account) | |||
| # Use invalid workflow run ID | |||
| invalid_workflow_run_id = str(uuid.uuid4()) | |||
| # Act: Get node executions with invalid ID | |||
| result = workflow_run_service.get_workflow_run_node_executions( | |||
| app_model=app, | |||
| run_id=invalid_workflow_run_id, | |||
| user=account, | |||
| ) | |||
| # Assert: Verify empty result | |||
| assert result is not None | |||
| assert len(result) == 0 | |||
| def test_get_workflow_run_node_executions_database_error( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test getting node executions when database encounters an error. | |||
| This test verifies: | |||
| - Proper error handling when database operations fail | |||
| - Graceful degradation in error scenarios | |||
| - Error propagation to calling code | |||
| """ | |||
| # Arrange: Setup test data | |||
| account_service = AccountService() | |||
| tenant_service = TenantService() | |||
| app_service = AppService() | |||
| workflow_run_service = WorkflowRunService() | |||
| # Create account and tenant | |||
| account = account_service.create_account( | |||
| email="test@example.com", | |||
| name="Test User", | |||
| password="password123", | |||
| interface_language="en-US", | |||
| ) | |||
| TenantService.create_owner_tenant_if_not_exist(account, name="test_tenant") | |||
| tenant = account.current_tenant | |||
| # Create app | |||
| app_args = { | |||
| "name": "Test App", | |||
| "mode": "chat", | |||
| "icon_type": "emoji", | |||
| "icon": "🚀", | |||
| "icon_background": "#4ECDC4", | |||
| } | |||
| app = app_service.create_app(tenant.id, app_args, account) | |||
| # Create workflow run | |||
| workflow_run = self._create_test_workflow_run(db_session_with_containers, app, account, "debugging") | |||
| # Mock database error by closing the session | |||
| db_session_with_containers.close() | |||
| # Act & Assert: Verify error handling | |||
| with pytest.raises((Exception, RuntimeError)): | |||
| workflow_run_service.get_workflow_run_node_executions( | |||
| app_model=app, | |||
| run_id=workflow_run.id, | |||
| user=account, | |||
| ) | |||
| def test_get_workflow_run_node_executions_end_user( | |||
| self, db_session_with_containers, mock_external_service_dependencies | |||
| ): | |||
| """ | |||
| Test node execution retrieval for end user. | |||
| This test verifies: | |||
| - Proper handling of end user vs account user | |||
| - Correct tenant ID extraction for end users | |||
| - Repository method calls with proper parameters | |||
| """ | |||
| # Arrange: Create test data | |||
| fake = Faker() | |||
| app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies) | |||
| # Create workflow run | |||
| workflow_run = self._create_test_workflow_run(db_session_with_containers, app, account, "debugging") | |||
| # Create end user | |||
| from extensions.ext_database import db | |||
| from models.model import EndUser | |||
| end_user = EndUser( | |||
| tenant_id=app.tenant_id, | |||
| app_id=app.id, | |||
| type="web_app", | |||
| is_anonymous=False, | |||
| session_id=str(uuid.uuid4()), | |||
| external_user_id=str(uuid.uuid4()), | |||
| name=fake.name(), | |||
| ) | |||
| db.session.add(end_user) | |||
| db.session.commit() | |||
| # Create node execution | |||
| from models.workflow import WorkflowNodeExecutionModel | |||
| node_execution = WorkflowNodeExecutionModel( | |||
| tenant_id=app.tenant_id, | |||
| app_id=app.id, | |||
| workflow_id=workflow_run.workflow_id, | |||
| triggered_from="workflow-run", | |||
| workflow_run_id=workflow_run.id, | |||
| index=0, | |||
| node_id="node_0", | |||
| node_type="llm", | |||
| title="Node 0", | |||
| inputs=json.dumps({"input": "test_input"}), | |||
| process_data=json.dumps({"process": "test_process"}), | |||
| status="succeeded", | |||
| elapsed_time=0.5, | |||
| execution_metadata=json.dumps({"tokens": 50}), | |||
| created_by_role=CreatorUserRole.END_USER.value, | |||
| created_by=end_user.id, | |||
| created_at=datetime.now(UTC), | |||
| ) | |||
| db.session.add(node_execution) | |||
| db.session.commit() | |||
| # Act: Execute the method under test | |||
| workflow_run_service = WorkflowRunService() | |||
| result = workflow_run_service.get_workflow_run_node_executions(app, workflow_run.id, end_user) | |||
| # Assert: Verify the expected outcomes | |||
| assert result is not None | |||
| assert len(result) == 1 | |||
| # Verify node execution properties | |||
| node_exec = result[0] | |||
| assert node_exec.tenant_id == app.tenant_id | |||
| assert node_exec.app_id == app.id | |||
| assert node_exec.workflow_run_id == workflow_run.id | |||
| assert node_exec.created_by == end_user.id | |||
| assert node_exec.created_by_role == CreatorUserRole.END_USER.value | |||