fix: resolve test failures after segment 2 merge

- Backend: fix deduct_llm_quota import path in llm node
- Backend: update core.variables → core.workflow.variables imports
- Frontend: update UpdateWorkflowNodesMapPayload in tests
- Frontend: fix various test expectations to match merged code

Made-with: Cursor
This commit is contained in:
Novice
2026-03-23 09:18:26 +08:00
parent 66e67caa2b
commit cbdcdcc2b9
18 changed files with 44 additions and 45 deletions

View File

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

View File

@ -6,14 +6,14 @@ from pathlib import PurePosixPath
from typing import Any, cast from typing import Any, cast
from core.sandbox.bash.session import SANDBOX_READY_TIMEOUT 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.command_future import CommandCancelledError, CommandTimeoutError
from core.virtual_environment.__base.helpers import pipeline from core.virtual_environment.__base.helpers import pipeline
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
from core.workflow.file import File, FileTransferMethod from core.workflow.file import File, FileTransferMethod
from core.workflow.node_events import NodeRunResult from core.workflow.node_events import NodeRunResult
from core.workflow.nodes.base.node import Node 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 core.zip_sandbox import SandboxDownloadItem
from .entities import FileUploadNodeData from .entities import FileUploadNodeData

View File

@ -1969,7 +1969,9 @@ class LLMNode(Node[LLMNodeData]):
LLMStructuredOutput(structured_output=structured_raw) if structured_raw else None 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 completed = True
case LLMStructuredOutput(): case LLMStructuredOutput():

View File

@ -12,8 +12,8 @@ from core.llm_generator.output_parser.file_ref import (
detect_file_path_fields, detect_file_path_fields,
is_file_path_property, is_file_path_property,
) )
from core.workflow.variables.segments import ArrayFileSegment, FileSegment
from core.workflow.file import File, FileTransferMethod, FileType from core.workflow.file import File, FileTransferMethod, FileType
from core.workflow.variables.segments import ArrayFileSegment, FileSegment
def _build_file(file_id: str) -> File: def _build_file(file_id: str) -> File:

View File

@ -29,7 +29,7 @@ def _drain(generator: Generator[NodeEventBase, None, Any]):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def patch_deduct_llm_quota(monkeypatch): def patch_deduct_llm_quota(monkeypatch):
# Avoid touching real quota logic during unit tests # 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: def _make_llm_node(reasoning_format: str) -> LLMNode:

View File

@ -281,7 +281,7 @@ describe('Operation', () => {
} }
renderOperation({ ...baseProps, item }) renderOperation({ ...baseProps, item })
await user.click(screen.getByTestId('copy-btn')) await user.click(screen.getByTestId('copy-btn'))
expect(copy).toHaveBeenCalledWith('Hello World') expect(copy).toHaveBeenCalledWith('[AGENT]\nTHOUGHT:\nHello \n\n[AGENT]\nTHOUGHT:\nWorld')
}) })
}) })

View File

@ -503,7 +503,7 @@ describe('TimePicker', () => {
const emitted = onChange.mock.calls[0][0] const emitted = onChange.mock.calls[0][0]
expect(isDayjsObject(emitted)).toBe(true) expect(isDayjsObject(emitted)).toBe(true)
// 10:30 UTC converted to America/New_York (UTC-5 in Jan) = 05:30 // 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.hour()).toBe(5)
expect(emitted.minute()).toBe(30) expect(emitted.minute()).toBe(30)
}) })

View File

