mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
Merge branch 'main' into feat/support-agent-sandbox
This commit is contained in:
commit
6756745062
9
.github/actions/setup-web/action.yml
vendored
9
.github/actions/setup-web/action.yml
vendored
@ -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
|
||||
|
||||
29
.github/workflows/style.yml
vendored
29
.github/workflows/style.yml
vendored
@ -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
|
||||
|
||||
2
.github/workflows/translate-i18n-claude.yml
vendored
2
.github/workflows/translate-i18n-claude.yml
vendored
@ -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 }}
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"))
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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",
|
||||
)
|
||||
|
||||
|
||||
@ -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="[]",
|
||||
)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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="{}",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"):
|
||||
|
||||
@ -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"])
|
||||
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -69,6 +69,7 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"flatted@<=3.4.1": "3.4.2",
|
||||
"rollup@>=4.0.0,<4.59.0": "4.59.0"
|
||||
}
|
||||
}
|
||||
|
||||
22
sdks/nodejs-client/pnpm-lock.yaml
generated
22
sdks/nodejs-client/pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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} />)
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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', () => ({
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export { default, isAmplitudeEnabled } from './AmplitudeProvider'
|
||||
export { default } from './lazy-amplitude-provider'
|
||||
export { resetUser, setUserId, setUserProperties, trackEvent } from './utils'
|
||||
|
||||
@ -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
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
13
web/app/components/devtools/agentation-loader.tsx
Normal file
13
web/app/components/devtools/agentation-loader.tsx
Normal 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 />
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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 } })
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
|
||||
16
web/app/components/lazy-sentry-initializer.tsx
Normal file
16
web/app/components/lazy-sentry-initializer.tsx
Normal 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
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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' }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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(() => {})
|
||||
})
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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' }))
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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' }))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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' }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user