fix: resolve import migrations and test failures after segment 3 merge

- Migrate core.model_runtime -> dify_graph.model_runtime across 20+ files
- Migrate core.workflow.file -> dify_graph.file across 15+ files
- Migrate core.workflow.enums -> dify_graph.enums in service files
- Fix SandboxContext phantom import in dify_graph/context/__init__.py
- Fix core.app.workflow.node_factory -> core.workflow.node_factory
- Fix toast import paths (useToastContext from toast/context)
- Fix app-info.tsx import paths for relocated app-operations
- Fix 15 frontend test files for API changes, missing QueryClientProvider,
  i18n key renames, and component behavior changes

Made-with: Cursor
This commit is contained in:
Novice
2026-03-23 10:31:11 +08:00
parent 94b01f6821
commit 6b75188ddc
58 changed files with 242 additions and 172 deletions

View File

@ -1,4 +1,4 @@
from core.workflow.file.helpers import get_signed_file_url_for_plugin
from dify_graph.file.helpers import get_signed_file_url_for_plugin
from flask import abort
from flask_restx import Resource
from pydantic import BaseModel

View File

@ -37,7 +37,7 @@ from controllers.console.wraps import (
only_edition_cloud,
setup_required,
)
from core.workflow.file import helpers as file_helpers
from dify_graph.file import helpers as file_helpers
from extensions.ext_database import db
from fields.member_fields import Account as AccountResponse
from libs.datetime_utils import naive_utc_now

View File

@ -1,6 +1,6 @@
import logging
from core.model_runtime.utils.encoders import jsonable_encoder
from dify_graph.model_runtime.utils.encoders import jsonable_encoder
from flask import request
from flask_restx import Resource, fields
from pydantic import BaseModel

View File

