import dataclasses import json from datetime import UTC, datetime from types import SimpleNamespace from unittest import mock from unittest.mock import Mock from uuid import NAMESPACE_URL, UUID, uuid4, uuid5 import pytest from pydantic import JsonValue from sqlalchemy import inspect, select from sqlalchemy.orm import Session, configure_mappers from constants import HIDDEN_VALUE from core.helper import encrypter from core.workflow.file_reference import build_file_reference from core.workflow.llm_environment_variable import LLMEnvironmentVariable from extensions.storage.storage_type import StorageType from factories import variable_factory from factories.variable_factory import build_segment from fields.workflow_run_fields import ( node_execution_response_source, workflow_run_pagination_response_source, workflow_run_response_source, ) from graphon.enums import ( BuiltinNodeTypes, WorkflowExecutionStatus, WorkflowNodeExecutionStatus, ) from graphon.file import File, FileTransferMethod, FileType from graphon.variables import FloatVariable, IntegerVariable, SecretVariable, SegmentType, StringVariable from graphon.variables.segments import IntegerSegment, Segment from models import ConversationVariable from models import Workflow as _workflow_feature_compatibility_Workflow from models.account import Account from models.base import Base, TypeBase from models.enums import CreatorUserRole, EndUserType, ExecutionOffLoadType, WorkflowRunTriggeredFrom from models.model import EndUser, UploadFile from models.tools import WorkflowToolProvider from models.workflow import ( Workflow as _workflow_Workflow, ) from models.workflow import ( Workflow as _workflow_models_Workflow, ) from models.workflow import ( WorkflowAppLog, WorkflowAppLogCreatedFrom, WorkflowDraftVariable, WorkflowNodeExecutionModel, WorkflowNodeExecutionOffload, WorkflowNodeExecutionTriggeredFrom, WorkflowRun, WorkflowType, WorkflowVersionCounter, is_system_variable_editable, ) from models.workflow import WorkflowType as ModelWorkflowType class TestConversationVariableCases: @staticmethod def test_from_variable_coerces_non_uuid_primary_key() -> None: variable = variable_factory.build_conversation_variable_from_mapping( { "id": "opt-comp-prompt-var", "name": "optimization_comparison_prompt", "value_type": SegmentType.STRING, "value": "-", } ) row = ConversationVariable.from_variable(app_id="app_id", conversation_id="conversation_id", variable=variable) expected = str(uuid5(NAMESPACE_URL, "dify:conversation-variable:optimization_comparison_prompt")) UUID(row.id) assert row.id == expected assert variable.id == "opt-comp-prompt-var" assert row.to_variable().id == "opt-comp-prompt-var" @staticmethod def test_from_variable_and_to_variable() -> None: variable = variable_factory.build_conversation_variable_from_mapping( { "id": str(uuid4()), "name": "name", "value_type": SegmentType.OBJECT, "value": { "key": { "key": "value", } }, } ) conversation_variable = ConversationVariable.from_variable( app_id="app_id", conversation_id="conversation_id", variable=variable ) assert conversation_variable.to_variable() == variable class TestWorkflowCases: @staticmethod def test_environment_variables() -> None: # tenant_id context variable removed - using current_user.current_tenant_id directly # Create a Workflow instance workflow = _workflow_Workflow( tenant_id="tenant_id", app_id="app_id", type=ModelWorkflowType("workflow"), version="draft", graph="{}", _features="{}", created_by="account_id", _environment_variables=[], _conversation_variables=[], ) # Create some EnvironmentVariable instances variable1 = StringVariable(name="var1", value="value1", id=str(uuid4()), selector=["env", "var1"]) variable2 = IntegerVariable(name="var2", value=123, id=str(uuid4()), selector=["env", "var2"]) variable3 = SecretVariable(name="var3", value="secret", id=str(uuid4()), selector=["env", "var3"]) variable4 = FloatVariable(name="var4", value=float("3.14"), id=str(uuid4()), selector=["env", "var4"]) with ( mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"), mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"), ): # Set the environment_variables property of the Workflow instance variables = [variable1, variable2, variable3, variable4] workflow.environment_variables = variables # Get the environment_variables property and assert its value assert workflow.environment_variables == variables @staticmethod def test_llm_environment_variable_round_trip() -> None: workflow = _workflow_Workflow( tenant_id="tenant_id", app_id="app_id", type=ModelWorkflowType("workflow"), version="draft", graph="{}", _features="{}", created_by="account_id", _environment_variables=[], _conversation_variables=[], ) variable = LLMEnvironmentVariable( name="for_research", value={"provider": "langgenius/anthropic/anthropic", "name": "claude-sonnet", "mode": "chat"}, id=str(uuid4()), selector=["env", "for_research"], ) workflow.environment_variables = [variable] assert workflow.environment_variables == [variable] assert json.loads(workflow._environment_variables)["for_research"]["value_type"] == "llm" assert workflow.to_dict()["environment_variables"][0]["value_type"] == "llm" @staticmethod def test_update_environment_variables() -> None: # tenant_id context variable removed - using current_user.current_tenant_id directly # Create a Workflow instance workflow = _workflow_Workflow( tenant_id="tenant_id", app_id="app_id", type=ModelWorkflowType("workflow"), version="draft", graph="{}", _features="{}", created_by="account_id", _environment_variables=[], _conversation_variables=[], ) # Create some EnvironmentVariable instances variable1 = StringVariable(name="var1", value="value1", id=str(uuid4()), selector=["env", "var1"]) variable2 = IntegerVariable(name="var2", value=123, id=str(uuid4()), selector=["env", "var2"]) variable3 = SecretVariable( name="var3", value="secret", id=str(uuid4()), selector=["env", "var3"], description="old description", ) variable4 = FloatVariable(name="var4", value=float("3.14"), id=str(uuid4()), selector=["env", "var4"]) with ( mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"), mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"), ): variables = [variable1, variable2, variable3, variable4] # Set the environment_variables property of the Workflow instance workflow.environment_variables = variables assert workflow.environment_variables == [variable1, variable2, variable3, variable4] # Update the name and description of variable3 and keep the value as it is variables[2] = variable3.model_copy( update={ "name": "new name", "description": "new description", "value": HIDDEN_VALUE, } ) workflow.environment_variables = variables assert workflow.environment_variables[2].name == "new name" assert workflow.environment_variables[2].description == "new description" assert workflow.environment_variables[2].value == variable3.value @staticmethod def test_to_dict() -> None: # tenant_id context variable removed - using current_user.current_tenant_id directly # Create a Workflow instance workflow = _workflow_Workflow( tenant_id="tenant_id", app_id="app_id", type=ModelWorkflowType("workflow"), version="draft", graph="{}", _features="{}", created_by="account_id", _environment_variables=[], _conversation_variables=[], ) # Create some EnvironmentVariable instances with ( mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"), mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"), ): # Set the environment_variables property of the Workflow instance workflow.environment_variables = [ SecretVariable(name="secret", value="secret", id=str(uuid4())), StringVariable(name="text", value="text", id=str(uuid4())), ] workflow_dict = workflow.to_dict() assert workflow_dict["environment_variables"][0]["value"] == "" assert workflow_dict["environment_variables"][1]["value"] == "text" workflow_dict = workflow.to_dict(include_secret=True) assert workflow_dict["environment_variables"][0]["value"] == "secret" assert workflow_dict["environment_variables"][1]["value"] == "text" @staticmethod @pytest.mark.parametrize("sqlite_session", [(_workflow_Workflow, Account)], indirect=True) def test_workflow_account_accessors_use_caller_session(sqlite_session: Session) -> None: created_account = Account(name="Created Account", email="created@example.com") created_account.id = "created-account-id" updated_account = Account(name="Updated Account", email="updated@example.com") updated_account.id = "updated-account-id" decoy_account = Account(name="Decoy Account", email="decoy@example.com") decoy_account.id = "decoy-account-id" workflow = _workflow_Workflow( tenant_id="tenant_id", app_id="app_id", type=ModelWorkflowType("workflow"), version="draft", graph="{}", _features="{}", created_by="created-account-id", _environment_variables=[], _conversation_variables=[], updated_by="updated-account-id", ) sqlite_session.add_all([decoy_account, updated_account, workflow, created_account]) sqlite_session.flush() assert workflow.created_by_account(sqlite_session) is created_account assert workflow.updated_by_account(sqlite_session) is updated_account @staticmethod @pytest.mark.parametrize("sqlite_session", [(_workflow_Workflow, WorkflowToolProvider)], indirect=True) def test_workflow_tool_published_accessor_uses_caller_session(sqlite_session: Session) -> None: workflow = _workflow_Workflow( tenant_id="tenant_id", app_id="app_id", type=ModelWorkflowType("workflow"), version="draft", graph="{}", _features="{}", created_by="account_id", _environment_variables=[], _conversation_variables=[], ) matching_provider = WorkflowToolProvider( name="matching-provider", label="Matching provider", icon="tool", app_id=workflow.app_id, version="1", user_id="account-id", tenant_id=workflow.tenant_id, description="Matching workflow tool", ) decoy_provider = WorkflowToolProvider( name="decoy-provider", label="Decoy provider", icon="tool", app_id="other-app", version="1", user_id="account-id", tenant_id=workflow.tenant_id, description="Different app", ) sqlite_session.add_all([decoy_provider, workflow, matching_provider]) sqlite_session.flush() with pytest.warns(DeprecationWarning, match="not accurate"): # The deprecated accessor is the compatibility surface under test. assert workflow.tool_published(sqlite_session) is True # pyrefly: ignore[deprecated] @staticmethod def test_normalize_environment_variable_mappings_converts_full_mask_to_hidden_value() -> None: normalized = _workflow_Workflow.normalize_environment_variable_mappings( [ { "id": str(uuid4()), "name": "secret", "value": encrypter.full_mask_token(), "value_type": "secret", } ] ) assert normalized[0]["value"] == HIDDEN_VALUE @staticmethod def test_normalize_environment_variable_mappings_keeps_hidden_value() -> None: normalized = _workflow_Workflow.normalize_environment_variable_mappings( [ { "id": str(uuid4()), "name": "secret", "value": HIDDEN_VALUE, "value_type": "secret", } ] ) assert normalized[0]["value"] == HIDDEN_VALUE class TestWorkflowNodeExecution: def test_execution_metadata_dict(self) -> None: node_exec = WorkflowNodeExecutionModel( execution_metadata=None, ) assert node_exec.execution_metadata_dict == {} original = {"a": 1, "b": ["2"]} node_exec.execution_metadata = json.dumps(original) assert node_exec.execution_metadata_dict == original class TestIsSystemVariableEditable: def test_is_system_variable(self) -> None: cases = [ ("query", True), ("files", True), ("dialogue_count", False), ("conversation_id", False), ("user_id", False), ("app_id", False), ("workflow_id", False), ("workflow_run_id", False), ] for name, editable in cases: assert editable == is_system_variable_editable(name) assert is_system_variable_editable("invalid_or_new_system_variable") is False class TestWorkflowDraftVariableGetValue: def test_get_value_by_case(self) -> None: @dataclasses.dataclass class TestCase: name: str value: Segment test_file = File( file_type=FileType.IMAGE, transfer_method=FileTransferMethod.REMOTE_URL, remote_url="https://example.com/example.jpg", filename="example.jpg", extension=".jpg", mime_type="image/jpeg", size=100, ) cases: list[TestCase] = [ TestCase( name="number/int", value=build_segment(1), ), TestCase( name="number/float", value=build_segment(1.0), ), TestCase( name="string", value=build_segment("a"), ), TestCase( name="object", value=build_segment({}), ), TestCase( name="file", value=build_segment(test_file), ), TestCase( name="array[any]", value=build_segment([1, "a"]), ), TestCase( name="array[string]", value=build_segment(["a", "b"]), ), TestCase( name="array[number]/int", value=build_segment([1, 2]), ), TestCase( name="array[number]/float", value=build_segment([1.0, 2.0]), ), TestCase( name="array[number]/mixed", value=build_segment([1, 2.0]), ), TestCase( name="array[object]", value=build_segment([{}, {"a": 1}]), ), TestCase( name="none", value=build_segment(None), ), ] for idx, c in enumerate(cases, 1): fail_msg = f"test case {c.name} failed, index={idx}" draft_var = WorkflowDraftVariable() draft_var.set_value(c.value) assert c.value == draft_var.get_value(), fail_msg def test_file_variable_preserves_all_fields(self) -> None: """Test that File type variables preserve all fields during encoding/decoding.""" # Create a File with specific field values test_file = File( file_id="test_file_id", file_type=FileType.IMAGE, transfer_method=FileTransferMethod.REMOTE_URL, remote_url="https://example.com/test.jpg", filename="test.jpg", extension=".jpg", mime_type="image/jpeg", size=12345, # Specific size to test preservation storage_key="test_storage_key", ) # Create a FileSegment and WorkflowDraftVariable file_segment = build_segment(test_file) draft_var = WorkflowDraftVariable() draft_var.set_value(file_segment) # Retrieve the value and verify all fields are preserved retrieved_segment = draft_var.get_value() retrieved_file = retrieved_segment.value # Verify all important fields are preserved assert retrieved_file.id == test_file.id assert retrieved_file.type == test_file.type assert retrieved_file.transfer_method == test_file.transfer_method assert retrieved_file.remote_url == test_file.remote_url assert retrieved_file.filename == test_file.filename assert retrieved_file.extension == test_file.extension assert retrieved_file.mime_type == test_file.mime_type assert retrieved_file.size == test_file.size # This was the main issue being fixed # Note: storage_key is not serialized in model_dump() so it won't be preserved # Verify the segments have the same type and the important fields match assert file_segment.value_type == retrieved_segment.value_type def test_file_variable_rebuilds_storage_backed_payloads_with_app_tenant(self) -> None: persisted_file = File( file_id="test_file_id", file_type=FileType.DOCUMENT, transfer_method=FileTransferMethod.LOCAL_FILE, reference=build_file_reference(record_id="upload-1", storage_key="legacy-storage-key"), filename="test.txt", extension=".txt", mime_type="text/plain", size=12, ) rebuilt_file = File( file_id="test_file_id", file_type=FileType.DOCUMENT, transfer_method=FileTransferMethod.LOCAL_FILE, reference=build_file_reference(record_id="upload-1"), filename="test.txt", extension=".txt", mime_type="text/plain", size=12, storage_key="canonical-storage-key", ) draft_var = WorkflowDraftVariable( app_id="app-1", ) draft_var.set_value(build_segment(persisted_file)) draft_var._WorkflowDraftVariable__value = None # pyrefly: ignore[missing-attribute] with ( mock.patch("models.workflow._resolve_workflow_app_tenant_id", return_value="tenant-1"), mock.patch("models.workflow.build_file_from_stored_mapping", return_value=rebuilt_file) as rebuild_file, ): retrieved_segment = draft_var.get_value() assert retrieved_segment.value == rebuilt_file rebuild_file.assert_called_once() assert rebuild_file.call_args.kwargs["tenant_id"] == "tenant-1" def test_get_and_set_value(self) -> None: draft_var = WorkflowDraftVariable() int_var = IntegerSegment(value=1) draft_var.set_value(int_var) value = draft_var.get_value() assert value == int_var """Regression coverage for ``models.workflow.WorkflowAppLog`` account accessors. Ensures the ``@property``→session-parameter refactor preserves the role-based dispatch: ``created_by_account`` looks up an Account only when role is ACCOUNT; ``created_by_end_user`` looks up an EndUser only when role is END_USER. Both accessors are exercised against the real ``sqlite_session`` fixture (a genuine SQLAlchemy ``Session`` bound to a pristine full-schema SQLite database) so the assertions cover actual query behaviour rather than a mock's recorded call. """ def _log(role: CreatorUserRole, created_by: str) -> WorkflowAppLog: """Construct a WorkflowAppLog without touching the database.""" return WorkflowAppLog( tenant_id="00000000-0000-0000-0000-000000000001", app_id="00000000-0000-0000-0000-000000000002", workflow_id="00000000-0000-0000-0000-000000000003", workflow_run_id="00000000-0000-0000-0000-000000000004", created_from=WorkflowAppLogCreatedFrom.WEB_APP, created_by_role=role, created_by=created_by, ) class TestWorkflowAppLogModelsCases: class TestCreatedByAccount: def test_returns_account_lookup_when_role_is_account(self, sqlite_session: Session) -> None: account = Account(name="Test Account", email="test@example.com") sqlite_session.add(account) sqlite_session.flush() log = _log(CreatorUserRole.ACCOUNT, created_by=account.id) result = log.created_by_account(session=sqlite_session) assert result is not None assert result.id == account.id def test_returns_none_when_role_is_end_user(self, sqlite_session: Session) -> None: account = Account(name="Test Account", email="test@example.com") sqlite_session.add(account) sqlite_session.flush() log = _log(CreatorUserRole.END_USER, created_by=account.id) assert log.created_by_account(session=sqlite_session) is None class TestCreatedByEndUser: def test_returns_end_user_lookup_when_role_is_end_user(self, sqlite_session: Session) -> None: end_user = EndUser( tenant_id="00000000-0000-0000-0000-000000000001", type=EndUserType.BROWSER, session_id="session-1", ) sqlite_session.add(end_user) sqlite_session.flush() log = _log(CreatorUserRole.END_USER, created_by=end_user.id) result = log.created_by_end_user(session=sqlite_session) assert result is not None assert result.id == end_user.id def test_returns_none_when_role_is_account(self, sqlite_session: Session) -> None: end_user = EndUser( tenant_id="00000000-0000-0000-0000-000000000001", type=EndUserType.BROWSER, session_id="session-1", ) sqlite_session.add(end_user) sqlite_session.flush() log = _log(CreatorUserRole.ACCOUNT, created_by=end_user.id) assert log.created_by_end_user(session=sqlite_session) is None """ This unit test verifies Workflow compatibility before and after supporting multiple file types. """ OLD_VERSION_WORKFLOW_FEATURES: dict[str, JsonValue] = { "file_upload": { "image": { "enabled": True, "number_limits": 6, "transfer_methods": ["remote_url", "local_file"], } }, "opening_statement": "", "retriever_resource": {"enabled": True}, "sensitive_word_avoidance": {"enabled": False}, "speech_to_text": {"enabled": False}, "suggested_questions": [], "suggested_questions_after_answer": {"enabled": False}, "text_to_speech": {"enabled": False, "language": "", "voice": ""}, } NEW_VERSION_WORKFLOW_FEATURES: dict[str, JsonValue] = { "file_upload": { "enabled": True, "allowed_file_types": ["image"], "allowed_file_extensions": [], "allowed_file_upload_methods": ["remote_url", "local_file"], "number_limits": 6, }, "opening_statement": "", "retriever_resource": {"enabled": True}, "sensitive_word_avoidance": {"enabled": False}, "speech_to_text": {"enabled": False}, "suggested_questions": [], "suggested_questions_after_answer": {"enabled": False}, "text_to_speech": {"enabled": False, "language": "", "voice": ""}, } class TestWorkflowFeatureCompatibilityCases: @staticmethod def test_workflow_features() -> None: workflow = _workflow_feature_compatibility_Workflow( tenant_id="", app_id="", type=WorkflowType.WORKFLOW, version="", graph="", _features=json.dumps(OLD_VERSION_WORKFLOW_FEATURES), created_by="", _environment_variables=[], _conversation_variables=[], ) assert workflow.features_dict == NEW_VERSION_WORKFLOW_FEATURES """ Comprehensive unit tests for Workflow models. This test suite covers: - Workflow model validation - WorkflowRun state transitions - NodeExecution relationships - Graph configuration validation """ class TestWorkflowModelsCases: class TestWorkflowModelValidation: """Test suite for Workflow model validation and basic operations.""" def test_workflow_creation_with_required_fields(self) -> None: """Test creating a workflow with all required fields.""" # Arrange tenant_id = str(uuid4()) app_id = str(uuid4()) created_by = str(uuid4()) graph = json.dumps({"nodes": [], "edges": []}) features = json.dumps({"file_upload": {"enabled": True}}) # Act workflow = _workflow_models_Workflow.new( tenant_id=tenant_id, app_id=app_id, type=WorkflowType.WORKFLOW, version="draft", graph=graph, features=features, created_by=created_by, environment_variables=[], conversation_variables=[], rag_pipeline_variables=[], ) # Assert assert workflow.tenant_id == tenant_id assert workflow.app_id == app_id assert workflow.type == WorkflowType.WORKFLOW assert workflow.version == "draft" assert workflow.graph == graph assert workflow.created_by == created_by assert workflow.created_at is not None assert workflow.updated_at is not None def test_workflow_type_enum_values(self) -> None: """Test WorkflowType enum values.""" # Assert assert WorkflowType.WORKFLOW == "workflow" assert WorkflowType.CHAT.value == "chat" assert WorkflowType.RAG_PIPELINE.value == "rag-pipeline" def test_workflow_type_value_of(self) -> None: """Test WorkflowType.value_of method.""" # Act & Assert assert WorkflowType.value_of("workflow") == WorkflowType.WORKFLOW assert WorkflowType.value_of("chat") == WorkflowType.CHAT assert WorkflowType.value_of("rag-pipeline") == WorkflowType.RAG_PIPELINE with pytest.raises(ValueError, match="invalid workflow type value"): WorkflowType.value_of("invalid_type") def test_workflow_graph_dict_property(self) -> None: """Test graph_dict property parses JSON correctly.""" # Arrange graph_data = {"nodes": [{"id": "start", "type": "start"}], "edges": list[object]()} workflow = _workflow_models_Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_data), features="{}", created_by=str(uuid4()), environment_variables=[], conversation_variables=[], rag_pipeline_variables=[], ) # Act result = workflow.graph_dict # Assert assert result == graph_data assert "nodes" in result assert len(result["nodes"]) == 1 def test_workflow_features_dict_property(self) -> None: """Test features_dict property parses JSON correctly.""" # Arrange features_data = {"file_upload": {"enabled": True, "max_files": 5}} workflow = _workflow_models_Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), type=WorkflowType.WORKFLOW, version="draft", graph="{}", features=json.dumps(features_data), created_by=str(uuid4()), environment_variables=[], conversation_variables=[], rag_pipeline_variables=[], ) # Act result = workflow.features_dict # Assert assert result == features_data assert result["file_upload"]["enabled"] is True assert result["file_upload"]["max_files"] == 5 def test_workflow_with_marked_name_and_comment(self) -> None: """Test workflow creation with marked name and comment.""" # Arrange & Act workflow = _workflow_models_Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), type=WorkflowType.WORKFLOW, version="v1.0", graph="{}", features="{}", created_by=str(uuid4()), environment_variables=[], conversation_variables=[], rag_pipeline_variables=[], marked_name="Production Release", marked_comment="Initial production version", ) # Assert assert workflow.marked_name == "Production Release" assert workflow.marked_comment == "Initial production version" def test_workflow_version_draft_constant(self) -> None: """Test VERSION_DRAFT constant.""" # Assert assert _workflow_models_Workflow.VERSION_DRAFT == "draft" class TestWorkflowRunStateTransitions: """Test suite for WorkflowRun state transitions and lifecycle.""" def test_workflow_run_creation_with_required_fields(self) -> None: """Test creating a workflow run with required fields.""" # Arrange tenant_id = str(uuid4()) app_id = str(uuid4()) workflow_id = str(uuid4()) created_by = str(uuid4()) # Act workflow_run = WorkflowRun( tenant_id=tenant_id, app_id=app_id, workflow_id=workflow_id, type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, version="draft", status=WorkflowExecutionStatus.RUNNING, created_by_role=CreatorUserRole.ACCOUNT, created_by=created_by, ) # Assert assert workflow_run.tenant_id == tenant_id assert workflow_run.app_id == app_id assert workflow_run.workflow_id == workflow_id assert workflow_run.type == WorkflowType.WORKFLOW assert workflow_run.triggered_from == WorkflowRunTriggeredFrom.DEBUGGING assert workflow_run.status == WorkflowExecutionStatus.RUNNING assert workflow_run.created_by == created_by def test_workflow_run_state_transition_running_to_succeeded(self) -> None: """Test state transition from running to succeeded.""" # Arrange workflow_run = WorkflowRun( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", status=WorkflowExecutionStatus.RUNNING, created_by_role=CreatorUserRole.END_USER, created_by=str(uuid4()), ) # Act workflow_run.status = WorkflowExecutionStatus.SUCCEEDED workflow_run.finished_at = datetime.now(UTC) workflow_run.elapsed_time = 2.5 # Assert assert workflow_run.status == WorkflowExecutionStatus.SUCCEEDED assert workflow_run.finished_at is not None assert workflow_run.elapsed_time == 2.5 def test_workflow_run_state_transition_running_to_failed(self) -> None: """Test state transition from running to failed with error.""" # Arrange workflow_run = WorkflowRun( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", status=WorkflowExecutionStatus.RUNNING, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Act workflow_run.status = WorkflowExecutionStatus.FAILED workflow_run.error = "Node execution failed: Invalid input" workflow_run.finished_at = datetime.now(UTC) # Assert assert workflow_run.status == WorkflowExecutionStatus.FAILED assert workflow_run.error == "Node execution failed: Invalid input" assert workflow_run.finished_at is not None def test_workflow_run_state_transition_running_to_stopped(self) -> None: """Test state transition from running to stopped.""" # Arrange workflow_run = WorkflowRun( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, version="draft", status=WorkflowExecutionStatus.RUNNING, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Act workflow_run.status = WorkflowExecutionStatus.STOPPED workflow_run.finished_at = datetime.now(UTC) # Assert assert workflow_run.status == WorkflowExecutionStatus.STOPPED assert workflow_run.finished_at is not None def test_workflow_run_state_transition_running_to_paused(self) -> None: """Test state transition from running to paused.""" # Arrange workflow_run = WorkflowRun( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", status=WorkflowExecutionStatus.RUNNING, created_by_role=CreatorUserRole.END_USER, created_by=str(uuid4()), ) # Act workflow_run.status = WorkflowExecutionStatus.PAUSED # Assert assert workflow_run.status == WorkflowExecutionStatus.PAUSED assert workflow_run.finished_at is None # Not finished when paused def test_workflow_run_state_transition_paused_to_running(self) -> None: """Test state transition from paused back to running.""" # Arrange workflow_run = WorkflowRun( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", status=WorkflowExecutionStatus.PAUSED, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Act workflow_run.status = WorkflowExecutionStatus.RUNNING # Assert assert workflow_run.status == WorkflowExecutionStatus.RUNNING def test_workflow_run_with_partial_succeeded_status(self) -> None: """Test workflow run with partial-succeeded status.""" # Arrange & Act workflow_run = WorkflowRun( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", status=WorkflowExecutionStatus.PARTIAL_SUCCEEDED, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), exceptions_count=2, ) # Assert assert workflow_run.status == WorkflowExecutionStatus.PARTIAL_SUCCEEDED assert workflow_run.exceptions_count == 2 def test_workflow_run_with_inputs_and_outputs(self) -> None: """Test workflow run with inputs and outputs as JSON.""" # Arrange inputs = {"query": "What is AI?", "context": "technology"} outputs = {"answer": "AI is Artificial Intelligence", "confidence": 0.95} # Act workflow_run = WorkflowRun( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", status=WorkflowExecutionStatus.SUCCEEDED, created_by_role=CreatorUserRole.END_USER, created_by=str(uuid4()), inputs=json.dumps(inputs), outputs=json.dumps(outputs), ) # Assert assert workflow_run.inputs_dict == inputs assert workflow_run.outputs_dict == outputs def test_workflow_run_graph_dict_property(self) -> None: """Test graph_dict property for workflow run.""" # Arrange graph = {"nodes": [{"id": "start", "type": "start"}], "edges": list[object]()} workflow_run = WorkflowRun( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, version="draft", status=WorkflowExecutionStatus.RUNNING, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), graph=json.dumps(graph), ) # Act result = workflow_run.graph_dict # Assert assert result == graph assert "nodes" in result def test_workflow_run_to_dict_serialization(self) -> None: """Test WorkflowRun to_dict method.""" # Arrange workflow_run_id = str(uuid4()) tenant_id = str(uuid4()) app_id = str(uuid4()) workflow_id = str(uuid4()) created_by = str(uuid4()) workflow_run = WorkflowRun( tenant_id=tenant_id, app_id=app_id, workflow_id=workflow_id, type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", status=WorkflowExecutionStatus.SUCCEEDED, created_by_role=CreatorUserRole.ACCOUNT, created_by=created_by, total_tokens=1500, total_steps=5, ) workflow_run.id = workflow_run_id # Act result = workflow_run.to_dict() # Assert assert result["id"] == workflow_run_id assert result["tenant_id"] == tenant_id assert result["app_id"] == app_id assert result["workflow_id"] == workflow_id assert result["status"] == WorkflowExecutionStatus.SUCCEEDED assert result["total_tokens"] == 1500 assert result["total_steps"] == 5 def test_workflow_run_from_dict_deserialization(self) -> None: """Test WorkflowRun from_dict method.""" # Arrange data = { "id": str(uuid4()), "tenant_id": str(uuid4()), "app_id": str(uuid4()), "workflow_id": str(uuid4()), "type": WorkflowType.WORKFLOW, "triggered_from": WorkflowRunTriggeredFrom.APP_RUN, "version": "v1.0", "graph": {"nodes": list[object](), "edges": list[object]()}, "inputs": {"query": "test"}, "status": WorkflowExecutionStatus.SUCCEEDED, "outputs": {"result": "success"}, "error": None, "elapsed_time": 3.5, "total_tokens": 2000, "total_steps": 10, "created_by_role": CreatorUserRole.ACCOUNT, "created_by": str(uuid4()), "created_at": datetime.now(UTC), "finished_at": datetime.now(UTC), "exceptions_count": 0, } # Act workflow_run = WorkflowRun.from_dict(data) # Assert assert workflow_run.id == data["id"] assert workflow_run.workflow_id == data["workflow_id"] assert workflow_run.status == WorkflowExecutionStatus.SUCCEEDED assert workflow_run.total_tokens == 2000 class TestNodeExecutionRelationships: """Test suite for WorkflowNodeExecutionModel relationships and data.""" def test_node_execution_creation_with_required_fields(self) -> None: """Test creating a node execution with required fields.""" # Arrange tenant_id = str(uuid4()) app_id = str(uuid4()) workflow_id = str(uuid4()) workflow_run_id = str(uuid4()) created_by = str(uuid4()) # Act node_execution = WorkflowNodeExecutionModel( tenant_id=tenant_id, app_id=app_id, workflow_id=workflow_id, triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=workflow_run_id, index=1, node_id="start", node_type=BuiltinNodeTypes.START, title="Start Node", status=WorkflowNodeExecutionStatus.SUCCEEDED, created_by_role=CreatorUserRole.ACCOUNT, created_by=created_by, ) # Assert assert node_execution.tenant_id == tenant_id assert node_execution.app_id == app_id assert node_execution.workflow_id == workflow_id assert node_execution.workflow_run_id == workflow_run_id assert node_execution.node_id == "start" assert node_execution.node_type == BuiltinNodeTypes.START assert node_execution.index == 1 def test_node_execution_with_predecessor_relationship(self) -> None: """Test node execution with predecessor node relationship.""" # Arrange predecessor_node_id = "start" current_node_id = "llm_1" # Act node_execution = WorkflowNodeExecutionModel( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=2, predecessor_node_id=predecessor_node_id, node_id=current_node_id, node_type=BuiltinNodeTypes.LLM, title="LLM Node", status=WorkflowNodeExecutionStatus.RUNNING, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Assert assert node_execution.predecessor_node_id == predecessor_node_id assert node_execution.node_id == current_node_id assert node_execution.index == 2 def test_node_execution_single_step_debugging(self) -> None: """Test node execution for single-step debugging (no workflow_run_id).""" # Arrange & Act node_execution = WorkflowNodeExecutionModel( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), triggered_from=WorkflowNodeExecutionTriggeredFrom.SINGLE_STEP, workflow_run_id=None, # Single-step has no workflow run index=1, node_id="llm_test", node_type=BuiltinNodeTypes.LLM, title="Test LLM", status=WorkflowNodeExecutionStatus.SUCCEEDED, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Assert assert node_execution.triggered_from == WorkflowNodeExecutionTriggeredFrom.SINGLE_STEP.value assert node_execution.workflow_run_id is None def test_node_execution_with_inputs_outputs_process_data(self) -> None: """Test node execution with inputs, outputs, and process_data.""" # Arrange inputs = {"query": "What is AI?", "temperature": 0.7} outputs = {"answer": "AI is Artificial Intelligence"} process_data = {"tokens_used": 150, "model": "gpt-4"} # Act node_execution = WorkflowNodeExecutionModel( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="llm_1", node_type=BuiltinNodeTypes.LLM, title="LLM Node", status=WorkflowNodeExecutionStatus.SUCCEEDED, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), inputs=json.dumps(inputs), outputs=json.dumps(outputs), process_data=json.dumps(process_data), ) # Assert assert node_execution.inputs_dict == inputs assert node_execution.outputs_dict == outputs assert node_execution.process_data_dict == process_data def test_node_execution_status_transitions(self) -> None: """Test node execution status transitions.""" # Arrange node_execution = WorkflowNodeExecutionModel( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="code_1", node_type=BuiltinNodeTypes.CODE, title="Code Node", status=WorkflowNodeExecutionStatus.RUNNING, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Act - transition to succeeded node_execution.status = WorkflowNodeExecutionStatus.SUCCEEDED node_execution.elapsed_time = 1.2 node_execution.finished_at = datetime.now(UTC) # Assert assert node_execution.status == WorkflowNodeExecutionStatus.SUCCEEDED assert node_execution.elapsed_time == 1.2 assert node_execution.finished_at is not None def test_node_execution_with_error(self) -> None: """Test node execution with error status.""" # Arrange error_message = "Code execution failed: SyntaxError on line 5" # Act node_execution = WorkflowNodeExecutionModel( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=3, node_id="code_1", node_type=BuiltinNodeTypes.CODE, title="Code Node", status=WorkflowNodeExecutionStatus.FAILED, error=error_message, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Assert assert node_execution.status == WorkflowNodeExecutionStatus.FAILED assert node_execution.error == error_message def test_node_execution_with_metadata(self) -> None: """Test node execution with execution metadata.""" # Arrange metadata = { "total_tokens": 500, "total_price": 0.01, "currency": "USD", "tool_info": {"provider": "openai", "tool": "gpt-4"}, } # Act node_execution = WorkflowNodeExecutionModel( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="llm_1", node_type=BuiltinNodeTypes.LLM, title="LLM Node", status=WorkflowNodeExecutionStatus.SUCCEEDED, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), execution_metadata=json.dumps(metadata), ) # Assert assert node_execution.execution_metadata_dict == metadata assert node_execution.execution_metadata_dict["total_tokens"] == 500 def test_node_execution_metadata_dict_empty(self) -> None: """Test execution_metadata_dict returns empty dict when metadata is None.""" # Arrange node_execution = WorkflowNodeExecutionModel( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="start", node_type=BuiltinNodeTypes.START, title="Start", status=WorkflowNodeExecutionStatus.SUCCEEDED, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), execution_metadata=None, ) # Act result = node_execution.execution_metadata_dict # Assert assert result == {} def test_node_execution_different_node_types(self) -> None: """Test node execution with different node types.""" # Test various node types node_types = [ (BuiltinNodeTypes.START, "Start Node"), (BuiltinNodeTypes.LLM, "LLM Node"), (BuiltinNodeTypes.CODE, "Code Node"), (BuiltinNodeTypes.TOOL, "Tool Node"), (BuiltinNodeTypes.IF_ELSE, "Conditional Node"), (BuiltinNodeTypes.END, "End Node"), ] for node_type, title in node_types: # Act node_execution = WorkflowNodeExecutionModel( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id=f"{node_type}_1", node_type=node_type, title=title, status=WorkflowNodeExecutionStatus.SUCCEEDED, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), ) # Assert assert node_execution.node_type == node_type assert node_execution.title == title class TestGraphConfigurationValidation: """Test suite for graph configuration validation.""" def test_workflow_graph_with_nodes_and_edges(self) -> None: """Test workflow graph configuration with nodes and edges.""" # Arrange graph_config = { "nodes": [ {"id": "start", "type": "start", "data": {"title": "Start"}}, {"id": "llm_1", "type": "llm", "data": {"title": "LLM Node", "model": "gpt-4"}}, {"id": "end", "type": "end", "data": {"title": "End"}}, ], "edges": [ {"source": "start", "target": "llm_1"}, {"source": "llm_1", "target": "end"}, ], } # Act workflow = _workflow_models_Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_config), features="{}", created_by=str(uuid4()), environment_variables=[], conversation_variables=[], rag_pipeline_variables=[], ) # Assert graph_dict = workflow.graph_dict assert len(graph_dict["nodes"]) == 3 assert len(graph_dict["edges"]) == 2 assert graph_dict["nodes"][0]["id"] == "start" assert graph_dict["edges"][0]["source"] == "start" assert graph_dict["edges"][0]["target"] == "llm_1" def test_workflow_graph_empty_configuration(self) -> None: """Test workflow with empty graph configuration.""" # Arrange graph_config = {"nodes": list[object](), "edges": list[object]()} # Act workflow = _workflow_models_Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_config), features="{}", created_by=str(uuid4()), environment_variables=[], conversation_variables=[], rag_pipeline_variables=[], ) # Assert graph_dict = workflow.graph_dict assert graph_dict["nodes"] == [] assert graph_dict["edges"] == [] def test_workflow_graph_complex_node_data(self) -> None: """Test workflow graph with complex node data structures.""" # Arrange graph_config = { "nodes": [ { "id": "llm_1", "type": "llm", "data": { "title": "Advanced LLM", "model": {"provider": "openai", "name": "gpt-4", "mode": "chat"}, "prompt_template": [ {"role": "system", "text": "You are a helpful assistant"}, {"role": "user", "text": "{{query}}"}, ], "model_parameters": {"temperature": 0.7, "max_tokens": 2000}, }, } ], "edges": list[object](), } # Act workflow = _workflow_models_Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_config), features="{}", created_by=str(uuid4()), environment_variables=[], conversation_variables=[], rag_pipeline_variables=[], ) # Assert graph_dict = workflow.graph_dict node_data = graph_dict["nodes"][0]["data"] assert node_data["model"]["provider"] == "openai" assert node_data["model_parameters"]["temperature"] == 0.7 assert len(node_data["prompt_template"]) == 2 def test_workflow_run_graph_preservation(self) -> None: """Test that WorkflowRun preserves graph configuration from Workflow.""" # Arrange original_graph = { "nodes": [ {"id": "start", "type": "start"}, {"id": "end", "type": "end"}, ], "edges": [{"source": "start", "target": "end"}], } # Act workflow_run = WorkflowRun( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", status=WorkflowExecutionStatus.RUNNING, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), graph=json.dumps(original_graph), ) # Assert assert workflow_run.graph_dict == original_graph assert len(workflow_run.graph_dict["nodes"]) == 2 def test_workflow_graph_with_conditional_branches(self) -> None: """Test workflow graph with conditional branching (if-else).""" # Arrange graph_config = { "nodes": [ {"id": "start", "type": "start"}, {"id": "if_else_1", "type": "if-else", "data": {"conditions": list[object]()}}, {"id": "branch_true", "type": "llm"}, {"id": "branch_false", "type": "code"}, {"id": "end", "type": "end"}, ], "edges": [ {"source": "start", "target": "if_else_1"}, {"source": "if_else_1", "sourceHandle": "true", "target": "branch_true"}, {"source": "if_else_1", "sourceHandle": "false", "target": "branch_false"}, {"source": "branch_true", "target": "end"}, {"source": "branch_false", "target": "end"}, ], } # Act workflow = _workflow_models_Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_config), features="{}", created_by=str(uuid4()), environment_variables=[], conversation_variables=[], rag_pipeline_variables=[], ) # Assert graph_dict = workflow.graph_dict assert len(graph_dict["nodes"]) == 5 assert len(graph_dict["edges"]) == 5 # Verify conditional edges conditional_edges = [e for e in graph_dict["edges"] if "sourceHandle" in e] assert len(conditional_edges) == 2 def test_workflow_graph_with_loop_structure(self) -> None: """Test workflow graph with loop/iteration structure.""" # Arrange graph_config = { "nodes": [ {"id": "start", "type": "start"}, {"id": "iteration_1", "type": "iteration", "data": {"iterator": "items"}}, {"id": "loop_body", "type": "llm"}, {"id": "end", "type": "end"}, ], "edges": [ {"source": "start", "target": "iteration_1"}, {"source": "iteration_1", "target": "loop_body"}, {"source": "loop_body", "target": "iteration_1"}, {"source": "iteration_1", "target": "end"}, ], } # Act workflow = _workflow_models_Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), type=WorkflowType.WORKFLOW, version="draft", graph=json.dumps(graph_config), features="{}", created_by=str(uuid4()), environment_variables=[], conversation_variables=[], rag_pipeline_variables=[], ) # Assert graph_dict = workflow.graph_dict iteration_node = next(n for n in graph_dict["nodes"] if n["type"] == "iteration") assert iteration_node["data"]["iterator"] == "items" def test_workflow_graph_dict_with_null_graph(self) -> None: """Test graph_dict property when graph is None.""" # Arrange workflow = _workflow_models_Workflow.new( tenant_id=str(uuid4()), app_id=str(uuid4()), type=WorkflowType.WORKFLOW, version="draft", graph=None, # pyrefly: ignore[bad-argument-type] features="{}", created_by=str(uuid4()), environment_variables=[], conversation_variables=[], rag_pipeline_variables=[], ) # Act result = workflow.graph_dict # Assert assert result == {} def test_workflow_run_inputs_dict_with_null_inputs(self) -> None: """Test inputs_dict property when inputs is None.""" # Arrange workflow_run = WorkflowRun( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", status=WorkflowExecutionStatus.RUNNING, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), inputs=None, ) # Act result = workflow_run.inputs_dict # Assert assert result == {} def test_workflow_run_outputs_dict_with_null_outputs(self) -> None: """Test outputs_dict property when outputs is None.""" # Arrange workflow_run = WorkflowRun( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), type=WorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="v1.0", status=WorkflowExecutionStatus.RUNNING, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), outputs=None, ) # Act result = workflow_run.outputs_dict # Assert assert result == {} def test_node_execution_inputs_dict_with_null_inputs(self) -> None: """Test node execution inputs_dict when inputs is None.""" # Arrange node_execution = WorkflowNodeExecutionModel( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="start", node_type=BuiltinNodeTypes.START, title="Start", status=WorkflowNodeExecutionStatus.SUCCEEDED, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), inputs=None, ) # Act result = node_execution.inputs_dict # Assert assert result is None def test_node_execution_outputs_dict_with_null_outputs(self) -> None: """Test node execution outputs_dict when outputs is None.""" # Arrange node_execution = WorkflowNodeExecutionModel( tenant_id=str(uuid4()), app_id=str(uuid4()), workflow_id=str(uuid4()), triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=str(uuid4()), index=1, node_id="start", node_type=BuiltinNodeTypes.START, title="Start", status=WorkflowNodeExecutionStatus.SUCCEEDED, created_by_role=CreatorUserRole.ACCOUNT, created_by=str(uuid4()), outputs=None, ) # Act result = node_execution.outputs_dict # Assert assert result is None """Regression coverage for ``WorkflowNodeExecutionModel`` account accessors. Ensures the ``@property``→session-parameter refactor preserves the role-based dispatch: ``created_by_account`` looks up an Account only when role is ACCOUNT; ``created_by_end_user`` looks up an EndUser only when role is END_USER. Also covers the session-carrying response source used by the console endpoints that validate ``WorkflowRunNodeExecutionResponse`` with ``from_attributes=True``. """ def _execution(role: CreatorUserRole, created_by: str) -> WorkflowNodeExecutionModel: """Construct a WorkflowNodeExecutionModel without touching the database.""" return WorkflowNodeExecutionModel( tenant_id="00000000-0000-0000-0000-000000000001", app_id="00000000-0000-0000-0000-000000000002", workflow_id=str(uuid4()), triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id=None, index=1, predecessor_node_id=None, node_execution_id=None, node_id="n1", node_type="start", title="Start", inputs=None, process_data=None, outputs=None, status=WorkflowNodeExecutionStatus.SUCCEEDED, error=None, elapsed_time=0.0, execution_metadata=None, created_by_role=role, created_by=created_by, ) class TestWorkflowNodeExecutionAccessorsCases: class TestCreatedByAccount: def test_returns_account_lookup_when_role_is_account(self, sqlite_session: Session) -> None: account = Account(name="Test Account", email="test@example.com") sqlite_session.add(account) sqlite_session.flush() execution = _execution(CreatorUserRole.ACCOUNT, created_by=account.id) result = execution.created_by_account(session=sqlite_session) assert result is not None assert result.id == account.id def test_returns_none_when_role_is_end_user(self, sqlite_session: Session) -> None: account = Account(name="Test Account", email="test@example.com") sqlite_session.add(account) sqlite_session.flush() execution = _execution(CreatorUserRole.END_USER, created_by=account.id) assert execution.created_by_account(session=sqlite_session) is None class TestCreatedByEndUser: def test_returns_end_user_lookup_when_role_is_end_user(self, sqlite_session: Session) -> None: end_user = EndUser( tenant_id="00000000-0000-0000-0000-000000000001", type=EndUserType.BROWSER, session_id="session-1", ) sqlite_session.add(end_user) sqlite_session.flush() execution = _execution(CreatorUserRole.END_USER, created_by=end_user.id) result = execution.created_by_end_user(session=sqlite_session) assert result is not None assert result.id == end_user.id def test_returns_none_when_role_is_account(self, sqlite_session: Session) -> None: end_user = EndUser( tenant_id="00000000-0000-0000-0000-000000000001", type=EndUserType.BROWSER, session_id="session-1", ) sqlite_session.add(end_user) sqlite_session.flush() execution = _execution(CreatorUserRole.ACCOUNT, created_by=end_user.id) assert execution.created_by_end_user(session=sqlite_session) is None class TestNodeExecutionResponseSource: def test_accessors_resolve_via_wrapped_session_and_other_attributes_proxy( self, sqlite_session: Session ) -> None: account = Account(name="Test Account", email="test@example.com") sqlite_session.add(account) sqlite_session.flush() execution = _execution(CreatorUserRole.ACCOUNT, created_by=account.id) source = node_execution_response_source(execution, session=sqlite_session) resolved = source.created_by_account assert resolved is not None assert resolved.id == account.id assert source.created_by_end_user is None assert source.node_id == "n1" """SQLite-backed tests for workflow node execution offload behavior.""" TABLES = (WorkflowNodeExecutionModel, WorkflowNodeExecutionOffload, UploadFile) def _persist_upload_file(session: Session, *, key: str = "offload/process-data.json") -> UploadFile: upload_file = UploadFile( tenant_id="tenant-1", storage_type=StorageType.LOCAL, key=key, name="process-data.json", size=18, extension="json", mime_type="application/json", created_by_role=CreatorUserRole.ACCOUNT, created_by="account-1", created_at=datetime.now(UTC), used=True, ) session.add(upload_file) session.flush() return upload_file def _persist_execution( session: Session, *, process_data: str = '{"test": "data"}', offloads: tuple[tuple[ExecutionOffLoadType, str], ...] = (), ) -> WorkflowNodeExecutionModel: execution = WorkflowNodeExecutionModel( tenant_id="tenant-1", app_id="app-1", workflow_id="workflow-1", triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, workflow_run_id="run-1", index=1, predecessor_node_id=None, node_execution_id="node-execution-1", node_id="node-1", node_type="code", title="Code", inputs='{"input": "value"}', process_data=process_data, outputs='{"output": "value"}', status=WorkflowNodeExecutionStatus.SUCCEEDED, error=None, elapsed_time=0, execution_metadata=None, created_by_role=CreatorUserRole.ACCOUNT, created_by="account-1", finished_at=None, ) session.add(execution) session.flush() session.add_all( [ WorkflowNodeExecutionOffload( tenant_id=execution.tenant_id, app_id=execution.app_id, node_execution_id=execution.id, type_=type_, file_id=file_id, ) for type_, file_id in offloads ] ) session.commit() session.expunge_all() stmt = WorkflowNodeExecutionModel.preload_offload_data( select(WorkflowNodeExecutionModel).where(WorkflowNodeExecutionModel.id == execution.id) ) loaded_execution = session.scalar(stmt) assert loaded_execution is not None return loaded_execution class TestWorkflowNodeExecutionOffloadCases: @pytest.mark.parametrize("sqlite_session", [TABLES], indirect=True) class TestWorkflowNodeExecutionModel: """Exercise truncation flags and content restoration using persisted offload rows.""" def test_process_data_truncated_property_false_when_no_offload_data(self, sqlite_session: Session) -> None: execution = _persist_execution(sqlite_session) assert execution.process_data_truncated is False def test_process_data_truncated_property_false_when_no_process_data_file(self, sqlite_session: Session) -> None: execution = _persist_execution( sqlite_session, offloads=( (ExecutionOffLoadType.INPUTS, "inputs-file"), (ExecutionOffLoadType.OUTPUTS, "outputs-file"), ), ) assert execution.process_data_truncated is False def test_process_data_truncated_property_true_when_process_data_file_exists( self, sqlite_session: Session ) -> None: execution = _persist_execution( sqlite_session, offloads=((ExecutionOffLoadType.PROCESS_DATA, "process-data-file-id"),), ) assert execution.process_data_truncated is True def test_load_full_process_data_with_no_offload_data(self, sqlite_session: Session) -> None: execution = _persist_execution(sqlite_session) storage = Mock() result = execution.load_full_process_data(sqlite_session, storage) assert result == {"test": "data"} storage.load.assert_not_called() def test_load_full_process_data_with_no_file(self, sqlite_session: Session) -> None: execution = _persist_execution( sqlite_session, offloads=((ExecutionOffLoadType.INPUTS, "inputs-file"),), ) storage = Mock() result = execution.load_full_process_data(sqlite_session, storage) assert result == {"test": "data"} storage.load.assert_not_called() def test_load_full_process_data_with_file(self, sqlite_session: Session) -> None: upload_file = _persist_upload_file(sqlite_session) execution = _persist_execution( sqlite_session, process_data='{"truncated": true}', offloads=((ExecutionOffLoadType.PROCESS_DATA, upload_file.id),), ) full_process_data = {"full": "data", "large_field": "x" * 10000} storage = Mock() storage.load.return_value = json.dumps(full_process_data) result = execution.load_full_process_data(sqlite_session, storage) assert result == full_process_data storage.load.assert_called_once_with(upload_file.key) def test_consistency_with_inputs_outputs_truncation(self, sqlite_session: Session) -> None: execution = _persist_execution( sqlite_session, offloads=( (ExecutionOffLoadType.INPUTS, "inputs-file"), (ExecutionOffLoadType.OUTPUTS, "outputs-file"), (ExecutionOffLoadType.PROCESS_DATA, "process-data-file"), ), ) assert execution.inputs_truncated is True assert execution.outputs_truncated is True assert execution.process_data_truncated is True def test_mixed_truncation_states(self, sqlite_session: Session) -> None: execution = _persist_execution( sqlite_session, offloads=((ExecutionOffLoadType.PROCESS_DATA, "process-data-file"),), ) assert execution.inputs_truncated is False assert execution.outputs_truncated is False assert execution.process_data_truncated is True def test_preload_offload_data_and_files_loads_process_data_file(self, sqlite_session: Session) -> None: upload_file = _persist_upload_file(sqlite_session) execution = _persist_execution( sqlite_session, offloads=((ExecutionOffLoadType.PROCESS_DATA, upload_file.id),), ) execution_id = execution.id sqlite_session.expunge_all() stmt = WorkflowNodeExecutionModel.preload_offload_data_and_files( select(WorkflowNodeExecutionModel).where(WorkflowNodeExecutionModel.id == execution_id) ) preloaded_execution = sqlite_session.scalar(stmt) assert preloaded_execution is not None assert len(preloaded_execution.offload_data) == 1 assert preloaded_execution.offload_data[0].file is not None assert preloaded_execution.offload_data[0].file.key == upload_file.key """Regression coverage for ``WorkflowRun`` account accessors. Ensures the ``@property``→session-parameter refactor preserves the role-based dispatch: ``created_by_account`` looks up an Account only when role is ACCOUNT; ``created_by_end_user`` looks up an EndUser only when role is END_USER. Also covers the session-carrying response sources used by the console endpoints that validate the run detail / pagination responses with ``from_attributes=True``. """ def _run(role: CreatorUserRole, created_by: str) -> WorkflowRun: """Construct a WorkflowRun without touching the database.""" return WorkflowRun( id=str(uuid4()), tenant_id="00000000-0000-0000-0000-000000000001", app_id="00000000-0000-0000-0000-000000000002", workflow_id=str(uuid4()), type=ModelWorkflowType.WORKFLOW, triggered_from=WorkflowRunTriggeredFrom.APP_RUN, version="1.0", status=WorkflowExecutionStatus.SUCCEEDED, created_by_role=role, created_by=created_by, ) class TestWorkflowRunAccessorsCases: class TestCreatedByAccount: def test_returns_account_lookup_when_role_is_account(self, sqlite_session: Session) -> None: account = Account(name="Test Account", email="test@example.com") sqlite_session.add(account) sqlite_session.flush() run = _run(CreatorUserRole.ACCOUNT, created_by=account.id) result = run.created_by_account(session=sqlite_session) assert result is not None assert result.id == account.id def test_returns_none_when_role_is_end_user(self, sqlite_session: Session) -> None: account = Account(name="Test Account", email="test@example.com") sqlite_session.add(account) sqlite_session.flush() run = _run(CreatorUserRole.END_USER, created_by=account.id) assert run.created_by_account(session=sqlite_session) is None class TestCreatedByEndUser: def test_returns_end_user_lookup_when_role_is_end_user(self, sqlite_session: Session) -> None: end_user = EndUser( tenant_id="00000000-0000-0000-0000-000000000001", type=EndUserType.BROWSER, session_id="session-1", ) sqlite_session.add(end_user) sqlite_session.flush() run = _run(CreatorUserRole.END_USER, created_by=end_user.id) result = run.created_by_end_user(session=sqlite_session) assert result is not None assert result.id == end_user.id def test_returns_none_when_role_is_account(self, sqlite_session: Session) -> None: end_user = EndUser( tenant_id="00000000-0000-0000-0000-000000000001", type=EndUserType.BROWSER, session_id="session-1", ) sqlite_session.add(end_user) sqlite_session.flush() run = _run(CreatorUserRole.ACCOUNT, created_by=end_user.id) assert run.created_by_end_user(session=sqlite_session) is None class TestWorkflowRunResponseSources: def test_detail_source_resolves_accessors_and_proxies_other_attributes(self, sqlite_session: Session) -> None: account = Account(name="Test Account", email="test@example.com") sqlite_session.add(account) sqlite_session.flush() run = _run(CreatorUserRole.ACCOUNT, created_by=account.id) source = workflow_run_response_source(run, session=sqlite_session) resolved = source.created_by_account assert resolved is not None assert resolved.id == account.id assert source.created_by_end_user is None assert source.version == "1.0" def test_pagination_source_wraps_each_run(self, sqlite_session: Session) -> None: account = Account(name="Test Account", email="test@example.com") sqlite_session.add(account) sqlite_session.flush() runs = [_run(CreatorUserRole.ACCOUNT, created_by=account.id) for _ in range(2)] pagination = SimpleNamespace(data=runs, limit=20, has_more=False) payload = workflow_run_pagination_response_source(pagination, session=sqlite_session) assert payload["limit"] == 20 assert payload["has_more"] is False assert [item.created_by_account.id for item in payload["data"]] == [account.id, account.id] @pytest.mark.parametrize( ("features", "expected"), [ pytest.param({}, {}, id="missing-upload"), pytest.param({"file_upload": None}, {"file_upload": None}, id="null-upload"), pytest.param({"file_upload": False}, {"file_upload": False}, id="non-object-upload"), pytest.param({"file_upload": {}}, {"file_upload": {}}, id="missing-image"), pytest.param({"file_upload": {"image": None}}, {"file_upload": {"image": None}}, id="null-image"), pytest.param({"file_upload": {"image": False}}, {"file_upload": {"image": False}}, id="non-object-image"), pytest.param( {"file_upload": {"enabled": False, "image": {"enabled": False}}}, {"file_upload": {"enabled": False, "image": {"enabled": False}}}, id="disabled-image", ), pytest.param(OLD_VERSION_WORKFLOW_FEATURES, NEW_VERSION_WORKFLOW_FEATURES, id="legacy-image"), pytest.param(NEW_VERSION_WORKFLOW_FEATURES, NEW_VERSION_WORKFLOW_FEATURES, id="modern-upload"), ], ) def test_normalized_features_preserves_stored_payload_without_dirtying_workflow( sqlite_session: Session, features: dict[str, JsonValue], expected: dict[str, JsonValue], ) -> None: stored_features = json.dumps(features) workflow = _workflow_feature_compatibility_Workflow( tenant_id=str(uuid4()), app_id=str(uuid4()), type=WorkflowType.CHAT, version="published", graph="{}", _features=stored_features, created_by=str(uuid4()), ) sqlite_session.add(workflow) sqlite_session.commit() assert workflow.normalized_features_dict == expected assert workflow.serialized_features == stored_features assert workflow not in sqlite_session.dirty sqlite_session.expire(workflow) assert workflow.serialized_features == stored_features class TestWorkflowTypeBaseCases: @staticmethod def test_workflow_models_use_typebase_registry_and_generated_defaults() -> None: models = ( _workflow_Workflow, WorkflowVersionCounter, WorkflowRun, WorkflowNodeExecutionModel, WorkflowDraftVariable, ) typebase_models = {mapper.class_ for mapper in TypeBase.registry.mappers} base_models = {mapper.class_ for mapper in Base.registry.mappers} assert all(model in typebase_models for model in models) assert all(model not in base_models for model in models) instances = (_workflow_Workflow(), WorkflowRun(), WorkflowNodeExecutionModel(), WorkflowDraftVariable()) assert all(instance.id for instance in instances) assert len({instance.id for instance in instances}) == len(instances) assert isinstance(instances[0].created_at, datetime) assert isinstance(instances[-1].created_at, datetime) @staticmethod def test_workflow_constructor_serializes_variable_collections_and_configures_offload_relationship() -> None: workflow = _workflow_Workflow( _features="{}", _environment_variables=[], _conversation_variables=[], _rag_pipeline_variables=[], ) assert workflow._features == "{}" assert json.loads(workflow._environment_variables) == {} assert json.loads(workflow._conversation_variables) == {} assert json.loads(workflow._rag_pipeline_variables) == {} configure_mappers() relationship = inspect(WorkflowNodeExecutionModel).relationships["offload_data"] assert relationship.mapper.class_ is WorkflowNodeExecutionOffload assert relationship.back_populates == "execution"