Merge branch 'main' into feat/support-agent-sandbox

This commit is contained in:
Novice 2026-03-24 17:12:13 +08:00
commit 6756745062
No known key found for this signature in database
GPG Key ID: A253106A7475AA3E
136 changed files with 4018 additions and 706 deletions

View File

@ -4,10 +4,9 @@ runs:
using: composite
steps:
- name: Setup Vite+
uses: voidzero-dev/setup-vp@4a524139920f87f9f7080d3b8545acac019e1852 # v1.0.0
uses: voidzero-dev/setup-vp@20553a7a7429c429a74894104a2835d7fed28a72 # v1.3.0
with:
node-version-file: web/.nvmrc
working-directory: web
node-version-file: .nvmrc
cache: true
cache-dependency-path: web/pnpm-lock.yaml
run-install: |
cwd: ./web
run-install: true

View File

@ -84,20 +84,20 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/actions/setup-web
- name: Restore ESLint cache
if: steps.changed-files.outputs.any_changed == 'true'
id: eslint-cache-restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: web/.eslintcache
key: ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'web/pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'web/pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-
- name: Web style check
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ./web
run: |
vp run lint:ci
# pnpm run lint:report
# continue-on-error: true
# - name: Annotate Code
# if: steps.changed-files.outputs.any_changed == 'true' && github.event_name == 'pull_request'
# uses: DerLev/eslint-annotations@51347b3a0abfb503fc8734d5ae31c4b151297fae
# with:
# eslint-report: web/eslint_report.json
# github-token: ${{ secrets.GITHUB_TOKEN }}
run: vp run lint:ci
- name: Web tsslint
if: steps.changed-files.outputs.any_changed == 'true'
@ -114,6 +114,13 @@ jobs:
working-directory: ./web
run: vp run knip
- name: Save ESLint cache
if: steps.changed-files.outputs.any_changed == 'true' && success() && steps.eslint-cache-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: web/.eslintcache
key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }}
superlinter:
name: SuperLinter
runs-on: ubuntu-latest

View File

@ -120,7 +120,7 @@ jobs:
- name: Run Claude Code for Translation Sync
if: steps.detect_changes.outputs.CHANGED_FILES != ''
uses: anthropics/claude-code-action@6062f3709600659be5e47fcddf2cf76993c235c2 # v1.0.76
uses: anthropics/claude-code-action@ff9acae5886d41a99ed4ec14b7dc147d55834722 # v1.0.77
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -10,6 +10,7 @@ from configs import dify_config
from core.rag.datasource.vdb.vector_factory import Vector
from core.rag.datasource.vdb.vector_type import VectorType
from core.rag.index_processor.constant.built_in_field import BuiltInField
from core.rag.index_processor.constant.index_type import IndexStructureType
from core.rag.models.document import ChildDocument, Document
from extensions.ext_database import db
from models.dataset import Dataset, DatasetCollectionBinding, DatasetMetadata, DatasetMetadataBinding, DocumentSegment
@ -269,7 +270,7 @@ def migrate_knowledge_vector_database():
"dataset_id": segment.dataset_id,
},
)
if dataset_document.doc_form == "hierarchical_model":
if dataset_document.doc_form == IndexStructureType.PARENT_CHILD_INDEX:
child_chunks = segment.get_child_chunks()
if child_chunks:
child_documents = []

View File

@ -102,7 +102,7 @@ class CreateAppPayload(BaseModel):
name: str = Field(..., min_length=1, description="App name")
description: str | None = Field(default=None, description="App description (max 400 chars)", max_length=400)
mode: Literal["chat", "agent-chat", "advanced-chat", "workflow", "completion"] = Field(..., description="App mode")
icon_type: str | None = Field(default=None, description="Icon type")
icon_type: IconType | None = Field(default=None, description="Icon type")
icon: str | None = Field(default=None, description="Icon")
icon_background: str | None = Field(default=None, description="Icon background color")
@ -110,7 +110,7 @@ class CreateAppPayload(BaseModel):
class UpdateAppPayload(BaseModel):
name: str = Field(..., min_length=1, description="App name")
description: str | None = Field(default=None, description="App description (max 400 chars)", max_length=400)
icon_type: str | None = Field(default=None, description="Icon type")
icon_type: IconType | None = Field(default=None, description="Icon type")
icon: str | None = Field(default=None, description="Icon")
icon_background: str | None = Field(default=None, description="Icon background color")
use_icon_as_answer_icon: bool | None = Field(default=None, description="Use icon as answer icon")
@ -120,7 +120,7 @@ class UpdateAppPayload(BaseModel):
class CopyAppPayload(BaseModel):
name: str | None = Field(default=None, description="Name for the copied app")
description: str | None = Field(default=None, description="Description for the copied app", max_length=400)
icon_type: str | None = Field(default=None, description="Icon type")
icon_type: IconType | None = Field(default=None, description="Icon type")
icon: str | None = Field(default=None, description="Icon")
icon_background: str | None = Field(default=None, description="Icon background color")
@ -613,7 +613,7 @@ class AppApi(Resource):
args_dict: AppService.ArgsDict = {
"name": args.name,
"description": args.description or "",
"icon_type": args.icon_type or "",
"icon_type": args.icon_type,
"icon": args.icon or "",
"icon_background": args.icon_background or "",
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,

View File

@ -19,6 +19,7 @@ class RateLimit:
_REQUEST_MAX_ALIVE_TIME = 10 * 60 # 10 minutes
_ACTIVE_REQUESTS_COUNT_FLUSH_INTERVAL = 5 * 60 # recalculate request_count from request_detail every 5 minutes
_instance_dict: dict[str, "RateLimit"] = {}
max_active_requests: int
def __new__(cls, client_id: str, max_active_requests: int):
if client_id not in cls._instance_dict:
@ -27,7 +28,13 @@ class RateLimit:
return cls._instance_dict[client_id]
def __init__(self, client_id: str, max_active_requests: int):
flush_cache = hasattr(self, "max_active_requests") and self.max_active_requests != max_active_requests
self.max_active_requests = max_active_requests
# Only flush here if this instance has already been fully initialized,
# i.e. the Redis key attributes exist. Otherwise, rely on the flush at
# the end of initialization below.
if flush_cache and hasattr(self, "active_requests_key") and hasattr(self, "max_active_requests_key"):
self.flush_cache(use_local_value=True)
# must be called after max_active_requests is set
if self.disabled():
return
@ -41,8 +48,6 @@ class RateLimit:
self.flush_cache(use_local_value=True)
def flush_cache(self, use_local_value=False):
if self.disabled():
return
self.last_recalculate_time = time.time()
# flush max active requests
if use_local_value or not redis_client.exists(self.max_active_requests_key):
@ -50,7 +55,8 @@ class RateLimit:
else:
self.max_active_requests = int(redis_client.get(self.max_active_requests_key).decode("utf-8"))
redis_client.expire(self.max_active_requests_key, timedelta(days=1))
if self.disabled():
return
# flush max active requests (in-transit request list)
if not redis_client.exists(self.active_requests_key):
return

View File

@ -496,7 +496,9 @@ class Document(Base):
)
doc_type = mapped_column(EnumText(DocumentDocType, length=40), nullable=True)
doc_metadata = mapped_column(AdjustedJSON, nullable=True)
doc_form = mapped_column(String(255), nullable=False, server_default=sa.text("'text_model'"))
doc_form: Mapped[IndexStructureType] = mapped_column(
EnumText(IndexStructureType, length=255), nullable=False, server_default=sa.text("'text_model'")
)
doc_language = mapped_column(String(255), nullable=True)
need_summary: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))

View File

@ -145,7 +145,9 @@ class ApiToolProvider(TypeBase):
icon: Mapped[str] = mapped_column(String(255), nullable=False)
# original schema
schema: Mapped[str] = mapped_column(LongText, nullable=False)
schema_type_str: Mapped[str] = mapped_column(String(40), nullable=False)
schema_type_str: Mapped[ApiProviderSchemaType] = mapped_column(
EnumText(ApiProviderSchemaType, length=40), nullable=False
)
# who created this tool
user_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
# tenant id

View File

@ -241,7 +241,7 @@ class AppService:
class ArgsDict(TypedDict):
name: str
description: str
icon_type: str
icon_type: IconType | str | None
icon: str
icon_background: str
use_icon_as_answer_icon: bool
@ -257,7 +257,13 @@ class AppService:
assert current_user is not None
app.name = args["name"]
app.description = args["description"]
app.icon_type = IconType(args["icon_type"]) if args["icon_type"] else None
icon_type = args.get("icon_type")
if icon_type is None:
resolved_icon_type = app.icon_type
else:
resolved_icon_type = IconType(icon_type)
app.icon_type = resolved_icon_type
app.icon = args["icon"]
app.icon_background = args["icon_background"]
app.use_icon_as_answer_icon = args.get("use_icon_as_answer_icon", False)

View File

@ -1,8 +1,16 @@
from abc import ABC, abstractmethod
from typing import Any
from typing_extensions import TypedDict
class AuthCredentials(TypedDict):
auth_type: str
config: dict[str, Any]
class ApiKeyAuthBase(ABC):
def __init__(self, credentials: dict):
def __init__(self, credentials: AuthCredentials):
self.credentials = credentials
@abstractmethod

View File

@ -1,9 +1,9 @@
from services.auth.api_key_auth_base import ApiKeyAuthBase
from services.auth.api_key_auth_base import ApiKeyAuthBase, AuthCredentials
from services.auth.auth_type import AuthType
class ApiKeyAuthFactory:
def __init__(self, provider: str, credentials: dict):
def __init__(self, provider: str, credentials: AuthCredentials):
auth_factory = self.get_apikey_auth_factory(provider)
self.auth = auth_factory(credentials)

View File

@ -2,11 +2,11 @@ import json
import httpx
from services.auth.api_key_auth_base import ApiKeyAuthBase
from services.auth.api_key_auth_base import ApiKeyAuthBase, AuthCredentials
class FirecrawlAuth(ApiKeyAuthBase):
def __init__(self, credentials: dict):
def __init__(self, credentials: AuthCredentials):
super().__init__(credentials)
auth_type = credentials.get("auth_type")
if auth_type != "bearer":

View File

@ -2,11 +2,11 @@ import json
import httpx
from services.auth.api_key_auth_base import ApiKeyAuthBase
from services.auth.api_key_auth_base import ApiKeyAuthBase, AuthCredentials
class JinaAuth(ApiKeyAuthBase):
def __init__(self, credentials: dict):
def __init__(self, credentials: AuthCredentials):
super().__init__(credentials)
auth_type = credentials.get("auth_type")
if auth_type != "bearer":

View File

@ -2,11 +2,11 @@ import json
import httpx
from services.auth.api_key_auth_base import ApiKeyAuthBase
from services.auth.api_key_auth_base import ApiKeyAuthBase, AuthCredentials
class JinaAuth(ApiKeyAuthBase):
def __init__(self, credentials: dict):
def __init__(self, credentials: AuthCredentials):
super().__init__(credentials)
auth_type = credentials.get("auth_type")
if auth_type != "bearer":

View File

@ -3,11 +3,11 @@ from urllib.parse import urljoin
import httpx
from services.auth.api_key_auth_base import ApiKeyAuthBase
from services.auth.api_key_auth_base import ApiKeyAuthBase, AuthCredentials
class WatercrawlAuth(ApiKeyAuthBase):
def __init__(self, credentials: dict):
def __init__(self, credentials: AuthCredentials):
super().__init__(credentials)
auth_type = credentials.get("auth_type")
if auth_type != "x-api-key":

View File

@ -1440,7 +1440,7 @@ class DocumentService:
.filter(
Document.id.in_(document_id_list),
Document.dataset_id == dataset_id,
Document.doc_form != "qa_model", # Skip qa_model documents
Document.doc_form != IndexStructureType.QA_INDEX, # Skip qa_model documents
)
.update({Document.need_summary: need_summary}, synchronize_session=False)
)
@ -2040,7 +2040,7 @@ class DocumentService:
document.dataset_process_rule_id = dataset_process_rule.id
document.updated_at = naive_utc_now()
document.created_from = created_from
document.doc_form = knowledge_config.doc_form
document.doc_form = IndexStructureType(knowledge_config.doc_form)
document.doc_language = knowledge_config.doc_language
document.data_source_info = json.dumps(data_source_info)
document.batch = batch
@ -2640,7 +2640,7 @@ class DocumentService:
document.splitting_completed_at = None
document.updated_at = naive_utc_now()
document.created_from = created_from
document.doc_form = document_data.doc_form
document.doc_form = IndexStructureType(document_data.doc_form)
db.session.add(document)
db.session.commit()
# update document segment
@ -3101,7 +3101,7 @@ class DocumentService:
class SegmentService:
@classmethod
def segment_create_args_validate(cls, args: dict, document: Document):
if document.doc_form == "qa_model":
if document.doc_form == IndexStructureType.QA_INDEX:
if "answer" not in args or not args["answer"]:
raise ValueError("Answer is required")
if not args["answer"].strip():
@ -3158,7 +3158,7 @@ class SegmentService:
completed_at=naive_utc_now(),
created_by=current_user.id,
)
if document.doc_form == "qa_model":
if document.doc_form == IndexStructureType.QA_INDEX:
segment_document.word_count += len(args["answer"])
segment_document.answer = args["answer"]
@ -3232,7 +3232,7 @@ class SegmentService:
tokens = 0
if dataset.indexing_technique == "high_quality" and embedding_model:
# calc embedding use tokens
if document.doc_form == "qa_model":
if document.doc_form == IndexStructureType.QA_INDEX:
tokens = embedding_model.get_text_embedding_num_tokens(
texts=[content + segment_item["answer"]]
)[0]
@ -3255,7 +3255,7 @@ class SegmentService:
completed_at=naive_utc_now(),
created_by=current_user.id,
)
if document.doc_form == "qa_model":
if document.doc_form == IndexStructureType.QA_INDEX:
segment_document.answer = segment_item["answer"]
segment_document.word_count += len(segment_item["answer"])
increment_word_count += segment_document.word_count
@ -3322,7 +3322,7 @@ class SegmentService:
content = args.content or segment.content
if segment.content == content:
segment.word_count = len(content)
if document.doc_form == "qa_model":
if document.doc_form == IndexStructureType.QA_INDEX:
segment.answer = args.answer
segment.word_count += len(args.answer) if args.answer else 0
word_count_change = segment.word_count - word_count_change
@ -3419,7 +3419,7 @@ class SegmentService:
)
# calc embedding use tokens
if document.doc_form == "qa_model":
if document.doc_form == IndexStructureType.QA_INDEX:
segment.answer = args.answer
tokens = embedding_model.get_text_embedding_num_tokens(texts=[content + segment.answer])[0] # type: ignore
else:
@ -3436,7 +3436,7 @@ class SegmentService:
segment.enabled = True
segment.disabled_at = None
segment.disabled_by = None
if document.doc_form == "qa_model":
if document.doc_form == IndexStructureType.QA_INDEX:
segment.answer = args.answer
segment.word_count += len(args.answer) if args.answer else 0
word_count_change = segment.word_count - word_count_change

