diff --git a/api/core/llm_generator/output_parser/file_ref.py b/api/core/llm_generator/output_parser/file_ref.py
index c0c3a24a19..e1f4200ee1 100644
--- a/api/core/llm_generator/output_parser/file_ref.py
+++ b/api/core/llm_generator/output_parser/file_ref.py
@@ -10,8 +10,8 @@ This module provides utilities to:
from collections.abc import Callable, Mapping, Sequence
from typing import Any, cast
-from core.workflow.variables.segments import ArrayFileSegment, FileSegment
from core.workflow.file import File
+from core.workflow.variables.segments import ArrayFileSegment, FileSegment
FILE_PATH_FORMAT = "file-path"
FILE_PATH_DESCRIPTION_SUFFIX = "this field contains a file path from the Dify sandbox"
diff --git a/api/core/workflow/nodes/file_upload/node.py b/api/core/workflow/nodes/file_upload/node.py
index 396d544fb2..79e1db26ad 100644
--- a/api/core/workflow/nodes/file_upload/node.py
+++ b/api/core/workflow/nodes/file_upload/node.py
@@ -6,14 +6,14 @@ from pathlib import PurePosixPath
from typing import Any, cast
from core.sandbox.bash.session import SANDBOX_READY_TIMEOUT
-from core.workflow.variables import ArrayFileSegment
-from core.workflow.variables.segments import ArrayStringSegment, FileSegment
from core.virtual_environment.__base.command_future import CommandCancelledError, CommandTimeoutError
from core.virtual_environment.__base.helpers import pipeline
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
from core.workflow.file import File, FileTransferMethod
from core.workflow.node_events import NodeRunResult
from core.workflow.nodes.base.node import Node
+from core.workflow.variables import ArrayFileSegment
+from core.workflow.variables.segments import ArrayStringSegment, FileSegment
from core.zip_sandbox import SandboxDownloadItem
from .entities import FileUploadNodeData
diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py
index c09d5137ef..d24807f2a1 100644
--- a/api/core/workflow/nodes/llm/node.py
+++ b/api/core/workflow/nodes/llm/node.py
@@ -1969,7 +1969,9 @@ class LLMNode(Node[LLMNodeData]):
LLMStructuredOutput(structured_output=structured_raw) if structured_raw else None
)
- llm_utils.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage)
+ from core.app.llm.quota import deduct_llm_quota
+
+ deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage)
completed = True
case LLMStructuredOutput():
diff --git a/api/tests/unit_tests/core/llm_generator/output_parser/test_file_ref.py b/api/tests/unit_tests/core/llm_generator/output_parser/test_file_ref.py
index 6a83105fa6..c8545d88cf 100644
--- a/api/tests/unit_tests/core/llm_generator/output_parser/test_file_ref.py
+++ b/api/tests/unit_tests/core/llm_generator/output_parser/test_file_ref.py
@@ -12,8 +12,8 @@ from core.llm_generator.output_parser.file_ref import (
detect_file_path_fields,
is_file_path_property,
)
-from core.workflow.variables.segments import ArrayFileSegment, FileSegment
from core.workflow.file import File, FileTransferMethod, FileType
+from core.workflow.variables.segments import ArrayFileSegment, FileSegment
def _build_file(file_id: str) -> File:
diff --git a/api/tests/unit_tests/core/workflow/nodes/test_llm_node_streaming.py b/api/tests/unit_tests/core/workflow/nodes/test_llm_node_streaming.py
index ddc9fe99b7..0774348ac6 100644
--- a/api/tests/unit_tests/core/workflow/nodes/test_llm_node_streaming.py
+++ b/api/tests/unit_tests/core/workflow/nodes/test_llm_node_streaming.py
@@ -29,7 +29,7 @@ def _drain(generator: Generator[NodeEventBase, None, Any]):
@pytest.fixture(autouse=True)
def patch_deduct_llm_quota(monkeypatch):
# Avoid touching real quota logic during unit tests
- monkeypatch.setattr("core.workflow.nodes.llm.node.llm_utils.deduct_llm_quota", lambda **_: None)
+ monkeypatch.setattr("core.app.llm.quota.deduct_llm_quota", lambda **_: None)
def _make_llm_node(reasoning_format: str) -> LLMNode:
diff --git a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx
index 0c5a43e62a..055fb9b543 100644
--- a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx
+++ b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx
@@ -281,7 +281,7 @@ describe('Operation', () => {
}
renderOperation({ ...baseProps, item })
await user.click(screen.getByTestId('copy-btn'))
- expect(copy).toHaveBeenCalledWith('Hello World')
+ expect(copy).toHaveBeenCalledWith('[AGENT]\nTHOUGHT:\nHello \n\n[AGENT]\nTHOUGHT:\nWorld')
})
})
diff --git a/web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx b/web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx
index 81e065c827..9ff2b53500 100644
--- a/web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx
+++ b/web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx
@@ -503,7 +503,7 @@ describe('TimePicker', () => {
const emitted = onChange.mock.calls[0][0]
expect(isDayjsObject(emitted)).toBe(true)
// 10:30 UTC converted to America/New_York (UTC-5 in Jan) = 05:30
- expect(emitted.utcOffset()).toBe(dayjs().tz('America/New_York').utcOffset())
+ expect(emitted.utcOffset()).toBe(dayjs('2024-01-01').tz('America/New_York').utcOffset())
expect(emitted.hour()).toBe(5)
expect(emitted.minute()).toBe(30)
})
diff --git a/web/app/components/base/markdown-blocks/__tests__/code-block.spec.tsx b/web/app/components/base/markdown-blocks/__tests__/code-block.spec.tsx
index 190825647a..333ed83e37 100644
--- a/web/app/components/base/markdown-blocks/__tests__/code-block.spec.tsx
+++ b/web/app/components/base/markdown-blocks/__tests__/code-block.spec.tsx
@@ -157,7 +157,6 @@ describe('CodeBlock', () => {
it('should render mermaid controls when language is mermaid', async () => {
render(graph TB; A-->B;)
- expect(await screen.findByText('app.mermaid.classic')).toBeInTheDocument()
expect(screen.getByText('Mermaid')).toBeInTheDocument()
})
diff --git a/web/app/components/base/prompt-editor/__tests__/index.spec.tsx b/web/app/components/base/prompt-editor/__tests__/index.spec.tsx
index 40ca8c3d76..0b86b4e006 100644
--- a/web/app/components/base/prompt-editor/__tests__/index.spec.tsx
+++ b/web/app/components/base/prompt-editor/__tests__/index.spec.tsx
@@ -30,6 +30,9 @@ const mocks = vi.hoisted(() => {
registerUpdateListener: vi.fn(() => vi.fn()),
dispatchCommand: vi.fn(),
getRootElement: vi.fn(() => rootElement),
+ getEditorState: vi.fn(() => ({
+ read: (fn: () => T): T => fn(),
+ })),
parseEditorState: vi.fn(() => ({ state: 'parsed' })),
setEditorState: vi.fn(),
focus: vi.fn(),
diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/component.spec.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/component.spec.tsx
index ff064f2a99..bab03dd055 100644
--- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/component.spec.tsx
+++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/component.spec.tsx
@@ -488,17 +488,20 @@ describe('WorkflowVariableBlockComponent', () => {
/>,
)
- const updateHandler = mockRegisterCommand.mock.calls[0][1] as (map: Record) => boolean
+ const updateHandler = mockRegisterCommand.mock.calls[0][1] as (payload: { workflowNodesMap: Record, nodeOutputVars: unknown[] }) => boolean
let result = false
act(() => {
result = updateHandler({
- 'node-1': {
- title: 'Updated',
- type: BlockEnum.LLM,
- width: 100,
- height: 50,
- position: { x: 0, y: 0 },
+ workflowNodesMap: {
+ 'node-1': {
+ title: 'Updated',
+ type: BlockEnum.LLM,
+ width: 100,
+ height: 50,
+ position: { x: 0, y: 0 },
+ },
},
+ nodeOutputVars: [],
})
})
diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/index.spec.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/index.spec.tsx
index ca4973b830..c949a60e5f 100644
--- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/index.spec.tsx
+++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/index.spec.tsx
@@ -106,7 +106,7 @@ describe('WorkflowVariableBlock', () => {
)
expect(mockUpdate).toHaveBeenCalled()
- expect(mockDispatchCommand).toHaveBeenCalledWith(UPDATE_WORKFLOW_NODES_MAP, workflowNodesMap)
+ expect(mockDispatchCommand).toHaveBeenCalledWith(UPDATE_WORKFLOW_NODES_MAP, { workflowNodesMap, nodeOutputVars: [] })
})
it('should throw when WorkflowVariableBlockNode is not registered', () => {
@@ -139,6 +139,10 @@ describe('WorkflowVariableBlock', () => {
['node-1', 'answer'],
workflowNodesMap,
getVarType,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
)
expect($insertNodes).toHaveBeenCalledWith([{ id: 'workflow-node' }])
expect(onInsert).toHaveBeenCalledTimes(1)
diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/workflow-variable-block-replacement-block.spec.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/workflow-variable-block-replacement-block.spec.tsx
index b9cb1faa37..f1e866852a 100644
--- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/workflow-variable-block-replacement-block.spec.tsx
+++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/__tests__/workflow-variable-block-replacement-block.spec.tsx
@@ -189,6 +189,7 @@ describe('WorkflowVariableBlockReplacementBlock', () => {
{ variable: 'ragVarA', type: VarType.string, isRagVariable: true },
{ variable: 'rag.shared.answer', type: VarType.string, isRagVariable: true },
],
+ variables,
)
expect($applyNodeReplacement).toHaveBeenCalledWith({ type: 'workflow-node' })
expect(created).toEqual({ type: 'workflow-node' })
@@ -216,6 +217,7 @@ describe('WorkflowVariableBlockReplacementBlock', () => {
[],
[],
undefined,
+ undefined,
)
})
})
diff --git a/web/app/components/header/account-setting/api-based-extension-page/empty.spec.tsx b/web/app/components/header/account-setting/api-based-extension-page/empty.spec.tsx
index 11a4e8278f..6a79b064f0 100644
--- a/web/app/components/header/account-setting/api-based-extension-page/empty.spec.tsx
+++ b/web/app/components/header/account-setting/api-based-extension-page/empty.spec.tsx
@@ -12,7 +12,7 @@ describe('Empty State', () => {
const link = screen.getByText('common.apiBasedExtension.link')
expect(link).toBeInTheDocument()
// The real useDocLink includes the language prefix (defaulting to /en in tests)
- expect(link.closest('a')).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/workspace/api-extension/api-extension')
+ expect(link.closest('a')).toHaveAttribute('href', 'https://docs.bash-is-all-you-need.dify.dev/en/use-dify/workspace/api-extension/api-extension')
})
})
})
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.spec.tsx
index e785ec58c7..3bb5cc2b3a 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.spec.tsx
@@ -43,8 +43,8 @@ describe('FeatureIcon', () => {
}
})
- it('should render nothing for unsupported feature', () => {
+ it('should render tool call icon with tooltip', () => {
const { container } = render()
- expect(container).toBeEmptyDOMElement()
+ expect(container).not.toBeEmptyDOMElement()
})
})
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.spec.tsx
index 4083f4a37c..f8d5ae2c86 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.spec.tsx
@@ -193,7 +193,7 @@ describe('Popup', () => {
fireEvent.click(screen.getByText('common.model.settingsLink'))
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
- payload: 'provider',
+ payload: 'model-provider',
})
})
})
diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/__tests__/uploading.spec.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/__tests__/uploading.spec.tsx
index aace5dcbe9..7bc7402d73 100644
--- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/__tests__/uploading.spec.tsx
+++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/__tests__/uploading.spec.tsx
@@ -161,20 +161,12 @@ describe('Uploading', () => {
})
})
- // NOTE: The uploadFile API has an unconventional contract where it always rejects.
- // Success vs failure is determined by whether response.message exists:
- // - If response.message exists → treated as failure (calls onFailed)
- // - If response.message is absent → treated as success (calls onPackageUploaded/onBundleUploaded)
- // This explains why we use mockRejectedValue for "success" scenarios below.
-
- it('should call onPackageUploaded when upload rejects without error message (success case)', async () => {
+ it('should call onPackageUploaded when upload resolves (success case)', async () => {
const mockResult = {
unique_identifier: 'test-uid',
manifest: createMockManifest(),
}
- mockUploadFile.mockRejectedValue({
- response: mockResult,
- })
+ mockUploadFile.mockResolvedValue(mockResult)
const onPackageUploaded = vi.fn()
render(
@@ -193,11 +185,9 @@ describe('Uploading', () => {
})
})
- it('should call onBundleUploaded when upload rejects without error message (success case)', async () => {
+ it('should call onBundleUploaded when upload resolves (success case)', async () => {
const mockDependencies = createMockDependencies()
- mockUploadFile.mockRejectedValue({
- response: mockDependencies,
- })
+ mockUploadFile.mockResolvedValue(mockDependencies)
const onBundleUploaded = vi.fn()
render(
@@ -261,9 +251,7 @@ describe('Uploading', () => {
// ================================
describe('Edge Cases', () => {
it('should handle empty response gracefully', async () => {
- mockUploadFile.mockRejectedValue({
- response: {},
- })
+ mockUploadFile.mockResolvedValue({})
const onPackageUploaded = vi.fn()
render()
@@ -277,9 +265,7 @@ describe('Uploading', () => {
})
it('should handle response with only unique_identifier', async () => {
- mockUploadFile.mockRejectedValue({
- response: { unique_identifier: 'only-uid' },
- })
+ mockUploadFile.mockResolvedValue({ unique_identifier: 'only-uid' })
const onPackageUploaded = vi.fn()
render()
diff --git a/web/app/components/plugins/readme-panel/__tests__/entrance.spec.tsx b/web/app/components/plugins/readme-panel/__tests__/entrance.spec.tsx
index f1e3c548de..4132197ed5 100644
--- a/web/app/components/plugins/readme-panel/__tests__/entrance.spec.tsx
+++ b/web/app/components/plugins/readme-panel/__tests__/entrance.spec.tsx
@@ -42,7 +42,7 @@ describe('ReadmeEntrance', () => {
const button = screen.getByRole('button')
fireEvent.click(button)
- expect(mockSetCurrentPluginDetail).toHaveBeenCalledWith(pluginDetail, 'drawer')
+ expect(mockSetCurrentPluginDetail).toHaveBeenCalledWith(pluginDetail, 'drawer', 'left')
})
it('should return null for builtin tools', () => {
diff --git a/web/app/components/workflow/utils/__tests__/common.spec.ts b/web/app/components/workflow/utils/__tests__/common.spec.ts
index 8c84a21d09..dba8bd45c6 100644
--- a/web/app/components/workflow/utils/__tests__/common.spec.ts
+++ b/web/app/components/workflow/utils/__tests__/common.spec.ts
@@ -167,8 +167,8 @@ describe('formatWorkflowRunIdentifier', () => {
expect(formatWorkflowRunIdentifier(0)).toBe(' (Running)')
})
- it('should capitalize custom fallback text', () => {
- expect(formatWorkflowRunIdentifier(undefined, 'pending')).toBe(' (Pending)')
+ it('should use custom fallback text as-is', () => {
+ expect(formatWorkflowRunIdentifier(undefined, 'pending')).toBe(' (pending)')
})
it('should format a valid timestamp', () => {
@@ -178,6 +178,6 @@ describe('formatWorkflowRunIdentifier', () => {
})
it('should handle single-char fallback text', () => {
- expect(formatWorkflowRunIdentifier(undefined, 'x')).toBe(' (X)')
+ expect(formatWorkflowRunIdentifier(undefined, 'x')).toBe(' (x)')
})
})