@ -157,7 +157,6 @@ describe('CodeBlock', () => {
it('should render mermaid controls when language is mermaid', async () => { it('should render mermaid controls when language is mermaid', async () => {
render(<CodeBlock className="language-mermaid">graph TB; A--&gt;B;</CodeBlock>) render(<CodeBlock className="language-mermaid">graph TB; A--&gt;B;</CodeBlock>)
expect(await screen.findByText('app.mermaid.classic')).toBeInTheDocument()
expect(screen.getByText('Mermaid')).toBeInTheDocument() expect(screen.getByText('Mermaid')).toBeInTheDocument()
}) })

View File

@ -30,6 +30,9 @@ const mocks = vi.hoisted(() => {
registerUpdateListener: vi.fn(() => vi.fn()), registerUpdateListener: vi.fn(() => vi.fn()),
dispatchCommand: vi.fn(), dispatchCommand: vi.fn(),
getRootElement: vi.fn(() => rootElement), getRootElement: vi.fn(() => rootElement),
getEditorState: vi.fn(() => ({
read: <T,>(fn: () => T): T => fn(),
})),
parseEditorState: vi.fn(() => ({ state: 'parsed' })), parseEditorState: vi.fn(() => ({ state: 'parsed' })),
setEditorState: vi.fn(), setEditorState: vi.fn(),
focus: vi.fn(), focus: vi.fn(),

View File

@ -488,17 +488,20 @@ describe('WorkflowVariableBlockComponent', () => {
/>, />,
) )
const updateHandler = mockRegisterCommand.mock.calls[0][1] as (map: Record<string, unknown>) => boolean const updateHandler = mockRegisterCommand.mock.calls[0][1] as (payload: { workflowNodesMap: Record<string, unknown>, nodeOutputVars: unknown[] }) => boolean
let result = false let result = false
act(() => { act(() => {
result = updateHandler({ result = updateHandler({
'node-1': { workflowNodesMap: {
title: 'Updated', 'node-1': {
type: BlockEnum.LLM, title: 'Updated',
width: 100, type: BlockEnum.LLM,
height: 50, width: 100,
position: { x: 0, y: 0 }, height: 50,
position: { x: 0, y: 0 },
},
}, },
nodeOutputVars: [],
}) })
}) })

View File

@ -106,7 +106,7 @@ describe('WorkflowVariableBlock', () => {
) )
expect(mockUpdate).toHaveBeenCalled() 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', () => { it('should throw when WorkflowVariableBlockNode is not registered', () => {
@ -139,6 +139,10 @@ describe('WorkflowVariableBlock', () => {
['node-1', 'answer'], ['node-1', 'answer'],
workflowNodesMap, workflowNodesMap,
getVarType, getVarType,
undefined,
undefined,
undefined,
undefined,
) )
expect($insertNodes).toHaveBeenCalledWith([{ id: 'workflow-node' }]) expect($insertNodes).toHaveBeenCalledWith([{ id: 'workflow-node' }])
expect(onInsert).toHaveBeenCalledTimes(1) expect(onInsert).toHaveBeenCalledTimes(1)

View File

@ -189,6 +189,7 @@ describe('WorkflowVariableBlockReplacementBlock', () => {
{ variable: 'ragVarA', type: VarType.string, isRagVariable: true }, { variable: 'ragVarA', type: VarType.string, isRagVariable: true },
{ variable: 'rag.shared.answer', type: VarType.string, isRagVariable: true }, { variable: 'rag.shared.answer', type: VarType.string, isRagVariable: true },
], ],
variables,
) )
expect($applyNodeReplacement).toHaveBeenCalledWith({ type: 'workflow-node' }) expect($applyNodeReplacement).toHaveBeenCalledWith({ type: 'workflow-node' })
expect(created).toEqual({ type: 'workflow-node' }) expect(created).toEqual({ type: 'workflow-node' })
@ -216,6 +217,7 @@ describe('WorkflowVariableBlockReplacementBlock', () => {
[], [],
[], [],
undefined, undefined,
undefined,
) )
}) })
}) })

View File

@ -12,7 +12,7 @@ describe('Empty State', () => {
const link = screen.getByText('common.apiBasedExtension.link') const link = screen.getByText('common.apiBasedExtension.link')
expect(link).toBeInTheDocument() expect(link).toBeInTheDocument()
// The real useDocLink includes the language prefix (defaulting to /en in tests) // 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')
}) })
}) })
}) })

View File

@ -43,8 +43,8 @@ describe('FeatureIcon', () => {
} }
}) })
it('should render nothing for unsupported feature', () => { it('should render tool call icon with tooltip', () => {
const { container } = render(<FeatureIcon feature={ModelFeatureEnum.toolCall} />) const { container } = render(<FeatureIcon feature={ModelFeatureEnum.toolCall} />)
expect(container).toBeEmptyDOMElement() expect(container).not.toBeEmptyDOMElement()
}) })
}) })

