Merge main HEAD (segment 5) into sandboxed-agent-rebase

Resolve 83 conflicts: 10 backend, 62 frontend, 11 config/lock files.
Preserve sandbox/agent/collaboration features while adopting main's
UI refactorings (Dialog/AlertDialog/Popover), model provider updates,
and enterprise features.

Made-with: Cursor
This commit is contained in:
Novice
2026-03-23 14:20:06 +08:00
1671 changed files with 124822 additions and 22302 deletions

View File

@ -12,6 +12,7 @@ import pytest
from sqlalchemy.orm import Session
from models.dataset import DatasetCollectionBinding
from models.enums import CollectionBindingType
from services.dataset_service import DatasetCollectionBindingService
@ -32,7 +33,7 @@ class DatasetCollectionBindingTestDataFactory:
provider_name: str = "openai",
model_name: str = "text-embedding-ada-002",
collection_name: str = "collection-abc",
collection_type: str = "dataset",
collection_type: str = CollectionBindingType.DATASET,
) -> DatasetCollectionBinding:
"""
Create a DatasetCollectionBinding with specified attributes.
@ -41,7 +42,7 @@ class DatasetCollectionBindingTestDataFactory:
provider_name: Name of the embedding model provider (e.g., "openai", "cohere")
model_name: Name of the embedding model (e.g., "text-embedding-ada-002")
collection_name: Name of the vector database collection
collection_type: Type of collection (default: "dataset")
collection_type: Type of collection (default: CollectionBindingType.DATASET)
Returns:
DatasetCollectionBinding instance
@ -76,7 +77,7 @@ class TestDatasetCollectionBindingServiceGetBinding:
# Arrange
provider_name = "openai"
model_name = "text-embedding-ada-002"
collection_type = "dataset"
collection_type = CollectionBindingType.DATASET
existing_binding = DatasetCollectionBindingTestDataFactory.create_collection_binding(
db_session_with_containers,
provider_name=provider_name,
@ -104,7 +105,7 @@ class TestDatasetCollectionBindingServiceGetBinding:
# Arrange
provider_name = f"provider-{uuid4()}"
model_name = f"model-{uuid4()}"
collection_type = "dataset"
collection_type = CollectionBindingType.DATASET
# Act
result = DatasetCollectionBindingService.get_dataset_collection_binding(
@ -145,7 +146,7 @@ class TestDatasetCollectionBindingServiceGetBinding:
result = DatasetCollectionBindingService.get_dataset_collection_binding(provider_name, model_name)
# Assert
assert result.type == "dataset"
assert result.type == CollectionBindingType.DATASET
assert result.provider_name == provider_name
assert result.model_name == model_name
@ -186,18 +187,20 @@ class TestDatasetCollectionBindingServiceGetBindingByIdAndType:
provider_name="openai",
model_name="text-embedding-ada-002",
collection_name="test-collection",
collection_type="dataset",
collection_type=CollectionBindingType.DATASET,
)
# Act
result = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(binding.id, "dataset")
result = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(
binding.id, CollectionBindingType.DATASET
)
# Assert
assert result.id == binding.id
assert result.provider_name == "openai"
assert result.model_name == "text-embedding-ada-002"
assert result.collection_name == "test-collection"
assert result.type == "dataset"
assert result.type == CollectionBindingType.DATASET
def test_get_dataset_collection_binding_by_id_and_type_not_found_error(self, db_session_with_containers: Session):
"""Test error handling when collection binding is not found by ID and type."""
@ -206,7 +209,9 @@ class TestDatasetCollectionBindingServiceGetBindingByIdAndType:
# Act & Assert
with pytest.raises(ValueError, match="Dataset collection binding not found"):
DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(non_existent_id, "dataset")
DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(
non_existent_id, CollectionBindingType.DATASET
)
def test_get_dataset_collection_binding_by_id_and_type_different_collection_type(
self, db_session_with_containers: Session
@ -240,7 +245,7 @@ class TestDatasetCollectionBindingServiceGetBindingByIdAndType:
provider_name="openai",
model_name="text-embedding-ada-002",
collection_name="test-collection",
collection_type="dataset",
collection_type=CollectionBindingType.DATASET,
)
# Act
@ -248,7 +253,7 @@ class TestDatasetCollectionBindingServiceGetBindingByIdAndType:
# Assert
assert result.id == binding.id
assert result.type == "dataset"
assert result.type == CollectionBindingType.DATASET
def test_get_dataset_collection_binding_by_id_and_type_wrong_type_error(self, db_session_with_containers: Session):
"""Test error when binding exists but with wrong collection type."""
@ -258,7 +263,7 @@ class TestDatasetCollectionBindingServiceGetBindingByIdAndType:
provider_name="openai",
model_name="text-embedding-ada-002",
collection_name="test-collection",
collection_type="dataset",
collection_type=CollectionBindingType.DATASET,
)
# Act & Assert

View File

@ -15,6 +15,7 @@ from werkzeug.exceptions import NotFound
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import AppDatasetJoin, Dataset, DatasetPermissionEnum
from models.enums import DataSourceType
from models.model import App
from services.dataset_service import DatasetService
from services.errors.account import NoPermissionError
@ -72,7 +73,7 @@ class DatasetUpdateDeleteTestDataFactory:
tenant_id=tenant_id,
name=name,
description="Test description",
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
indexing_technique="high_quality",
created_by=created_by,
permission=permission,

View File

@ -13,9 +13,10 @@ from uuid import uuid4
import pytest
from extensions.storage.storage_type import StorageType
from models import Account
from models.dataset import Dataset, Document
from models.enums import CreatorUserRole
from models.enums import CreatorUserRole, DataSourceType, DocumentCreatedFrom, IndexingStatus
from models.model import UploadFile
from services.dataset_service import DocumentService
from services.errors.document import DocumentIndexingError
@ -88,7 +89,7 @@ class DocumentStatusTestDataFactory:
data_source_info=json.dumps(data_source_info or {}),
batch=f"batch-{uuid4()}",
name=name,
created_from="web",
created_from=DocumentCreatedFrom.WEB,
created_by=created_by,
doc_form="text_model",
)
@ -100,7 +101,7 @@ class DocumentStatusTestDataFactory:
document.paused_by = paused_by
document.paused_at = paused_at
document.doc_metadata = doc_metadata or {}
if indexing_status == "completed" and "completed_at" not in kwargs:
if indexing_status == IndexingStatus.COMPLETED and "completed_at" not in kwargs:
document.completed_at = FIXED_TIME
for key, value in kwargs.items():
@ -139,7 +140,7 @@ class DocumentStatusTestDataFactory:
dataset = Dataset(
tenant_id=tenant_id,
name=name,
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
created_by=created_by,
)
dataset.id = dataset_id
@ -198,7 +199,7 @@ class DocumentStatusTestDataFactory:
"""
upload_file = UploadFile(
tenant_id=tenant_id,
storage_type="local",
storage_type=StorageType.LOCAL,
key=f"uploads/{uuid4()}",
name=name,
size=128,
@ -291,7 +292,7 @@ class TestDocumentServicePauseDocument:
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="waiting",
indexing_status=IndexingStatus.WAITING,
is_paused=False,
)
@ -326,7 +327,7 @@ class TestDocumentServicePauseDocument:
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="indexing",
indexing_status=IndexingStatus.INDEXING,
is_paused=False,
)
@ -354,7 +355,7 @@ class TestDocumentServicePauseDocument:
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="parsing",
indexing_status=IndexingStatus.PARSING,
is_paused=False,
)
@ -383,7 +384,7 @@ class TestDocumentServicePauseDocument:
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
is_paused=False,
)
@ -412,7 +413,7 @@ class TestDocumentServicePauseDocument:
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="error",
indexing_status=IndexingStatus.ERROR,
is_paused=False,
)
@ -487,7 +488,7 @@ class TestDocumentServiceRecoverDocument:
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="indexing",
indexing_status=IndexingStatus.INDEXING,
is_paused=True,
paused_by=str(uuid4()),
paused_at=paused_time,
@ -526,7 +527,7 @@ class TestDocumentServiceRecoverDocument:
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="indexing",
indexing_status=IndexingStatus.INDEXING,
is_paused=False,
)
@ -609,7 +610,7 @@ class TestDocumentServiceRetryDocument:
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
document_id=str(uuid4()),
indexing_status="error",
indexing_status=IndexingStatus.ERROR,
)
mock_document_service_dependencies["redis_client"].get.return_value = None
@ -619,7 +620,7 @@ class TestDocumentServiceRetryDocument:
# Assert
db_session_with_containers.refresh(document)
assert document.indexing_status == "waiting"
assert document.indexing_status == IndexingStatus.WAITING
expected_cache_key = f"document_{document.id}_is_retried"
mock_document_service_dependencies["redis_client"].setex.assert_called_once_with(expected_cache_key, 600, 1)
@ -646,14 +647,14 @@ class TestDocumentServiceRetryDocument:
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
document_id=str(uuid4()),
indexing_status="error",
indexing_status=IndexingStatus.ERROR,
)
document2 = DocumentStatusTestDataFactory.create_document(
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
document_id=str(uuid4()),
indexing_status="error",
indexing_status=IndexingStatus.ERROR,
position=2,
)
@ -665,8 +666,8 @@ class TestDocumentServiceRetryDocument:
# Assert
db_session_with_containers.refresh(document1)
db_session_with_containers.refresh(document2)
assert document1.indexing_status == "waiting"
assert document2.indexing_status == "waiting"
assert document1.indexing_status == IndexingStatus.WAITING
assert document2.indexing_status == IndexingStatus.WAITING
mock_document_service_dependencies["retry_task"].delay.assert_called_once_with(
dataset.id, [document1.id, document2.id], mock_document_service_dependencies["user_id"]
@ -693,7 +694,7 @@ class TestDocumentServiceRetryDocument:
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
document_id=str(uuid4()),
indexing_status="error",
indexing_status=IndexingStatus.ERROR,
)
mock_document_service_dependencies["redis_client"].get.return_value = "1"
@ -703,7 +704,7 @@ class TestDocumentServiceRetryDocument:
DocumentService.retry_document(dataset.id, [document])
db_session_with_containers.refresh(document)
assert document.indexing_status == "error"
assert document.indexing_status == IndexingStatus.ERROR
def test_retry_document_missing_current_user_error(
self, db_session_with_containers, mock_document_service_dependencies
@ -726,7 +727,7 @@ class TestDocumentServiceRetryDocument:
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
document_id=str(uuid4()),
indexing_status="error",
indexing_status=IndexingStatus.ERROR,
)
mock_document_service_dependencies["redis_client"].get.return_value = None
@ -816,7 +817,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
tenant_id=dataset.tenant_id,
document_id=str(uuid4()),
enabled=False,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
)
document2 = DocumentStatusTestDataFactory.create_document(
db_session_with_containers,
@ -824,7 +825,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
tenant_id=dataset.tenant_id,
document_id=str(uuid4()),
enabled=False,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
position=2,
)
document_ids = [document1.id, document2.id]
@ -866,7 +867,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
tenant_id=dataset.tenant_id,
document_id=str(uuid4()),
enabled=True,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
completed_at=FIXED_TIME,
)
document_ids = [document.id]
@ -909,7 +910,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
document_id=str(uuid4()),
archived=False,
enabled=True,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
)
document_ids = [document.id]
@ -951,7 +952,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
document_id=str(uuid4()),
archived=True,
enabled=True,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
)
document_ids = [document.id]
@ -1015,7 +1016,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
document_id=str(uuid4()),
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
)
document_ids = [document.id]
@ -1098,7 +1099,7 @@ class TestDocumentServiceRenameDocument:
document_id=document_id,
dataset_id=dataset.id,
tenant_id=tenant_id,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
)
# Act
@ -1139,7 +1140,7 @@ class TestDocumentServiceRenameDocument:
dataset_id=dataset.id,
tenant_id=tenant_id,
doc_metadata={"existing_key": "existing_value"},
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
)
# Act
@ -1187,7 +1188,7 @@ class TestDocumentServiceRenameDocument:
dataset_id=dataset.id,
tenant_id=tenant_id,
data_source_info={"upload_file_id": upload_file.id},
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
)
# Act
@ -1277,7 +1278,7 @@ class TestDocumentServiceRenameDocument:
document_id=document_id,
dataset_id=dataset.id,
tenant_id=str(uuid4()),
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
)
# Act & Assert

