From b43ebf539df71dafe2a1354c8ec02a236184e56f Mon Sep 17 00:00:00 2001 From: Jingyi Date: Mon, 4 May 2026 08:07:21 -0700 Subject: [PATCH] fix: preserve single-run input variable types (#35710) --- .../hooks/__tests__/use-one-step-run.spec.ts | 239 ++++++++++++++++++ .../nodes/_base/hooks/use-one-step-run.ts | 19 +- 2 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts diff --git a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts new file mode 100644 index 0000000000..108ab818f3 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts @@ -0,0 +1,239 @@ +import { renderHook } from '@testing-library/react' +import { + BlockEnum, + InputVarType, + VarType, +} from '@/app/components/workflow/types' +import { FlowType } from '@/types/common' +import useOneStepRun from '../use-one-step-run' + +const mockWorkflowState = { + conversationVariables: [], + dataSourceList: [], + nodesWithInspectVars: [], + setNodesWithInspectVars: vi.fn(), + setShowSingleRunPanel: vi.fn(), + setIsListening: vi.fn(), + setListeningTriggerType: vi.fn(), + setListeningTriggerNodeId: vi.fn(), + setListeningTriggerNodeIds: vi.fn(), + setListeningTriggerIsAll: vi.fn(), + setShowVariableInspectPanel: vi.fn(), +} + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@langgenius/dify-ui/toast', () => ({ + toast: { + error: vi.fn(), + }, +})) + +vi.mock('@/app/components/base/amplitude', () => ({ + trackEvent: vi.fn(), +})) + +vi.mock('@/app/components/workflow/hooks', () => ({ + useIsChatMode: () => false, + useNodeDataUpdate: () => ({ + handleNodeDataUpdate: vi.fn(), + }), + useWorkflow: () => ({ + getBeforeNodesInSameBranch: () => [ + { + id: 'start', + data: { + type: 'start', + title: 'Start', + variables: [], + }, + }, + ], + getBeforeNodesInSameBranchIncludeParent: () => [ + { + id: 'start', + data: { + type: 'start', + title: 'Start', + variables: [], + }, + }, + ], + }), +})) + +vi.mock('@/app/components/workflow/hooks/use-inspect-vars-crud', () => ({ + default: () => ({ + appendNodeInspectVars: vi.fn(), + invalidateSysVarValues: vi.fn(), + invalidateConversationVarValues: vi.fn(), + }), +})) + +vi.mock('@/app/components/workflow/store', () => ({ + useStore: (selector: (state: typeof mockWorkflowState) => unknown) => selector(mockWorkflowState), + useWorkflowStore: () => ({ + getState: () => mockWorkflowState, + }), +})) + +vi.mock('reactflow', () => ({ + useStoreApi: () => ({ + getState: () => ({ + getNodes: () => [], + }), + }), +})) + +vi.mock('@/service/use-tools', () => ({ + useAllBuiltInTools: () => ({ data: [] }), + useAllCustomTools: () => ({ data: [] }), + useAllWorkflowTools: () => ({ data: [] }), + useAllMCPTools: () => ({ data: [] }), +})) + +vi.mock('@/service/use-workflow', () => ({ + useInvalidLastRun: () => vi.fn(), +})) + +vi.mock('@/service/workflow', () => ({ + fetchNodeInspectVars: vi.fn(), + getIterationSingleNodeRunUrl: vi.fn(), + getLoopSingleNodeRunUrl: vi.fn(), + singleNodeRun: vi.fn(), +})) + +vi.mock('@/service/base', () => ({ + post: vi.fn(), + ssePost: vi.fn(), +})) + +vi.mock('@/context/event-emitter', () => ({ + useEventEmitterContextContext: () => ({ + eventEmitter: { + useSubscription: vi.fn(), + }, + }), +})) + +vi.mock('../components/variable/use-match-schema-type', () => ({ + default: () => ({ + schemaTypeDefinitions: [], + }), +})) + +vi.mock('@/app/components/workflow/nodes/_base/components/variable/use-match-schema-type', () => ({ + default: () => ({ + schemaTypeDefinitions: [], + }), +})) + +vi.mock('@/app/components/workflow/nodes/assigner/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/code/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/document-extractor/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/http/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/human-input/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/if-else/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/iteration/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/knowledge-retrieval/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/llm/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/loop/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/parameter-extractor/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/question-classifier/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/template-transform/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/tool/default', () => ({ + default: {}, +})) +vi.mock('@/app/components/workflow/nodes/variable-assigner/default', () => ({ + default: {}, +})) + +const renderUseOneStepRun = () => renderHook(() => useOneStepRun({ + id: 'if-else-node', + flowId: 'app-id', + flowType: FlowType.appFlow, + data: { + type: BlockEnum.IfElse, + title: 'IF/ELSE', + desc: '', + }, + defaultRunInputData: {}, + isRunAfterSingleRun: false, + isPaused: false, +})) + +describe('useOneStepRun single-run input vars', () => { + beforeEach(() => { + vi.clearAllMocks() + Object.defineProperty(globalThis, 'location', { + value: { + pathname: '/app/test-app/workflow', + }, + configurable: true, + }) + }) + + it('uses value_type when the variable cannot be resolved from output vars', () => { + const { result } = renderUseOneStepRun() + + const inputs = result.current.toVarInputs([ + { + variable: '#start.amount#', + value_selector: ['start', 'amount'], + value_type: VarType.number, + }, + ]) + + expect(inputs).toMatchObject([ + { + variable: '#start.amount#', + type: InputVarType.number, + }, + ]) + }) + + it('resolves global system vars by full variable name', () => { + const { result } = renderUseOneStepRun() + + const inputs = result.current.varSelectorsToVarInputs([ + ['sys', 'timestamp'], + ]) + + expect(inputs).toMatchObject([ + { + variable: '#sys.timestamp#', + type: InputVarType.number, + }, + ]) + }) +}) diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index a455ea480a..810029f5ad 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -178,13 +178,15 @@ const useOneStepRun = ({ } const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables, [], allPluginInfoList, schemaTypeDefinitions) - const targetVar = allOutputVars.find(item => isSystem ? !!item.isStartNode : item.nodeId === valueSelector[0]) + if (isSystem) { + const selectorKey = valueSelector.join('.') + return allOutputVars.flatMap(item => item.vars).find(item => item.variable === selectorKey) + } + + const targetVar = allOutputVars.find(item => item.nodeId === valueSelector[0]) if (!targetVar) return undefined - if (isSystem) - return targetVar.vars.find(item => item.variable.split('.')[1] === valueSelector[1]) - let curr: any = targetVar.vars for (let i = 1; i < valueSelector.length; i++) { const key = valueSelector[i] @@ -1079,12 +1081,19 @@ const useOneStepRun = ({ const varInputs = variables.filter(item => !isENV(item.value_selector)).map((item) => { const originalVar = getVar(item.value_selector) if (!originalVar) { + const fallbackType = item.value_type + ? varTypeToInputVarType(item.value_type, { + isSelect: !!item.options?.length, + isParagraph: !!item.isParagraph, + }) + : InputVarType.textInput return { label: item.label || item.variable, variable: item.variable, - type: InputVarType.textInput, + type: fallbackType, required: true, value_selector: item.value_selector, + options: item.options, } } return {