View File

@ -193,7 +193,7 @@ describe('Popup', () => {
fireEvent.click(screen.getByText('common.model.settingsLink')) fireEvent.click(screen.getByText('common.model.settingsLink'))
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
payload: 'provider', payload: 'model-provider',
}) })
}) })
}) })

View File

@ -161,20 +161,12 @@ describe('Uploading', () => {
}) })
}) })
// NOTE: The uploadFile API has an unconventional contract where it always rejects. it('should call onPackageUploaded when upload resolves (success case)', async () => {
// 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 () => {
const mockResult = { const mockResult = {
unique_identifier: 'test-uid', unique_identifier: 'test-uid',
manifest: createMockManifest(), manifest: createMockManifest(),
} }
mockUploadFile.mockRejectedValue({ mockUploadFile.mockResolvedValue(mockResult)
response: mockResult,
})
const onPackageUploaded = vi.fn() const onPackageUploaded = vi.fn()
render( 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() const mockDependencies = createMockDependencies()
mockUploadFile.mockRejectedValue({ mockUploadFile.mockResolvedValue(mockDependencies)
response: mockDependencies,
})
const onBundleUploaded = vi.fn() const onBundleUploaded = vi.fn()
render( render(
@ -261,9 +251,7 @@ describe('Uploading', () => {
// ================================ // ================================
describe('Edge Cases', () => { describe('Edge Cases', () => {
it('should handle empty response gracefully', async () => { it('should handle empty response gracefully', async () => {
mockUploadFile.mockRejectedValue({ mockUploadFile.mockResolvedValue({})
response: {},
})
const onPackageUploaded = vi.fn() const onPackageUploaded = vi.fn()
render(<Uploading {...defaultProps} onPackageUploaded={onPackageUploaded} />) render(<Uploading {...defaultProps} onPackageUploaded={onPackageUploaded} />)
@ -277,9 +265,7 @@ describe('Uploading', () => {
}) })
it('should handle response with only unique_identifier', async () => { it('should handle response with only unique_identifier', async () => {
mockUploadFile.mockRejectedValue({ mockUploadFile.mockResolvedValue({ unique_identifier: 'only-uid' })
response: { unique_identifier: 'only-uid' },
})
const onPackageUploaded = vi.fn() const onPackageUploaded = vi.fn()
render(<Uploading {...defaultProps} onPackageUploaded={onPackageUploaded} />) render(<Uploading {...defaultProps} onPackageUploaded={onPackageUploaded} />)

View File

@ -42,7 +42,7 @@ describe('ReadmeEntrance', () => {
const button = screen.getByRole('button') const button = screen.getByRole('button')
fireEvent.click(button) fireEvent.click(button)
expect(mockSetCurrentPluginDetail).toHaveBeenCalledWith(pluginDetail, 'drawer') expect(mockSetCurrentPluginDetail).toHaveBeenCalledWith(pluginDetail, 'drawer', 'left')
}) })
it('should return null for builtin tools', () => { it('should return null for builtin tools', () => {

View File

@ -167,8 +167,8 @@ describe('formatWorkflowRunIdentifier', () => {
expect(formatWorkflowRunIdentifier(0)).toBe(' (Running)') expect(formatWorkflowRunIdentifier(0)).toBe(' (Running)')
}) })
it('should capitalize custom fallback text', () => { it('should use custom fallback text as-is', () => {
expect(formatWorkflowRunIdentifier(undefined, 'pending')).toBe(' (Pending)') expect(formatWorkflowRunIdentifier(undefined, 'pending')).toBe(' (pending)')
}) })
it('should format a valid timestamp', () => { it('should format a valid timestamp', () => {
@ -178,6 +178,6 @@ describe('formatWorkflowRunIdentifier', () => {
}) })
it('should handle single-char fallback text', () => { it('should handle single-char fallback text', () => {
expect(formatWorkflowRunIdentifier(undefined, 'x')).toBe(' (X)') expect(formatWorkflowRunIdentifier(undefined, 'x')).toBe(' (x)')
}) })
}) })