View File

@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
from core.plugin.impl.exc import PluginDaemonClientSideError
from models import Account
from models.enums import ConversationFromSource, MessageFileBelongsTo
from models.model import AppModelConfig, Conversation, EndUser, Message, MessageAgentThought
from services.account_service import AccountService, TenantService
from services.agent_service import AgentService
@ -164,7 +165,7 @@ class TestAgentService:
inputs={},
status="normal",
mode="chat",
from_source="api",
from_source=ConversationFromSource.API,
)
db_session_with_containers.add(conversation)
db_session_with_containers.commit()
@ -203,7 +204,7 @@ class TestAgentService:
answer_unit_price=0.001,
provider_response_latency=1.5,
currency="USD",
from_source="api",
from_source=ConversationFromSource.API,
)
db_session_with_containers.add(message)
db_session_with_containers.commit()
@ -405,7 +406,7 @@ class TestAgentService:
inputs={},
status="normal",
mode="chat",
from_source="api",
from_source=ConversationFromSource.API,
)
db_session_with_containers.add(conversation)
db_session_with_containers.commit()
@ -444,7 +445,7 @@ class TestAgentService:
answer_unit_price=0.001,
provider_response_latency=1.5,
currency="USD",
from_source="api",
from_source=ConversationFromSource.API,
)
db_session_with_containers.add(message)
db_session_with_containers.commit()
@ -477,7 +478,7 @@ class TestAgentService:
inputs={},
status="normal",
mode="chat",
from_source="api",
from_source=ConversationFromSource.API,
)
db_session_with_containers.add(conversation)
db_session_with_containers.commit()
@ -516,7 +517,7 @@ class TestAgentService:
answer_unit_price=0.001,
provider_response_latency=1.5,
currency="USD",
from_source="api",
from_source=ConversationFromSource.API,
)
db_session_with_containers.add(message)
db_session_with_containers.commit()
@ -623,7 +624,7 @@ class TestAgentService:
inputs={},
status="normal",
mode="chat",
from_source="api",
from_source=ConversationFromSource.API,
app_model_config_id=None, # Explicitly set to None
)
db_session_with_containers.add(conversation)
@ -646,7 +647,7 @@ class TestAgentService:
answer_unit_price=0.001,
provider_response_latency=1.5,
currency="USD",
from_source="api",
from_source=ConversationFromSource.API,
)
db_session_with_containers.add(message)
db_session_with_containers.commit()
@ -852,7 +853,7 @@ class TestAgentService:
type=FileType.IMAGE,
transfer_method=FileTransferMethod.REMOTE_URL,
url="http://example.com/file1.jpg",
belongs_to="user",
belongs_to=MessageFileBelongsTo.USER,
created_by_role=CreatorUserRole.ACCOUNT,
created_by=message.from_account_id,
)
@ -861,7 +862,7 @@ class TestAgentService:
type=FileType.IMAGE,
transfer_method=FileTransferMethod.REMOTE_URL,
url="http://example.com/file2.png",
belongs_to="user",
belongs_to=MessageFileBelongsTo.USER,
created_by_role=CreatorUserRole.ACCOUNT,
created_by=message.from_account_id,
)

View File

@ -6,6 +6,7 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
from models import Account
from models.enums import ConversationFromSource, InvokeFrom
from models.model import MessageAnnotation
from services.annotation_service import AppAnnotationService
from services.app_service import AppService
@ -136,8 +137,8 @@ class TestAnnotationService:
system_instruction="",
system_instruction_tokens=0,
status="normal",
invoke_from="console",
from_source="console",
invoke_from=InvokeFrom.EXPLORE,
from_source=ConversationFromSource.CONSOLE,
from_end_user_id=None,
from_account_id=account.id,
)
@ -174,8 +175,8 @@ class TestAnnotationService:
provider_response_latency=0,
total_price=0,
currency="USD",
invoke_from="console",
from_source="console",
invoke_from=InvokeFrom.EXPLORE,
from_source=ConversationFromSource.CONSOLE,
from_end_user_id=None,
from_account_id=account.id,
)
@ -721,7 +722,7 @@ class TestAnnotationService:
query=f"Query {i}: {fake.sentence()}",
user_id=account.id,
message_id=fake.uuid4(),
from_source="console",
from_source=ConversationFromSource.CONSOLE,
score=0.8 + (i * 0.1),
)
@ -772,7 +773,7 @@ class TestAnnotationService:
query=query,
user_id=account.id,
message_id=message_id,
from_source="console",
from_source=ConversationFromSource.CONSOLE,
score=score,
)