View File

@ -9,6 +9,7 @@ from flask_login import current_user
from constants import DOCUMENT_EXTENSIONS
from core.plugin.impl.plugin import PluginInstaller
from core.rag.index_processor.constant.index_type import IndexStructureType
from core.rag.retrieval.retrieval_methods import RetrievalMethod
from extensions.ext_database import db
from factories import variable_factory
@ -79,9 +80,9 @@ class RagPipelineTransformService:
pipeline = self._create_pipeline(pipeline_yaml)
# save chunk structure to dataset
if doc_form == "hierarchical_model":
if doc_form == IndexStructureType.PARENT_CHILD_INDEX:
dataset.chunk_structure = "hierarchical_model"
elif doc_form == "text_model":
elif doc_form == IndexStructureType.PARAGRAPH_INDEX:
dataset.chunk_structure = "text_model"
else:
raise ValueError("Unsupported doc form")
@ -101,7 +102,7 @@ class RagPipelineTransformService:
def _get_transform_yaml(self, doc_form: str, datasource_type: str, indexing_technique: str | None):
pipeline_yaml = {}
if doc_form == "text_model":
if doc_form == IndexStructureType.PARAGRAPH_INDEX:
match datasource_type:
case DataSourceType.UPLOAD_FILE:
if indexing_technique == "high_quality":
@ -132,7 +133,7 @@ class RagPipelineTransformService:
pipeline_yaml = yaml.safe_load(f)
case _:
raise ValueError("Unsupported datasource type")
elif doc_form == "hierarchical_model":
elif doc_form == IndexStructureType.PARENT_CHILD_INDEX:
match datasource_type:
case DataSourceType.UPLOAD_FILE:
# get graph from transform.file-parentchild.yml

View File

@ -11,6 +11,7 @@ from sqlalchemy import func
from core.db.session_factory import session_factory
from core.model_manager import ModelManager
from core.rag.index_processor.constant.index_type import IndexStructureType
from dify_graph.model_runtime.entities.model_entities import ModelType
from extensions.ext_redis import redis_client
from extensions.ext_storage import storage
@ -109,7 +110,7 @@ def batch_create_segment_to_index_task(
df = pd.read_csv(file_path)
content = []
for _, row in df.iterrows():
if document_config["doc_form"] == "qa_model":
if document_config["doc_form"] == IndexStructureType.QA_INDEX:
data = {"content": row.iloc[0], "answer": row.iloc[1]}
else:
data = {"content": row.iloc[0]}
@ -159,7 +160,7 @@ def batch_create_segment_to_index_task(
status="completed",
completed_at=naive_utc_now(),
)
if document_config["doc_form"] == "qa_model":
if document_config["doc_form"] == IndexStructureType.QA_INDEX:
segment_document.answer = segment["answer"]
segment_document.word_count += len(segment["answer"])
word_count_change += segment_document.word_count

View File

@ -10,6 +10,7 @@ from configs import dify_config
from core.db.session_factory import session_factory
from core.entities.document_task import DocumentTask
from core.indexing_runner import DocumentIsPausedError, IndexingRunner
from core.rag.index_processor.constant.index_type import IndexStructureType
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
from enums.cloud_plan import CloudPlan
from libs.datetime_utils import naive_utc_now
@ -150,7 +151,7 @@ def _document_indexing(dataset_id: str, document_ids: Sequence[str]):
)
if (
document.indexing_status == IndexingStatus.COMPLETED
and document.doc_form != "qa_model"
and document.doc_form != IndexStructureType.QA_INDEX
and document.need_summary is True
):
try:

View File

@ -9,6 +9,7 @@ from celery import shared_task
from sqlalchemy import or_, select
from core.db.session_factory import session_factory
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.dataset import Dataset, DocumentSegment, DocumentSegmentSummary
from models.dataset import Document as DatasetDocument
from services.summary_index_service import SummaryIndexService
@ -106,7 +107,7 @@ def regenerate_summary_index_task(
),
DatasetDocument.enabled == True, # Document must be enabled
DatasetDocument.archived == False, # Document must not be archived
DatasetDocument.doc_form != "qa_model", # Skip qa_model documents
DatasetDocument.doc_form != IndexStructureType.QA_INDEX, # Skip qa_model documents
)
.order_by(DocumentSegment.document_id.asc(), DocumentSegment.position.asc())
.all()
@ -209,7 +210,7 @@ def regenerate_summary_index_task(
for dataset_document in dataset_documents:
# Skip qa_model documents
if dataset_document.doc_form == "qa_model":
if dataset_document.doc_form == IndexStructureType.QA_INDEX:
continue
try:

View File

@ -4,6 +4,7 @@ from unittest.mock import patch
import pytest
from faker import Faker
from core.rag.index_processor.constant.index_type import IndexStructureType
from core.rag.retrieval.dataset_retrieval import DatasetRetrieval
from core.workflow.nodes.knowledge_retrieval.retrieval import KnowledgeRetrievalRequest
from models.dataset import Dataset, Document
@ -55,7 +56,7 @@ class TestGetAvailableDatasetsIntegration:
name=f"Document {i}",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -112,7 +113,7 @@ class TestGetAvailableDatasetsIntegration:
created_from=DocumentCreatedFrom.WEB,
name=f"Archived Document {i}",
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
archived=True, # Archived
@ -165,7 +166,7 @@ class TestGetAvailableDatasetsIntegration:
created_from=DocumentCreatedFrom.WEB,
name=f"Disabled Document {i}",
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
indexing_status=IndexingStatus.COMPLETED,
enabled=False, # Disabled
archived=False,
@ -218,7 +219,7 @@ class TestGetAvailableDatasetsIntegration:
created_from=DocumentCreatedFrom.WEB,
name=f"Document {status}",
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
indexing_status=status, # Not completed
enabled=True,
archived=False,
@ -336,7 +337,7 @@ class TestGetAvailableDatasetsIntegration:
created_from=DocumentCreatedFrom.WEB,
name=f"Document for {dataset.name}",
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
archived=False,
@ -416,7 +417,7 @@ class TestGetAvailableDatasetsIntegration:
created_from=DocumentCreatedFrom.WEB,
name=f"Document {i}",
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
archived=False,
@ -476,7 +477,7 @@ class TestKnowledgeRetrievalIntegration:
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
archived=False,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
db_session_with_containers.add(document)
db_session_with_containers.commit()

View File

@ -13,6 +13,7 @@ from uuid import uuid4
import pytest
from core.rag.index_processor.constant.index_type import IndexStructureType
from extensions.storage.storage_type import StorageType
from models import Account
from models.dataset import Dataset, Document
@ -91,7 +92,7 @@ class DocumentStatusTestDataFactory:
name=name,
created_from=DocumentCreatedFrom.WEB,
created_by=created_by,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
document.id = document_id
document.indexing_status = indexing_status

View File

@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
from constants.model_template import default_app_templates
from models import Account
from models.model import App, Site
from models.model import App, IconType, Site
from services.account_service import AccountService, TenantService
from tests.test_containers_integration_tests.helpers import generate_valid_password
@ -463,6 +463,109 @@ class TestAppService:
assert updated_app.tenant_id == app.tenant_id
assert updated_app.created_by == app.created_by
def test_update_app_should_preserve_icon_type_when_omitted(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
Test update_app keeps the persisted icon_type when the update payload omits it.
"""
fake = Faker()
account = AccountService.create_account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
password=generate_valid_password(fake),
)
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
tenant = account.current_tenant
from services.app_service import AppService
app_service = AppService()
app = app_service.create_app(
tenant.id,
{
"name": fake.company(),
"description": fake.text(max_nb_chars=100),
"mode": "chat",
"icon_type": "emoji",
"icon": "🎯",
"icon_background": "#45B7D1",
},
account,
)
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.id = account.id
mock_current_user.current_tenant_id = account.current_tenant_id
with patch("services.app_service.current_user", mock_current_user):
updated_app = app_service.update_app(
app,
{
"name": "Updated App Name",
"description": "Updated app description",
"icon_type": None,
"icon": "🔄",
"icon_background": "#FF8C42",
"use_icon_as_answer_icon": True,
},
)
assert updated_app.icon_type == IconType.EMOJI
def test_update_app_should_reject_empty_icon_type(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""
Test update_app rejects an explicit empty icon_type.
"""
fake = Faker()
account = AccountService.create_account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
password=generate_valid_password(fake),
)
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
tenant = account.current_tenant
from services.app_service import AppService
app_service = AppService()
app = app_service.create_app(
tenant.id,
{
"name": fake.company(),
"description": fake.text(max_nb_chars=100),
"mode": "chat",
"icon_type": "emoji",
"icon": "🎯",
"icon_background": "#45B7D1",
},
account,
)
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.id = account.id
mock_current_user.current_tenant_id = account.current_tenant_id
with patch("services.app_service.current_user", mock_current_user):
with pytest.raises(ValueError):
app_service.update_app(
app,
{
"name": "Updated App Name",
"description": "Updated app description",
"icon_type": "",
"icon": "🔄",
"icon_background": "#FF8C42",
"use_icon_as_answer_icon": True,
},
)
def test_update_app_name_success(self, db_session_with_containers: Session, mock_external_service_dependencies):
"""
Test successful app name update.

View File

@ -11,6 +11,7 @@ from uuid import uuid4
import pytest
from sqlalchemy.orm import Session
from core.rag.index_processor.constant.index_type import IndexStructureType
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
@ -106,7 +107,7 @@ class DatasetServiceIntegrationDataFactory:
created_from=DocumentCreatedFrom.WEB,
created_by=created_by,
indexing_status=IndexingStatus.COMPLETED,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
db_session_with_containers.add(document)
db_session_with_containers.flush()

View File

@ -13,6 +13,7 @@ from uuid import uuid4
import pytest
from sqlalchemy.orm import Session
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.dataset import Dataset, Document
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
from services.dataset_service import DocumentService
@ -79,7 +80,7 @@ class DocumentBatchUpdateIntegrationDataFactory:
name=name,
created_from=DocumentCreatedFrom.WEB,
created_by=created_by or str(uuid4()),
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
document.id = document_id or str(uuid4())
document.enabled = enabled

View File

@ -3,6 +3,7 @@
from unittest.mock import patch
from uuid import uuid4
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, Document
from models.enums import DataSourceType, DocumentCreatedFrom
@ -78,7 +79,7 @@ class DatasetDeleteIntegrationDataFactory:
tenant_id: str,
dataset_id: str,
created_by: str,
doc_form: str = "text_model",
doc_form: str = IndexStructureType.PARAGRAPH_INDEX,
) -> Document:
"""Persist a document so dataset.doc_form resolves through the real document path."""
document = Document(
@ -119,7 +120,7 @@ class TestDatasetServiceDeleteDataset:
tenant_id=tenant.id,
dataset_id=dataset.id,
created_by=owner.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
# Act

View File

@ -3,6 +3,7 @@ from uuid import uuid4
from sqlalchemy import select
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.dataset import Dataset, Document
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
from services.dataset_service import DocumentService
@ -42,7 +43,7 @@ def _create_document(
name=f"doc-{uuid4()}",
created_from=DocumentCreatedFrom.WEB,
created_by=str(uuid4()),
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
document.id = str(uuid4())
document.indexing_status = indexing_status

View File

@ -7,6 +7,7 @@ from uuid import uuid4
import pytest
from core.rag.index_processor.constant.index_type import IndexStructureType
from extensions.storage.storage_type import StorageType
from models import Account
from models.dataset import Dataset, Document
@ -69,7 +70,7 @@ def make_document(
name=name,
created_from=DocumentCreatedFrom.WEB,
created_by=str(uuid4()),
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
doc.id = document_id
doc.indexing_status = "completed"

View File

@ -5,6 +5,7 @@ from faker import Faker
from sqlalchemy.orm import Session
from core.rag.index_processor.constant.built_in_field import BuiltInField
from core.rag.index_processor.constant.index_type import IndexStructureType
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, DatasetMetadata, DatasetMetadataBinding, Document
from models.enums import DatasetMetadataType, DataSourceType, DocumentCreatedFrom
@ -139,7 +140,7 @@ class TestMetadataService:
name=fake.file_name(),
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
)

View File

@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
from core.tools.entities.api_entities import ToolProviderApiEntity
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolProviderType
from core.tools.entities.tool_entities import ApiProviderSchemaType, ToolProviderType
from models.tools import ApiToolProvider, BuiltinToolProvider, MCPToolProvider, WorkflowToolProvider
from services.plugin.plugin_service import PluginService
from services.tools.tools_transform_service import ToolTransformService
@ -52,7 +52,7 @@ class TestToolTransformService:
user_id="test_user_id",
credentials_str='{"auth_type": "api_key_header", "api_key": "test_key"}',
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
tools_str="[]",
)
elif provider_type == "builtin":
@ -659,7 +659,7 @@ class TestToolTransformService:
user_id=fake.uuid4(),
credentials_str='{"auth_type": "api_key_header", "api_key": "test_key"}',
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
tools_str="[]",
)
@ -695,7 +695,7 @@ class TestToolTransformService:
user_id=fake.uuid4(),
credentials_str='{"auth_type": "api_key_query", "api_key": "test_key"}',
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
tools_str="[]",
)
@ -731,7 +731,7 @@ class TestToolTransformService:
user_id=fake.uuid4(),
credentials_str='{"auth_type": "api_key", "api_key": "test_key"}',
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
tools_str="[]",
)

View File

@ -13,6 +13,7 @@ import pytest
from faker import Faker
from sqlalchemy.orm import Session
from core.rag.index_processor.constant.index_type import IndexStructureType
from extensions.storage.storage_type import StorageType
from libs.datetime_utils import naive_utc_now
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
@ -152,7 +153,7 @@ class TestBatchCleanDocumentTask:
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
indexing_status=IndexingStatus.COMPLETED,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
db_session_with_containers.add(document)
@ -392,7 +393,12 @@ class TestBatchCleanDocumentTask:
db_session_with_containers.commit()
# Execute the task with non-existent dataset
batch_clean_document_task(document_ids=[document_id], dataset_id=dataset_id, doc_form="text_model", file_ids=[])
batch_clean_document_task(
document_ids=[document_id],
dataset_id=dataset_id,
doc_form=IndexStructureType.PARAGRAPH_INDEX,
file_ids=[],
)
# Verify that no index processing occurred
mock_external_service_dependencies["index_processor"].clean.assert_not_called()
@ -525,7 +531,11 @@ class TestBatchCleanDocumentTask:
account = self._create_test_account(db_session_with_containers)
# Test different doc_form types
doc_forms = ["text_model", "qa_model", "hierarchical_model"]
doc_forms = [
IndexStructureType.PARAGRAPH_INDEX,
IndexStructureType.QA_INDEX,
IndexStructureType.PARENT_CHILD_INDEX,
]
for doc_form in doc_forms:
dataset = self._create_test_dataset(db_session_with_containers, account)

View File

@ -19,6 +19,7 @@ import pytest
from faker import Faker
from sqlalchemy.orm import Session
from core.rag.index_processor.constant.index_type import IndexStructureType
from extensions.storage.storage_type import StorageType
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, Document, DocumentSegment
@ -179,7 +180,7 @@ class TestBatchCreateSegmentToIndexTask:
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
archived=False,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
word_count=0,
)
@ -221,17 +222,17 @@ class TestBatchCreateSegmentToIndexTask:
return upload_file
def _create_test_csv_content(self, content_type="text_model"):
def _create_test_csv_content(self, content_type=IndexStructureType.PARAGRAPH_INDEX):
"""
Helper method to create test CSV content.
Args:
content_type: Type of content to create ("text_model" or "qa_model")
content_type: Type of content to create (IndexStructureType.PARAGRAPH_INDEX or IndexStructureType.QA_INDEX)
Returns:
str: CSV content as string
"""
if content_type == "qa_model":
if content_type == IndexStructureType.QA_INDEX:
csv_content = "content,answer\n"
csv_content += "This is the first segment content,This is the first answer\n"
csv_content += "This is the second segment content,This is the second answer\n"
@ -264,7 +265,7 @@ class TestBatchCreateSegmentToIndexTask:
upload_file = self._create_test_upload_file(db_session_with_containers, account, tenant)
# Create CSV content
csv_content = self._create_test_csv_content("text_model")
csv_content = self._create_test_csv_content(IndexStructureType.PARAGRAPH_INDEX)
# Mock storage to return our CSV content
mock_storage = mock_external_service_dependencies["storage"]
@ -451,7 +452,7 @@ class TestBatchCreateSegmentToIndexTask:
indexing_status=IndexingStatus.COMPLETED,
enabled=False, # Document is disabled
archived=False,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
word_count=0,
),
# Archived document
@ -467,7 +468,7 @@ class TestBatchCreateSegmentToIndexTask:
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
archived=True, # Document is archived
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
word_count=0,
),
# Document with incomplete indexing
@ -483,7 +484,7 @@ class TestBatchCreateSegmentToIndexTask:
indexing_status=IndexingStatus.INDEXING, # Not completed
enabled=True,
archived=False,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
word_count=0,
),
]
@ -655,7 +656,7 @@ class TestBatchCreateSegmentToIndexTask:
db_session_with_containers.commit()
# Create CSV content
csv_content = self._create_test_csv_content("text_model")
csv_content = self._create_test_csv_content(IndexStructureType.PARAGRAPH_INDEX)
# Mock storage to return our CSV content
mock_storage = mock_external_service_dependencies["storage"]

View File

@ -18,6 +18,7 @@ import pytest
from faker import Faker
from sqlalchemy.orm import Session
from core.rag.index_processor.constant.index_type import IndexStructureType
from extensions.storage.storage_type import StorageType
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import (
@ -192,7 +193,7 @@ class TestCleanDatasetTask:
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
archived=False,
doc_form="paragraph_index",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
word_count=100,
created_at=datetime.now(),
updated_at=datetime.now(),

View File

@ -12,6 +12,7 @@ from unittest.mock import Mock, patch
import pytest
from faker import Faker
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.dataset import Dataset, Document, DocumentSegment
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus
from services.account_service import AccountService, TenantService
@ -114,7 +115,7 @@ class TestCleanNotionDocumentTask:
name=f"Notion Page {i}",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model", # Set doc_form to ensure dataset.doc_form works
doc_form=IndexStructureType.PARAGRAPH_INDEX, # Set doc_form to ensure dataset.doc_form works
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
)
@ -261,7 +262,7 @@ class TestCleanNotionDocumentTask:
# Test different index types
# Note: Only testing text_model to avoid dependency on external services
index_types = ["text_model"]
index_types = [IndexStructureType.PARAGRAPH_INDEX]
for index_type in index_types:
# Create dataset (doc_form will be set via document creation)

View File

@ -12,6 +12,7 @@ from uuid import uuid4
import pytest
from faker import Faker
from core.rag.index_processor.constant.index_type import IndexStructureType
from extensions.ext_redis import redis_client
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, Document, DocumentSegment
@ -141,7 +142,7 @@ class TestCreateSegmentToIndexTask:
enabled=True,
archived=False,
indexing_status=IndexingStatus.COMPLETED,
doc_form="qa_model",
doc_form=IndexStructureType.QA_INDEX,
)
db_session_with_containers.add(document)
db_session_with_containers.commit()
@ -301,7 +302,7 @@ class TestCreateSegmentToIndexTask:
enabled=True,
archived=False,
indexing_status=IndexingStatus.COMPLETED,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
db_session_with_containers.add(document)
db_session_with_containers.commit()
@ -552,7 +553,11 @@ class TestCreateSegmentToIndexTask:
- Processing completes successfully for different forms
"""
# Arrange: Test different doc_forms
doc_forms = ["qa_model", "text_model", "web_model"]
doc_forms = [
IndexStructureType.QA_INDEX,
IndexStructureType.PARAGRAPH_INDEX,
IndexStructureType.PARAGRAPH_INDEX,
]
for doc_form in doc_forms:
# Create fresh test data for each form

View File

@ -12,6 +12,7 @@ from unittest.mock import ANY, Mock, patch
import pytest
from faker import Faker
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.dataset import Dataset, Document, DocumentSegment
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus
from services.account_service import AccountService, TenantService
@ -107,7 +108,7 @@ class TestDealDatasetVectorIndexTask:
name="Document for doc_form",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -167,7 +168,7 @@ class TestDealDatasetVectorIndexTask:
name="Document for doc_form",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -187,7 +188,7 @@ class TestDealDatasetVectorIndexTask:
name="Test Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -268,7 +269,7 @@ class TestDealDatasetVectorIndexTask:
name="Document for doc_form",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="parent_child_index",
doc_form=IndexStructureType.PARENT_CHILD_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -288,7 +289,7 @@ class TestDealDatasetVectorIndexTask:
name="Test Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="parent_child_index",
doc_form=IndexStructureType.PARENT_CHILD_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -416,7 +417,7 @@ class TestDealDatasetVectorIndexTask:
name="Test Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -505,7 +506,7 @@ class TestDealDatasetVectorIndexTask:
name="Document for doc_form",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -525,7 +526,7 @@ class TestDealDatasetVectorIndexTask:
name="Test Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -601,7 +602,7 @@ class TestDealDatasetVectorIndexTask:
name="Test Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="qa_index",
doc_form=IndexStructureType.QA_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -638,7 +639,7 @@ class TestDealDatasetVectorIndexTask:
assert updated_document.indexing_status == IndexingStatus.COMPLETED
# Verify index processor was initialized with custom index type
mock_index_processor_factory.assert_called_once_with("qa_index")
mock_index_processor_factory.assert_called_once_with(IndexStructureType.QA_INDEX)
mock_factory = mock_index_processor_factory.return_value
mock_processor = mock_factory.init_index_processor.return_value
mock_processor.load.assert_called_once()
@ -677,7 +678,7 @@ class TestDealDatasetVectorIndexTask:
name="Test Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -714,7 +715,7 @@ class TestDealDatasetVectorIndexTask:
assert updated_document.indexing_status == IndexingStatus.COMPLETED
# Verify index processor was initialized with the document's index type
mock_index_processor_factory.assert_called_once_with("text_model")
mock_index_processor_factory.assert_called_once_with(IndexStructureType.PARAGRAPH_INDEX)
mock_factory = mock_index_processor_factory.return_value
mock_processor = mock_factory.init_index_processor.return_value
mock_processor.load.assert_called_once()
@ -753,7 +754,7 @@ class TestDealDatasetVectorIndexTask:
name="Document for doc_form",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -775,7 +776,7 @@ class TestDealDatasetVectorIndexTask:
name=f"Test Document {i}",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -856,7 +857,7 @@ class TestDealDatasetVectorIndexTask:
name="Document for doc_form",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -876,7 +877,7 @@ class TestDealDatasetVectorIndexTask:
name="Test Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -953,7 +954,7 @@ class TestDealDatasetVectorIndexTask:
name="Document for doc_form",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -973,7 +974,7 @@ class TestDealDatasetVectorIndexTask:
name="Enabled Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -992,7 +993,7 @@ class TestDealDatasetVectorIndexTask:
name="Disabled Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=False, # This document should be skipped
@ -1074,7 +1075,7 @@ class TestDealDatasetVectorIndexTask:
name="Document for doc_form",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -1094,7 +1095,7 @@ class TestDealDatasetVectorIndexTask:
name="Active Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -1113,7 +1114,7 @@ class TestDealDatasetVectorIndexTask:
name="Archived Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -1195,7 +1196,7 @@ class TestDealDatasetVectorIndexTask:
name="Document for doc_form",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -1215,7 +1216,7 @@ class TestDealDatasetVectorIndexTask:
name="Completed Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.COMPLETED,
enabled=True,
@ -1234,7 +1235,7 @@ class TestDealDatasetVectorIndexTask:
name="Incomplete Document",
created_from=DocumentCreatedFrom.WEB,
created_by=account.id,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
indexing_status=IndexingStatus.INDEXING, # This document should be skipped
enabled=True,

View File

@ -15,6 +15,7 @@ import pytest
from faker import Faker
from sqlalchemy.orm import Session
from core.rag.index_processor.constant.index_type import IndexStructureType
from extensions.ext_redis import redis_client
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, Document, DocumentSegment
@ -113,7 +114,7 @@ class TestDisableSegmentFromIndexTask:
dataset: Dataset,
tenant: Tenant,
account: Account,
doc_form: str = "text_model",
doc_form: str = IndexStructureType.PARAGRAPH_INDEX,
) -> Document:
"""
Helper method to create a test document.
@ -476,7 +477,11 @@ class TestDisableSegmentFromIndexTask:
- Index processor clean method is called correctly
"""
# Test different document forms
doc_forms = ["text_model", "qa_model", "table_model"]
doc_forms = [
IndexStructureType.PARAGRAPH_INDEX,
IndexStructureType.QA_INDEX,
IndexStructureType.PARENT_CHILD_INDEX,
]
for doc_form in doc_forms:
# Arrange: Create test data for each form

