fix: allow publishing workflows with legacy Agent nodes (#41368)

This commit is contained in:
Joel
2026-08-27 07:26:48 +00:00
committed by GitHub
parent 1ccac4792c
commit 1be72af661
4 changed files with 112 additions and 34 deletions
@@ -40,7 +40,7 @@ describe('useAvailableNodesMetaData', () => {
)
})
it('should expose legacy Agent instead of Agent v2 in chat mode when Agent v2 is enabled', () => {
it('should expose only legacy Agent in chat mode while retaining Agent v2 metadata', () => {
mockUseIsChatMode.mockReturnValue(true)
const { result } = renderHook(() => useAvailableNodesMetaData())
@@ -49,7 +49,7 @@ describe('useAvailableNodesMetaData', () => {
expect(nodeTypes).toContain(BlockEnum.Agent)
expect(nodeTypes).not.toContain(BlockEnum.AgentV2)
expect(result.current.nodesMap?.[BlockEnum.Agent]).toBeDefined()
expect(result.current.nodesMap?.[BlockEnum.AgentV2]).toBeUndefined()
expect(result.current.nodesMap?.[BlockEnum.AgentV2]).toBeDefined()
})
it('should include workflow-specific trigger and end nodes outside chat mode', () => {
@@ -88,7 +88,7 @@ describe('useAvailableNodesMetaData', () => {
)
})
it('should expose Agent v2 instead of legacy Agent when Agent v2 is enabled', () => {
it('should hide legacy Agent from the node picker but retain its validator when Agent v2 is enabled', () => {
mockUseIsChatMode.mockReturnValue(false)
const { result } = renderHook(() => useAvailableNodesMetaData())
@@ -96,14 +96,14 @@ describe('useAvailableNodesMetaData', () => {
expect(nodeTypes).toContain(BlockEnum.AgentV2)
expect(nodeTypes).not.toContain(BlockEnum.Agent)
expect(result.current.nodesMap?.[BlockEnum.AgentV2]).toBeDefined()
expect(result.current.nodesMap?.[BlockEnum.Agent]).toBeUndefined()
expect(result.current.nodesMap?.[BlockEnum.AgentV2]?.checkValid).toEqual(expect.any(Function))
expect(result.current.nodesMap?.[BlockEnum.Agent]?.checkValid).toEqual(expect.any(Function))
expect(result.current.nodesMap?.[BlockEnum.AgentV2]?.metaData.helpLinkUri).toBe(
'/docs/use-dify/nodes/agent#choose-an-agent',
)
})
it('should expose legacy Agent instead of Agent v2 when Agent v2 is disabled', () => {
it('should expose only legacy Agent while retaining Agent v2 metadata when Agent v2 is disabled', () => {
mockUseIsChatMode.mockReturnValue(false)
mockIsAgentV2Enabled.mockReturnValue(false)
@@ -113,7 +113,7 @@ describe('useAvailableNodesMetaData', () => {
expect(nodeTypes).toContain(BlockEnum.Agent)
expect(nodeTypes).not.toContain(BlockEnum.AgentV2)
expect(result.current.nodesMap?.[BlockEnum.Agent]).toBeDefined()
expect(result.current.nodesMap?.[BlockEnum.AgentV2]).toBeUndefined()
expect(result.current.nodesMap?.[BlockEnum.AgentV2]).toBeDefined()
expect(result.current.nodesMap?.[BlockEnum.Agent]?.metaData.helpLinkUri).toBe(
'/docs/use-dify/nodes/agent',
)
@@ -43,14 +43,8 @@ export const useAvailableNodesMetaData = () => {
)
const mergedNodesMetaData = useMemo(() => {
const commonNodes = WORKFLOW_COMMON_NODES.filter((node) =>
shouldUseAgentV2
? node.metaData.type !== BlockEnum.Agent
: node.metaData.type !== BlockEnum.AgentV2,
)
return [
...commonNodes,
...WORKFLOW_COMMON_NODES,
startNodeMetaData,
...(isChatMode
? [AnswerDefault]
@@ -62,9 +56,9 @@ export const useAvailableNodesMetaData = () => {
TriggerPluginDefault,
]),
]
}, [isChatMode, shouldUseAgentV2, startNodeMetaData])
}, [isChatMode, startNodeMetaData])
const availableNodesMetaData = useMemo(
const nodesMetaData = useMemo(
() =>
mergedNodesMetaData.map((node) => {
const { metaData } = node
@@ -93,25 +87,35 @@ export const useAvailableNodesMetaData = () => {
[mergedNodesMetaData, t, docLink],
)
const availableNodesMetaDataMap = useMemo(
const availableNodesMetaData = useMemo(
() =>
availableNodesMetaData.reduce(
nodesMetaData.filter((node) =>
shouldUseAgentV2
? node.metaData.type !== BlockEnum.Agent
: node.metaData.type !== BlockEnum.AgentV2,
),
[nodesMetaData, shouldUseAgentV2],
)
const nodesMetaDataMap = useMemo(
() =>
nodesMetaData.reduce(
(acc, node) => {
acc![node.metaData.type] = node
return acc
},
{} as AvailableNodesMetaData['nodesMap'],
),
[availableNodesMetaData],
[nodesMetaData],
)
return useMemo(() => {
return {
nodes: availableNodesMetaData,
nodesMap: {
...availableNodesMetaDataMap,
[BlockEnum.VariableAssigner]: availableNodesMetaDataMap?.[BlockEnum.VariableAggregator],
...nodesMetaDataMap,
[BlockEnum.VariableAssigner]: nodesMetaDataMap?.[BlockEnum.VariableAggregator],
},
}
}, [availableNodesMetaData, availableNodesMetaDataMap])
}, [availableNodesMetaData, nodesMetaDataMap])
}
@@ -4,7 +4,7 @@ import type { ChecklistItem } from '../use-checklist'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { zWorkflowAgentComposerResponse } from '@dify/contracts/api/console/apps/zod.gen'
import { QueryClient } from '@tanstack/react-query'
import { screen, waitFor } from '@testing-library/react'
import { act, screen, waitFor } from '@testing-library/react'
import { createElement, Fragment } from 'react'
import { CollectionType } from '@/app/components/tools/types'
import { consoleQuery } from '@/service/client'
@@ -14,7 +14,7 @@ import { resetReactFlowMockState, rfState } from '../../__tests__/reactflow-mock
import { renderWorkflowComponent, renderWorkflowHook } from '../../__tests__/workflow-test-env'
import { useStore } from '../../store'
import { BlockEnum } from '../../types'
import { useChecklist, useWorkflowRunValidation } from '../use-checklist'
import { useChecklist, useChecklistBeforePublish, useWorkflowRunValidation } from '../use-checklist'
// ---------------------------------------------------------------------------
// Mocks
@@ -113,7 +113,11 @@ vi.mock('../use-nodes-available-var-list', () => ({
}
return map
},
useGetNodesAvailableVarList: () => ({ getNodesAvailableVarList: vi.fn(() => ({})) }),
useGetNodesAvailableVarList: () => ({
getNodesAvailableVarList: vi.fn((nodes: Node[]) =>
Object.fromEntries(nodes.map((node) => [node.id, { availableVars: [] }])),
),
}),
}))
vi.mock('../../nodes/_base/components/variable/utils', () => ({
@@ -223,6 +227,24 @@ function buildConnectedGraph() {
return { nodes, edges }
}
function buildLegacyAgentGraph() {
const startNode = createNode({ id: 'start', data: { type: BlockEnum.Start, title: 'Start' } })
const agentNode = createNode({
id: 'legacy-agent',
data: {
type: BlockEnum.Agent,
title: 'Legacy Agent',
agent_strategy_provider_name: 'provider',
agent_strategy_name: 'strategy',
},
})
return {
nodes: [startNode, agentNode],
edges: [createEdge({ source: 'start', target: 'legacy-agent' })],
}
}
function buildInlineAgentGraph({
difyTools = [],
hasModel = true,
@@ -399,6 +421,19 @@ describe('useChecklist', () => {
expect(warning!.errorMessages).toContain('Model not configured')
})
it('should validate legacy Agent nodes when their metadata is hidden by Agent v2', () => {
const { nodes, edges } = buildLegacyAgentGraph()
const { result } = renderWorkflowHook(() => useChecklist(nodes, edges))
expect(result.current).toEqual([
expect.objectContaining({
id: 'legacy-agent',
errorMessages: ['workflow.nodes.agent.checkList.strategyNotSelected'],
}),
])
})
it.each([
{
errorMessage: 'agentV2.agentDetail.configure.files.missing',
@@ -768,6 +803,27 @@ describe('useChecklist', () => {
})
})
// ---------------------------------------------------------------------------
// useChecklistBeforePublish
// ---------------------------------------------------------------------------
describe('useChecklistBeforePublish', () => {
it('should reject an invalid legacy Agent instead of throwing when its metadata is hidden', async () => {
const { nodes, edges } = buildLegacyAgentGraph()
rfState.nodes = nodes as unknown as typeof rfState.nodes
rfState.edges = edges as unknown as typeof rfState.edges
const { result } = renderWorkflowHook(() => useChecklistBeforePublish())
let isValid: boolean | undefined
await act(async () => {
isValid = await result.current.handleCheckBeforePublish()
})
expect(isValid).toBe(false)
})
})
// ---------------------------------------------------------------------------
// useWorkflowRunValidation
// ---------------------------------------------------------------------------
@@ -55,6 +55,7 @@ import { useDatasetsDetailStore } from '../datasets-detail-store/store'
import { useHooksStore } from '../hooks-store/store'
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
import { hasValidInlineAgentBinding, isAgentV2NodeData } from '../nodes/agent-v2/types'
import AgentDefault from '../nodes/agent/default'
import { IndexMethodEnum } from '../nodes/knowledge-base/types'
import {
getLLMModelIssue,
@@ -94,6 +95,9 @@ export type ChecklistItem = {
}
type CheckValidExtraData = Record<string, unknown> | undefined
type NodeValidator = NonNullable<
ReturnType<typeof useNodesMetaData>['nodesMap']
>[BlockEnum]['checkValid']
const EMPTY_ENVIRONMENT_VARIABLES: EnvironmentVariable[] = []
@@ -106,6 +110,17 @@ const withFlowType = (moreDataForCheckValid: CheckValidExtraData, flowType?: Flo
}
}
const resolveNodeValidator = (
data: CommonNodeType,
nodesExtraData: ReturnType<typeof useNodesMetaData>['nodesMap'],
): NodeValidator | undefined => {
const validator = nodesExtraData?.[getNodeCatalogType(data)]?.checkValid
if (validator) return validator
if (data.type === BlockEnum.Agent && !isAgentV2NodeData(data))
return AgentDefault.checkValid as NodeValidator
}
const START_NODE_TYPES: BlockEnum[] = [
BlockEnum.Start,
BlockEnum.TriggerSchedule,
@@ -391,7 +406,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
if (node!.type === CUSTOM_NODE) {
const checkData = getCheckData(node!.data)
const validator = nodesExtraData?.[getNodeCatalogType(node!.data)]?.checkValid
const validator = resolveNodeValidator(node!.data, nodesExtraData)
const isPluginMissing = isNodePluginMissing(node!.data, {
builtInTools: buildInTools,
customTools,
@@ -785,15 +800,18 @@ export const useChecklistBeforePublish = () => {
}
const checkData = getCheckData(node!.data, datasets, embeddingProviderModelMap)
const { errorMessage } = nodesExtraData![getNodeCatalogType(node!.data)].checkValid(
checkData,
t,
withFlowType(moreDataForCheckValid, flowType),
)
const validator = resolveNodeValidator(node!.data, nodesExtraData)
if (validator) {
const { errorMessage } = validator(
checkData,
t,
withFlowType(moreDataForCheckValid, flowType),
)
if (errorMessage) {
toast.error(`[${node!.data.title}] ${errorMessage}`)
return false
if (errorMessage) {
toast.error(`[${node!.data.title}] ${errorMessage}`)
return false
}
}
const duplicateOutputMessages = duplicateEndOutputMessages.get(node!.id) || []