View File

@ -10,6 +10,7 @@ from sqlalchemy import select
from core.app.entities.app_invoke_entities import InvokeFrom
from models.account import Account, Tenant, TenantAccountJoin
from models.enums import ConversationFromSource
from models.model import App, Conversation, EndUser, Message, MessageAnnotation
from services.annotation_service import AppAnnotationService
from services.conversation_service import ConversationService
@ -107,7 +108,7 @@ class ConversationServiceIntegrationTestDataFactory:
system_instruction_tokens=0,
status="normal",
invoke_from=invoke_from.value,
from_source="api" if isinstance(user, EndUser) else "console",
from_source=ConversationFromSource.API if isinstance(user, EndUser) else ConversationFromSource.CONSOLE,
from_end_user_id=user.id if isinstance(user, EndUser) else None,
from_account_id=user.id if isinstance(user, Account) else None,
dialogue_count=0,
@ -154,7 +155,7 @@ class ConversationServiceIntegrationTestDataFactory:
currency="USD",
status="normal",
invoke_from=InvokeFrom.WEB_APP.value,
from_source="api" if isinstance(user, EndUser) else "console",
from_source=ConversationFromSource.API if isinstance(user, EndUser) else ConversationFromSource.CONSOLE,
from_end_user_id=user.id if isinstance(user, EndUser) else None,
from_account_id=user.id if isinstance(user, Account) else None,
)

View File

@ -16,6 +16,7 @@ from models.dataset import (
DatasetPermission,
DatasetPermissionEnum,
)
from models.enums import DataSourceType
from services.dataset_service import DatasetPermissionService, DatasetService
from services.errors.account import NoPermissionError
@ -67,7 +68,7 @@ class DatasetPermissionTestDataFactory:
tenant_id=tenant_id,
name=name,
description="desc",
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
indexing_technique="high_quality",
created_by=created_by,
permission=permission,

View File

@ -15,6 +15,7 @@ from core.rag.retrieval.retrieval_methods import RetrievalMethod
from dify_graph.model_runtime.entities.model_entities import ModelType
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, DatasetPermissionEnum, Document, ExternalKnowledgeBindings, Pipeline
from models.enums import DatasetRuntimeMode, DataSourceType, DocumentCreatedFrom, IndexingStatus
from services.dataset_service import DatasetService
from services.entities.knowledge_entities.knowledge_entities import RerankingModel, RetrievalModel
from services.entities.knowledge_entities.rag_pipeline_entities import IconInfo, RagPipelineDatasetCreateEntity
@ -74,7 +75,7 @@ class DatasetServiceIntegrationDataFactory:
tenant_id=tenant_id,
name=name,
description=description,
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
indexing_technique=indexing_technique,
created_by=created_by,
provider=provider,
@ -98,13 +99,13 @@ class DatasetServiceIntegrationDataFactory:
tenant_id=dataset.tenant_id,
dataset_id=dataset.id,
position=1,
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
data_source_info='{"upload_file_id": "upload-file-id"}',
batch=str(uuid4()),
name=name,
created_from="web",
created_from=DocumentCreatedFrom.WEB,
created_by=created_by,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
doc_form="text_model",
)
db_session_with_containers.add(document)
@ -437,7 +438,7 @@ class TestDatasetServiceCreateRagPipelineDataset:
created_pipeline = db_session_with_containers.get(Pipeline, result.pipeline_id)
assert created_dataset is not None
assert created_dataset.name == entity.name
assert created_dataset.runtime_mode == "rag_pipeline"
assert created_dataset.runtime_mode == DatasetRuntimeMode.RAG_PIPELINE
assert created_dataset.created_by == account.id
assert created_dataset.permission == DatasetPermissionEnum.ONLY_ME
assert created_pipeline is not None

View File

@ -14,6 +14,7 @@ import pytest
from sqlalchemy.orm import Session
from models.dataset import Dataset, Document
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
from services.dataset_service import DocumentService
from services.errors.document import DocumentIndexingError
@ -42,7 +43,7 @@ class DocumentBatchUpdateIntegrationDataFactory:
dataset = Dataset(
tenant_id=tenant_id or str(uuid4()),
name=name,
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
created_by=created_by or str(uuid4()),
)
if dataset_id:
@ -72,11 +73,11 @@ class DocumentBatchUpdateIntegrationDataFactory:
tenant_id=dataset.tenant_id,
dataset_id=dataset.id,
position=position,
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
data_source_info=json.dumps({"upload_file_id": str(uuid4())}),
batch=f"batch-{uuid4()}",
name=name,
created_from="web",
created_from=DocumentCreatedFrom.WEB,
created_by=created_by or str(uuid4()),
doc_form="text_model",
)
@ -85,7 +86,9 @@ class DocumentBatchUpdateIntegrationDataFactory:
document.archived = archived
document.indexing_status = indexing_status
document.completed_at = (
completed_at if completed_at is not None else (FIXED_TIME if indexing_status == "completed" else None)
completed_at
if completed_at is not None
else (FIXED_TIME if indexing_status == IndexingStatus.COMPLETED else None)
)
for key, value in kwargs.items():
@ -243,7 +246,7 @@ class TestDatasetServiceBatchUpdateDocumentStatus:
dataset=dataset,
document_ids=document_ids,
enabled=True,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
)
# Act
@ -277,7 +280,7 @@ class TestDatasetServiceBatchUpdateDocumentStatus:
db_session_with_containers,
dataset=dataset,
enabled=False,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
completed_at=FIXED_TIME,
)
@ -306,7 +309,7 @@ class TestDatasetServiceBatchUpdateDocumentStatus:
db_session_with_containers,
dataset=dataset,
enabled=True,
indexing_status="indexing",
indexing_status=IndexingStatus.INDEXING,
completed_at=None,
)

View File

@ -5,6 +5,7 @@ from uuid import uuid4
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, Document
from models.enums import DataSourceType, DocumentCreatedFrom
from services.dataset_service import DatasetService
@ -58,7 +59,7 @@ class DatasetDeleteIntegrationDataFactory:
dataset = Dataset(
tenant_id=tenant_id,
name=f"dataset-{uuid4()}",
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
indexing_technique=indexing_technique,
index_struct=index_struct,
created_by=created_by,
@ -84,10 +85,10 @@ class DatasetDeleteIntegrationDataFactory:
tenant_id=tenant_id,
dataset_id=dataset_id,
position=1,
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
batch=f"batch-{uuid4()}",
name="Document",
created_from="upload_file",
created_from=DocumentCreatedFrom.WEB,
created_by=created_by,
doc_form=doc_form,
)

View File

@ -14,6 +14,7 @@ from sqlalchemy.orm import Session
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, DatasetPermissionEnum, Document, DocumentSegment
from models.enums import DataSourceType, DocumentCreatedFrom
from services.dataset_service import SegmentService
@ -62,7 +63,7 @@ class SegmentServiceTestDataFactory:
tenant_id=tenant_id,
name=f"Test Dataset {uuid4()}",
description="Test description",
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
indexing_technique="high_quality",
created_by=created_by,
permission=DatasetPermissionEnum.ONLY_ME,
@ -82,10 +83,10 @@ class SegmentServiceTestDataFactory:
tenant_id=tenant_id,
dataset_id=dataset_id,
position=1,
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
batch=f"batch-{uuid4()}",
name=f"test-doc-{uuid4()}.txt",
created_from="api",
created_from=DocumentCreatedFrom.API,
created_by=created_by,
)
db_session_with_containers.add(document)

View File

