mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 23:18:05 +08:00
fix(tests): fix broken tests and linter issues
This commit is contained in:
@ -1,213 +0,0 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session, joinedload, selectinload
|
||||
|
||||
from extensions.ext_database import db
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from libs.uuid_utils import uuidv7
|
||||
from models.enums import CreatorUserRole
|
||||
from models.model import UploadFile
|
||||
from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionOffload, WorkflowNodeExecutionTriggeredFrom
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session(flask_req_ctx):
|
||||
with Session(bind=db.engine, expire_on_commit=False) as session:
|
||||
yield session
|
||||
|
||||
|
||||
def test_offload(session, setup_account):
|
||||
tenant_id = str(uuid.uuid4())
|
||||
app_id = str(uuid.uuid4())
|
||||
# step 1: create a UploadFile
|
||||
input_upload_file = UploadFile(
|
||||
tenant_id=tenant_id,
|
||||
storage_type="local",
|
||||
key="fake_storage_key",
|
||||
name="test_file.txt",
|
||||
size=1024,
|
||||
extension="txt",
|
||||
mime_type="text/plain",
|
||||
created_by_role=CreatorUserRole.ACCOUNT,
|
||||
created_by=setup_account.id,
|
||||
created_at=naive_utc_now(),
|
||||
used=False,
|
||||
)
|
||||
output_upload_file = UploadFile(
|
||||
tenant_id=tenant_id,
|
||||
storage_type="local",
|
||||
key="fake_storage_key",
|
||||
name="test_file.txt",
|
||||
size=1024,
|
||||
extension="txt",
|
||||
mime_type="text/plain",
|
||||
created_by_role=CreatorUserRole.ACCOUNT,
|
||||
created_by=setup_account.id,
|
||||
created_at=naive_utc_now(),
|
||||
used=False,
|
||||
)
|
||||
session.add(input_upload_file)
|
||||
session.add(output_upload_file)
|
||||
session.flush()
|
||||
|
||||
# step 2: create a WorkflowNodeExecutionModel
|
||||
node_execution = WorkflowNodeExecutionModel(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
workflow_id=str(uuid.uuid4()),
|
||||
triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
|
||||
index=1,
|
||||
node_id="test_node_id",
|
||||
node_type="test",
|
||||
title="Test Node",
|
||||
status="succeeded",
|
||||
created_by_role=CreatorUserRole.ACCOUNT.value,
|
||||
created_by=setup_account.id,
|
||||
)
|
||||
session.add(node_execution)
|
||||
session.flush()
|
||||
|
||||
# step 3: create a WorkflowNodeExecutionOffload
|
||||
offload = WorkflowNodeExecutionOffload(
|
||||
id=uuidv7(),
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
node_execution_id=node_execution.id,
|
||||
inputs_file_id=input_upload_file.id,
|
||||
outputs_file_id=output_upload_file.id,
|
||||
)
|
||||
session.add(offload)
|
||||
session.flush()
|
||||
|
||||
# Test preloading - this should work without raising LazyLoadError
|
||||
result = (
|
||||
session.query(WorkflowNodeExecutionModel)
|
||||
.options(
|
||||
selectinload(WorkflowNodeExecutionModel.offload_data).options(
|
||||
joinedload(
|
||||
WorkflowNodeExecutionOffload.inputs_file,
|
||||
),
|
||||
joinedload(
|
||||
WorkflowNodeExecutionOffload.outputs_file,
|
||||
),
|
||||
)
|
||||
)
|
||||
.filter(WorkflowNodeExecutionModel.id == node_execution.id)
|
||||
.first()
|
||||
)
|
||||
|
||||
# Verify the relationships are properly loaded
|
||||
assert result is not None
|
||||
assert result.offload_data is not None
|
||||
assert result.offload_data.inputs_file is not None
|
||||
assert result.offload_data.inputs_file.id == input_upload_file.id
|
||||
assert result.offload_data.inputs_file.name == "test_file.txt"
|
||||
|
||||
# Test the computed properties
|
||||
assert result.inputs_truncated is True
|
||||
assert result.outputs_truncated is False
|
||||
assert False
|
||||
|
||||
|
||||
def _test_offload_save(session, setup_account):
|
||||
tenant_id = str(uuid.uuid4())
|
||||
app_id = str(uuid.uuid4())
|
||||
# step 1: create a UploadFile
|
||||
input_upload_file = UploadFile(
|
||||
tenant_id=tenant_id,
|
||||
storage_type="local",
|
||||
key="fake_storage_key",
|
||||
name="test_file.txt",
|
||||
size=1024,
|
||||
extension="txt",
|
||||
mime_type="text/plain",
|
||||
created_by_role=CreatorUserRole.ACCOUNT,
|
||||
created_by=setup_account.id,
|
||||
created_at=naive_utc_now(),
|
||||
used=False,
|
||||
)
|
||||
output_upload_file = UploadFile(
|
||||
tenant_id=tenant_id,
|
||||
storage_type="local",
|
||||
key="fake_storage_key",
|
||||
name="test_file.txt",
|
||||
size=1024,
|
||||
extension="txt",
|
||||
mime_type="text/plain",
|
||||
created_by_role=CreatorUserRole.ACCOUNT,
|
||||
created_by=setup_account.id,
|
||||
created_at=naive_utc_now(),
|
||||
used=False,
|
||||
)
|
||||
|
||||
node_execution_id = id = str(uuid.uuid4())
|
||||
|
||||
# step 3: create a WorkflowNodeExecutionOffload
|
||||
offload = WorkflowNodeExecutionOffload(
|
||||
id=uuidv7(),
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
node_execution_id=node_execution_id,
|
||||
)
|
||||
offload.inputs_file = input_upload_file
|
||||
offload.outputs_file = output_upload_file
|
||||
|
||||
# step 2: create a WorkflowNodeExecutionModel
|
||||
node_execution = WorkflowNodeExecutionModel(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
workflow_id=str(uuid.uuid4()),
|
||||
triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
|
||||
index=1,
|
||||
node_id="test_node_id",
|
||||
node_type="test",
|
||||
title="Test Node",
|
||||
status="succeeded",
|
||||
created_by_role=CreatorUserRole.ACCOUNT.value,
|
||||
created_by=setup_account.id,
|
||||
)
|
||||
node_execution.offload_data = offload
|
||||
session.add(node_execution)
|
||||
session.flush()
|
||||
|
||||
assert False
|
||||
|
||||
|
||||
"""
|
||||
2025-08-21 15:34:49,570 INFO sqlalchemy.engine.Engine BEGIN (implicit)
|
||||
2025-08-21 15:34:49,572 INFO sqlalchemy.engine.Engine INSERT INTO upload_files (id, tenant_id, storage_type, key, name, size, extension, mime_type, created_by_role, created_by, created_at, used, used_by, used_at, hash, source_url) VALUES (%(id__0)s::UUID, %(tenant_id__0)s::UUID, %(storage_type__0)s, %(k ... 410 characters truncated ... (created_at__1)s, %(used__1)s, %(used_by__1)s::UUID, %(used_at__1)s, %(hash__1)s, %(source_url__1)s)
|
||||
2025-08-21 15:34:49,572 INFO sqlalchemy.engine.Engine [generated in 0.00009s (insertmanyvalues) 1/1 (unordered)] {'created_at__0': datetime.datetime(2025, 8, 21, 15, 34, 49, 570482), 'id__0': '366621fa-4326-403e-8709-62e4d0de7367', 'storage_type__0': 'local', 'extension__0': 'txt', 'created_by__0': 'ccc7657c-fb48-46bd-8f42-c837b14eab18', 'used_at__0': None, 'used_by__0': None, 'source_url__0': '', 'mime_type__0': 'text/plain', 'created_by_role__0': 'account', 'used__0': False, 'size__0': 1024, 'tenant_id__0': '4c1bbfc9-a28b-4d93-8987-45db78e3269c', 'hash__0': None, 'key__0': 'fake_storage_key', 'name__0': 'test_file.txt', 'created_at__1': datetime.datetime(2025, 8, 21, 15, 34, 49, 570563), 'id__1': '3cdec641-a452-4df0-a9af-4a1a30c27ea5', 'storage_type__1': 'local', 'extension__1': 'txt', 'created_by__1': 'ccc7657c-fb48-46bd-8f42-c837b14eab18', 'used_at__1': None, 'used_by__1': None, 'source_url__1': '', 'mime_type__1': 'text/plain', 'created_by_role__1': 'account', 'used__1': False, 'size__1': 1024, 'tenant_id__1': '4c1bbfc9-a28b-4d93-8987-45db78e3269c', 'hash__1': None, 'key__1': 'fake_storage_key', 'name__1': 'test_file.txt'}
|
||||
2025-08-21 15:34:49,576 INFO sqlalchemy.engine.Engine INSERT INTO workflow_node_executions (id, tenant_id, app_id, workflow_id, triggered_from, workflow_run_id, index, predecessor_node_id, node_execution_id, node_id, node_type, title, inputs, process_data, outputs, status, error, execution_metadata, created_by_role, created_by, finished_at) VALUES (%(id)s::UUID, %(tenant_id)s::UUID, %(app_id)s::UUID, %(workflow_id)s::UUID, %(triggered_from)s, %(workflow_run_id)s::UUID, %(index)s, %(predecessor_node_id)s, %(node_execution_id)s, %(node_id)s, %(node_type)s, %(title)s, %(inputs)s, %(process_data)s, %(outputs)s, %(status)s, %(error)s, %(execution_metadata)s, %(created_by_role)s, %(created_by)s::UUID, %(finished_at)s) RETURNING workflow_node_executions.elapsed_time, workflow_node_executions.created_at
|
||||
2025-08-21 15:34:49,576 INFO sqlalchemy.engine.Engine [generated in 0.00019s] {'id': '9aac28b6-b6fc-4aea-abdf-21da3227e621', 'tenant_id': '4c1bbfc9-a28b-4d93-8987-45db78e3269c', 'app_id': '79fa81c7-2760-40db-af54-74cb2fea2ce7', 'workflow_id': '95d341e3-381c-4c54-a383-f685a9741053', 'triggered_from': <WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN: 'workflow-run'>, 'workflow_run_id': None, 'index': 1, 'predecessor_node_id': None, 'node_execution_id': None, 'node_id': 'test_node_id', 'node_type': 'test', 'title': 'Test Node', 'inputs': None, 'process_data': None, 'outputs': None, 'status': 'succeeded', 'error': None, 'execution_metadata': None, 'created_by_role': 'account', 'created_by': 'ccc7657c-fb48-46bd-8f42-c837b14eab18', 'finished_at': None}
|
||||
2025-08-21 15:34:49,579 INFO sqlalchemy.engine.Engine INSERT INTO workflow_node_execution_offload (id, created_at, tenant_id, app_id, node_execution_id, inputs_file_id, outputs_file_id) VALUES (%(id)s::UUID, %(created_at)s, %(tenant_id)s::UUID, %(app_id)s::UUID, %(node_execution_id)s::UUID, %(inputs_file_id)s::UUID, %(outputs_file_id)s::UUID)
|
||||
2025-08-21 15:34:49,579 INFO sqlalchemy.engine.Engine [generated in 0.00016s] {'id': '0198cd44-b7ea-724b-9e1b-5f062a2ef45b', 'created_at': datetime.datetime(2025, 8, 21, 15, 34, 49, 579072), 'tenant_id': '4c1bbfc9-a28b-4d93-8987-45db78e3269c', 'app_id': '79fa81c7-2760-40db-af54-74cb2fea2ce7', 'node_execution_id': '9aac28b6-b6fc-4aea-abdf-21da3227e621', 'inputs_file_id': '366621fa-4326-403e-8709-62e4d0de7367', 'outputs_file_id': '3cdec641-a452-4df0-a9af-4a1a30c27ea5'}
|
||||
2025-08-21 15:34:49,581 INFO sqlalchemy.engine.Engine SELECT workflow_node_executions.id AS workflow_node_executions_id, workflow_node_executions.tenant_id AS workflow_node_executions_tenant_id, workflow_node_executions.app_id AS workflow_node_executions_app_id, workflow_node_executions.workflow_id AS workflow_node_executions_workflow_id, workflow_node_executions.triggered_from AS workflow_node_executions_triggered_from, workflow_node_executions.workflow_run_id AS workflow_node_executions_workflow_run_id, workflow_node_executions.index AS workflow_node_executions_index, workflow_node_executions.predecessor_node_id AS workflow_node_executions_predecessor_node_id, workflow_node_executions.node_execution_id AS workflow_node_executions_node_execution_id, workflow_node_executions.node_id AS workflow_node_executions_node_id, workflow_node_executions.node_type AS workflow_node_executions_node_type, workflow_node_executions.title AS workflow_node_executions_title, workflow_node_executions.inputs AS workflow_node_executions_inputs, workflow_node_executions.process_data AS workflow_node_executions_process_data, workflow_node_executions.outputs AS workflow_node_executions_outputs, workflow_node_executions.status AS workflow_node_executions_status, workflow_node_executions.error AS workflow_node_executions_error, workflow_node_executions.elapsed_time AS workflow_node_executions_elapsed_time, workflow_node_executions.execution_metadata AS workflow_node_executions_execution_metadata, workflow_node_executions.created_at AS workflow_node_executions_created_at, workflow_node_executions.created_by_role AS workflow_node_executions_created_by_role, workflow_node_executions.created_by AS workflow_node_executions_created_by, workflow_node_executions.finished_at AS workflow_node_executions_finished_at
|
||||
FROM workflow_node_executions
|
||||
WHERE workflow_node_executions.id = %(id_1)s::UUID
|
||||
LIMIT %(param_1)s
|
||||
2025-08-21 15:34:49,581 INFO sqlalchemy.engine.Engine [generated in 0.00009s] {'id_1': '9aac28b6-b6fc-4aea-abdf-21da3227e621', 'param_1': 1}
|
||||
2025-08-21 15:34:49,585 INFO sqlalchemy.engine.Engine SELECT workflow_node_execution_offload.node_execution_id AS workflow_node_execution_offload_node_execution_id, workflow_node_execution_offload.id AS workflow_node_execution_offload_id, workflow_node_execution_offload.created_at AS workflow_node_execution_offload_created_at, workflow_node_execution_offload.tenant_id AS workflow_node_execution_offload_tenant_id, workflow_node_execution_offload.app_id AS workflow_node_execution_offload_app_id, workflow_node_execution_offload.inputs_file_id AS workflow_node_execution_offload_inputs_file_id, workflow_node_execution_offload.outputs_file_id AS workflow_node_execution_offload_outputs_file_id
|
||||
FROM workflow_node_execution_offload
|
||||
WHERE workflow_node_execution_offload.node_execution_id IN (%(primary_keys_1)s::UUID)
|
||||
2025-08-21 15:34:49,585 INFO sqlalchemy.engine.Engine [generated in 0.00021s] {'primary_keys_1': '9aac28b6-b6fc-4aea-abdf-21da3227e621'}
|
||||
2025-08-21 15:34:49,587 INFO sqlalchemy.engine.Engine SELECT upload_files.id AS upload_files_id, upload_files.tenant_id AS upload_files_tenant_id, upload_files.storage_type AS upload_files_storage_type, upload_files.key AS upload_files_key, upload_files.name AS upload_files_name, upload_files.size AS upload_files_size, upload_files.extension AS upload_files_extension, upload_files.mime_type AS upload_files_mime_type, upload_files.created_by_role AS upload_files_created_by_role, upload_files.created_by AS upload_files_created_by, upload_files.created_at AS upload_files_created_at, upload_files.used AS upload_files_used, upload_files.used_by AS upload_files_used_by, upload_files.used_at AS upload_files_used_at, upload_files.hash AS upload_files_hash, upload_files.source_url AS upload_files_source_url
|
||||
FROM upload_files
|
||||
WHERE upload_files.id IN (%(primary_keys_1)s::UUID)
|
||||
2025-08-21 15:34:49,587 INFO sqlalchemy.engine.Engine [generated in 0.00012s] {'primary_keys_1': '3cdec641-a452-4df0-a9af-4a1a30c27ea5'}
|
||||
2025-08-21 15:34:49,588 INFO sqlalchemy.engine.Engine SELECT upload_files.id AS upload_files_id, upload_files.tenant_id AS upload_files_tenant_id, upload_files.storage_type AS upload_files_storage_type, upload_files.key AS upload_files_key, upload_files.name AS upload_files_name, upload_files.size AS upload_files_size, upload_files.extension AS upload_files_extension, upload_files.mime_type AS upload_files_mime_type, upload_files.created_by_role AS upload_files_created_by_role, upload_files.created_by AS upload_files_created_by, upload_files.created_at AS upload_files_created_at, upload_files.used AS upload_files_used, upload_files.used_by AS upload_files_used_by, upload_files.used_at AS upload_files_used_at, upload_files.hash AS upload_files_hash, upload_files.source_url AS upload_files_source_url
|
||||
FROM upload_files
|
||||
WHERE upload_files.id IN (%(primary_keys_1)s::UUID)
|
||||
2025-08-21 15:34:49,588 INFO sqlalchemy.engine.Engine [generated in 0.00010s] {'primary_keys_1': '366621fa-4326-403e-8709-62e4d0de7367'}
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
upload_file_id: 366621fa-4326-403e-8709-62e4d0de7367 3cdec641-a452-4df0-a9af-4a1a30c27ea5
|
||||
|
||||
workflow_node_executions_id: 9aac28b6-b6fc-4aea-abdf-21da3227e621
|
||||
|
||||
offload_id: 0198cd44-b7ea-724b-9e1b-5f062a2ef45b
|
||||
"""
|
||||
@ -24,6 +24,7 @@ from models.workflow import WorkflowNodeExecutionTriggeredFrom
|
||||
@dataclass
|
||||
class TruncationTestData:
|
||||
"""Test data for truncation scenarios."""
|
||||
|
||||
name: str
|
||||
process_data: dict[str, any]
|
||||
should_truncate: bool
|
||||
@ -32,16 +33,17 @@ class TruncationTestData:
|
||||
|
||||
class TestProcessDataTruncationIntegration:
|
||||
"""Integration tests for process_data truncation functionality."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def in_memory_db_engine(self):
|
||||
"""Create an in-memory SQLite database for testing."""
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
|
||||
|
||||
# Create minimal table structure for testing
|
||||
with engine.connect() as conn:
|
||||
# Create workflow_node_executions table
|
||||
conn.execute(text("""
|
||||
conn.execute(
|
||||
text("""
|
||||
CREATE TABLE workflow_node_executions (
|
||||
id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
@ -67,10 +69,12 @@ class TestProcessDataTruncationIntegration:
|
||||
created_by TEXT NOT NULL,
|
||||
finished_at DATETIME
|
||||
)
|
||||
"""))
|
||||
|
||||
""")
|
||||
)
|
||||
|
||||
# Create workflow_node_execution_offload table
|
||||
conn.execute(text("""
|
||||
conn.execute(
|
||||
text("""
|
||||
CREATE TABLE workflow_node_execution_offload (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
@ -81,10 +85,12 @@ class TestProcessDataTruncationIntegration:
|
||||
outputs_file_id TEXT,
|
||||
process_data_file_id TEXT
|
||||
)
|
||||
"""))
|
||||
|
||||
""")
|
||||
)
|
||||
|
||||
# Create upload_files table (simplified)
|
||||
conn.execute(text("""
|
||||
conn.execute(
|
||||
text("""
|
||||
CREATE TABLE upload_files (
|
||||
id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
@ -93,10 +99,11 @@ class TestProcessDataTruncationIntegration:
|
||||
size INTEGER NOT NULL,
|
||||
created_at DATETIME NOT NULL
|
||||
)
|
||||
"""))
|
||||
|
||||
""")
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
|
||||
return engine
|
||||
|
||||
@pytest.fixture
|
||||
@ -111,7 +118,7 @@ class TestProcessDataTruncationIntegration:
|
||||
def repository(self, in_memory_db_engine, mock_account):
|
||||
"""Create a repository instance for testing."""
|
||||
session_factory = sessionmaker(bind=in_memory_db_engine)
|
||||
|
||||
|
||||
return SQLAlchemyWorkflowNodeExecutionRepository(
|
||||
session_factory=session_factory,
|
||||
user=mock_account,
|
||||
@ -120,9 +127,7 @@ class TestProcessDataTruncationIntegration:
|
||||
)
|
||||
|
||||
def create_test_execution(
|
||||
self,
|
||||
process_data: dict[str, any] | None = None,
|
||||
execution_id: str = "test-execution-id"
|
||||
self, process_data: dict[str, any] | None = None, execution_id: str = "test-execution-id"
|
||||
) -> WorkflowNodeExecution:
|
||||
"""Create a test execution with process_data."""
|
||||
return WorkflowNodeExecution(
|
||||
@ -160,104 +165,96 @@ class TestProcessDataTruncationIntegration:
|
||||
"logs": ["log entry"] * 500, # Large array
|
||||
"config": {"setting": "value"},
|
||||
"status": "processing",
|
||||
"details": {"description": "y" * 5000} # Large string
|
||||
"details": {"description": "y" * 5000}, # Large string
|
||||
},
|
||||
should_truncate=True,
|
||||
expected_storage_interaction=True,
|
||||
),
|
||||
]
|
||||
|
||||
@patch('core.repositories.sqlalchemy_workflow_node_execution_repository.dify_config')
|
||||
@patch('services.file_service.FileService.upload_file')
|
||||
@patch('extensions.ext_storage.storage')
|
||||
def test_end_to_end_process_data_truncation(
|
||||
self,
|
||||
mock_storage,
|
||||
mock_upload_file,
|
||||
mock_config,
|
||||
repository
|
||||
):
|
||||
@patch("core.repositories.sqlalchemy_workflow_node_execution_repository.dify_config")
|
||||
@patch("services.file_service.FileService.upload_file")
|
||||
@patch("extensions.ext_storage.storage")
|
||||
def test_end_to_end_process_data_truncation(self, mock_storage, mock_upload_file, mock_config, repository):
|
||||
"""Test end-to-end process_data truncation functionality."""
|
||||
# Configure truncation limits
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_MAX_SIZE = 1000
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_ARRAY_LENGTH = 100
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_STRING_LENGTH = 500
|
||||
|
||||
|
||||
# Create large process_data that should be truncated
|
||||
large_process_data = {
|
||||
"large_field": "x" * 10000, # Exceeds string length limit
|
||||
"metadata": {"type": "processing", "timestamp": 1234567890}
|
||||
"metadata": {"type": "processing", "timestamp": 1234567890},
|
||||
}
|
||||
|
||||
|
||||
# Mock file upload
|
||||
mock_file = Mock()
|
||||
mock_file.id = "mock-process-data-file-id"
|
||||
mock_upload_file.return_value = mock_file
|
||||
|
||||
|
||||
# Create and save execution
|
||||
execution = self.create_test_execution(process_data=large_process_data)
|
||||
repository.save(execution)
|
||||
|
||||
|
||||
# Verify truncation occurred
|
||||
assert execution.process_data_truncated is True
|
||||
truncated_data = execution.get_truncated_process_data()
|
||||
assert truncated_data is not None
|
||||
assert truncated_data != large_process_data # Should be different due to truncation
|
||||
|
||||
|
||||
# Verify file upload was called for process_data
|
||||
assert mock_upload_file.called
|
||||
upload_args = mock_upload_file.call_args
|
||||
assert "_process_data" in upload_args[1]["filename"]
|
||||
|
||||
@patch('core.repositories.sqlalchemy_workflow_node_execution_repository.dify_config')
|
||||
@patch("core.repositories.sqlalchemy_workflow_node_execution_repository.dify_config")
|
||||
def test_small_process_data_no_truncation(self, mock_config, repository):
|
||||
"""Test that small process_data is not truncated."""
|
||||
# Configure truncation limits
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_MAX_SIZE = 1000
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_ARRAY_LENGTH = 100
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_STRING_LENGTH = 500
|
||||
|
||||
|
||||
# Create small process_data
|
||||
small_process_data = {"small": "data", "count": 5}
|
||||
|
||||
|
||||
execution = self.create_test_execution(process_data=small_process_data)
|
||||
repository.save(execution)
|
||||
|
||||
|
||||
# Verify no truncation occurred
|
||||
assert execution.process_data_truncated is False
|
||||
assert execution.get_truncated_process_data() is None
|
||||
assert execution.get_response_process_data() == small_process_data
|
||||
|
||||
@pytest.mark.parametrize("test_data", [
|
||||
data for data in get_truncation_test_data(None)
|
||||
], ids=[data.name for data in get_truncation_test_data(None)])
|
||||
@patch('core.repositories.sqlalchemy_workflow_node_execution_repository.dify_config')
|
||||
@patch('services.file_service.FileService.upload_file')
|
||||
@pytest.mark.parametrize(
|
||||
"test_data",
|
||||
get_truncation_test_data(None),
|
||||
ids=[data.name for data in get_truncation_test_data(None)],
|
||||
)
|
||||
@patch("core.repositories.sqlalchemy_workflow_node_execution_repository.dify_config")
|
||||
@patch("services.file_service.FileService.upload_file")
|
||||
def test_various_truncation_scenarios(
|
||||
self,
|
||||
mock_upload_file,
|
||||
mock_config,
|
||||
test_data: TruncationTestData,
|
||||
repository
|
||||
self, mock_upload_file, mock_config, test_data: TruncationTestData, repository
|
||||
):
|
||||
"""Test various process_data truncation scenarios."""
|
||||
# Configure truncation limits
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_MAX_SIZE = 1000
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_ARRAY_LENGTH = 100
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_STRING_LENGTH = 500
|
||||
|
||||
|
||||
if test_data.expected_storage_interaction:
|
||||
# Mock file upload for truncation scenarios
|
||||
mock_file = Mock()
|
||||
mock_file.id = f"file-{test_data.name}"
|
||||
mock_upload_file.return_value = mock_file
|
||||
|
||||
|
||||
execution = self.create_test_execution(process_data=test_data.process_data)
|
||||
repository.save(execution)
|
||||
|
||||
|
||||
# Verify truncation behavior matches expectations
|
||||
assert execution.process_data_truncated == test_data.should_truncate
|
||||
|
||||
|
||||
if test_data.should_truncate:
|
||||
assert execution.get_truncated_process_data() is not None
|
||||
assert execution.get_truncated_process_data() != test_data.process_data
|
||||
@ -266,40 +263,32 @@ class TestProcessDataTruncationIntegration:
|
||||
assert execution.get_truncated_process_data() is None
|
||||
assert execution.get_response_process_data() == test_data.process_data
|
||||
|
||||
@patch('core.repositories.sqlalchemy_workflow_node_execution_repository.dify_config')
|
||||
@patch('services.file_service.FileService.upload_file')
|
||||
@patch('extensions.ext_storage.storage')
|
||||
@patch("core.repositories.sqlalchemy_workflow_node_execution_repository.dify_config")
|
||||
@patch("services.file_service.FileService.upload_file")
|
||||
@patch("extensions.ext_storage.storage")
|
||||
def test_load_truncated_execution_from_database(
|
||||
self,
|
||||
mock_storage,
|
||||
mock_upload_file,
|
||||
mock_config,
|
||||
repository,
|
||||
in_memory_db_engine
|
||||
self, mock_storage, mock_upload_file, mock_config, repository, in_memory_db_engine
|
||||
):
|
||||
"""Test loading an execution with truncated process_data from database."""
|
||||
# Configure truncation
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_MAX_SIZE = 1000
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_ARRAY_LENGTH = 100
|
||||
mock_config.WORKFLOW_VARIABLE_TRUNCATION_STRING_LENGTH = 500
|
||||
|
||||
|
||||
# Create and save execution with large process_data
|
||||
large_process_data = {
|
||||
"large_field": "x" * 10000,
|
||||
"metadata": "info"
|
||||
}
|
||||
|
||||
large_process_data = {"large_field": "x" * 10000, "metadata": "info"}
|
||||
|
||||
# Mock file upload
|
||||
mock_file = Mock()
|
||||
mock_file.id = "process-data-file-id"
|
||||
mock_upload_file.return_value = mock_file
|
||||
|
||||
|
||||
execution = self.create_test_execution(process_data=large_process_data)
|
||||
repository.save(execution)
|
||||
|
||||
|
||||
# Mock storage load for reconstruction
|
||||
mock_storage.load.return_value = json.dumps(large_process_data).encode()
|
||||
|
||||
|
||||
# Create a new repository instance to simulate fresh load
|
||||
session_factory = sessionmaker(bind=in_memory_db_engine)
|
||||
new_repository = SQLAlchemyWorkflowNodeExecutionRepository(
|
||||
@ -308,17 +297,17 @@ class TestProcessDataTruncationIntegration:
|
||||
app_id="test-app-id",
|
||||
triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
|
||||
)
|
||||
|
||||
|
||||
# Load executions from database
|
||||
executions = new_repository.get_by_workflow_run("test-run-id")
|
||||
|
||||
|
||||
assert len(executions) == 1
|
||||
loaded_execution = executions[0]
|
||||
|
||||
|
||||
# Verify that full data is loaded
|
||||
assert loaded_execution.process_data == large_process_data
|
||||
assert loaded_execution.process_data_truncated is True
|
||||
|
||||
|
||||
# Verify truncated data for responses
|
||||
response_data = loaded_execution.get_response_process_data()
|
||||
assert response_data != large_process_data # Should be truncated version
|
||||
@ -327,7 +316,7 @@ class TestProcessDataTruncationIntegration:
|
||||
"""Test handling of None process_data."""
|
||||
execution = self.create_test_execution(process_data=None)
|
||||
repository.save(execution)
|
||||
|
||||
|
||||
# Should handle None gracefully
|
||||
assert execution.process_data is None
|
||||
assert execution.process_data_truncated is False
|
||||
@ -337,7 +326,7 @@ class TestProcessDataTruncationIntegration:
|
||||
"""Test handling of empty process_data."""
|
||||
execution = self.create_test_execution(process_data={})
|
||||
repository.save(execution)
|
||||
|
||||
|
||||
# Should handle empty dict gracefully
|
||||
assert execution.process_data == {}
|
||||
assert execution.process_data_truncated is False
|
||||
@ -346,13 +335,13 @@ class TestProcessDataTruncationIntegration:
|
||||
|
||||
class TestProcessDataTruncationApiIntegration:
|
||||
"""Integration tests for API responses with process_data truncation."""
|
||||
|
||||
|
||||
def test_api_response_includes_truncated_flag(self):
|
||||
"""Test that API responses include the process_data_truncated flag."""
|
||||
from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter
|
||||
from core.app.entities.app_invoke_entities import WorkflowAppGenerateEntity
|
||||
from core.app.entities.queue_entities import QueueNodeSucceededEvent
|
||||
|
||||
|
||||
# Create execution with truncated process_data
|
||||
execution = WorkflowNodeExecution(
|
||||
id="test-execution-id",
|
||||
@ -367,18 +356,15 @@ class TestProcessDataTruncationApiIntegration:
|
||||
created_at=datetime.now(),
|
||||
finished_at=datetime.now(),
|
||||
)
|
||||
|
||||
|
||||
# Set truncated data
|
||||
execution.set_truncated_process_data({"large": "[TRUNCATED]"})
|
||||
|
||||
|
||||
# Create converter and event
|
||||
converter = WorkflowResponseConverter(
|
||||
application_generate_entity=Mock(
|
||||
spec=WorkflowAppGenerateEntity,
|
||||
app_config=Mock(tenant_id="test-tenant")
|
||||
)
|
||||
application_generate_entity=Mock(spec=WorkflowAppGenerateEntity, app_config=Mock(tenant_id="test-tenant"))
|
||||
)
|
||||
|
||||
|
||||
event = QueueNodeSucceededEvent(
|
||||
node_id="test-node-id",
|
||||
node_type=NodeType.LLM,
|
||||
@ -390,19 +376,19 @@ class TestProcessDataTruncationApiIntegration:
|
||||
in_iteration_id=None,
|
||||
in_loop_id=None,
|
||||
)
|
||||
|
||||
|
||||
# Generate response
|
||||
response = converter.workflow_node_finish_to_stream_response(
|
||||
event=event,
|
||||
task_id="test-task-id",
|
||||
workflow_node_execution=execution,
|
||||
)
|
||||
|
||||
|
||||
# Verify response includes truncated flag and data
|
||||
assert response is not None
|
||||
assert response.data.process_data_truncated is True
|
||||
assert response.data.process_data == {"large": "[TRUNCATED]"}
|
||||
|
||||
|
||||
# Verify response can be serialized
|
||||
response_dict = response.to_dict()
|
||||
assert "process_data_truncated" in response_dict["data"]
|
||||
@ -411,11 +397,12 @@ class TestProcessDataTruncationApiIntegration:
|
||||
def test_workflow_run_fields_include_truncated_flag(self):
|
||||
"""Test that workflow run fields include process_data_truncated."""
|
||||
from fields.workflow_run_fields import workflow_run_node_execution_fields
|
||||
|
||||
|
||||
# Verify the field is included in the definition
|
||||
assert "process_data_truncated" in workflow_run_node_execution_fields
|
||||
|
||||
|
||||
# The field should be a Boolean field
|
||||
field = workflow_run_node_execution_fields["process_data_truncated"]
|
||||
from flask_restful import fields
|
||||
assert isinstance(field, fields.Boolean)
|
||||
|
||||
assert isinstance(field, fields.Boolean)
|
||||
|
||||
Reference in New Issue
Block a user