View File

@ -11,6 +11,7 @@ from unittest.mock import MagicMock, patch
from faker import Faker
from sqlalchemy.orm import Session
from core.rag.index_processor.constant.index_type import IndexStructureType
from models import Account, Dataset, DocumentSegment
from models import Document as DatasetDocument
from models.dataset import DatasetProcessRule
@ -153,7 +154,7 @@ class TestDisableSegmentsFromIndexTask:
document.indexing_status = "completed"
document.enabled = True
document.archived = False
document.doc_form = "text_model" # Use text_model form for testing
document.doc_form = IndexStructureType.PARAGRAPH_INDEX # Use text_model form for testing
document.doc_language = "en"
db_session_with_containers.add(document)
db_session_with_containers.commit()
@ -500,7 +501,11 @@ class TestDisableSegmentsFromIndexTask:
segment_ids = [segment.id for segment in segments]
# Test different document forms
doc_forms = ["text_model", "qa_model", "hierarchical_model"]
doc_forms = [
IndexStructureType.PARAGRAPH_INDEX,
IndexStructureType.QA_INDEX,
IndexStructureType.PARENT_CHILD_INDEX,
]
for doc_form in doc_forms:
# Update document form

View File

@ -14,6 +14,7 @@ from uuid import uuid4
import pytest
from core.indexing_runner import DocumentIsPausedError, IndexingRunner
from core.rag.index_processor.constant.index_type import IndexStructureType
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, Document, DocumentSegment
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus
@ -85,7 +86,7 @@ class DocumentIndexingSyncTaskTestDataFactory:
created_by=created_by,
indexing_status=indexing_status,
enabled=True,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
doc_language="en",
)
db_session_with_containers.add(document)

View File

@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch
import pytest
from faker import Faker
from core.rag.index_processor.constant.index_type import IndexStructureType
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, Document, DocumentSegment
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus
@ -80,7 +81,7 @@ class TestDocumentIndexingUpdateTask:
created_by=account.id,
indexing_status=IndexingStatus.WAITING,
enabled=True,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
db_session_with_containers.add(document)
db_session_with_containers.commit()

View File

@ -4,6 +4,7 @@ import pytest
from faker import Faker
from core.indexing_runner import DocumentIsPausedError
from core.rag.index_processor.constant.index_type import IndexStructureType
from enums.cloud_plan import CloudPlan
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.dataset import Dataset, Document, DocumentSegment
@ -130,7 +131,7 @@ class TestDuplicateDocumentIndexingTasks:
created_by=account.id,
indexing_status=IndexingStatus.WAITING,
enabled=True,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
db_session_with_containers.add(document)
documents.append(document)
@ -265,7 +266,7 @@ class TestDuplicateDocumentIndexingTasks:
created_by=account.id,
indexing_status=IndexingStatus.WAITING,
enabled=True,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
db_session_with_containers.add(document)
documents.append(document)
@ -524,7 +525,7 @@ class TestDuplicateDocumentIndexingTasks:
created_by=dataset.created_by,
indexing_status=IndexingStatus.WAITING,
enabled=True,
doc_form="text_model",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
db_session_with_containers.add(document)
extra_documents.append(document)

View File

@ -7,14 +7,19 @@ from __future__ import annotations
import uuid
from types import SimpleNamespace
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import pytest
from pydantic import ValidationError
from werkzeug.exceptions import BadRequest, NotFound
from controllers.console import console_ns
from controllers.console.app import (
annotation as annotation_module,
)
from controllers.console.app import (
app as app_module,
)
from controllers.console.app import (
completion as completion_module,
)
@ -203,6 +208,48 @@ class TestCompletionEndpoints:
method(app_model=MagicMock(id="app-1"))
class TestAppEndpoints:
"""Tests for app endpoints."""
def test_app_put_should_preserve_icon_type_when_payload_omits_it(self, app, monkeypatch):
api = app_module.AppApi()
method = _unwrap(api.put)
payload = {
"name": "Updated App",
"description": "Updated description",
"icon": "🤖",
"icon_background": "#FFFFFF",
}
app_service = MagicMock()
app_service.update_app.return_value = SimpleNamespace()
response_model = MagicMock()
response_model.model_dump.return_value = {"id": "app-1"}
monkeypatch.setattr(app_module, "AppService", lambda: app_service)
monkeypatch.setattr(app_module.AppDetailWithSite, "model_validate", MagicMock(return_value=response_model))
with (
app.test_request_context("/console/api/apps/app-1", method="PUT", json=payload),
patch.object(type(console_ns), "payload", payload),
):
response = method(app_model=SimpleNamespace(icon_type=app_module.IconType.EMOJI))
assert response == {"id": "app-1"}
assert app_service.update_app.call_args.args[1]["icon_type"] is None
def test_update_app_payload_should_reject_empty_icon_type(self):
with pytest.raises(ValidationError):
app_module.UpdateAppPayload.model_validate(
{
"name": "Updated App",
"description": "Updated description",
"icon_type": "",
"icon": "🤖",
"icon_background": "#FFFFFF",
}
)
# ========== OpsTrace Tests ==========
class TestOpsTraceEndpoints:
"""Tests for ops_trace endpoint."""