@ -24,6 +24,7 @@ from models.dataset import (
DatasetProcessRule,
DatasetQuery,
)
from models.enums import DatasetQuerySource, DataSourceType, ProcessRuleMode
from models.model import Tag, TagBinding
from services.dataset_service import DatasetService, DocumentService
@ -100,7 +101,7 @@ class DatasetRetrievalTestDataFactory:
tenant_id=tenant_id,
name=name,
description="desc",
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
indexing_technique="high_quality",
created_by=created_by,
permission=permission,
@ -149,7 +150,7 @@ class DatasetRetrievalTestDataFactory:
dataset_query = DatasetQuery(
dataset_id=dataset_id,
content=content,
source="web",
source=DatasetQuerySource.APP,
source_app_id=None,
created_by_role="account",
created_by=created_by,
@ -601,7 +602,7 @@ class TestDatasetServiceGetProcessRules:
db_session_with_containers,
dataset_id=dataset.id,
created_by=account.id,
mode="custom",
mode=ProcessRuleMode.CUSTOM,
rules=rules_data,
)

View File

@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
from dify_graph.model_runtime.entities.model_entities import ModelType
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, ExternalKnowledgeBindings
from models.enums import DataSourceType
from services.dataset_service import DatasetService
from services.errors.account import NoPermissionError
@ -64,7 +65,7 @@ class DatasetUpdateTestDataFactory:
tenant_id=tenant_id,
name=name,
description=description,
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
indexing_technique=indexing_technique,
created_by=created_by,
provider=provider,

View File