@ -8,7 +8,7 @@ from core.agent.entities import AgentEntity, AgentLog, AgentResult
from core.agent.patterns.strategy_factory import StrategyFactory
from core.app.apps.base_app_queue_manager import PublishFrom
from core.app.entities.queue_entities import QueueAgentThoughtEvent, QueueMessageEndEvent, QueueMessageFileEvent
from core.model_runtime.entities import (
from dify_graph.model_runtime.entities import (
AssistantPromptMessage,
LLMResult,
LLMResultChunk,
@ -19,12 +19,12 @@ from core.model_runtime.entities import (
TextPromptMessageContent,
UserPromptMessage,
)
from core.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes
from dify_graph.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes
from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform
from core.tools.__base.tool import Tool
from core.tools.entities.tool_entities import ToolInvokeMeta
from core.tools.tool_engine import ToolEngine
from core.workflow.file import file_manager
from dify_graph.file import file_manager
from models.model import Message
logger = logging.getLogger(__name__)

View File

@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any
from core.agent.entities import AgentLog, AgentResult, ExecutionContext
from core.model_manager import ModelInstance
from core.model_runtime.entities import (
from dify_graph.model_runtime.entities import (
AssistantPromptMessage,
LLMResult,
LLMResultChunk,
@ -19,10 +19,10 @@ from core.model_runtime.entities import (
PromptMessage,
PromptMessageTool,
)
from core.model_runtime.entities.llm_entities import LLMUsage
from core.model_runtime.entities.message_entities import TextPromptMessageContent
from dify_graph.model_runtime.entities.llm_entities import LLMUsage
from dify_graph.model_runtime.entities.message_entities import TextPromptMessageContent
from core.tools.entities.tool_entities import ToolInvokeMessage, ToolInvokeMeta
from core.workflow.file import File
from dify_graph.file import File
if TYPE_CHECKING:
from core.tools.__base.tool import Tool

View File

@ -12,7 +12,7 @@ from collections.abc import Generator
from typing import Any, Union
from core.agent.entities import AgentLog, AgentResult
from core.model_runtime.entities import (
from dify_graph.model_runtime.entities import (
AssistantPromptMessage,
LLMResult,
LLMResultChunk,
@ -23,7 +23,7 @@ from core.model_runtime.entities import (
ToolPromptMessage,
)
from core.tools.entities.tool_entities import ToolInvokeMeta
from core.workflow.file import File
from dify_graph.file import File
from .base import AgentPattern

View File

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Union
from core.agent.entities import AgentLog, AgentResult, AgentScratchpadUnit, ExecutionContext
from core.agent.output_parser.cot_output_parser import CotAgentOutputParser
from core.model_manager import ModelInstance
from core.model_runtime.entities import (
from dify_graph.model_runtime.entities import (
AssistantPromptMessage,
LLMResult,
LLMResultChunk,
@ -17,7 +17,7 @@ from core.model_runtime.entities import (
PromptMessage,
SystemPromptMessage,
)
from core.workflow.file import File
from dify_graph.file import File
from .base import AgentPattern, ToolInvokeHook
@ -204,7 +204,7 @@ class ReActStrategy(AgentPattern):
tool_names = [tool.name for tool in prompt_tools]
# Format tools as JSON for comprehensive information
from core.model_runtime.utils.encoders import jsonable_encoder
from dify_graph.model_runtime.utils.encoders import jsonable_encoder
tools_str = json.dumps(jsonable_encoder(prompt_tools), indent=2)
tool_names_str = ", ".join(f'"{name}"' for name in tool_names)

View File

@ -6,8 +6,8 @@ from typing import TYPE_CHECKING
from core.agent.entities import AgentEntity, ExecutionContext
from core.model_manager import ModelInstance
from core.model_runtime.entities.model_entities import ModelFeature
from core.workflow.file.models import File
from dify_graph.model_runtime.entities.model_entities import ModelFeature
from dify_graph.file.models import File
from .base import AgentPattern, ToolInvokeHook
from .function_call import FunctionCallStrategy

View File

@ -1,7 +1,7 @@
import logging
from core.workflow.graph_engine.layers.base import GraphEngineLayer
from core.workflow.graph_events.base import GraphEngineEvent
from dify_graph.graph_engine.layers.base import GraphEngineLayer
from dify_graph.graph_events.base import GraphEngineEvent
from core.sandbox import Sandbox

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field
from core.workflow.variables.types import SegmentType
from dify_graph.variables.types import SegmentType
class SuggestedQuestionsOutput(BaseModel):

View File

@ -10,8 +10,8 @@ This module provides utilities to:
from collections.abc import Callable, Mapping, Sequence
from typing import Any, cast
from core.workflow.file import File
from core.workflow.variables.segments import ArrayFileSegment, FileSegment
from dify_graph.file import File
from dify_graph.variables.segments import ArrayFileSegment, FileSegment
FILE_PATH_FORMAT = "file-path"
FILE_PATH_DESCRIPTION_SUFFIX = "this field contains a file path from the Dify sandbox"

View File

@ -1,6 +1,6 @@
"""Utility functions for LLM generator."""
from core.model_runtime.entities.message_entities import (
from dify_graph.model_runtime.entities.message_entities import (
AssistantPromptMessage,
PromptMessage,
PromptMessageRole,

View File

@ -7,7 +7,7 @@ This module defines the common protocol for memory implementations.
from abc import ABC, abstractmethod
from collections.abc import Sequence
from core.model_runtime.entities import ImagePromptMessageContent, PromptMessage
from dify_graph.model_runtime.entities import ImagePromptMessageContent, PromptMessage
class BaseMemory(ABC):
@ -49,7 +49,7 @@ class BaseMemory(ABC):
:param message_limit: Maximum number of messages
:return: Formatted history text
"""
from core.model_runtime.entities import (
from dify_graph.model_runtime.entities import (
PromptMessageRole,
TextPromptMessageContent,
)

View File

@ -22,7 +22,7 @@ from sqlalchemy.orm import Session
from core.memory.base import BaseMemory
from core.model_manager import ModelInstance
from core.model_runtime.entities import (
from dify_graph.model_runtime.entities import (
AssistantPromptMessage,
MultiModalPromptMessageContent,
PromptMessage,
@ -31,9 +31,9 @@ from core.model_runtime.entities import (
ToolPromptMessage,
UserPromptMessage,
)
from core.model_runtime.entities.message_entities import PromptMessageContentUnionTypes
from dify_graph.model_runtime.entities.message_entities import PromptMessageContentUnionTypes
from core.prompt.utils.extract_thread_messages import extract_thread_messages
from core.workflow.file import file_manager
from dify_graph.file import file_manager
from extensions.ext_database import db
from models.model import Message
from models.workflow import WorkflowNodeExecutionModel

View File

@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any
from pydantic import BaseModel, Field
from core.model_runtime.utils.encoders import jsonable_encoder
from dify_graph.model_runtime.utils.encoders import jsonable_encoder
from core.session.cli_api import CliApiSession
from core.skill.entities import ToolDependencies, ToolReference
from core.tools.entities.tool_entities import ToolParameter, ToolProviderType

View File

@ -14,7 +14,7 @@ from core.skill.entities.tool_dependencies import ToolDependencies
from core.tools.signature import sign_tool_file
from core.tools.tool_file_manager import ToolFileManager
from core.virtual_environment.__base.helpers import pipeline
from core.workflow.file import File, FileTransferMethod, FileType
from dify_graph.file import File, FileTransferMethod, FileType
from ..bash.dify_cli import DifyCliConfig
from ..entities import DifyCli

View File

@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
if TYPE_CHECKING: # pragma: no cover
from models.model import File
from core.model_runtime.entities.message_entities import PromptMessageTool
from dify_graph.model_runtime.entities.message_entities import PromptMessageTool
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.entities.tool_entities import (

View File

@ -1,4 +1,4 @@
from core.workflow.nodes.base import BaseNodeData
from dify_graph.nodes.base import BaseNodeData
class CommandNodeData(BaseNodeData):

View File

@ -2,17 +2,17 @@ import logging
from collections.abc import Mapping, Sequence
from typing import Any
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
from core.workflow.nodes.base.entities import VariableSelector
from core.workflow.nodes.base.node import Node
from core.workflow.nodes.base.variable_template_parser import VariableTemplateParser
from dify_graph.enums import NodeType, WorkflowNodeExecutionStatus
from dify_graph.nodes.base.entities import VariableSelector
from dify_graph.nodes.base.node import Node
from dify_graph.nodes.base.variable_template_parser import VariableTemplateParser
from core.sandbox import sandbox_debug
from core.sandbox.bash.session import SANDBOX_READY_TIMEOUT
from core.virtual_environment.__base.command_future import CommandCancelledError, CommandTimeoutError
from core.virtual_environment.__base.helpers import submit_command, with_connection
from core.workflow.node_events import NodeRunResult
from core.workflow.nodes.base import variable_template_parser
from dify_graph.node_events import NodeRunResult
from dify_graph.nodes.base import variable_template_parser
from core.workflow.nodes.command.entities import CommandNodeData
from core.workflow.nodes.command.exc import CommandExecutionError

View File

@ -1,6 +1,6 @@
from collections.abc import Sequence
from core.workflow.nodes.base import BaseNodeData
from dify_graph.nodes.base import BaseNodeData
class FileUploadNodeData(BaseNodeData):

View File

@ -5,16 +5,16 @@ from collections.abc import Mapping, Sequence
from pathlib import PurePosixPath
from typing import Any, cast
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
from core.workflow.nodes.base.node import Node
from core.workflow.variables.segments import ArrayStringSegment, FileSegment
from dify_graph.enums import NodeType, WorkflowNodeExecutionStatus
from dify_graph.nodes.base.node import Node
from dify_graph.variables.segments import ArrayStringSegment, FileSegment
from core.sandbox.bash.session import SANDBOX_READY_TIMEOUT
from core.virtual_environment.__base.command_future import CommandCancelledError, CommandTimeoutError
from core.virtual_environment.__base.helpers import pipeline
from core.workflow.file import File, FileTransferMethod
from core.workflow.node_events import NodeRunResult
from core.workflow.variables import ArrayFileSegment
from dify_graph.file import File, FileTransferMethod
from dify_graph.node_events import NodeRunResult
from dify_graph.variables import ArrayFileSegment
from core.zip_sandbox import SandboxDownloadItem
from .entities import FileUploadNodeData

View File

@ -17,15 +17,12 @@ from dify_graph.context.execution_context import (
register_context_capturer,
reset_context_provider,
)
from dify_graph.context.models import SandboxContext
__all__ = [
"AppContext",
"ContextProviderNotFoundError",
"ExecutionContext",
"IExecutionContext",
"NullAppContext",
"SandboxContext",
"capture_current_context",
"read_context",
"register_context",

View File

@ -517,7 +517,7 @@ class AgentNode(Node[AgentNodeData]):
Fetch memory instance for saving node memory.
This is a simplified version that doesn't require model_instance.
"""
from core.model_runtime.entities.model_entities import ModelType
from dify_graph.model_runtime.entities.model_entities import ModelType
from core.model_manager import ModelManager

View File

@ -378,7 +378,7 @@ class Node(Generic[NodeDataT]):
Nested nodes are nodes with parent_node_id == self._node_id.
They are executed before the main node to extract values from list[PromptMessage].
"""
from core.app.workflow.node_factory import DifyNodeFactory
from core.workflow.node_factory import DifyNodeFactory
extractor_configs = self._find_extractor_node_configs()
logger.debug("[NestedNode] Found %d nested nodes for parent '%s'", len(extractor_configs), self._node_id)
@ -689,7 +689,7 @@ class Node(Generic[NodeDataT]):
@_dispatch.register
def _(self, event: StreamChunkEvent) -> NodeRunStreamChunkEvent:
from core.workflow.graph_events import ChunkType
from dify_graph.graph_events import ChunkType
return NodeRunStreamChunkEvent(
id=self.execution_id,
@ -711,7 +711,7 @@ class Node(Generic[NodeDataT]):
@_dispatch.register
def _(self, event: ToolCallChunkEvent) -> NodeRunStreamChunkEvent:
from core.workflow.graph_events import ChunkType
from dify_graph.graph_events import ChunkType
return NodeRunStreamChunkEvent(
id=self._node_execution_id,
@ -726,8 +726,8 @@ class Node(Generic[NodeDataT]):
@_dispatch.register
def _(self, event: ToolResultChunkEvent) -> NodeRunStreamChunkEvent:
from core.workflow.entities import ToolResult, ToolResultStatus
from core.workflow.graph_events import ChunkType
from dify_graph.entities import ToolResult, ToolResultStatus
from dify_graph.graph_events import ChunkType
tool_result = event.tool_result or ToolResult()
status: ToolResultStatus = tool_result.status or ToolResultStatus.SUCCESS
@ -748,7 +748,7 @@ class Node(Generic[NodeDataT]):
@_dispatch.register
def _(self, event: ThoughtChunkEvent) -> NodeRunStreamChunkEvent:
from core.workflow.graph_events import ChunkType
from dify_graph.graph_events import ChunkType
return NodeRunStreamChunkEvent(
id=self._node_execution_id,

View File

@ -199,7 +199,7 @@ def _build_messages_from_trace(
assistant_response: str,
file_suffix: str = "",
) -> list[PromptMessage]:
from core.workflow.nodes.llm.entities import ModelTraceSegment, ToolTraceSegment
from dify_graph.nodes.llm.entities import ModelTraceSegment, ToolTraceSegment
messages: list[PromptMessage] = []
covered_text_len = 0
@ -266,12 +266,12 @@ def _truncate_multimodal_content(message: PromptMessage) -> PromptMessage:
def restore_multimodal_content_in_messages(messages: Sequence[PromptMessage]) -> list[PromptMessage]:
from core.workflow.file import file_manager
return [_restore_message_content(msg, file_manager) for msg in messages]
return [_restore_message_content(msg) for msg in messages]
def _restore_message_content(message: PromptMessage, file_manager) -> PromptMessage:
def _restore_message_content(message: PromptMessage) -> PromptMessage:
from dify_graph.file.file_manager import restore_multimodal_content
content = message.content
if content is None or isinstance(content, str):
return message
@ -279,7 +279,7 @@ def _restore_message_content(message: PromptMessage, file_manager) -> PromptMess
restored_content: list[PromptMessageContentUnionTypes] = []
for item in content:
if isinstance(item, MultiModalPromptMessageContent):
restored_item = file_manager.restore_multimodal_content(item)
restored_item = restore_multimodal_content(item)
restored_content.append(cast(PromptMessageContentUnionTypes, restored_item))
else:
restored_content.append(item)

View File

@ -2201,7 +2201,7 @@ class LLMNode(Node[LLMNodeData]):
def _extract_prompt_files(self, variable_pool: VariablePool) -> list[File]:
"""Extract files from prompt template variables."""
from core.workflow.variables import ArrayFileVariable, FileVariable
from dify_graph.variables import ArrayFileVariable, FileVariable
files: list[File] = []

View File

@ -568,7 +568,7 @@ class ToolNode(Node[ToolNodeData]):
:param parent_node_id: the parent node id to find nested nodes for
:return: mapping of variable key to variable selector
"""
from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING
from dify_graph.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING
result: dict[str, Sequence[str]] = {}
nodes = graph_config.get("nodes", [])

View File

@ -3,7 +3,7 @@ from typing import Any
import orjson
from core.model_runtime.entities import PromptMessage
from dify_graph.model_runtime.entities import PromptMessage
from .segment_group import SegmentGroup
from .segments import ArrayFileSegment, ArrayPromptMessageSegment, FileSegment, Segment

View File

@ -18,7 +18,7 @@ from collections.abc import Mapping
from functools import reduce
from typing import Any, cast
from core.workflow.enums import NodeType
from dify_graph.enums import NodeType
from core.app.entities.app_asset_entities import AppAssetFileTree, AppAssetNode
from core.sandbox.entities.config import AppAssets

View File

@ -7,10 +7,10 @@ extracting values from list[PromptMessage] variables.
from typing import Any
from core.workflow.enums import NodeType
from dify_graph.enums import NodeType
from sqlalchemy.orm import Session
from core.model_runtime.entities import LLMMode
from dify_graph.model_runtime.entities import LLMMode
from services.model_provider_service import ModelProviderService
from services.workflow.entities import NestedNodeGraphRequest, NestedNodeGraphResponse, NestedNodeParameterSchema

View File

@ -4,7 +4,7 @@ from decimal import Decimal
from unittest.mock import MagicMock
import pytest
from core.model_runtime.entities.llm_entities import LLMUsage
from dify_graph.model_runtime.entities.llm_entities import LLMUsage
from core.agent.entities import AgentLog, ExecutionContext
from core.agent.patterns.base import AgentPattern

View File

@ -4,8 +4,8 @@ from decimal import Decimal
from unittest.mock import MagicMock
import pytest
from core.model_runtime.entities.llm_entities import LLMUsage
from core.model_runtime.entities.message_entities import (
from dify_graph.model_runtime.entities.llm_entities import LLMUsage
from dify_graph.model_runtime.entities.message_entities import (
PromptMessageTool,
SystemPromptMessage,
UserPromptMessage,
@ -312,7 +312,7 @@ class TestPromptMessageHandling:
def test_assistant_message_with_tool_calls(self, mock_model_instance, mock_context, mock_tool):
"""Test that assistant messages can contain tool calls."""
from core.model_runtime.entities.message_entities import AssistantPromptMessage
from dify_graph.model_runtime.entities.message_entities import AssistantPromptMessage
tool_call = AssistantPromptMessage.ToolCall(
id="call_123",

View File

@ -6,7 +6,7 @@ import pytest
from core.agent.entities import ExecutionContext
from core.agent.patterns.react import ReActStrategy
from core.model_runtime.entities import SystemPromptMessage, UserPromptMessage
from dify_graph.model_runtime.entities import SystemPromptMessage, UserPromptMessage
@pytest.fixture
@ -33,7 +33,7 @@ def mock_context():
@pytest.fixture
def mock_tool():
"""Create a mock tool."""
from core.model_runtime.entities.message_entities import PromptMessageTool
from dify_graph.model_runtime.entities.message_entities import PromptMessageTool
tool = MagicMock()
tool.entity.identity.name = "test_tool"
@ -158,7 +158,7 @@ class TestBuildPromptWithReactFormat:
def test_scratchpad_appended_as_assistant_message(self, mock_model_instance, mock_context):
"""Test that agent scratchpad is appended as AssistantPromptMessage."""
from core.agent.entities import AgentScratchpadUnit
from core.model_runtime.entities import AssistantPromptMessage
from dify_graph.model_runtime.entities import AssistantPromptMessage
strategy = ReActStrategy(
model_instance=mock_model_instance,

View File

@ -3,7 +3,7 @@
from unittest.mock import MagicMock
import pytest
from core.model_runtime.entities.model_entities import ModelFeature
from dify_graph.model_runtime.entities.model_entities import ModelFeature
from core.agent.entities import AgentEntity, ExecutionContext
from core.agent.patterns.function_call import FunctionCallStrategy

View File

@ -4,10 +4,10 @@ from decimal import Decimal
from unittest.mock import MagicMock, patch
import pytest
from core.model_runtime.entities.llm_entities import LLMUsage
from dify_graph.model_runtime.entities.llm_entities import LLMUsage
from core.agent.entities import AgentEntity, AgentLog, AgentPromptEntity, AgentResult
from core.model_runtime.entities import SystemPromptMessage, UserPromptMessage
from dify_graph.model_runtime.entities import SystemPromptMessage, UserPromptMessage
class TestOrganizePromptMessages:
@ -184,7 +184,7 @@ class TestClearUserPromptImageMessages:
def test_original_messages_not_modified(self, mock_runner):
"""Test that original messages are not modified (deep copy)."""
from core.model_runtime.entities.message_entities import (
from dify_graph.model_runtime.entities.message_entities import (
ImagePromptMessageContent,
TextPromptMessageContent,
)
@ -365,13 +365,13 @@ class TestOrganizeUserQuery:
def test_query_with_files(self, mock_runner):
"""Test organizing a query with files."""
from core.workflow.file.models import File
from dify_graph.file.models import File
mock_file = MagicMock(spec=File)
mock_runner.files = [mock_file]
with patch("core.agent.agent_app_runner.file_manager") as mock_fm:
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
from dify_graph.model_runtime.entities.message_entities import ImagePromptMessageContent
mock_fm.to_prompt_message_content.return_value = ImagePromptMessageContent(
data="http://example.com/image.jpg",

View File

@ -2,8 +2,8 @@ from unittest.mock import MagicMock
from core.app.apps.base_app_queue_manager import PublishFrom
from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner
from core.workflow.graph_events import NodeRunStreamChunkEvent
from core.workflow.nodes import NodeType
from dify_graph.graph_events import NodeRunStreamChunkEvent
from dify_graph.enums import NodeType
class DummyQueueManager:

View File

@ -2,14 +2,14 @@
from unittest.mock import patch
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
from core.workflow.file.file_manager import (
from dify_graph.model_runtime.entities.message_entities import ImagePromptMessageContent
from dify_graph.file.file_manager import (
_encode_file_ref,
restore_multimodal_content,
to_prompt_message_content,
)
from core.workflow.file import File, FileTransferMethod, FileType
from dify_graph.file import File, FileTransferMethod, FileType
class TestEncodeFileRef:
@ -52,8 +52,8 @@ class TestEncodeFileRef:
class TestToPromptMessageContent:
"""Tests for to_prompt_message_content function with file_ref field."""
@patch("core.workflow.file.file_manager.dify_config")
@patch("core.workflow.file.file_manager._get_encoded_string")
@patch("dify_graph.file.file_manager.dify_config")
@patch("dify_graph.file.file_manager._get_encoded_string")
def test_includes_file_ref(self, mock_get_encoded, mock_config):
"""Generated content should include file_ref field."""
mock_config.MULTIMODAL_SEND_FORMAT = "base64"
@ -121,9 +121,9 @@ class TestRestoreMultimodalContent:
assert result.url == "https://example.com/image.png"
@patch("core.workflow.file.file_manager.dify_config")
@patch("core.workflow.file.file_manager._build_file_from_ref")
@patch("core.workflow.file.file_manager._to_url")
@patch("dify_graph.file.file_manager.dify_config")
@patch("dify_graph.file.file_manager._build_file_from_ref")
@patch("dify_graph.file.file_manager._to_url")
def test_restores_url_from_file_ref(self, mock_to_url, mock_build_file, mock_config):
"""Content should be restored from file_ref when url is empty (url mode)."""
mock_config.MULTIMODAL_SEND_FORMAT = "url"
@ -144,9 +144,9 @@ class TestRestoreMultimodalContent:
assert result.url == "https://restored-url.com/image.png"
mock_build_file.assert_called_once()
@patch("core.workflow.file.file_manager.dify_config")
@patch("core.workflow.file.file_manager._build_file_from_ref")
@patch("core.workflow.file.file_manager._get_encoded_string")
@patch("dify_graph.file.file_manager.dify_config")
@patch("dify_graph.file.file_manager._build_file_from_ref")
@patch("dify_graph.file.file_manager._get_encoded_string")
def test_restores_base64_from_file_ref(self, mock_get_encoded, mock_build_file, mock_config):
"""Content should be restored as base64 when in base64 mode."""
mock_config.MULTIMODAL_SEND_FORMAT = "base64"

View File

@ -3,7 +3,7 @@ Unit tests for sandbox file path detection and conversion.
"""
import pytest
from core.workflow.variables.segments import ArrayFileSegment, FileSegment
from dify_graph.variables.segments import ArrayFileSegment, FileSegment
from core.llm_generator.output_parser.file_ref import (
FILE_PATH_DESCRIPTION_SUFFIX,
@ -13,7 +13,7 @@ from core.llm_generator.output_parser.file_ref import (
detect_file_path_fields,
is_file_path_property,
)
from core.workflow.file import File, FileTransferMethod, FileType
from dify_graph.file import File, FileTransferMethod, FileType
def _build_file(file_id: str) -> File:

View File

@ -2,21 +2,21 @@
from unittest.mock import MagicMock
from core.workflow.entities.tool_entities import ToolResultStatus
from core.workflow.enums import NodeType
from core.workflow.graph.graph import Graph
from core.workflow.graph_engine.response_coordinator.coordinator import ResponseStreamCoordinator
from core.workflow.graph_engine.response_coordinator.session import ResponseSession
from core.workflow.nodes.base.entities import BaseNodeData
from core.workflow.nodes.base.template import Template, VariableSegment
from dify_graph.entities.tool_entities import ToolResultStatus
from dify_graph.enums import NodeType
from dify_graph.graph.graph import Graph
from dify_graph.graph_engine.response_coordinator.coordinator import ResponseStreamCoordinator
from dify_graph.graph_engine.response_coordinator.session import ResponseSession
from dify_graph.nodes.base.entities import BaseNodeData
from dify_graph.nodes.base.template import Template, VariableSegment
from core.workflow.graph_events import (
from dify_graph.graph_events import (
ChunkType,
NodeRunStreamChunkEvent,
ToolCall,
ToolResult,
)
from core.workflow.runtime import VariablePool
from dify_graph.runtime import VariablePool
class TestResponseCoordinatorObjectStreaming:

View File

@ -1,7 +1,7 @@
"""Tests for StreamChunkEvent and its subclasses."""
from core.workflow.entities import ToolCall, ToolResult, ToolResultStatus
from core.workflow.node_events import (
from dify_graph.entities import ToolCall, ToolResult, ToolResultStatus
from dify_graph.node_events import (
ChunkType,
StreamChunkEvent,
ThoughtChunkEvent,

View File

@ -4,8 +4,8 @@ from io import BytesIO
from typing import Any
from unittest.mock import MagicMock
from core.workflow.enums import WorkflowNodeExecutionStatus
from core.workflow.system_variable import SystemVariable
from dify_graph.enums import WorkflowNodeExecutionStatus
from dify_graph.system_variable import SystemVariable
from core.entities.provider_entities import BasicProviderConfig
from core.virtual_environment.__base.entities import (
@ -19,9 +19,10 @@ from core.virtual_environment.__base.entities import (
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
from core.virtual_environment.channel.queue_transport import QueueTransportReadCloser
from core.virtual_environment.channel.transport import NopTransportWriteCloser
from core.workflow.entities import GraphInitParams
from dify_graph.entities import GraphInitParams
from dify_graph.entities.graph_init_params import DIFY_RUN_CONTEXT_KEY
from core.workflow.nodes.command.node import CommandNode
from core.workflow.runtime import GraphRuntimeState, VariablePool
from dify_graph.runtime import GraphRuntimeState, VariablePool
class FakeVirtualEnvironment(VirtualEnvironment):
@ -138,14 +139,18 @@ def _make_node(
variable_pool = VariablePool(system_variables=system_variables, user_inputs={})
runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
init_params = GraphInitParams(
tenant_id="t",
app_id="a",
workflow_id="w",
graph_config={},
user_id="u",
user_from="account",
invoke_from="debugger",
call_depth=0,
run_context={
DIFY_RUN_CONTEXT_KEY: {
"tenant_id": "t",
"app_id": "a",
"user_id": "u",
"user_from": "account",
"invoke_from": "debugger",
}
},
)
if vm is not None:

View File

@ -3,12 +3,12 @@
import string
from unittest.mock import patch
from core.model_runtime.entities.message_entities import (
from dify_graph.model_runtime.entities.message_entities import (
ImagePromptMessageContent,
TextPromptMessageContent,
UserPromptMessage,
)
from core.workflow.nodes.llm.llm_utils import (
from dify_graph.nodes.llm.llm_utils import (
_truncate_multimodal_content,
build_context,
restore_multimodal_content_in_messages,
@ -100,7 +100,7 @@ class TestBuildContext:
def test_excludes_system_messages(self):
"""System messages should be excluded from context."""
from core.model_runtime.entities.message_entities import SystemPromptMessage
from dify_graph.model_runtime.entities.message_entities import SystemPromptMessage
messages = [
SystemPromptMessage(content="You are a helpful assistant."),
@ -125,12 +125,12 @@ class TestBuildContext:
def test_builds_context_with_tool_calls_from_generation_data(self):
"""Should reconstruct full conversation including tool calls when generation_data is provided."""
from core.model_runtime.entities.llm_entities import LLMUsage
from core.model_runtime.entities.message_entities import (
from dify_graph.model_runtime.entities.llm_entities import LLMUsage
from dify_graph.model_runtime.entities.message_entities import (
AssistantPromptMessage,
ToolPromptMessage,
)
from core.workflow.nodes.llm.entities import (
from dify_graph.nodes.llm.entities import (
LLMGenerationData,
LLMTraceSegment,
ModelTraceSegment,
@ -199,12 +199,12 @@ class TestBuildContext:
def test_builds_context_with_multiple_tool_calls(self):
"""Should handle multiple tool calls in a single conversation."""
from core.model_runtime.entities.llm_entities import LLMUsage
from core.model_runtime.entities.message_entities import (
from dify_graph.model_runtime.entities.llm_entities import LLMUsage
from dify_graph.model_runtime.entities.message_entities import (
AssistantPromptMessage,
ToolPromptMessage,
)
from core.workflow.nodes.llm.entities import (
from dify_graph.nodes.llm.entities import (
LLMGenerationData,
LLMTraceSegment,
ModelTraceSegment,
@ -291,8 +291,8 @@ class TestBuildContext:
def test_builds_context_with_empty_trace(self):
"""Should fallback to simple context when trace is empty."""
from core.model_runtime.entities.llm_entities import LLMUsage
from core.workflow.nodes.llm.entities import LLMGenerationData
from dify_graph.model_runtime.entities.llm_entities import LLMUsage
from dify_graph.nodes.llm.entities import LLMGenerationData
messages = [UserPromptMessage(content="Hello!")]
@ -318,7 +318,7 @@ class TestBuildContext:
class TestRestoreMultimodalContentInMessages:
"""Tests for restore_multimodal_content_in_messages function."""
@patch("core.workflow.file.file_manager.restore_multimodal_content")
@patch("dify_graph.file.file_manager.restore_multimodal_content")
def test_restores_multimodal_content(self, mock_restore):
"""Should restore multimodal content in messages."""
# Setup mock

View File

@ -3,12 +3,12 @@ from collections.abc import Generator
from typing import Any
import pytest
from core.model_runtime.entities.llm_entities import LLMUsage
from core.workflow.entities.tool_entities import ToolResultStatus
from core.workflow.nodes.llm.node import LLMNode
from dify_graph.model_runtime.entities.llm_entities import LLMUsage
from dify_graph.entities.tool_entities import ToolResultStatus
from dify_graph.nodes.llm.node import LLMNode
from core.workflow.entities import ToolCallResult
from core.workflow.node_events import ModelInvokeCompletedEvent, NodeEventBase
from dify_graph.entities import ToolCallResult
from dify_graph.node_events import ModelInvokeCompletedEvent, NodeEventBase
class _StubModelInstance:
@ -109,9 +109,9 @@ def test_stream_llm_events_no_reasoning_results_in_empty_sequence():
def test_serialize_tool_call_strips_files_to_ids():
file_cls = pytest.importorskip("core.workflow.file").File
file_type = pytest.importorskip("core.workflow.file.enums").FileType
transfer_method = pytest.importorskip("core.workflow.file.enums").FileTransferMethod
file_cls = pytest.importorskip("dify_graph.file").File
file_type = pytest.importorskip("dify_graph.file.enums").FileType
transfer_method = pytest.importorskip("dify_graph.file.enums").FileTransferMethod
file_with_id = file_cls(
id="f1",

View File

@ -8,6 +8,7 @@
*/
import type { AppListResponse } from '@/models/app'
import type { App } from '@/types/app'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import List from '@/app/components/apps/list'
@ -165,9 +166,15 @@ const createPage = (apps: App[], hasMore = false, page = 1): AppListResponse =>
total: apps.length,
})
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const renderList = (searchParams?: Record<string, string>) => {
return renderWithNuqs(
<List controlRefreshList={0} />,
<QueryClientProvider client={queryClient}>
<List controlRefreshList={0} />
</QueryClientProvider>,
{ searchParams },
)
}
@ -213,7 +220,9 @@ describe('App List Browsing Flow', () => {
it('should transition from loading to content when data loads', () => {
mockIsLoading = true
const { rerender } = renderWithNuqs(<List controlRefreshList={0} />)
const { rerender } = renderWithNuqs(
<QueryClientProvider client={queryClient}><List controlRefreshList={0} /></QueryClientProvider>,
)
const skeletonCards = document.querySelectorAll('.animate-pulse')
expect(skeletonCards.length).toBeGreaterThan(0)
@ -224,7 +233,9 @@ describe('App List Browsing Flow', () => {
createMockApp({ id: 'app-1', name: 'Loaded App' }),
])]
rerender(<List controlRefreshList={0} />)
rerender(
<QueryClientProvider client={queryClient}><List controlRefreshList={0} /></QueryClientProvider>,
)
expect(screen.getByText('Loaded App')).toBeInTheDocument()
})
@ -420,9 +431,13 @@ describe('App List Browsing Flow', () => {
it('should call refetch when controlRefreshList increments', () => {
mockPages = [createPage([createMockApp()])]
const { rerender } = renderWithNuqs(<List controlRefreshList={0} />)
const { rerender } = renderWithNuqs(
<QueryClientProvider client={queryClient}><List controlRefreshList={0} /></QueryClientProvider>,
)
rerender(<List controlRefreshList={1} />)
rerender(
<QueryClientProvider client={queryClient}><List controlRefreshList={1} /></QueryClientProvider>,
)
expect(mockRefetch).toHaveBeenCalled()
})

View File

@ -9,6 +9,7 @@
*/
import type { AppListResponse } from '@/models/app'
import type { App } from '@/types/app'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import List from '@/app/components/apps/list'
@ -218,8 +219,16 @@ const createPage = (apps: App[]): AppListResponse => ({
total: apps.length,
})
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const renderList = () => {
return renderWithNuqs(<List controlRefreshList={0} />)
return renderWithNuqs(
<QueryClientProvider client={queryClient}>
<List controlRefreshList={0} />
</QueryClientProvider>,
)
}
describe('Create App Flow', () => {
@ -245,7 +254,7 @@ describe('Create App Flow', () => {
expect(screen.getByText('app.createApp')).toBeInTheDocument()
expect(screen.getByText('app.newApp.startFromBlank')).toBeInTheDocument()
expect(screen.getByText('app.newApp.startFromTemplate')).toBeInTheDocument()
expect(screen.getByText('app.importDSL')).toBeInTheDocument()
expect(screen.getByText('app.importApp')).toBeInTheDocument()
})
it('should not render NewAppCard when user is not an editor', () => {
@ -354,7 +363,7 @@ describe('Create App Flow', () => {
it('should open DSL import modal when "Import DSL" is clicked', async () => {
renderList()
fireEvent.click(screen.getByText('app.importDSL'))
fireEvent.click(screen.getByText('app.importApp'))
await waitFor(() => {
expect(screen.getByTestId('create-from-dsl-modal')).toBeInTheDocument()
@ -364,7 +373,7 @@ describe('Create App Flow', () => {
it('should close DSL import modal on cancel', async () => {
renderList()
fireEvent.click(screen.getByText('app.importDSL'))
fireEvent.click(screen.getByText('app.importApp'))
await waitFor(() => {
expect(screen.getByTestId('create-from-dsl-modal')).toBeInTheDocument()
})
@ -378,7 +387,7 @@ describe('Create App Flow', () => {
it('should call onPlanInfoChanged and refetch on successful DSL import', async () => {
renderList()
fireEvent.click(screen.getByText('app.importDSL'))
fireEvent.click(screen.getByText('app.importApp'))
await waitFor(() => {
expect(screen.getByTestId('create-from-dsl-modal')).toBeInTheDocument()
})
@ -451,7 +460,7 @@ describe('Create App Flow', () => {
// Rapidly click different create options
fireEvent.click(screen.getByText('app.newApp.startFromBlank'))
fireEvent.click(screen.getByText('app.newApp.startFromTemplate'))
fireEvent.click(screen.getByText('app.importDSL'))
fireEvent.click(screen.getByText('app.importApp'))
// Should not crash, and some modal should be present
await waitFor(() => {

View File

@ -3,11 +3,19 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { AppModeEnum } from '@/types/app'
import AppInfo from '..'
import AppInfo from '../index'
let mockIsCurrentWorkspaceEditor = true
const mockSetPanelOpen = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({ replace: vi.fn() }),
}))
vi.mock('@/service/use-apps', () => ({
useInvalidateAppList: () => vi.fn(),
}))
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor,

View File

@ -263,11 +263,10 @@ describe('AppCard', () => {
})
it('should render app icon', () => {
// AppIcon component renders the emoji icon from app data
const { container } = render(<AppCard app={mockApp} />)
// Check that the icon container is rendered (AppIcon renders within the card)
const iconElement = container.querySelector('[class*="icon"]') || container.querySelector('img')
expect(iconElement || screen.getByText(mockApp.icon)).toBeTruthy()
const emojiElement = container.querySelector('em-emoji')
expect(emojiElement).toBeTruthy()
expect(emojiElement?.getAttribute('id')).toBe(mockApp.icon)
})
it('should render app type icon', () => {

View File

@ -20,6 +20,11 @@ vi.mock('@/app/education-apply/hooks', () => ({
},
}))
vi.mock('next/navigation', () => ({
useRouter: () => ({ replace: vi.fn() }),
useSearchParams: () => new URLSearchParams(),
}))
vi.mock('@/hooks/use-import-dsl', () => ({
useImportDSL: () => ({
handleImportDSL: vi.fn(),

View File

@ -1,3 +1,4 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, screen } from '@testing-library/react'
import * as React from 'react'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
@ -200,9 +201,17 @@ beforeAll(() => {
} as unknown as typeof IntersectionObserver
})
// Render helper wrapping with shared nuqs testing helper.
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const renderList = (searchParams = '') => {
return renderWithNuqs(<List />, { searchParams })
return renderWithNuqs(
<QueryClientProvider client={queryClient}>
<List />
</QueryClientProvider>,
{ searchParams },
)
}
describe('List', () => {
@ -399,10 +408,14 @@ describe('List', () => {
describe('Edge Cases', () => {
it('should handle multiple renders without issues', () => {
const { rerender } = renderWithNuqs(<List />)
const { rerender } = renderWithNuqs(
<QueryClientProvider client={queryClient}><List /></QueryClientProvider>,
)
expect(screen.getByText('app.types.all')).toBeInTheDocument()
rerender(<List />)
rerender(
<QueryClientProvider client={queryClient}><List /></QueryClientProvider>,
)
expect(screen.getByText('app.types.all')).toBeInTheDocument()
})

View File

@ -71,7 +71,7 @@ describe('CreateAppCard', () => {
expect(screen.getByText('app.newApp.startFromBlank')).toBeInTheDocument()
expect(screen.getByText('app.newApp.startFromTemplate')).toBeInTheDocument()
expect(screen.getByText('app.importDSL')).toBeInTheDocument()
expect(screen.getByText('app.importApp')).toBeInTheDocument()
})
it('should render all buttons as clickable', () => {
@ -190,7 +190,7 @@ describe('CreateAppCard', () => {
it('should open DSL modal when clicking Import DSL', () => {
render(<CreateAppCard ref={defaultRef} />)
fireEvent.click(screen.getByText('app.importDSL'))
fireEvent.click(screen.getByText('app.importApp'))
expect(screen.getByTestId('create-dsl-modal')).toBeInTheDocument()
})
@ -198,7 +198,7 @@ describe('CreateAppCard', () => {
it('should close DSL modal when clicking close button', () => {
render(<CreateAppCard ref={defaultRef} />)
fireEvent.click(screen.getByText('app.importDSL'))
fireEvent.click(screen.getByText('app.importApp'))
expect(screen.getByTestId('create-dsl-modal')).toBeInTheDocument()
fireEvent.click(screen.getByTestId('close-dsl-modal'))
@ -209,7 +209,7 @@ describe('CreateAppCard', () => {
const mockOnSuccess = vi.fn()
render(<CreateAppCard ref={defaultRef} onSuccess={mockOnSuccess} />)
fireEvent.click(screen.getByText('app.importDSL'))
fireEvent.click(screen.getByText('app.importApp'))
fireEvent.click(screen.getByTestId('success-dsl-modal'))
expect(mockOnPlanInfoChanged).toHaveBeenCalled()
@ -245,7 +245,7 @@ describe('CreateAppCard', () => {
fireEvent.click(screen.getByText('app.newApp.startFromTemplate'))
fireEvent.click(screen.getByTestId('close-template-dialog'))
fireEvent.click(screen.getByText('app.importDSL'))
fireEvent.click(screen.getByText('app.importApp'))
fireEvent.click(screen.getByTestId('close-dsl-modal'))
expect(screen.queryByTestId('create-app-modal')).not.toBeInTheDocument()

View File

@ -14,6 +14,7 @@ const getPromptEditor = () => {
vi.mock('@/utils/var', () => ({
checkKeys: (_keys: string[]) => ({ isValid: true }),
getNewVar: (key: string, type: string) => ({ key, name: key, type, required: true }),
basePath: '',
}))
vi.mock('@/app/components/app/configuration/config-prompt/confirm-add-var', () => ({

View File

@ -68,6 +68,7 @@ vi.mock('lexical', async (importOriginal) => {
getChildren: () => mocks.rootLines.map(line => ({
getTextContent: () => line,
})),
getAllTextNodes: () => [],
}),
TextNode: class TextNode {
__text: string

View File

@ -77,12 +77,16 @@ const { mockConfig, mockEnv } = vi.hoisted(() => ({
},
},
}))
vi.mock('@/config', () => ({
get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION },
get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY },
IS_DEV: false,
IS_CE_EDITION: false,
}))
vi.mock('@/config', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config')>()
return {
...actual,
get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION },
get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY },
IS_DEV: false,
IS_CE_EDITION: false,
}
})
vi.mock('@/env', () => mockEnv)
const baseAppContextValue: AppContextValue = {

View File

@ -55,8 +55,20 @@ vi.mock('@/service/workflow', () => ({
syncWorkflowDraft: (p: unknown) => mockSyncWorkflowDraft(p),
}))
vi.mock('@/service/fetch', () => ({ postWithKeepalive: vi.fn() }))
vi.mock('@/config', () => ({ API_PREFIX: '/api' }))
vi.mock('@/service/fetch', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/service/fetch')>()
return {
...actual,
postWithKeepalive: vi.fn(),
}
})
vi.mock('@/config', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config')>()
return {
...actual,
API_PREFIX: '/api',
}
})
const mockHandleRefreshWorkflowDraft = vi.fn()
vi.mock('@/app/components/workflow-app/hooks', () => ({

View File

@ -11,7 +11,7 @@ vi.mock('@/app/components/workflow/store', () => ({
getState: () => ({
appId: 'app-1',
isWorkflowDataLoaded: true,
debouncedSyncWorkflowDraft: undefined,
debouncedSyncWorkflowDraft: { cancel: vi.fn() },
setSyncWorkflowDraftHash: mockSetSyncWorkflowDraftHash,
setIsSyncingWorkflowDraft: vi.fn(),
setEnvironmentVariables: vi.fn(),

View File

@ -26,6 +26,7 @@ vi.mock('../use-workflow', () => ({
vi.mock('../../utils', () => ({
getNodesConnectedSourceOrTargetHandleIdsMap: vi.fn(() => ({})),
genNodeMetaData: vi.fn(({ type, sort }: { type: string, sort: number }) => ({ type, sort })),
}))
// useNodesSyncDraft is used REAL — via renderWorkflowHook + hooksStoreProps

View File

@ -53,7 +53,7 @@ describe('useWorkflowTextChunk', () => {
},
})
result.current.handleWorkflowTextChunk({ data: { text: ' World' } } as TextChunkResponse)
result.current.handleWorkflowTextChunk({ data: { text: ' World', chunk_type: 'text' } } as TextChunkResponse)
const state = store.getState().workflowRunningData!
expect(state.resultText).toBe('Hello World')

View File

@ -172,9 +172,9 @@ describe('createWorkflowStore', () => {
expect(store.getState().controlMode).toBe('pointer')
})
it('should default controlMode to hand when localStorage has no value', () => {
it('should default controlMode to pointer when localStorage has no value', () => {
const store = createStore()
expect(store.getState().controlMode).toBe('hand')
expect(store.getState().controlMode).toBe('pointer')
})
it('should read panelWidth from localStorage', () => {