View File

@ -11,6 +11,7 @@ from controllers.console.datasets.data_source import (
DataSourceNotionDocumentSyncApi,
DataSourceNotionListApi,
)
from core.rag.index_processor.constant.index_type import IndexStructureType
def unwrap(func):
@ -343,7 +344,7 @@ class TestDataSourceNotionApi:
}
],
"process_rule": {"rules": {}},
"doc_form": "text_model",
"doc_form": IndexStructureType.PARAGRAPH_INDEX,
"doc_language": "English",
}

View File

@ -28,6 +28,7 @@ from controllers.console.datasets.datasets import (
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.provider_manager import ProviderManager
from core.rag.index_processor.constant.index_type import IndexStructureType
from extensions.storage.storage_type import StorageType
from models.enums import CreatorUserRole
from models.model import ApiToken, UploadFile
@ -1146,7 +1147,7 @@ class TestDatasetIndexingEstimateApi:
},
"process_rule": {"chunk_size": 100},
"indexing_technique": "high_quality",
"doc_form": "text_model",
"doc_form": IndexStructureType.PARAGRAPH_INDEX,
"doc_language": "English",
"dataset_id": None,
}

View File

@ -30,6 +30,7 @@ from controllers.console.datasets.error import (
InvalidActionError,
InvalidMetadataError,
)
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.enums import DataSourceType, IndexingStatus
@ -66,7 +67,7 @@ def document():
indexing_status=IndexingStatus.INDEXING,
data_source_type=DataSourceType.UPLOAD_FILE,
data_source_info_dict={"upload_file_id": "file-1"},
doc_form="text",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
archived=False,
is_paused=False,
dataset_process_rule=None,
@ -765,8 +766,8 @@ class TestDocumentGenerateSummaryApi:
summary_index_setting={"enable": True},
)
doc1 = MagicMock(id="doc-1", doc_form="qa_model")
doc2 = MagicMock(id="doc-2", doc_form="text")
doc1 = MagicMock(id="doc-1", doc_form=IndexStructureType.QA_INDEX)
doc2 = MagicMock(id="doc-2", doc_form=IndexStructureType.PARAGRAPH_INDEX)
payload = {"document_list": ["doc-1", "doc-2"]}
@ -822,7 +823,7 @@ class TestDocumentIndexingEstimateApi:
data_source_type=DataSourceType.UPLOAD_FILE,
data_source_info_dict={"upload_file_id": "file-1"},
tenant_id="tenant-1",
doc_form="text",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
dataset_process_rule=None,
)
@ -849,7 +850,7 @@ class TestDocumentIndexingEstimateApi:
data_source_type=DataSourceType.UPLOAD_FILE,
data_source_info_dict={"upload_file_id": "file-1"},
tenant_id="tenant-1",
doc_form="text",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
dataset_process_rule=None,
)
@ -973,7 +974,7 @@ class TestDocumentBatchIndexingEstimateApi:
"mode": "single",
"only_main_content": True,
},
doc_form="text",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
with (
@ -1001,7 +1002,7 @@ class TestDocumentBatchIndexingEstimateApi:
"notion_page_id": "p1",
"type": "page",
},
doc_form="text",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
with (
@ -1024,7 +1025,7 @@ class TestDocumentBatchIndexingEstimateApi:
indexing_status=IndexingStatus.INDEXING,
data_source_type="unknown",
data_source_info_dict={},
doc_form="text",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
with app.test_request_context("/"), patch.object(api, "get_batch_documents", return_value=[document]):
@ -1353,7 +1354,7 @@ class TestDocumentIndexingEdgeCases:
data_source_type=DataSourceType.UPLOAD_FILE,
data_source_info_dict={"upload_file_id": "file-1"},
tenant_id="tenant-1",
doc_form="text",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
dataset_process_rule=None,
)

View File

@ -24,6 +24,7 @@ from controllers.console.datasets.error import (
InvalidActionError,
)
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.dataset import ChildChunk, DocumentSegment
from models.model import UploadFile
@ -366,7 +367,7 @@ class TestDatasetDocumentSegmentAddApi:
dataset.indexing_technique = "economy"
document = MagicMock()
document.doc_form = "text"
document.doc_form = IndexStructureType.PARAGRAPH_INDEX
segment = MagicMock()
segment.id = "seg-1"
@ -505,7 +506,7 @@ class TestDatasetDocumentSegmentUpdateApi:
dataset.indexing_technique = "economy"
document = MagicMock()
document.doc_form = "text"
document.doc_form = IndexStructureType.PARAGRAPH_INDEX
segment = MagicMock()

View File

@ -12,6 +12,7 @@ from unittest.mock import Mock
import pytest
from flask import Flask
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.account import TenantStatus
from models.model import App, AppMode, EndUser
from tests.unit_tests.conftest import setup_mock_tenant_account_query
@ -175,7 +176,7 @@ def mock_document():
document.name = "test_document.txt"
document.indexing_status = "completed"
document.enabled = True
document.doc_form = "text_model"
document.doc_form = IndexStructureType.PARAGRAPH_INDEX
return document

View File

@ -31,6 +31,7 @@ from controllers.service_api.dataset.segment import (
SegmentCreatePayload,
SegmentListQuery,
)
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.dataset import ChildChunk, Dataset, Document, DocumentSegment
from models.enums import IndexingStatus
from services.dataset_service import DocumentService, SegmentService
@ -788,7 +789,7 @@ class TestSegmentApiGet:
# Arrange
mock_account_fn.return_value = (Mock(), mock_tenant.id)
mock_db.session.query.return_value.where.return_value.first.return_value = mock_dataset
mock_doc_svc.get_document.return_value = Mock(doc_form="text_model")
mock_doc_svc.get_document.return_value = Mock(doc_form=IndexStructureType.PARAGRAPH_INDEX)
mock_seg_svc.get_segments.return_value = ([mock_segment], 1)
mock_marshal.return_value = [{"id": mock_segment.id}]
@ -903,7 +904,7 @@ class TestSegmentApiPost:
mock_doc = Mock()
mock_doc.indexing_status = "completed"
mock_doc.enabled = True
mock_doc.doc_form = "text_model"
mock_doc.doc_form = IndexStructureType.PARAGRAPH_INDEX
mock_doc_svc.get_document.return_value = mock_doc
mock_seg_svc.segment_create_args_validate.return_value = None
@ -1091,7 +1092,7 @@ class TestDatasetSegmentApiDelete:
mock_doc = Mock()
mock_doc.indexing_status = "completed"
mock_doc.enabled = True
mock_doc.doc_form = "text_model"
mock_doc.doc_form = IndexStructureType.PARAGRAPH_INDEX
mock_doc_svc.get_document.return_value = mock_doc
mock_seg_svc.get_segment_by_id.return_value = None # Segment not found
@ -1371,7 +1372,7 @@ class TestDatasetSegmentApiGetSingle:
mock_account_fn.return_value = (Mock(), mock_tenant.id)
mock_db.session.query.return_value.where.return_value.first.return_value = mock_dataset
mock_dataset_svc.check_dataset_model_setting.return_value = None
mock_doc = Mock(doc_form="text_model")
mock_doc = Mock(doc_form=IndexStructureType.PARAGRAPH_INDEX)
mock_doc_svc.get_document.return_value = mock_doc
mock_seg_svc.get_segment_by_id.return_value = mock_segment
mock_marshal.return_value = {"id": mock_segment.id}
@ -1390,7 +1391,7 @@ class TestDatasetSegmentApiGetSingle:
assert status == 200
assert "data" in response
assert response["doc_form"] == "text_model"
assert response["doc_form"] == IndexStructureType.PARAGRAPH_INDEX
@patch("controllers.service_api.dataset.segment.current_account_with_tenant")
@patch("controllers.service_api.dataset.segment.db")

View File

@ -35,6 +35,7 @@ from controllers.service_api.dataset.document import (
InvalidMetadataError,
)
from controllers.service_api.dataset.error import ArchivedDocumentImmutableError
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.enums import IndexingStatus
from services.dataset_service import DocumentService
from services.entities.knowledge_entities.knowledge_entities import ProcessRule, RetrievalModel
@ -52,7 +53,7 @@ class TestDocumentTextCreatePayload:
def test_payload_with_defaults(self):
"""Test payload default values."""
payload = DocumentTextCreatePayload(name="Doc", text="Content")
assert payload.doc_form == "text_model"
assert payload.doc_form == IndexStructureType.PARAGRAPH_INDEX
assert payload.doc_language == "English"
assert payload.process_rule is None
assert payload.indexing_technique is None
@ -62,14 +63,14 @@ class TestDocumentTextCreatePayload:
payload = DocumentTextCreatePayload(
name="Full Document",
text="Complete document content here",
doc_form="qa_model",
doc_form=IndexStructureType.QA_INDEX,
doc_language="Chinese",
indexing_technique="high_quality",
embedding_model="text-embedding-ada-002",
embedding_model_provider="openai",
)
assert payload.name == "Full Document"
assert payload.doc_form == "qa_model"
assert payload.doc_form == IndexStructureType.QA_INDEX
assert payload.doc_language == "Chinese"
assert payload.indexing_technique == "high_quality"
assert payload.embedding_model == "text-embedding-ada-002"
@ -147,8 +148,8 @@ class TestDocumentTextUpdate:
def test_payload_with_doc_form_update(self):
"""Test payload with doc_form update."""
payload = DocumentTextUpdate(doc_form="qa_model")
assert payload.doc_form == "qa_model"
payload = DocumentTextUpdate(doc_form=IndexStructureType.QA_INDEX)
assert payload.doc_form == IndexStructureType.QA_INDEX
def test_payload_with_language_update(self):
"""Test payload with doc_language update."""
@ -158,7 +159,7 @@ class TestDocumentTextUpdate:
def test_payload_default_values(self):
"""Test payload default values."""
payload = DocumentTextUpdate()
assert payload.doc_form == "text_model"
assert payload.doc_form == IndexStructureType.PARAGRAPH_INDEX
assert payload.doc_language == "English"
@ -272,14 +273,24 @@ class TestDocumentDocForm:
def test_text_model_form(self):
"""Test text_model form."""
doc_form = "text_model"
valid_forms = ["text_model", "qa_model", "hierarchical_model", "parent_child_model"]
doc_form = IndexStructureType.PARAGRAPH_INDEX
valid_forms = [
IndexStructureType.PARAGRAPH_INDEX,
IndexStructureType.QA_INDEX,
IndexStructureType.PARENT_CHILD_INDEX,
"parent_child_model",
]
assert doc_form in valid_forms
def test_qa_model_form(self):
"""Test qa_model form."""
doc_form = "qa_model"
valid_forms = ["text_model", "qa_model", "hierarchical_model", "parent_child_model"]
doc_form = IndexStructureType.QA_INDEX
valid_forms = [
IndexStructureType.PARAGRAPH_INDEX,
IndexStructureType.QA_INDEX,
IndexStructureType.PARENT_CHILD_INDEX,
"parent_child_model",
]
assert doc_form in valid_forms
@ -504,7 +515,7 @@ class TestDocumentApiGet:
doc.name = "test_document.txt"
doc.indexing_status = "completed"
doc.enabled = True
doc.doc_form = "text_model"
doc.doc_form = IndexStructureType.PARAGRAPH_INDEX
doc.doc_language = "English"
doc.doc_type = "book"
doc.doc_metadata_details = {"source": "upload"}

View File