@ -4,6 +4,7 @@ from uuid import uuid4
from sqlalchemy import select
from models.dataset import Dataset, Document
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
from services.dataset_service import DocumentService
@ -11,7 +12,7 @@ def _create_dataset(db_session_with_containers) -> Dataset:
dataset = Dataset(
tenant_id=str(uuid4()),
name=f"dataset-{uuid4()}",
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
created_by=str(uuid4()),
)
dataset.id = str(uuid4())
@ -35,11 +36,11 @@ def _create_document(
tenant_id=tenant_id,
dataset_id=dataset_id,
position=position,
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
data_source_info="{}",
batch=f"batch-{uuid4()}",
name=f"doc-{uuid4()}",
created_from="web",
created_from=DocumentCreatedFrom.WEB,
created_by=str(uuid4()),
doc_form="text_model",
)
@ -48,7 +49,7 @@ def _create_document(
document.enabled = enabled
document.archived = archived
document.is_paused = is_paused
if indexing_status == "completed":
if indexing_status == IndexingStatus.COMPLETED:
document.completed_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
db_session_with_containers.add(document)
@ -62,7 +63,7 @@ def test_build_display_status_filters_available(db_session_with_containers):
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
archived=False,
position=1,
@ -71,7 +72,7 @@ def test_build_display_status_filters_available(db_session_with_containers):
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
enabled=False,
archived=False,
position=2,
@ -80,7 +81,7 @@ def test_build_display_status_filters_available(db_session_with_containers):
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
archived=True,
position=3,
@ -101,14 +102,14 @@ def test_apply_display_status_filter_applies_when_status_present(db_session_with
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="waiting",
indexing_status=IndexingStatus.WAITING,
position=1,
)
_create_document(
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
position=2,
)
@ -125,14 +126,14 @@ def test_apply_display_status_filter_returns_same_when_invalid(db_session_with_c
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="waiting",
indexing_status=IndexingStatus.WAITING,
position=1,
)
doc2 = _create_document(
db_session_with_containers,
dataset_id=dataset.id,
tenant_id=dataset.tenant_id,
indexing_status="completed",
indexing_status=IndexingStatus.COMPLETED,
position=2,
)

View File

@ -7,9 +7,10 @@ from uuid import uuid4
import pytest
from extensions.storage.storage_type import StorageType
from models import Account
from models.dataset import Dataset, Document
from models.enums import CreatorUserRole
from models.enums import CreatorUserRole, DataSourceType, DocumentCreatedFrom
from models.model import UploadFile
from services.dataset_service import DocumentService
@ -33,7 +34,7 @@ def make_dataset(db_session_with_containers, dataset_id=None, tenant_id=None, bu
dataset = Dataset(
tenant_id=tenant_id,
name=f"dataset-{uuid4()}",
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
created_by=str(uuid4()),
)
dataset.id = dataset_id
@ -62,11 +63,11 @@ def make_document(
tenant_id=tenant_id,
dataset_id=dataset_id,
position=1,
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
data_source_info=json.dumps(data_source_info or {}),
batch=f"batch-{uuid4()}",
name=name,
created_from="web",
created_from=DocumentCreatedFrom.WEB,
created_by=str(uuid4()),
doc_form="text_model",
)
@ -83,7 +84,7 @@ def make_upload_file(db_session_with_containers, tenant_id: str, file_id: str, n
"""Persist an upload file row referenced by document.data_source_info."""
upload_file = UploadFile(
tenant_id=tenant_id,
storage_type="local",
storage_type=StorageType.LOCAL,
key=f"uploads/{uuid4()}",
name=name,
size=128,

View File

@ -360,10 +360,9 @@ class TestFeatureService:
assert result is not None
assert isinstance(result, SystemFeatureModel)
# --- 1. Verify Response Payload Optimization (Data Minimization) ---
# Ensure only essential UI flags are returned to unauthenticated clients
# to keep the payload lightweight and adhere to architectural boundaries.
assert result.license.status == LicenseStatus.NONE
# --- 1. Verify only license *status* is exposed to unauthenticated clients ---
# Detailed license info (expiry, workspaces) remains auth-gated.
assert result.license.status == LicenseStatus.ACTIVE
assert result.license.expired_at == ""
assert result.license.workspaces.enabled is False
assert result.license.workspaces.limit == 0

View File

@ -8,6 +8,7 @@ from unittest import mock
import pytest
from extensions.ext_database import db
from models.enums import FeedbackFromSource, FeedbackRating
from models.model import App, Conversation, Message
from services.feedback_service import FeedbackService
@ -47,8 +48,8 @@ class TestFeedbackService:
app_id=app_id,
conversation_id="test-conversation-id",
message_id="test-message-id",
rating="like",
from_source="user",
rating=FeedbackRating.LIKE,
from_source=FeedbackFromSource.USER,
content="Great answer!",
from_end_user_id="user-123",
from_account_id=None,
@ -61,8 +62,8 @@ class TestFeedbackService:
app_id=app_id,
conversation_id="test-conversation-id",
message_id="test-message-id",
rating="dislike",
from_source="admin",
rating=FeedbackRating.DISLIKE,
from_source=FeedbackFromSource.ADMIN,
content="Could be more detailed",
from_end_user_id=None,
from_account_id="admin-456",
@ -179,8 +180,8 @@ class TestFeedbackService:
# Test with filters
result = FeedbackService.export_feedbacks(
app_id=sample_data["app"].id,
from_source="admin",
rating="dislike",
from_source=FeedbackFromSource.ADMIN,
rating=FeedbackRating.DISLIKE,
has_comment=True,
start_date="2024-01-01",
end_date="2024-12-31",
@ -293,8 +294,8 @@ class TestFeedbackService:
app_id=sample_data["app"].id,
conversation_id="test-conversation-id",
message_id="test-message-id",
rating="dislike",
from_source="user",
rating=FeedbackRating.DISLIKE,
from_source=FeedbackFromSource.USER,
content="回答不够详细,需要更多信息",
from_end_user_id="user-123",
from_account_id=None,

View File

@ -9,6 +9,7 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
from configs import dify_config
from extensions.storage.storage_type import StorageType
from models import Account, Tenant
from models.enums import CreatorUserRole
from models.model import EndUser, UploadFile
@ -140,7 +141,7 @@ class TestFileService:
upload_file = UploadFile(
tenant_id=account.current_tenant_id if hasattr(account, "current_tenant_id") else str(fake.uuid4()),
storage_type="local",
storage_type=StorageType.LOCAL,
key=f"upload_files/test/{fake.uuid4()}.txt",
name="test_file.txt",
size=1024,

View File

@ -7,6 +7,7 @@ import pytest
from sqlalchemy.orm import Session
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.enums import ConversationFromSource, FeedbackFromSource, FeedbackRating
from models.model import (
App,
AppAnnotationHitHistory,
@ -93,7 +94,7 @@ class TestAppMessageExportServiceIntegration:
name="conv",
inputs={"seed": 1},
status="normal",
from_source="api",
from_source=ConversationFromSource.API,
from_end_user_id=str(uuid.uuid4()),
)
session.add(conversation)
@ -128,7 +129,7 @@ class TestAppMessageExportServiceIntegration:
total_price=Decimal("0.003"),
currency="USD",
message_metadata=message_metadata,
from_source="api",
from_source=ConversationFromSource.API,
from_end_user_id=conversation.from_end_user_id,
created_at=created_at,
)
@ -172,8 +173,8 @@ class TestAppMessageExportServiceIntegration:
app_id=app.id,
conversation_id=conversation.id,
message_id=first_message.id,
rating="like",
from_source="user",
rating=FeedbackRating.LIKE,
from_source=FeedbackFromSource.USER,
content="first",
from_end_user_id=conversation.from_end_user_id,
)
@ -181,8 +182,8 @@ class TestAppMessageExportServiceIntegration:
app_id=app.id,
conversation_id=conversation.id,
message_id=first_message.id,
rating="dislike",
from_source="user",
rating=FeedbackRating.DISLIKE,
from_source=FeedbackFromSource.USER,
content="second",
from_end_user_id=conversation.from_end_user_id,
)
@ -190,8 +191,8 @@ class TestAppMessageExportServiceIntegration:
app_id=app.id,
conversation_id=conversation.id,
message_id=first_message.id,
rating="like",
from_source="admin",
rating=FeedbackRating.LIKE,
from_source=FeedbackFromSource.ADMIN,
content="should-be-filtered",
from_account_id=str(uuid.uuid4()),
)

View File

@ -4,6 +4,7 @@ import pytest
from faker import Faker
from sqlalchemy.orm import Session
from models.enums import ConversationFromSource, FeedbackRating, InvokeFrom
from models.model import MessageFeedback
from services.app_service import AppService
from services.errors.message import (
@ -148,8 +149,8 @@ class TestMessageService:
system_instruction="",
system_instruction_tokens=0,
status="normal",
invoke_from="console",
from_source="console",
invoke_from=InvokeFrom.EXPLORE,
from_source=ConversationFromSource.CONSOLE,
from_end_user_id=None,
from_account_id=account.id,
)
@ -186,8 +187,8 @@ class TestMessageService:
provider_response_latency=0,
total_price=0,
currency="USD",
invoke_from="console",
from_source="console",
invoke_from=InvokeFrom.EXPLORE,
from_source=ConversationFromSource.CONSOLE,
from_end_user_id=None,
from_account_id=account.id,
)
@ -405,7 +406,7 @@ class TestMessageService:
message = self._create_test_message(db_session_with_containers, app, conversation, account, fake)
# Create feedback
rating = "like"
rating = FeedbackRating.LIKE
content = fake.text(max_nb_chars=100)
feedback = MessageService.create_feedback(
app_model=app, message_id=message.id, user=account, rating=rating, content=content
@ -435,7 +436,11 @@ class TestMessageService:
# Test creating feedback with no user
with pytest.raises(ValueError, match="user cannot be None"):
MessageService.create_feedback(
app_model=app, message_id=message.id, user=None, rating="like", content=fake.text(max_nb_chars=100)
app_model=app,
message_id=message.id,
user=None,
rating=FeedbackRating.LIKE,
content=fake.text(max_nb_chars=100),
)
def test_create_feedback_update_existing(
@ -452,14 +457,14 @@ class TestMessageService:
message = self._create_test_message(db_session_with_containers, app, conversation, account, fake)
# Create initial feedback
initial_rating = "like"
initial_rating = FeedbackRating.LIKE
initial_content = fake.text(max_nb_chars=100)
feedback = MessageService.create_feedback(
app_model=app, message_id=message.id, user=account, rating=initial_rating, content=initial_content
)
# Update feedback
updated_rating = "dislike"
updated_rating = FeedbackRating.DISLIKE
updated_content = fake.text(max_nb_chars=100)
updated_feedback = MessageService.create_feedback(
app_model=app, message_id=message.id, user=account, rating=updated_rating, content=updated_content
@ -487,7 +492,11 @@ class TestMessageService:
# Create initial feedback
feedback = MessageService.create_feedback(
app_model=app, message_id=message.id, user=account, rating="like", content=fake.text(max_nb_chars=100)
app_model=app,
message_id=message.id,
user=account,
rating=FeedbackRating.LIKE,
content=fake.text(max_nb_chars=100),
)
# Delete feedback by setting rating to None
@ -538,7 +547,7 @@ class TestMessageService:
app_model=app,
message_id=message.id,
user=account,
rating="like" if i % 2 == 0 else "dislike",
rating=FeedbackRating.LIKE if i % 2 == 0 else FeedbackRating.DISLIKE,
content=f"Feedback {i}: {fake.text(max_nb_chars=50)}",
)
feedbacks.append(feedback)
@ -568,7 +577,11 @@ class TestMessageService:
message = self._create_test_message(db_session_with_containers, app, conversation, account, fake)
MessageService.create_feedback(
app_model=app, message_id=message.id, user=account, rating="like", content=f"Feedback {i}"
app_model=app,
message_id=message.id,
user=account,
rating=FeedbackRating.LIKE,
content=f"Feedback {i}",
)
# Get feedbacks with pagination

View File

@ -4,6 +4,7 @@ from decimal import Decimal
import pytest
from models.enums import ConversationFromSource
from models.model import Message
from services import message_service
from tests.test_containers_integration_tests.helpers.execution_extra_content import (
@ -36,7 +37,7 @@ def test_attach_message_extra_contents_assigns_serialized_payload(db_session_wit
total_price=Decimal(0),
currency="USD",
status="normal",
from_source="console",
from_source=ConversationFromSource.CONSOLE,
from_account_id=fixture.account.id,
)
db_session_with_containers.add(message_without_extra_content)

View File

@ -11,6 +11,14 @@ from sqlalchemy.orm import Session
from enums.cloud_plan import CloudPlan
from extensions.ext_redis import redis_client
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.enums import (
ConversationFromSource,
DataSourceType,
FeedbackFromSource,
FeedbackRating,
MessageChainType,
MessageFileBelongsTo,
)
from models.model import (
App,
AppAnnotationHitHistory,
@ -165,7 +173,7 @@ class TestMessagesCleanServiceIntegration:
name="Test conversation",
inputs={},
status="normal",
from_source="api",
from_source=ConversationFromSource.API,
from_end_user_id=str(uuid.uuid4()),
)
db_session_with_containers.add(conversation)
@ -195,7 +203,7 @@ class TestMessagesCleanServiceIntegration:
answer_unit_price=Decimal("0.002"),
total_price=Decimal("0.003"),
currency="USD",
from_source="api",
from_source=ConversationFromSource.API,
from_account_id=conversation.from_end_user_id,
created_at=created_at,
)
@ -215,8 +223,8 @@ class TestMessagesCleanServiceIntegration:
app_id=message.app_id,
conversation_id=message.conversation_id,
message_id=message.id,
rating="like",
from_source="api",
rating=FeedbackRating.LIKE,
from_source=FeedbackFromSource.USER,
from_end_user_id=str(uuid.uuid4()),
)
db_session_with_containers.add(feedback)
@ -235,7 +243,7 @@ class TestMessagesCleanServiceIntegration:
# MessageChain
chain = MessageChain(
message_id=message.id,
type="system",
type=MessageChainType.SYSTEM,
input=json.dumps({"test": "input"}),
output=json.dumps({"test": "output"}),
)
@ -248,7 +256,7 @@ class TestMessagesCleanServiceIntegration:
type="image",
transfer_method="local_file",
url="http://example.com/test.jpg",
belongs_to="user",
belongs_to=MessageFileBelongsTo.USER,
created_by_role="end_user",
created_by=str(uuid.uuid4()),
)
@ -287,7 +295,7 @@ class TestMessagesCleanServiceIntegration:
dataset_name="Test dataset",
document_id=str(uuid.uuid4()),
document_name="Test document",
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
segment_id=str(uuid.uuid4()),
score=0.9,
content="Test content",

View File

@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
from core.rag.index_processor.constant.built_in_field import BuiltInField
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, DatasetMetadata, DatasetMetadataBinding, Document
from models.enums import DatasetMetadataType, DataSourceType, DocumentCreatedFrom
from services.entities.knowledge_entities.knowledge_entities import MetadataArgs
from services.metadata_service import MetadataService
@ -101,7 +102,7 @@ class TestMetadataService:
tenant_id=tenant.id,
name=fake.company(),
description=fake.text(max_nb_chars=100),
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
created_by=account.id,
built_in_field_enabled=False,
)
@ -132,11 +133,11 @@ class TestMetadataService:
tenant_id=dataset.tenant_id,
dataset_id=dataset.id,
position=1,
data_source_type="upload_file",
data_source_type=DataSourceType.UPLOAD_FILE,
data_source_info="{}",
batch="test-batch",
name=fake.file_name(),
created_from="web",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text",
doc_language="en",
@ -163,7 +164,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id
mock_external_service_dependencies["current_user"].id = account.id
metadata_args = MetadataArgs(type="string", name="test_metadata")
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
# Act: Execute the method under test
result = MetadataService.create_metadata(dataset.id, metadata_args)
@ -201,7 +202,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
long_name = "a" * 256 # 256 characters, exceeding 255 limit
metadata_args = MetadataArgs(type="string", name=long_name)
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name=long_name)
# Act & Assert: Verify proper error handling
with pytest.raises(ValueError, match="Metadata name cannot exceed 255 characters."):
@ -226,11 +227,11 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create first metadata
first_metadata_args = MetadataArgs(type="string", name="duplicate_name")
first_metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="duplicate_name")
MetadataService.create_metadata(dataset.id, first_metadata_args)
# Try to create second metadata with same name
second_metadata_args = MetadataArgs(type="number", name="duplicate_name")
second_metadata_args = MetadataArgs(type=DatasetMetadataType.NUMBER, name="duplicate_name")
# Act & Assert: Verify proper error handling
with pytest.raises(ValueError, match="Metadata name already exists."):
@ -256,7 +257,7 @@ class TestMetadataService:
# Try to create metadata with built-in field name
built_in_field_name = BuiltInField.document_name
metadata_args = MetadataArgs(type="string", name=built_in_field_name)
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name=built_in_field_name)
# Act & Assert: Verify proper error handling
with pytest.raises(ValueError, match="Metadata name already exists in Built-in fields."):
@ -281,7 +282,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create metadata first
metadata_args = MetadataArgs(type="string", name="old_name")
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="old_name")
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
# Act: Execute the method under test
@ -318,7 +319,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create metadata first
metadata_args = MetadataArgs(type="string", name="old_name")
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="old_name")
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
# Try to update with too long name
@ -347,10 +348,10 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create two metadata entries
first_metadata_args = MetadataArgs(type="string", name="first_metadata")
first_metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="first_metadata")
first_metadata = MetadataService.create_metadata(dataset.id, first_metadata_args)
second_metadata_args = MetadataArgs(type="number", name="second_metadata")
second_metadata_args = MetadataArgs(type=DatasetMetadataType.NUMBER, name="second_metadata")
second_metadata = MetadataService.create_metadata(dataset.id, second_metadata_args)
# Try to update first metadata with second metadata's name
@ -376,7 +377,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create metadata first
metadata_args = MetadataArgs(type="string", name="old_name")
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="old_name")
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
# Try to update with built-in field name
@ -432,7 +433,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create metadata first
metadata_args = MetadataArgs(type="string", name="to_be_deleted")
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="to_be_deleted")
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
# Act: Execute the method under test
@ -496,7 +497,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create metadata
metadata_args = MetadataArgs(type="string", name="test_metadata")
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
# Create metadata binding
@ -798,7 +799,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create metadata
metadata_args = MetadataArgs(type="string", name="test_metadata")
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
# Mock DocumentService.get_document
@ -866,7 +867,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create metadata
metadata_args = MetadataArgs(type="string", name="test_metadata")
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
# Mock DocumentService.get_document
@ -917,7 +918,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create metadata
metadata_args = MetadataArgs(type="string", name="test_metadata")
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
# Create metadata operation data
@ -1038,7 +1039,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create metadata
metadata_args = MetadataArgs(type="string", name="test_metadata")
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
# Create document and metadata binding
@ -1101,7 +1102,7 @@ class TestMetadataService:
mock_external_service_dependencies["current_user"].id = account.id
# Create metadata
metadata_args = MetadataArgs(type="string", name="test_metadata")
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
# Act: Execute the method under test