@ -68,8 +68,8 @@ class TestRateLimit:
assert rate_limit.disabled()
assert not hasattr(rate_limit, "initialized")
def test_should_skip_reinitialization_of_existing_instance(self, redis_patch):
"""Test that existing instance doesn't reinitialize."""
def test_should_flush_cache_when_reinitializing_existing_instance(self, redis_patch):
"""Test existing instance refreshes Redis cache on reinitialization."""
redis_patch.configure_mock(
**{
"exists.return_value": False,
@ -82,7 +82,37 @@ class TestRateLimit:
RateLimit("client1", 10)
redis_patch.setex.assert_called_once_with(
"dify:rate_limit:client1:max_active_requests",
timedelta(days=1),
10,
)
def test_should_reinitialize_after_being_disabled(self, redis_patch):
"""Test disabled instance can be reinitialized and writes max_active_requests to Redis."""
redis_patch.configure_mock(
**{
"exists.return_value": False,
"setex.return_value": True,
}
)
# First construct with max_active_requests = 0 (disabled), which should skip initialization.
RateLimit("client1", 0)
# Redis should not have been written to during disabled initialization.
redis_patch.setex.assert_not_called()
redis_patch.reset_mock()
# Reinitialize with a positive max_active_requests value; this should not raise
# and must write the max_active_requests key to Redis.
RateLimit("client1", 10)
redis_patch.setex.assert_called_once_with(
"dify:rate_limit:client1:max_active_requests",
timedelta(days=1),
10,
)
def test_should_be_disabled_when_max_requests_is_zero_or_negative(self):
"""Test disabled state for zero or negative limits."""

View File

@ -4800,8 +4800,8 @@ class TestInternalHooksCoverage:
dataset_docs = [
SimpleNamespace(id="doc-a", doc_form=IndexStructureType.PARENT_CHILD_INDEX),
SimpleNamespace(id="doc-b", doc_form=IndexStructureType.PARENT_CHILD_INDEX),
SimpleNamespace(id="doc-c", doc_form="qa_model"),
SimpleNamespace(id="doc-d", doc_form="qa_model"),
SimpleNamespace(id="doc-c", doc_form=IndexStructureType.QA_INDEX),
SimpleNamespace(id="doc-d", doc_form=IndexStructureType.QA_INDEX),
]
child_chunks = [SimpleNamespace(index_node_id="idx-a", segment_id="seg-a")]
segments = [SimpleNamespace(index_node_id="idx-c", id="seg-c")]

View File

@ -238,7 +238,7 @@ class TestApiToolProviderValidation:
name=provider_name,
icon='{"type": "emoji", "value": "🔧"}',
schema=schema,
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="Custom API for testing",
tools_str=json.dumps(tools),
credentials_str=json.dumps(credentials),
@ -249,7 +249,7 @@ class TestApiToolProviderValidation:
assert api_provider.user_id == user_id
assert api_provider.name == provider_name
assert api_provider.schema == schema
assert api_provider.schema_type_str == "openapi"
assert api_provider.schema_type_str == ApiProviderSchemaType.OPENAPI
assert api_provider.description == "Custom API for testing"
def test_api_tool_provider_schema_type_property(self):
@ -261,7 +261,7 @@ class TestApiToolProviderValidation:
name="Test API",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="Test",
tools_str="[]",
credentials_str="{}",
@ -314,7 +314,7 @@ class TestApiToolProviderValidation:
name="Weather API",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="Weather API",
tools_str=json.dumps(tools_data),
credentials_str="{}",
@ -343,7 +343,7 @@ class TestApiToolProviderValidation:
name="Secure API",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="Secure API",
tools_str="[]",
credentials_str=json.dumps(credentials_data),
@ -369,7 +369,7 @@ class TestApiToolProviderValidation:
name="Privacy API",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="API with privacy policy",
tools_str="[]",
credentials_str="{}",
@ -391,7 +391,7 @@ class TestApiToolProviderValidation:
name="Disclaimer API",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="API with disclaimer",
tools_str="[]",
credentials_str="{}",
@ -410,7 +410,7 @@ class TestApiToolProviderValidation:
name="Default API",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="API",
tools_str="[]",
credentials_str="{}",
@ -432,7 +432,7 @@ class TestApiToolProviderValidation:
name=provider_name,
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="Unique API",
tools_str="[]",
credentials_str="{}",
@ -454,7 +454,7 @@ class TestApiToolProviderValidation:
name="Public API",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="Public API with no auth",
tools_str="[]",
credentials_str=json.dumps(credentials),
@ -479,7 +479,7 @@ class TestApiToolProviderValidation:
name="Query Auth API",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="API with query auth",
tools_str="[]",
credentials_str=json.dumps(credentials),
@ -741,7 +741,7 @@ class TestCredentialStorage:
name="Test API",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="Test",
tools_str="[]",
credentials_str=json.dumps(credentials),
@ -788,7 +788,7 @@ class TestCredentialStorage:
name="Update Test",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="Test",
tools_str="[]",
credentials_str=json.dumps(original_credentials),
@ -897,7 +897,7 @@ class TestToolProviderRelationships:
name="User API",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="Test",
tools_str="[]",
credentials_str="{}",
@ -931,7 +931,7 @@ class TestToolProviderRelationships:
name="Custom API 1",
icon="{}",
schema="{}",
schema_type_str="openapi",
schema_type_str=ApiProviderSchemaType.OPENAPI,
description="Test",
tools_str="[]",
credentials_str="{}",

View File

@ -13,13 +13,13 @@ class ConcreteApiKeyAuth(ApiKeyAuthBase):
class TestApiKeyAuthBase:
def test_should_store_credentials_on_init(self):
"""Test that credentials are properly stored during initialization"""
credentials = {"api_key": "test_key", "auth_type": "bearer"}
credentials = {"auth_type": "bearer", "config": {"api_key": "test_key"}}
auth = ConcreteApiKeyAuth(credentials)
assert auth.credentials == credentials
def test_should_not_instantiate_abstract_class(self):
"""Test that ApiKeyAuthBase cannot be instantiated directly"""
credentials = {"api_key": "test_key"}
credentials = {"auth_type": "bearer", "config": {"api_key": "test_key"}}
with pytest.raises(TypeError) as exc_info:
ApiKeyAuthBase(credentials)
@ -29,7 +29,7 @@ class TestApiKeyAuthBase:
def test_should_allow_subclass_implementation(self):
"""Test that subclasses can properly implement the abstract method"""
credentials = {"api_key": "test_key", "auth_type": "bearer"}
credentials = {"auth_type": "bearer", "config": {"api_key": "test_key"}}
auth = ConcreteApiKeyAuth(credentials)
# Should not raise any exception

View File

@ -58,7 +58,7 @@ class TestApiKeyAuthFactory:
mock_get_factory.return_value = mock_auth_class
# Act
factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, {"api_key": "test_key"})
factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, {"auth_type": "bearer", "config": {"api_key": "test_key"}})
result = factory.validate_credentials()
# Assert
@ -75,7 +75,7 @@ class TestApiKeyAuthFactory:
mock_get_factory.return_value = mock_auth_class
# Act & Assert
factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, {"api_key": "test_key"})
factory = ApiKeyAuthFactory(AuthType.FIRECRAWL, {"auth_type": "bearer", "config": {"api_key": "test_key"}})
with pytest.raises(Exception) as exc_info:
factory.validate_credentials()
assert str(exc_info.value) == "Authentication error"

View File

@ -111,6 +111,7 @@ from unittest.mock import Mock, patch
import pytest
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.rag.index_processor.constant.index_type import IndexStructureType
from dify_graph.model_runtime.entities.model_entities import ModelType
from models.dataset import Dataset, DatasetProcessRule, Document
from services.dataset_service import DatasetService, DocumentService
@ -188,7 +189,7 @@ class DocumentValidationTestDataFactory:
def create_knowledge_config_mock(
data_source: DataSource | None = None,
process_rule: ProcessRule | None = None,
doc_form: str = "text_model",
doc_form: str = IndexStructureType.PARAGRAPH_INDEX,
indexing_technique: str = "high_quality",
**kwargs,
) -> Mock:
@ -326,8 +327,8 @@ class TestDatasetServiceCheckDocForm:
- Validation logic works correctly
"""
# Arrange
dataset = DocumentValidationTestDataFactory.create_dataset_mock(doc_form="text_model")
doc_form = "text_model"
dataset = DocumentValidationTestDataFactory.create_dataset_mock(doc_form=IndexStructureType.PARAGRAPH_INDEX)
doc_form = IndexStructureType.PARAGRAPH_INDEX
# Act (should not raise)
DatasetService.check_doc_form(dataset, doc_form)
@ -349,7 +350,7 @@ class TestDatasetServiceCheckDocForm:
"""
# Arrange
dataset = DocumentValidationTestDataFactory.create_dataset_mock(doc_form=None)
doc_form = "text_model"
doc_form = IndexStructureType.PARAGRAPH_INDEX
# Act (should not raise)
DatasetService.check_doc_form(dataset, doc_form)
@ -370,8 +371,8 @@ class TestDatasetServiceCheckDocForm:
- Error type is correct
"""
# Arrange
dataset = DocumentValidationTestDataFactory.create_dataset_mock(doc_form="text_model")
doc_form = "table_model" # Different form
dataset = DocumentValidationTestDataFactory.create_dataset_mock(doc_form=IndexStructureType.PARAGRAPH_INDEX)
doc_form = IndexStructureType.PARENT_CHILD_INDEX # Different form
# Act & Assert
with pytest.raises(ValueError, match="doc_form is different from the dataset doc_form"):
@ -390,7 +391,7 @@ class TestDatasetServiceCheckDocForm:
"""
# Arrange
dataset = DocumentValidationTestDataFactory.create_dataset_mock(doc_form="knowledge_card")
doc_form = "text_model" # Different form
doc_form = IndexStructureType.PARAGRAPH_INDEX # Different form
# Act & Assert
with pytest.raises(ValueError, match="doc_form is different from the dataset doc_form"):

View File

@ -2,6 +2,7 @@ from unittest.mock import MagicMock, Mock, patch
import pytest
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.account import Account
from models.dataset import ChildChunk, Dataset, Document, DocumentSegment
from models.enums import SegmentType
@ -91,7 +92,7 @@ class SegmentTestDataFactory:
document_id: str = "doc-123",
dataset_id: str = "dataset-123",
tenant_id: str = "tenant-123",
doc_form: str = "text_model",
doc_form: str = IndexStructureType.PARAGRAPH_INDEX,
word_count: int = 100,
**kwargs,
) -> Mock:
@ -210,7 +211,7 @@ class TestSegmentServiceCreateSegment:
def test_create_segment_with_qa_model(self, mock_db_session, mock_current_user):
"""Test creation of segment with QA model (requires answer)."""
# Arrange
document = SegmentTestDataFactory.create_document_mock(doc_form="qa_model", word_count=100)
document = SegmentTestDataFactory.create_document_mock(doc_form=IndexStructureType.QA_INDEX, word_count=100)
dataset = SegmentTestDataFactory.create_dataset_mock(indexing_technique="economy")
args = {"content": "What is AI?", "answer": "AI is Artificial Intelligence", "keywords": ["ai"]}
@ -429,7 +430,7 @@ class TestSegmentServiceUpdateSegment:
"""Test update segment with QA model (includes answer)."""
# Arrange
segment = SegmentTestDataFactory.create_segment_mock(enabled=True, word_count=10)
document = SegmentTestDataFactory.create_document_mock(doc_form="qa_model", word_count=100)
document = SegmentTestDataFactory.create_document_mock(doc_form=IndexStructureType.QA_INDEX, word_count=100)
dataset = SegmentTestDataFactory.create_dataset_mock(indexing_technique="economy")
args = SegmentUpdateArgs(content="Updated question", answer="Updated answer", keywords=["qa"])

View File

@ -9,7 +9,7 @@ import pytest
from core.errors.error import ProviderTokenNotInitError
from models import Account, Tenant
from models.model import App, AppMode
from models.model import App, AppMode, IconType
from services.app_service import AppService
@ -411,6 +411,7 @@ class TestAppServiceGetAndUpdate:
# Assert
assert updated is app
assert updated.icon_type == IconType.IMAGE
assert renamed is app
assert iconed is app
assert site_same is app
@ -419,6 +420,79 @@ class TestAppServiceGetAndUpdate:
assert api_changed is app
assert mock_db.session.commit.call_count >= 5
def test_update_app_should_preserve_icon_type_when_not_provided(self, service: AppService) -> None:
"""Test update_app keeps the existing icon_type when the payload omits it."""
# Arrange
app = cast(
App,
SimpleNamespace(
name="old",
description="old",
icon_type=IconType.EMOJI,
icon="a",
icon_background="#111",
use_icon_as_answer_icon=False,
max_active_requests=1,
),
)
args = {
"name": "new",
"description": "new-desc",
"icon_type": None,
"icon": "new-icon",
"icon_background": "#222",
"use_icon_as_answer_icon": True,
"max_active_requests": 5,
}
user = SimpleNamespace(id="user-1")
with (
patch("services.app_service.current_user", user),
patch("services.app_service.db") as mock_db,
patch("services.app_service.naive_utc_now", return_value="now"),
):
# Act
updated = service.update_app(app, args)
# Assert
assert updated is app
assert updated.icon_type == IconType.EMOJI
mock_db.session.commit.assert_called_once()
def test_update_app_should_reject_empty_icon_type(self, service: AppService) -> None:
"""Test update_app rejects an explicit empty icon_type."""
app = cast(
App,
SimpleNamespace(
name="old",
description="old",
icon_type=IconType.EMOJI,
icon="a",
icon_background="#111",
use_icon_as_answer_icon=False,
max_active_requests=1,
),
)
args = {
"name": "new",
"description": "new-desc",
"icon_type": "",
"icon": "new-icon",
"icon_background": "#222",
"use_icon_as_answer_icon": True,
"max_active_requests": 5,
}
user = SimpleNamespace(id="user-1")
with (
patch("services.app_service.current_user", user),
patch("services.app_service.db") as mock_db,
):
with pytest.raises(ValueError):
service.update_app(app, args)
mock_db.session.commit.assert_not_called()
class TestAppServiceDeleteAndMeta:
"""Test suite for delete and metadata methods."""

View File

@ -4,6 +4,7 @@ from unittest.mock import Mock, create_autospec
import pytest
from redis.exceptions import LockNotOwnedError
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.account import Account
from models.dataset import Dataset, Document
from services.dataset_service import DocumentService, SegmentService
@ -76,7 +77,7 @@ def test_save_document_with_dataset_id_ignores_lock_not_owned(
info_list = types.SimpleNamespace(data_source_type="upload_file")
data_source = types.SimpleNamespace(info_list=info_list)
knowledge_config = types.SimpleNamespace(
doc_form="qa_model",
doc_form=IndexStructureType.QA_INDEX,
original_document_id=None, # go into "new document" branch
data_source=data_source,
indexing_technique="high_quality",
@ -131,7 +132,7 @@ def test_add_segment_ignores_lock_not_owned(
document.id = "doc-1"
document.dataset_id = dataset.id
document.word_count = 0
document.doc_form = "qa_model"
document.doc_form = IndexStructureType.QA_INDEX
# Minimal args required by add_segment
args = {
@ -174,4 +175,4 @@ def test_multi_create_segment_ignores_lock_not_owned(
document.id = "doc-1"
document.dataset_id = dataset.id
document.word_count = 0
document.doc_form = "qa_model"
document.doc_form = IndexStructureType.QA_INDEX

View File

@ -11,6 +11,7 @@ from unittest.mock import MagicMock
import pytest
import services.summary_index_service as summary_module
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.enums import SegmentStatus, SummaryStatus
from services.summary_index_service import SummaryIndexService
@ -48,7 +49,7 @@ def _segment(*, has_document: bool = True) -> MagicMock:
if has_document:
doc = MagicMock(name="document")
doc.doc_language = "en"
doc.doc_form = "text_model"
doc.doc_form = IndexStructureType.PARAGRAPH_INDEX
segment.document = doc
else:
segment.document = None
@ -623,13 +624,13 @@ def test_generate_summaries_for_document_skip_conditions(monkeypatch: pytest.Mon
dataset = _dataset(indexing_technique="economy")
document = MagicMock(spec=summary_module.DatasetDocument)
document.id = "doc-1"
document.doc_form = "text_model"
document.doc_form = IndexStructureType.PARAGRAPH_INDEX
assert SummaryIndexService.generate_summaries_for_document(dataset, document, {"enable": True}) == []
dataset = _dataset()
assert SummaryIndexService.generate_summaries_for_document(dataset, document, {"enable": False}) == []
document.doc_form = "qa_model"
document.doc_form = IndexStructureType.QA_INDEX
assert SummaryIndexService.generate_summaries_for_document(dataset, document, {"enable": True}) == []
@ -637,7 +638,7 @@ def test_generate_summaries_for_document_runs_and_handles_errors(monkeypatch: py
dataset = _dataset()
document = MagicMock(spec=summary_module.DatasetDocument)
document.id = "doc-1"
document.doc_form = "text_model"
document.doc_form = IndexStructureType.PARAGRAPH_INDEX
seg1 = _segment()
seg2 = _segment()
@ -673,7 +674,7 @@ def test_generate_summaries_for_document_no_segments_returns_empty(monkeypatch:
dataset = _dataset()
document = MagicMock(spec=summary_module.DatasetDocument)
document.id = "doc-1"
document.doc_form = "text_model"
document.doc_form = IndexStructureType.PARAGRAPH_INDEX
session = MagicMock()
query = MagicMock()
@ -696,7 +697,7 @@ def test_generate_summaries_for_document_applies_segment_ids_and_only_parent_chu
dataset = _dataset()
document = MagicMock(spec=summary_module.DatasetDocument)
document.id = "doc-1"
document.doc_form = "text_model"
document.doc_form = IndexStructureType.PARAGRAPH_INDEX
seg = _segment()
session = MagicMock()
@ -935,7 +936,7 @@ def test_update_summary_for_segment_skip_conditions() -> None:
SummaryIndexService.update_summary_for_segment(_segment(), _dataset(indexing_technique="economy"), "x") is None
)
seg = _segment(has_document=True)
seg.document.doc_form = "qa_model"
seg.document.doc_form = IndexStructureType.QA_INDEX
assert SummaryIndexService.update_summary_for_segment(seg, _dataset(), "x") is None

View File

@ -9,6 +9,7 @@ from unittest.mock import MagicMock
import pytest
import services.vector_service as vector_service_module
from core.rag.index_processor.constant.index_type import IndexStructureType
from services.vector_service import VectorService
@ -32,7 +33,7 @@ class _ParentDocStub:
def _make_dataset(
*,
indexing_technique: str = "high_quality",
doc_form: str = "text_model",
doc_form: str = IndexStructureType.PARAGRAPH_INDEX,
tenant_id: str = "tenant-1",
dataset_id: str = "dataset-1",
is_multimodal: bool = False,
@ -106,7 +107,7 @@ def test_create_segments_vector_regular_indexing_loads_documents_and_keywords(mo
factory_instance.init_index_processor.return_value = index_processor
monkeypatch.setattr(vector_service_module, "IndexProcessorFactory", MagicMock(return_value=factory_instance))
VectorService.create_segments_vector([["k1"]], [segment], dataset, "text_model")
VectorService.create_segments_vector([["k1"]], [segment], dataset, IndexStructureType.PARAGRAPH_INDEX)
index_processor.load.assert_called_once()
args, kwargs = index_processor.load.call_args
@ -131,7 +132,7 @@ def test_create_segments_vector_regular_indexing_loads_multimodal_documents(monk
factory_instance.init_index_processor.return_value = index_processor
monkeypatch.setattr(vector_service_module, "IndexProcessorFactory", MagicMock(return_value=factory_instance))
VectorService.create_segments_vector([["k1"]], [segment], dataset, "text_model")
VectorService.create_segments_vector([["k1"]], [segment], dataset, IndexStructureType.PARAGRAPH_INDEX)
assert index_processor.load.call_count == 2
first_args, first_kwargs = index_processor.load.call_args_list[0]
@ -153,7 +154,7 @@ def test_create_segments_vector_with_no_segments_does_not_load(monkeypatch: pyte
factory_instance.init_index_processor.return_value = index_processor
monkeypatch.setattr(vector_service_module, "IndexProcessorFactory", MagicMock(return_value=factory_instance))
VectorService.create_segments_vector(None, [], dataset, "text_model")
VectorService.create_segments_vector(None, [], dataset, IndexStructureType.PARAGRAPH_INDEX)
index_processor.load.assert_not_called()
@ -392,7 +393,7 @@ def test_update_segment_vector_economy_uses_keyword_without_keywords_list(monkey
def test_generate_child_chunks_regenerate_cleans_then_saves_children(monkeypatch: pytest.MonkeyPatch) -> None:
dataset = _make_dataset(doc_form="text_model", tenant_id="tenant-1", dataset_id="dataset-1")
dataset = _make_dataset(doc_form=IndexStructureType.PARAGRAPH_INDEX, tenant_id="tenant-1", dataset_id="dataset-1")
segment = _make_segment(segment_id="seg-1")
dataset_document = MagicMock()
@ -439,7 +440,7 @@ def test_generate_child_chunks_regenerate_cleans_then_saves_children(monkeypatch
def test_generate_child_chunks_commits_even_when_no_children(monkeypatch: pytest.MonkeyPatch) -> None:
dataset = _make_dataset(doc_form="text_model")
dataset = _make_dataset(doc_form=IndexStructureType.PARAGRAPH_INDEX)
segment = _make_segment()
dataset_document = MagicMock()
dataset_document.doc_language = "en"

View File

@ -121,6 +121,7 @@ import pytest
from core.rag.datasource.vdb.vector_base import BaseVector
from core.rag.datasource.vdb.vector_factory import Vector
from core.rag.datasource.vdb.vector_type import VectorType
from core.rag.index_processor.constant.index_type import IndexStructureType
from core.rag.models.document import Document
from models.dataset import ChildChunk, Dataset, DatasetDocument, DatasetProcessRule, DocumentSegment
from services.vector_service import VectorService
@ -151,7 +152,7 @@ class VectorServiceTestDataFactory:
def create_dataset_mock(
dataset_id: str = "dataset-123",
tenant_id: str = "tenant-123",
doc_form: str = "text_model",
doc_form: str = IndexStructureType.PARAGRAPH_INDEX,
indexing_technique: str = "high_quality",
embedding_model_provider: str = "openai",
embedding_model: str = "text-embedding-ada-002",
@ -493,7 +494,7 @@ class TestVectorService:
"""
# Arrange
dataset = VectorServiceTestDataFactory.create_dataset_mock(
doc_form="text_model", indexing_technique="high_quality"
doc_form=IndexStructureType.PARAGRAPH_INDEX, indexing_technique="high_quality"
)
segment = VectorServiceTestDataFactory.create_document_segment_mock()
@ -505,7 +506,7 @@ class TestVectorService:
mock_index_processor_factory.return_value.init_index_processor.return_value = mock_index_processor
# Act
VectorService.create_segments_vector(keywords_list, [segment], dataset, "text_model")
VectorService.create_segments_vector(keywords_list, [segment], dataset, IndexStructureType.PARAGRAPH_INDEX)
# Assert
mock_index_processor.load.assert_called_once()
@ -649,7 +650,7 @@ class TestVectorService:
mock_index_processor_factory.return_value.init_index_processor.return_value = mock_index_processor
# Act
VectorService.create_segments_vector(None, [], dataset, "text_model")
VectorService.create_segments_vector(None, [], dataset, IndexStructureType.PARAGRAPH_INDEX)
# Assert
mock_index_processor.load.assert_not_called()

View File

@ -16,6 +16,7 @@ from unittest.mock import MagicMock, patch
import pytest
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.enums import DataSourceType
from tasks.clean_dataset_task import clean_dataset_task
@ -186,7 +187,7 @@ class TestErrorHandling:
indexing_technique="high_quality",
index_struct='{"type": "paragraph"}',
collection_binding_id=collection_binding_id,
doc_form="paragraph_index",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
# Assert
@ -231,7 +232,7 @@ class TestPipelineAndWorkflowDeletion:
indexing_technique="high_quality",
index_struct='{"type": "paragraph"}',
collection_binding_id=collection_binding_id,
doc_form="paragraph_index",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
pipeline_id=pipeline_id,
)
@ -267,7 +268,7 @@ class TestPipelineAndWorkflowDeletion:
indexing_technique="high_quality",
index_struct='{"type": "paragraph"}',
collection_binding_id=collection_binding_id,
doc_form="paragraph_index",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
pipeline_id=None,
)
@ -323,7 +324,7 @@ class TestSegmentAttachmentCleanup:
indexing_technique="high_quality",
index_struct='{"type": "paragraph"}',
collection_binding_id=collection_binding_id,
doc_form="paragraph_index",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
# Assert
@ -368,7 +369,7 @@ class TestSegmentAttachmentCleanup:
indexing_technique="high_quality",
index_struct='{"type": "paragraph"}',
collection_binding_id=collection_binding_id,
doc_form="paragraph_index",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
# Assert - storage delete was attempted
@ -410,7 +411,7 @@ class TestEdgeCases:
indexing_technique="high_quality",
index_struct='{"type": "paragraph"}',
collection_binding_id=collection_binding_id,
doc_form="paragraph_index",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
# Assert
@ -454,7 +455,7 @@ class TestIndexProcessorParameters:
indexing_technique=indexing_technique,
index_struct=index_struct,
collection_binding_id=collection_binding_id,
doc_form="paragraph_index",
doc_form=IndexStructureType.PARAGRAPH_INDEX,
)
# Assert

View File

@ -15,6 +15,7 @@ from unittest.mock import MagicMock, Mock, patch
import pytest
from core.indexing_runner import DocumentIsPausedError
from core.rag.index_processor.constant.index_type import IndexStructureType
from core.rag.pipeline.queue import TenantIsolatedTaskQueue
from enums.cloud_plan import CloudPlan
from extensions.ext_redis import redis_client
@ -222,7 +223,7 @@ def mock_documents(document_ids, dataset_id):
doc.stopped_at = None
doc.processing_started_at = None
# optional attribute used in some code paths
doc.doc_form = "text_model"
doc.doc_form = IndexStructureType.PARAGRAPH_INDEX
documents.append(doc)
return documents

View File

@ -11,6 +11,7 @@ from unittest.mock import MagicMock, Mock, patch
import pytest
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.dataset import Dataset, Document
from tasks.document_indexing_sync_task import document_indexing_sync_task
@ -62,7 +63,7 @@ def mock_document(document_id, dataset_id, notion_workspace_id, notion_page_id,
document.tenant_id = str(uuid.uuid4())
document.data_source_type = "notion_import"
document.indexing_status = "completed"
document.doc_form = "text_model"
document.doc_form = IndexStructureType.PARAGRAPH_INDEX
document.data_source_info_dict = {
"notion_workspace_id": notion_workspace_id,
"notion_page_id": notion_page_id,

View File

@ -69,6 +69,7 @@
},
"pnpm": {
"overrides": {
"flatted@<=3.4.1": "3.4.2",
"rollup@>=4.0.0,<4.59.0": "4.59.0"
}
}

View File

@ -5,6 +5,7 @@ settings:
excludeLinksFromLockfile: false
overrides:
flatted@<=3.4.1: 3.4.2
rollup@>=4.0.0,<4.59.0: 4.59.0
importers:
@ -324,66 +325,79 @@ packages:
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.59.0':
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.59.0':
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.59.0':
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.59.0':
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.59.0':
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.59.0':
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.59.0':
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.59.0':
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.59.0':
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.59.0':
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
@ -741,8 +755,8 @@ packages:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
flatted@3.4.1:
resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==}
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
@ -1836,10 +1850,10 @@ snapshots:
flat-cache@4.0.1:
dependencies:
flatted: 3.4.1
flatted: 3.4.2
keyv: 4.5.4
flatted@3.4.1: {}
flatted@3.4.2: {}
follow-redirects@1.15.11: {}

View File

@ -12,8 +12,16 @@ vi.mock('@/config', () => ({
}))
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({
default: { notify: (...args: unknown[]) => mockToastNotify(...args) },
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign((message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), {
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
}))
const mockUploadGitHub = vi.fn()

View File

@ -12,15 +12,15 @@ import { DSLImportMode } from '@/models/app'
import dynamic from '@/next/dynamic'
import { useRouter, useSearchParams } from '@/next/navigation'
import { fetchAppDetail } from '@/service/explore'
import DSLConfirmModal from '../app/create-from-dsl-modal/dsl-confirm-modal'
import CreateAppModal from '../explore/create-app-modal'
import TryApp from '../explore/try-app'
import List from './list'
const ImportFromMarketplaceTemplateModal = dynamic(
() => import('./import-from-marketplace-template-modal'),
{ ssr: false },
)
const DSLConfirmModal = dynamic(() => import('../app/create-from-dsl-modal/dsl-confirm-modal'), { ssr: false })
const CreateAppModal = dynamic(() => import('../explore/create-app-modal'), { ssr: false })
const TryApp = dynamic(() => import('../explore/try-app'), { ssr: false })
const Apps = () => {
const { t } = useTranslation()

View File

@ -7,13 +7,12 @@ import { parseAsStringLiteral, useQueryState } from 'nuqs'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Checkbox from '@/app/components/base/checkbox'
import Input from '@/app/components/base/input'
import TabSliderNew from '@/app/components/base/tab-slider-new'
import TagFilter from '@/app/components/base/tag-management/filter'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import Tooltip from '@/app/components/base/tooltip-plus'
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
@ -247,12 +246,12 @@ const List: FC<Props> = ({
options={options}
/>
<div className="flex items-center gap-2">
<CheckboxWithLabel
className="mr-2"
label={t('showMyCreatedAppsOnly', { ns: 'app' })}
isChecked={isCreatedByMe}
onChange={handleCreatedByMeChange}
/>
<label className="mr-2 flex h-7 items-center space-x-2">
<Checkbox checked={isCreatedByMe} onCheck={handleCreatedByMeChange} />
<div className="text-sm font-normal text-text-secondary">
{t('showMyCreatedAppsOnly', { ns: 'app' })}
</div>
</label>
<TagFilter type="app" value={tagFilterValue} onChange={handleTagsChange} />
<Input
showLeftIcon

View File

@ -5,17 +5,12 @@ import * as amplitude from '@amplitude/analytics-browser'
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
import * as React from 'react'
import { useEffect } from 'react'
import { AMPLITUDE_API_KEY, IS_CLOUD_EDITION } from '@/config'
import { AMPLITUDE_API_KEY, isAmplitudeEnabled } from '@/config'
export type IAmplitudeProps = {
sessionReplaySampleRate?: number
}
// Check if Amplitude should be enabled
export const isAmplitudeEnabled = () => {
return IS_CLOUD_EDITION && !!AMPLITUDE_API_KEY
}
// Map URL pathname to English page name for consistent Amplitude tracking
const getEnglishPageName = (pathname: string): string => {
// Remove leading slash and get the first segment
@ -59,7 +54,7 @@ const AmplitudeProvider: FC<IAmplitudeProps> = ({
}) => {
useEffect(() => {
// Only enable in Saas edition with valid API key
if (!isAmplitudeEnabled())
if (!isAmplitudeEnabled)
return
// Initialize Amplitude

View File

@ -2,14 +2,24 @@ import * as amplitude from '@amplitude/analytics-browser'
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
import { render } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import AmplitudeProvider, { isAmplitudeEnabled } from '../AmplitudeProvider'
import AmplitudeProvider from '../AmplitudeProvider'
const mockConfig = vi.hoisted(() => ({
AMPLITUDE_API_KEY: 'test-api-key',
IS_CLOUD_EDITION: true,
}))
vi.mock('@/config', () => mockConfig)
vi.mock('@/config', () => ({
get AMPLITUDE_API_KEY() {
return mockConfig.AMPLITUDE_API_KEY
},
get IS_CLOUD_EDITION() {
return mockConfig.IS_CLOUD_EDITION
},
get isAmplitudeEnabled() {
return mockConfig.IS_CLOUD_EDITION && !!mockConfig.AMPLITUDE_API_KEY
},
}))
vi.mock('@amplitude/analytics-browser', () => ({
init: vi.fn(),
@ -27,22 +37,6 @@ describe('AmplitudeProvider', () => {
mockConfig.IS_CLOUD_EDITION = true
})
describe('isAmplitudeEnabled', () => {
it('returns true when cloud edition and api key present', () => {
expect(isAmplitudeEnabled()).toBe(true)
})
it('returns false when cloud edition but no api key', () => {
mockConfig.AMPLITUDE_API_KEY = ''
expect(isAmplitudeEnabled()).toBe(false)
})
it('returns false when not cloud edition', () => {
mockConfig.IS_CLOUD_EDITION = false
expect(isAmplitudeEnabled()).toBe(false)
})
})
describe('Component', () => {
it('initializes amplitude when enabled', () => {
render(<AmplitudeProvider sessionReplaySampleRate={0.8} />)

View File

@ -1,32 +0,0 @@
import { describe, expect, it } from 'vitest'
import AmplitudeProvider, { isAmplitudeEnabled } from '../AmplitudeProvider'
import indexDefault, {
isAmplitudeEnabled as indexIsAmplitudeEnabled,
resetUser,
setUserId,
setUserProperties,
trackEvent,
} from '../index'
import {
resetUser as utilsResetUser,
setUserId as utilsSetUserId,
setUserProperties as utilsSetUserProperties,
trackEvent as utilsTrackEvent,
} from '../utils'
describe('Amplitude index exports', () => {
it('exports AmplitudeProvider as default', () => {
expect(indexDefault).toBe(AmplitudeProvider)
})
it('exports isAmplitudeEnabled', () => {
expect(indexIsAmplitudeEnabled).toBe(isAmplitudeEnabled)
})
it('exports utils', () => {
expect(resetUser).toBe(utilsResetUser)
expect(setUserId).toBe(utilsSetUserId)
expect(setUserProperties).toBe(utilsSetUserProperties)
expect(trackEvent).toBe(utilsTrackEvent)
})
})

View File

@ -20,8 +20,10 @@ const MockIdentify = vi.hoisted(() =>
},
)
vi.mock('../AmplitudeProvider', () => ({
isAmplitudeEnabled: () => mockState.enabled,
vi.mock('@/config', () => ({
get isAmplitudeEnabled() {
return mockState.enabled
},
}))
vi.mock('@amplitude/analytics-browser', () => ({

View File

@ -1,2 +1,2 @@
export { default, isAmplitudeEnabled } from './AmplitudeProvider'
export { default } from './lazy-amplitude-provider'
export { resetUser, setUserId, setUserProperties, trackEvent } from './utils'

View File

@ -0,0 +1,11 @@
'use client'
import type { FC } from 'react'
import type { IAmplitudeProps } from './AmplitudeProvider'
import dynamic from '@/next/dynamic'
const AmplitudeProvider = dynamic(() => import('./AmplitudeProvider'), { ssr: false })
const LazyAmplitudeProvider: FC<IAmplitudeProps> = props => <AmplitudeProvider {...props} />
export default LazyAmplitudeProvider

View File

@ -1,5 +1,5 @@
import * as amplitude from '@amplitude/analytics-browser'
import { isAmplitudeEnabled } from './AmplitudeProvider'
import { isAmplitudeEnabled } from '@/config'
/**
* Track custom event
@ -7,7 +7,7 @@ import { isAmplitudeEnabled } from './AmplitudeProvider'
* @param eventProperties Event properties (optional)
*/
export const trackEvent = (eventName: string, eventProperties?: Record<string, any>) => {
if (!isAmplitudeEnabled())
if (!isAmplitudeEnabled)
return
amplitude.track(eventName, eventProperties)
}
@ -17,7 +17,7 @@ export const trackEvent = (eventName: string, eventProperties?: Record<string, a
* @param userId User ID
*/
export const setUserId = (userId: string) => {
if (!isAmplitudeEnabled())
if (!isAmplitudeEnabled)
return
amplitude.setUserId(userId)
}
@ -27,7 +27,7 @@ export const setUserId = (userId: string) => {
* @param properties User properties
*/
export const setUserProperties = (properties: Record<string, any>) => {
if (!isAmplitudeEnabled())
if (!isAmplitudeEnabled)
return
const identifyEvent = new amplitude.Identify()
Object.entries(properties).forEach(([key, value]) => {
@ -40,7 +40,7 @@ export const setUserProperties = (properties: Record<string, any>) => {
* Reset user (e.g., when user logs out)
*/
export const resetUser = () => {
if (!isAmplitudeEnabled())
if (!isAmplitudeEnabled)
return
amplitude.reset()
}

View File

@ -224,6 +224,20 @@ describe('DocumentSettings', () => {
// Data source types
describe('Data Source Types', () => {
it('should handle upload_file_id data source format', () => {
mockDocumentDetail = {
name: 'test-document',
data_source_type: 'upload_file',
data_source_info: {
upload_file_id: '4a807f05-45d6-4fc4-b7a8-b009a4568b36',
},
}
render(<DocumentSettings {...defaultProps} />)
expect(screen.getByTestId('files-count')).toHaveTextContent('1')
})
it('should handle legacy upload_file data source', () => {
mockDocumentDetail = {
name: 'test-document',
@ -307,6 +321,18 @@ describe('DocumentSettings', () => {
expect(screen.getByTestId('files-count')).toHaveTextContent('0')
})
it('should handle empty data_source_info object', () => {
mockDocumentDetail = {
name: 'test-document',
data_source_type: 'upload_file',
data_source_info: {},
}
render(<DocumentSettings {...defaultProps} />)
expect(screen.getByTestId('files-count')).toHaveTextContent('0')
})
it('should maintain structure when rerendered', () => {
const { rerender } = render(
<DocumentSettings datasetId="dataset-1" documentId="doc-1" />,
@ -317,4 +343,37 @@ describe('DocumentSettings', () => {
expect(screen.getByTestId('step-two')).toBeInTheDocument()
})
})
describe('Files Extraction Regression Tests', () => {
it('should correctly extract file ID from upload_file_id format', () => {
const fileId = '4a807f05-45d6-4fc4-b7a8-b009a4568b36'
mockDocumentDetail = {
name: 'test-document.pdf',
data_source_type: 'upload_file',
data_source_info: {
upload_file_id: fileId,
},
}
render(<DocumentSettings {...defaultProps} />)
// Verify files array is populated with correct file ID
expect(screen.getByTestId('files-count')).toHaveTextContent('1')
})
it('should preserve document name when using upload_file_id format', () => {
const documentName = 'my-uploaded-document.txt'
mockDocumentDetail = {
name: documentName,
data_source_type: 'upload_file',
data_source_info: {
upload_file_id: 'some-file-id',
},
}
render(<DocumentSettings {...defaultProps} />)
expect(screen.getByTestId('files-count')).toHaveTextContent('1')
})
})
})

View File

@ -8,6 +8,7 @@ import type {
LegacyDataSourceInfo,
LocalFileInfo,
OnlineDocumentInfo,
UploadFileIdInfo,
WebsiteCrawlInfo,
} from '@/models/datasets'
import { useBoolean } from 'ahooks'
@ -61,6 +62,7 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
const dataSourceInfo = documentDetail?.data_source_info
// Type guards for DataSourceInfo union
const isLegacyDataSourceInfo = (info: DataSourceInfo | undefined): info is LegacyDataSourceInfo => {
return !!info && 'upload_file' in info
}
@ -73,10 +75,15 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
const isLocalFileInfo = (info: DataSourceInfo | undefined): info is LocalFileInfo => {
return !!info && 'related_id' in info && 'transfer_method' in info
}
const isUploadFileIdInfo = (info: DataSourceInfo | undefined): info is UploadFileIdInfo => {
return !!info && 'upload_file_id' in info
}
const legacyInfo = isLegacyDataSourceInfo(dataSourceInfo) ? dataSourceInfo : undefined
const websiteInfo = isWebsiteCrawlInfo(dataSourceInfo) ? dataSourceInfo : undefined
const onlineDocumentInfo = isOnlineDocumentInfo(dataSourceInfo) ? dataSourceInfo : undefined
const localFileInfo = isLocalFileInfo(dataSourceInfo) ? dataSourceInfo : undefined
const uploadFileIdInfo = isUploadFileIdInfo(dataSourceInfo) ? dataSourceInfo : undefined
const currentPage = useMemo(() => {
if (legacyInfo) {
@ -101,8 +108,20 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
}, [documentDetail?.data_source_type, documentDetail?.name, legacyInfo, onlineDocumentInfo])
const files = useMemo<CustomFile[]>(() => {
if (legacyInfo?.upload_file)
return [legacyInfo.upload_file as CustomFile]
// Handle upload_file_id format
if (uploadFileIdInfo) {
return [{
id: uploadFileIdInfo.upload_file_id,
name: documentDetail?.name || '',
} as unknown as CustomFile]
}
// Handle legacy upload_file format
if (legacyInfo?.upload_file) {
return [legacyInfo.upload_file as unknown as CustomFile]
}
// Handle local file info format
if (localFileInfo) {
const { related_id, name, extension } = localFileInfo
return [{
@ -111,8 +130,9 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
extension,
} as unknown as CustomFile]
}
return []
}, [legacyInfo?.upload_file, localFileInfo])
}, [uploadFileIdInfo, legacyInfo?.upload_file, localFileInfo, documentDetail?.name])
const websitePages = useMemo(() => {
if (!websiteInfo)

View File

@ -0,0 +1,13 @@
'use client'
import { IS_DEV } from '@/config'
import dynamic from '@/next/dynamic'
const Agentation = dynamic(() => import('agentation').then(module => module.Agentation), { ssr: false })
export function AgentationLoader() {
if (!IS_DEV)
return null
return <Agentation />
}

View File

@ -69,6 +69,7 @@ vi.mock('@/context/i18n', () => ({
const { mockConfig, mockEnv } = vi.hoisted(() => ({
mockConfig: {
IS_CLOUD_EDITION: false,
AMPLITUDE_API_KEY: '',
ZENDESK_WIDGET_KEY: '',
SUPPORT_EMAIL_ADDRESS: '',
},
@ -80,6 +81,8 @@ const { mockConfig, mockEnv } = vi.hoisted(() => ({
}))
vi.mock('@/config', () => ({
get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION },
get AMPLITUDE_API_KEY() { return mockConfig.AMPLITUDE_API_KEY },
get isAmplitudeEnabled() { return mockConfig.IS_CLOUD_EDITION && !!mockConfig.AMPLITUDE_API_KEY },
get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY },
get SUPPORT_EMAIL_ADDRESS() { return mockConfig.SUPPORT_EMAIL_ADDRESS },
IS_DEV: false,

View File

@ -315,14 +315,14 @@ describe('AccountSetting', () => {
it('should handle scroll event in panel', () => {
// Act
renderAccountSetting()
const scrollContainer = screen.getByRole('dialog').querySelector('.overflow-y-auto')
const scrollContainer = screen.getByRole('dialog').querySelector('.overscroll-contain')
// Assert
expect(scrollContainer).toBeInTheDocument()
if (scrollContainer) {
// Scroll down
fireEvent.scroll(scrollContainer, { target: { scrollTop: 100 } })
expect(scrollContainer).toHaveClass('overflow-y-auto')
expect(scrollContainer).toHaveClass('overscroll-contain')
// Scroll back up
fireEvent.scroll(scrollContainer, { target: { scrollTop: 0 } })

View File

@ -1,8 +1,9 @@
'use client'
import type { AccountSettingTab } from '@/app/components/header/account-setting/constants'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import SearchInput from '@/app/components/base/search-input'
import { ScrollArea } from '@/app/components/base/ui/scroll-area'
import BillingPage from '@/app/components/billing/billing-page'
import CustomPage from '@/app/components/custom/custom-page'
import {
@ -136,20 +137,6 @@ export default function AccountSetting({
],
},
]
const scrollRef = useRef<HTMLDivElement>(null)
const [scrolled, setScrolled] = useState(false)
useEffect(() => {
const targetElement = scrollRef.current
const scrollHandle = (e: Event) => {
const userScrolled = (e.target as HTMLDivElement).scrollTop > 0
setScrolled(userScrolled)
}
targetElement?.addEventListener('scroll', scrollHandle)
return () => {
targetElement?.removeEventListener('scroll', scrollHandle)
}
}, [])
const activeItem = [...menuItems[0].items, ...menuItems[1].items].find(item => item.key === activeMenu)
const [searchValue, setSearchValue] = useState<string>('')
@ -208,7 +195,7 @@ export default function AccountSetting({
}
</div>
</div>
<div className="relative flex w-[824px]">
<div className="relative flex min-h-0 w-[824px]">
<div className="fixed right-6 top-6 z-[9999] flex flex-col items-center">
<Button
variant="tertiary"
@ -221,8 +208,14 @@ export default function AccountSetting({
</Button>
<div className="mt-1 text-text-tertiary system-2xs-medium-uppercase">ESC</div>
</div>
<div ref={scrollRef} className="w-full overflow-y-auto bg-components-panel-bg pb-4">
<div className={cn('sticky top-0 z-20 mx-8 mb-[18px] flex items-center bg-components-panel-bg pb-2 pt-[27px]', scrolled && 'border-b border-divider-regular')}>
<ScrollArea
className="h-full min-h-0 flex-1 bg-components-panel-bg"
slotClassNames={{
viewport: 'overscroll-contain',
content: 'min-h-full pb-4',
}}
>
<div className="sticky top-0 z-20 mx-8 mb-[18px] flex items-center bg-components-panel-bg pb-2 pt-[27px]">
<div className="shrink-0 text-text-primary title-2xl-semi-bold">
{activeItem?.name}
{activeItem?.description && (
@ -249,7 +242,7 @@ export default function AccountSetting({
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
{activeMenu === ACCOUNT_SETTING_TAB.LANGUAGE && <LanguagePage />}
</div>
</div>
</ScrollArea>
</div>
</div>
</MenuDialog>

View File

@ -9,16 +9,18 @@ import { flatten } from 'es-toolkit/compat'
import { produce } from 'immer'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
import CreateAppModal from '@/app/components/app/create-app-modal'
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useAppContext } from '@/context/app-context'
import dynamic from '@/next/dynamic'
import { useParams } from '@/next/navigation'
import { useInfiniteAppList } from '@/service/use-apps'
import { AppModeEnum } from '@/types/app'
import Nav from '../nav'
const CreateAppTemplateDialog = dynamic(() => import('@/app/components/app/create-app-dialog'), { ssr: false })
const CreateAppModal = dynamic(() => import('@/app/components/app/create-app-modal'), { ssr: false })
const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-from-dsl-modal'), { ssr: false })
const AppNav = () => {
const { t } = useTranslation()
const { appId } = useParams()

View File

@ -0,0 +1,16 @@
'use client'
import { IS_DEV } from '@/config'
import { env } from '@/env'
import dynamic from '@/next/dynamic'
const SentryInitializer = dynamic(() => import('./sentry-initializer'), { ssr: false })
const LazySentryInitializer = () => {
if (IS_DEV || !env.NEXT_PUBLIC_SENTRY_DSN)
return null
return <SentryInitializer />
}
export default LazySentryInitializer

View File

@ -3,8 +3,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useGitHubReleases, useGitHubUpload } from '../hooks'
const mockNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({
default: { notify: (...args: unknown[]) => mockNotify(...args) },
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign((...args: unknown[]) => mockNotify(...args), {
success: (...args: unknown[]) => mockNotify(...args),
error: (...args: unknown[]) => mockNotify(...args),
warning: (...args: unknown[]) => mockNotify(...args),
info: (...args: unknown[]) => mockNotify(...args),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
}))
vi.mock('@/config', () => ({
@ -56,9 +64,7 @@ describe('install-plugin/hooks', () => {
const releases = await result.current.fetchReleases('owner', 'repo')
expect(releases).toEqual([])
expect(mockNotify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'error' }),
)
expect(mockNotify).toHaveBeenCalledWith('Failed to fetch repository releases')
})
})
@ -130,9 +136,7 @@ describe('install-plugin/hooks', () => {
await expect(
result.current.handleUpload('url', 'v1', 'pkg'),
).rejects.toThrow('Upload failed')
expect(mockNotify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'error', message: 'Error uploading package' }),
)
expect(mockNotify).toHaveBeenCalledWith('Error uploading package')
})
})
})

View File

@ -1,6 +1,5 @@
import type { GitHubRepoReleaseResponse } from '../types'
import type { IToastProps } from '@/app/components/base/toast'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { GITHUB_ACCESS_TOKEN } from '@/config'
import { uploadGitHub } from '@/service/plugins'
import { compareVersion, getLatestVersion } from '@/utils/semver'
@ -37,16 +36,10 @@ export const useGitHubReleases = () => {
}
catch (error) {
if (error instanceof Error) {
Toast.notify({
type: 'error',
message: error.message,
})
toast.error(error.message)
}
else {
Toast.notify({
type: 'error',
message: 'Failed to fetch repository releases',
})
toast.error('Failed to fetch repository releases')
}
return []
}
@ -54,7 +47,7 @@ export const useGitHubReleases = () => {
const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => {
let needUpdate = false
const toastProps: IToastProps = {
const toastProps: { type?: 'success' | 'error' | 'info' | 'warning', message: string } = {
type: 'info',
message: 'No new version available',
}
@ -99,10 +92,7 @@ export const useGitHubUpload = () => {
return GitHubPackage
}
catch (error) {
Toast.notify({
type: 'error',
message: 'Error uploading package',
})
toast.error('Error uploading package')
throw error
}
}

View File

@ -57,10 +57,16 @@ const createUpdatePayload = (overrides: Partial<UpdateFromGitHubPayload> = {}):
// Mock external dependencies
const mockNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({
default: {
notify: (props: { type: string, message: string }) => mockNotify(props),
},
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign((props: { type: string, message: string }) => mockNotify(props), {
success: (message: string) => mockNotify({ type: 'success', message }),
error: (message: string) => mockNotify({ type: 'error', message }),
warning: (message: string) => mockNotify({ type: 'warning', message }),
info: (message: string) => mockNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
}))
const mockGetIconUrl = vi.fn()

View File

@ -7,7 +7,7 @@ import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { cn } from '@/utils/classnames'
import { InstallStepFromGitHub } from '../../types'
@ -81,10 +81,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
const handleUrlSubmit = async () => {
const { isValid, owner, repo } = parseGitHubUrl(state.repoUrl)
if (!isValid || !owner || !repo) {
Toast.notify({
type: 'error',
message: t('error.inValidGitHubUrl', { ns: 'plugin' }),
})
toast.error(t('error.inValidGitHubUrl', { ns: 'plugin' }))
return
}
try {
@ -97,17 +94,11 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
}))
}
else {
Toast.notify({
type: 'error',
message: t('error.noReleasesFound', { ns: 'plugin' }),
})
toast.error(t('error.noReleasesFound', { ns: 'plugin' }))
}
}
catch {
Toast.notify({
type: 'error',
message: t('error.fetchReleasesError', { ns: 'plugin' }),
})
toast.error(t('error.fetchReleasesError', { ns: 'plugin' }))
}
}

View File

@ -2,10 +2,25 @@ import type { PluginDetail } from '../../types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as amplitude from '@/app/components/base/amplitude'
import Toast from '@/app/components/base/toast'
import { PluginSource } from '../../types'
import DetailHeader from '../detail-header'
const { mockToast } = vi.hoisted(() => ({
mockToast: Object.assign(vi.fn(), {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
}))
vi.mock('@/app/components/base/ui/toast', () => ({
toast: mockToast,
}))
const {
mockSetShowUpdatePluginModal,
mockRefreshModelProviders,
@ -272,7 +287,7 @@ describe('DetailHeader', () => {
vi.clearAllMocks()
mockAutoUpgradeInfo = null
mockEnableMarketplace = true
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
vi.clearAllMocks()
vi.spyOn(amplitude, 'trackEvent').mockImplementation(() => {})
})

View File

@ -1,7 +1,6 @@
import type { EndpointListItem, PluginDetail } from '../../types'
import { act, fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import EndpointCard from '../endpoint-card'
const mockHandleChange = vi.fn()
@ -9,6 +8,22 @@ const mockEnableEndpoint = vi.fn()
const mockDisableEndpoint = vi.fn()
const mockDeleteEndpoint = vi.fn()
const mockUpdateEndpoint = vi.fn()
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
}))
// Flags to control whether operations should fail
const failureFlags = {
@ -127,8 +142,6 @@ describe('EndpointCard', () => {
failureFlags.disable = false
failureFlags.delete = false
failureFlags.update = false
// Mock Toast.notify to prevent toast elements from accumulating in DOM
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
// Polyfill document.execCommand for copy-to-clipboard in jsdom
if (typeof document.execCommand !== 'function') {
document.execCommand = vi.fn().mockReturnValue(true)

View File

@ -2,9 +2,25 @@ import type { FormSchema } from '../../../base/form/types'
import type { PluginDetail } from '../../types'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import EndpointModal from '../endpoint-modal'
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
}))
vi.mock('@/hooks/use-i18n', () => ({
useRenderI18nObject: () => (obj: Record<string, string> | string) =>
typeof obj === 'string' ? obj : obj?.en_US || '',
@ -69,11 +85,9 @@ const mockPluginDetail: PluginDetail = {
describe('EndpointModal', () => {
const mockOnCancel = vi.fn()
const mockOnSaved = vi.fn()
let mockToastNotify: ReturnType<typeof vi.spyOn>
beforeEach(() => {
vi.clearAllMocks()
mockToastNotify = vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
})
describe('Rendering', () => {

View File

@ -3,7 +3,6 @@ import type { ModalStates, VersionTarget } from '../use-detail-header-state'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as amplitude from '@/app/components/base/amplitude'
import Toast from '@/app/components/base/toast'
import { PluginSource } from '../../../../types'
import { usePluginOperations } from '../use-plugin-operations'
@ -20,6 +19,7 @@ const {
mockUninstallPlugin,
mockFetchReleases,
mockCheckForUpdates,
mockToastNotify,
} = vi.hoisted(() => {
return {
mockSetShowUpdatePluginModal: vi.fn(),
@ -29,9 +29,25 @@ const {
mockUninstallPlugin: vi.fn(() => Promise.resolve({ success: true })),
mockFetchReleases: vi.fn(() => Promise.resolve([{ tag_name: 'v2.0.0' }])),
mockCheckForUpdates: vi.fn(() => ({ needUpdate: true, toastProps: { type: 'success', message: 'Update available' } })),
mockToastNotify: vi.fn(),
}
})
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
}))
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowUpdatePluginModal: mockSetShowUpdatePluginModal,
@ -124,7 +140,6 @@ describe('usePluginOperations', () => {
modalStates = createModalStatesMock()
versionPicker = createVersionPickerMock()
mockOnUpdate = vi.fn()
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
vi.spyOn(amplitude, 'trackEvent').mockImplementation(() => {})
})
@ -233,7 +248,7 @@ describe('usePluginOperations', () => {
})
expect(mockCheckForUpdates).toHaveBeenCalled()
expect(Toast.notify).toHaveBeenCalled()
expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: 'Update available' })
})
it('should show update plugin modal when update is needed', async () => {

View File

@ -5,7 +5,7 @@ import type { ModalStates, VersionTarget } from './use-detail-header-state'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { uninstallPlugin } from '@/service/plugins'
@ -60,10 +60,7 @@ export const usePluginOperations = ({
}
if (!meta?.repo || !meta?.version || !meta?.package) {
Toast.notify({
type: 'error',
message: 'Missing plugin metadata for GitHub update',
})
toast.error('Missing plugin metadata for GitHub update')
return
}
@ -74,7 +71,7 @@ export const usePluginOperations = ({
return
const { needUpdate, toastProps } = checkForUpdates(fetchedReleases, meta.version)
Toast.notify(toastProps)
toast(toastProps.message, { type: toastProps.type })
if (needUpdate) {
setShowUpdatePluginModal({
@ -122,10 +119,7 @@ export const usePluginOperations = ({
if (res.success) {
modalStates.hideDeleteConfirm()
Toast.notify({
type: 'success',
message: t('action.deleteSuccess', { ns: 'plugin' }),
})
toast.success(t('action.deleteSuccess', { ns: 'plugin' }))
handlePluginUpdated(true)
if (PluginCategoryEnum.model.includes(category))

View File

@ -9,8 +9,8 @@ import ActionButton from '@/app/components/base/action-button'
import Confirm from '@/app/components/base/confirm'
import { CopyCheck } from '@/app/components/base/icons/src/vender/line/files'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import { toast } from '@/app/components/base/ui/toast'
import Indicator from '@/app/components/header/indicator'
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import {
@ -47,7 +47,7 @@ const EndpointCard = ({
await handleChange()
},
onError: () => {
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
setActive(false)
},
})
@ -57,7 +57,7 @@ const EndpointCard = ({
hideDisableConfirm()
},
onError: () => {
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
setActive(false)
},
})
@ -83,7 +83,7 @@ const EndpointCard = ({
hideDeleteConfirm()
},
onError: () => {
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
},
})
@ -108,7 +108,7 @@ const EndpointCard = ({
hideEndpointModalConfirm()
},
onError: () => {
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
},
})
const handleUpdate = (state: Record<string, any>) => updateEndpoint({

View File

@ -9,8 +9,8 @@ import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import { toast } from '@/app/components/base/ui/toast'
import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useDocLink } from '@/context/i18n'
import {
@ -50,7 +50,7 @@ const EndpointList = ({ detail }: Props) => {
hideEndpointModal()
},
onError: () => {
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
},
})

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Drawer from '@/app/components/base/drawer'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import { cn } from '@/utils/classnames'
@ -48,7 +48,10 @@ const EndpointModal: FC<Props> = ({
const handleSave = () => {
for (const field of formSchemas) {
if (field.required && !tempCredential[field.name]) {
Toast.notify({ type: 'error', message: t('errorMsg.fieldRequired', { ns: 'common', field: typeof field.label === 'string' ? field.label : getValueFromI18nObject(field.label as Record<string, string>) }) })
toast.error(t('errorMsg.fieldRequired', {
ns: 'common',
field: typeof field.label === 'string' ? field.label : getValueFromI18nObject(field.label as Record<string, string>),
}))
return
}
}

View File

@ -1,14 +1,29 @@
import type { Model, ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Import component after mocks
import Toast from '@/app/components/base/toast'
import { ConfigurationMethodEnum, ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
// Import component after mocks
import ModelParameterModal from '../index'
// ==================== Mock Setup ====================
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
}))
// Mock provider context
const mockProviderContextValue = {
isAPIKeySet: true,
@ -53,8 +68,6 @@ vi.mock('@/utils/completion-params', () => ({
fetchAndMergeValidCompletionParams: (...args: unknown[]) => mockFetchAndMergeValidCompletionParams(...args),
}))
const mockToastNotify = vi.spyOn(Toast, 'notify')
// Mock child components
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
default: ({ defaultModel, modelList, scopeFeatures, onSelect }: {
@ -244,7 +257,6 @@ const setupModelLists = (config: {
describe('ModelParameterModal', () => {
beforeEach(() => {
vi.clearAllMocks()
mockToastNotify.mockReturnValue({})
mockProviderContextValue.isAPIKeySet = true
mockProviderContextValue.modelProviders = []
setupModelLists()
@ -865,9 +877,7 @@ describe('ModelParameterModal', () => {
// Assert
await waitFor(() => {
expect(Toast.notify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'warning' }),
)
expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'warning' }))
})
})
@ -892,9 +902,7 @@ describe('ModelParameterModal', () => {
// Assert
await waitFor(() => {
expect(Toast.notify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'error' }),
)
expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
})
})
})

View File

@ -10,12 +10,12 @@ import type {
import type { TriggerProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/app/components/base/ui/popover'
import { toast } from '@/app/components/base/ui/toast'
import { ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
useModelList,
@ -134,14 +134,11 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
const keys = Object.keys(removedDetails || {})
if (keys.length) {
Toast.notify({
type: 'warning',
message: `${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${keys.map(k => `${k} (${removedDetails[k]})`).join(', ')}`,
})
toast.warning(`${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${keys.map(k => `${k} (${removedDetails[k]})`).join(', ')}`)
}
}
catch {
Toast.notify({ type: 'error', message: t('error', { ns: 'common' }) })
toast.error(t('error', { ns: 'common' }))
}
}

View File

@ -1,12 +1,26 @@
import type { TriggerLogEntity } from '@/app/components/workflow/block-selector/types'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import LogViewer from '../log-viewer'
const mockToastNotify = vi.fn()
const mockWriteText = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
}))
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
default: ({ value }: { value: unknown }) => (
<div data-testid="code-editor">{JSON.stringify(value)}</div>
@ -57,10 +71,6 @@ beforeEach(() => {
},
configurable: true,
})
vi.spyOn(Toast, 'notify').mockImplementation((args) => {
mockToastNotify(args)
return { clear: vi.fn() }
})
})
describe('LogViewer', () => {

View File

@ -26,10 +26,16 @@ vi.mock('@/service/use-triggers', () => ({
useDeleteTriggerSubscription: () => ({ mutate: vi.fn(), isPending: false }),
}))
vi.mock('@/app/components/base/toast', () => ({
default: {
notify: vi.fn(),
},
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(vi.fn(), {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
}))
const createSubscription = (overrides: Partial<TriggerSubscription> = {}): TriggerSubscription => ({

Some files were not shown because too many files have changed in this diff Show More