View File

@ -4,6 +4,7 @@ import pytest
from faker import Faker
from sqlalchemy.orm import Session
from models.enums import ConversationFromSource
from models.model import EndUser, Message
from models.web import SavedMessage
from services.app_service import AppService
@ -132,11 +133,14 @@ class TestSavedMessageService:
# Create a simple conversation first
from models.model import Conversation
is_account = hasattr(user, "current_tenant")
from_source = ConversationFromSource.CONSOLE if is_account else ConversationFromSource.API
conversation = Conversation(
app_id=app.id,
from_source="account" if hasattr(user, "current_tenant") else "end_user",
from_end_user_id=user.id if not hasattr(user, "current_tenant") else None,
from_account_id=user.id if hasattr(user, "current_tenant") else None,
from_source=from_source,
from_end_user_id=user.id if not is_account else None,
from_account_id=user.id if is_account else None,
name=fake.sentence(nb_words=3),
inputs={},
status="normal",
@ -150,9 +154,9 @@ class TestSavedMessageService:
message = Message(
app_id=app.id,
conversation_id=conversation.id,
from_source="account" if hasattr(user, "current_tenant") else "end_user",
from_end_user_id=user.id if not hasattr(user, "current_tenant") else None,
from_account_id=user.id if hasattr(user, "current_tenant") else None,
from_source=from_source,
from_end_user_id=user.id if not is_account else None,
from_account_id=user.id if is_account else None,
inputs={},
query=fake.sentence(nb_words=5),
message=fake.text(max_nb_chars=100),

View File

@ -9,6 +9,7 @@ from werkzeug.exceptions import NotFound
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset
from models.enums import DataSourceType
from models.model import App, Tag, TagBinding
from services.tag_service import TagService
@ -100,7 +101,7 @@ class TestTagService:
description=fake.text(max_nb_chars=100),
provider="vendor",
permission="only_me",
data_source_type="upload",
data_source_type=DataSourceType.UPLOAD_FILE,
indexing_technique="high_quality",
tenant_id=tenant_id,
created_by=mock_external_service_dependencies["current_user"].id,

View File

@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
from core.app.entities.app_invoke_entities import InvokeFrom
from models import Account
from models.enums import ConversationFromSource
from models.model import Conversation, EndUser
from models.web import PinnedConversation
from services.account_service import AccountService, TenantService
@ -145,7 +146,7 @@ class TestWebConversationService:
system_instruction_tokens=50,
status="normal",
invoke_from=InvokeFrom.WEB_APP,
from_source="console" if isinstance(user, Account) else "api",
from_source=ConversationFromSource.CONSOLE if isinstance(user, Account) else ConversationFromSource.API,
from_end_user_id=user.id if isinstance(user, EndUser) else None,
from_account_id=user.id if isinstance(user, Account) else None,
dialogue_count=0,

View File

@ -122,6 +122,7 @@ class TestWorkflowDraftVariableService:
name,
value,
variable_type: DraftVariableType = DraftVariableType.CONVERSATION,
user_id: str | None = None,
fake=None,
):
"""
@ -144,10 +145,15 @@ class TestWorkflowDraftVariableService:
WorkflowDraftVariable: Created test variable instance with proper type configuration
"""
fake = fake or Faker()
if user_id is None:
app = db_session_with_containers.query(App).filter_by(id=app_id).first()
assert app is not None
user_id = app.created_by
if variable_type == "conversation":
# Create conversation variable using the appropriate factory method
variable = WorkflowDraftVariable.new_conversation_variable(
app_id=app_id,
user_id=user_id,
name=name,
value=value,
description=fake.text(max_nb_chars=20),
@ -156,6 +162,7 @@ class TestWorkflowDraftVariableService:
# Create system variable with editable flag and execution context
variable = WorkflowDraftVariable.new_sys_variable(
app_id=app_id,
user_id=user_id,
name=name,
value=value,
node_execution_id=fake.uuid4(),
@ -165,6 +172,7 @@ class TestWorkflowDraftVariableService:
# Create node variable with visibility and editability settings
variable = WorkflowDraftVariable.new_node_variable(
app_id=app_id,
user_id=user_id,
node_id=node_id,
name=name,
value=value,
@ -189,7 +197,13 @@ class TestWorkflowDraftVariableService:
app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake)
test_value = StringSegment(value=fake.word())
variable = self._create_test_variable(
db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "test_var", test_value, fake=fake
db_session_with_containers,
app.id,
CONVERSATION_VARIABLE_NODE_ID,
"test_var",
test_value,
user_id=app.created_by,
fake=fake,
)
service = WorkflowDraftVariableService(db_session_with_containers)
retrieved_variable = service.get_variable(variable.id)
@ -250,7 +264,7 @@ class TestWorkflowDraftVariableService:
["test_node_1", "var3"],
]
service = WorkflowDraftVariableService(db_session_with_containers)
retrieved_variables = service.get_draft_variables_by_selectors(app.id, selectors)
retrieved_variables = service.get_draft_variables_by_selectors(app.id, selectors, user_id=app.created_by)
assert len(retrieved_variables) == 3
var_names = [var.name for var in retrieved_variables]
assert "var1" in var_names
@ -288,7 +302,7 @@ class TestWorkflowDraftVariableService:
fake=fake,
)
service = WorkflowDraftVariableService(db_session_with_containers)
result = service.list_variables_without_values(app.id, page=1, limit=3)
result = service.list_variables_without_values(app.id, page=1, limit=3, user_id=app.created_by)
assert result.total == 5
assert len(result.variables) == 3
assert result.variables[0].created_at >= result.variables[1].created_at
@ -339,7 +353,7 @@ class TestWorkflowDraftVariableService:
fake=fake,
)
service = WorkflowDraftVariableService(db_session_with_containers)
result = service.list_node_variables(app.id, node_id)
result = service.list_node_variables(app.id, node_id, user_id=app.created_by)
assert len(result.variables) == 2
for var in result.variables:
assert var.node_id == node_id
@ -381,7 +395,7 @@ class TestWorkflowDraftVariableService:
fake=fake,
)
service = WorkflowDraftVariableService(db_session_with_containers)
result = service.list_conversation_variables(app.id)
result = service.list_conversation_variables(app.id, user_id=app.created_by)
assert len(result.variables) == 2
for var in result.variables:
assert var.node_id == CONVERSATION_VARIABLE_NODE_ID
@ -559,7 +573,7 @@ class TestWorkflowDraftVariableService:
assert len(app_variables) == 3
assert len(other_app_variables) == 1
service = WorkflowDraftVariableService(db_session_with_containers)
service.delete_workflow_variables(app.id)
service.delete_user_workflow_variables(app.id, user_id=app.created_by)
app_variables_after = db_session_with_containers.query(WorkflowDraftVariable).filter_by(app_id=app.id).all()
other_app_variables_after = (
db_session_with_containers.query(WorkflowDraftVariable).filter_by(app_id=other_app.id).all()
@ -567,6 +581,69 @@ class TestWorkflowDraftVariableService:
assert len(app_variables_after) == 0
assert len(other_app_variables_after) == 1
def test_draft_variables_are_isolated_between_users(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
Test draft variable isolation for different users in the same app.
This test verifies that:
1. Query APIs return only variables owned by the target user.
2. User-scoped deletion only removes variables for that user and keeps
other users' variables in the same app untouched.
"""
fake = Faker()
app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake)
user_a = app.created_by
user_b = fake.uuid4()
# Use identical variable names on purpose to verify uniqueness scope includes user_id.
self._create_test_variable(
db_session_with_containers,
app.id,
CONVERSATION_VARIABLE_NODE_ID,
"shared_name",
StringSegment(value="value_a"),
user_id=user_a,
fake=fake,
)
self._create_test_variable(
db_session_with_containers,
app.id,
CONVERSATION_VARIABLE_NODE_ID,
"shared_name",
StringSegment(value="value_b"),
user_id=user_b,
fake=fake,
)
self._create_test_variable(
db_session_with_containers,
app.id,
CONVERSATION_VARIABLE_NODE_ID,
"only_a",
StringSegment(value="only_a"),
user_id=user_a,
fake=fake,
)
service = WorkflowDraftVariableService(db_session_with_containers)
user_a_vars = service.list_conversation_variables(app.id, user_id=user_a)
user_b_vars = service.list_conversation_variables(app.id, user_id=user_b)
assert {v.name for v in user_a_vars.variables} == {"shared_name", "only_a"}
assert {v.name for v in user_b_vars.variables} == {"shared_name"}
service.delete_user_workflow_variables(app.id, user_id=user_a)
user_a_remaining = (
db_session_with_containers.query(WorkflowDraftVariable).filter_by(app_id=app.id, user_id=user_a).count()
)
user_b_remaining = (
db_session_with_containers.query(WorkflowDraftVariable).filter_by(app_id=app.id, user_id=user_b).count()
)
assert user_a_remaining == 0
assert user_b_remaining == 1
def test_delete_node_variables_success(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
@ -627,7 +704,7 @@ class TestWorkflowDraftVariableService:
assert len(other_node_variables) == 1
assert len(conv_variables) == 1
service = WorkflowDraftVariableService(db_session_with_containers)
service.delete_node_variables(app.id, node_id)
service.delete_node_variables(app.id, node_id, user_id=app.created_by)
target_node_variables_after = (
db_session_with_containers.query(WorkflowDraftVariable).filter_by(app_id=app.id, node_id=node_id).all()
)
@ -675,7 +752,7 @@ class TestWorkflowDraftVariableService:
db_session_with_containers.commit()
service = WorkflowDraftVariableService(db_session_with_containers)
service.prefill_conversation_variable_default_values(workflow)
service.prefill_conversation_variable_default_values(workflow, user_id="00000000-0000-0000-0000-000000000001")
draft_variables = (
db_session_with_containers.query(WorkflowDraftVariable)
.filter_by(app_id=app.id, node_id=CONVERSATION_VARIABLE_NODE_ID)
@ -715,7 +792,7 @@ class TestWorkflowDraftVariableService:
fake=fake,
)
service = WorkflowDraftVariableService(db_session_with_containers)
retrieved_conv_id = service._get_conversation_id_from_draft_variable(app.id)
retrieved_conv_id = service._get_conversation_id_from_draft_variable(app.id, app.created_by)
assert retrieved_conv_id == conversation_id
def test_get_conversation_id_from_draft_variable_not_found(
@ -731,7 +808,7 @@ class TestWorkflowDraftVariableService:
fake = Faker()
app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake)
service = WorkflowDraftVariableService(db_session_with_containers)
retrieved_conv_id = service._get_conversation_id_from_draft_variable(app.id)
retrieved_conv_id = service._get_conversation_id_from_draft_variable(app.id, app.created_by)
assert retrieved_conv_id is None
def test_list_system_variables_success(
@ -772,7 +849,7 @@ class TestWorkflowDraftVariableService:
db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "conv_var", conv_var_value, fake=fake
)
service = WorkflowDraftVariableService(db_session_with_containers)
result = service.list_system_variables(app.id)
result = service.list_system_variables(app.id, user_id=app.created_by)
assert len(result.variables) == 2
for var in result.variables:
assert var.node_id == SYSTEM_VARIABLE_NODE_ID
@ -819,15 +896,15 @@ class TestWorkflowDraftVariableService:
fake=fake,
)
service = WorkflowDraftVariableService(db_session_with_containers)
retrieved_conv_var = service.get_conversation_variable(app.id, "test_conv_var")
retrieved_conv_var = service.get_conversation_variable(app.id, "test_conv_var", user_id=app.created_by)
assert retrieved_conv_var is not None
assert retrieved_conv_var.name == "test_conv_var"
assert retrieved_conv_var.node_id == CONVERSATION_VARIABLE_NODE_ID
retrieved_sys_var = service.get_system_variable(app.id, "test_sys_var")
retrieved_sys_var = service.get_system_variable(app.id, "test_sys_var", user_id=app.created_by)
assert retrieved_sys_var is not None
assert retrieved_sys_var.name == "test_sys_var"
assert retrieved_sys_var.node_id == SYSTEM_VARIABLE_NODE_ID
retrieved_node_var = service.get_node_variable(app.id, "test_node", "test_node_var")
retrieved_node_var = service.get_node_variable(app.id, "test_node", "test_node_var", user_id=app.created_by)
assert retrieved_node_var is not None
assert retrieved_node_var.name == "test_node_var"
assert retrieved_node_var.node_id == "test_node"
@ -845,9 +922,14 @@ class TestWorkflowDraftVariableService:
fake = Faker()
app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake)
service = WorkflowDraftVariableService(db_session_with_containers)
retrieved_conv_var = service.get_conversation_variable(app.id, "non_existent_conv_var")
retrieved_conv_var = service.get_conversation_variable(app.id, "non_existent_conv_var", user_id=app.created_by)
assert retrieved_conv_var is None
retrieved_sys_var = service.get_system_variable(app.id, "non_existent_sys_var")
retrieved_sys_var = service.get_system_variable(app.id, "non_existent_sys_var", user_id=app.created_by)
assert retrieved_sys_var is None
retrieved_node_var = service.get_node_variable(app.id, "test_node", "non_existent_node_var")
retrieved_node_var = service.get_node_variable(
app.id,
"test_node",
"non_existent_node_var",
user_id=app.created_by,
)
assert retrieved_node_var is None

View File

@ -7,7 +7,7 @@ import pytest
from faker import Faker
from sqlalchemy.orm import Session
from models.enums import CreatorUserRole
from models.enums import ConversationFromSource, CreatorUserRole
from models.model import (
Message,
)
@ -165,7 +165,7 @@ class TestWorkflowRunService:
inputs={},
status="normal",
mode="chat",
from_source=CreatorUserRole.ACCOUNT,
from_source=ConversationFromSource.CONSOLE,
from_account_id=account.id,
)
db_session_with_containers.add(conversation)
@ -186,7 +186,7 @@ class TestWorkflowRunService:
message.answer_price_unit = 0.001
message.currency = "USD"
message.status = "normal"
message.from_source = CreatorUserRole.ACCOUNT
message.from_source = ConversationFromSource.CONSOLE
message.from_account_id = account.id
message.workflow_run_id = workflow_run.id
message.inputs = {"input": "test input"}

View File

@ -802,6 +802,81 @@ class TestWorkflowService:
with pytest.raises(ValueError, match="No valid workflow found"):
workflow_service.publish_workflow(session=db_session_with_containers, app_model=app, account=account)
def test_restore_published_workflow_to_draft_does_not_persist_normalized_source_features(
self, db_session_with_containers: Session
):
"""Restore copies legacy feature JSON into draft without rewriting the source row."""
fake = Faker()
account = self._create_test_account(db_session_with_containers, fake)
app = self._create_test_app(db_session_with_containers, fake)
app.mode = AppMode.ADVANCED_CHAT
legacy_features = {
"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": ""},
}
published_workflow = Workflow(
id=fake.uuid4(),
tenant_id=app.tenant_id,
app_id=app.id,
type=WorkflowType.WORKFLOW,
version="2026.03.19.001",
graph=json.dumps({"nodes": [], "edges": []}),
features=json.dumps(legacy_features),
created_by=account.id,
updated_by=account.id,
environment_variables=[],
conversation_variables=[],
)
draft_workflow = Workflow(
id=fake.uuid4(),
tenant_id=app.tenant_id,
app_id=app.id,
type=WorkflowType.WORKFLOW,
version=Workflow.VERSION_DRAFT,
graph=json.dumps({"nodes": [], "edges": []}),
features=json.dumps({}),
created_by=account.id,
updated_by=account.id,
environment_variables=[],
conversation_variables=[],
)
db_session_with_containers.add(published_workflow)
db_session_with_containers.add(draft_workflow)
db_session_with_containers.commit()
workflow_service = WorkflowService()
restored_workflow = workflow_service.restore_published_workflow_to_draft(
app_model=app,
workflow_id=published_workflow.id,
account=account,
)
db_session_with_containers.expire_all()
refreshed_published_workflow = (
db_session_with_containers.query(Workflow).filter_by(id=published_workflow.id).first()
)
refreshed_draft_workflow = db_session_with_containers.query(Workflow).filter_by(id=draft_workflow.id).first()
assert restored_workflow.id == draft_workflow.id
assert refreshed_published_workflow is not None
assert refreshed_draft_workflow is not None
assert refreshed_published_workflow.serialized_features == json.dumps(legacy_features)
assert refreshed_draft_workflow.serialized_features == json.dumps(legacy_features)
def test_get_default_block_configs(self, db_session_with_containers: Session):
"""
Test retrieval of default block configurations for all node types.

View File

@ -48,41 +48,42 @@ class TestToolTransformService:
name=fake.company(),
description=fake.text(max_nb_chars=100),
icon='{"background": "#FF6B6B", "content": "🔧"}',
icon_dark='{"background": "#252525", "content": "🔧"}',
tenant_id="test_tenant_id",
user_id="test_user_id",
credentials={"auth_type": "api_key_header", "api_key": "test_key"},
provider_type="api",
credentials_str='{"auth_type": "api_key_header", "api_key": "test_key"}',
schema="{}",
schema_type_str="openapi",
tools_str="[]",
)
elif provider_type == "builtin":
provider = BuiltinToolProvider(
name=fake.company(),
description=fake.text(max_nb_chars=100),
icon="🔧",
icon_dark="🔧",
tenant_id="test_tenant_id",
user_id="test_user_id",
provider="test_provider",
credential_type="api_key",
credentials={"api_key": "test_key"},
encrypted_credentials='{"api_key": "test_key"}',
)
elif provider_type == "workflow":
provider = WorkflowToolProvider(
name=fake.company(),
description=fake.text(max_nb_chars=100),
icon='{"background": "#FF6B6B", "content": "🔧"}',
icon_dark='{"background": "#252525", "content": "🔧"}',
tenant_id="test_tenant_id",
user_id="test_user_id",
workflow_id="test_workflow_id",
app_id="test_workflow_id",
label="Test Workflow",
version="1.0.0",
parameter_configuration="[]",
)
elif provider_type == "mcp":
provider = MCPToolProvider(
name=fake.company(),
description=fake.text(max_nb_chars=100),
provider_icon='{"background": "#FF6B6B", "content": "🔧"}',
icon='{"background": "#FF6B6B", "content": "🔧"}',
tenant_id="test_tenant_id",
user_id="test_user_id",
server_url="https://mcp.example.com",
server_url_hash="test_server_url_hash",
server_identifier="test_server",
tools='[{"name": "test_tool", "description": "Test tool"}]',
authed=True,

View File

@ -510,7 +510,7 @@ class TestWorkflowConverter:
retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.MULTIPLE,
top_k=10,
score_threshold=0.8,
reranking_model={"provider": "cohere", "model": "rerank-v2"},
reranking_model={"reranking_provider_name": "cohere", "reranking_model_name": "rerank-v2"},
reranking_enabled=True,
),
)
@ -543,8 +543,8 @@ class TestWorkflowConverter:
multiple_config = node["data"]["multiple_retrieval_config"]
assert multiple_config["top_k"] == 10
assert multiple_config["score_threshold"] == 0.8
assert multiple_config["reranking_model"]["provider"] == "cohere"
assert multiple_config["reranking_model"]["model"] == "rerank-v2"
assert multiple_config["reranking_model"]["reranking_provider_name"] == "cohere"
assert multiple_config["reranking_model"]["reranking_model_name"] == "rerank-v2"
# Verify single retrieval config is None for multiple strategy
assert node["data"]["single_retrieval_config"] is None