mirror of
https://github.com/langgenius/dify.git
synced 2026-04-30 15:38:08 +08:00
Merge main HEAD (segment 5) into sandboxed-agent-rebase
Resolve 83 conflicts: 10 backend, 62 frontend, 11 config/lock files. Preserve sandbox/agent/collaboration features while adopting main's UI refactorings (Dialog/AlertDialog/Popover), model provider updates, and enterprise features. Made-with: Cursor
This commit is contained in:
@ -0,0 +1,27 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { createDocLinkMock, resolveDocLink } from '../../__tests__/i18n'
|
||||
import Empty from '../empty'
|
||||
|
||||
const mockDocLink = createDocLinkMock()
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => mockDocLink,
|
||||
}))
|
||||
|
||||
describe('VariableInspect Empty', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render the empty-state copy and docs link', () => {
|
||||
render(<Empty />)
|
||||
|
||||
const link = screen.getByRole('link', { name: 'workflow.debug.variableInspect.emptyLink' })
|
||||
|
||||
expect(screen.getByText('workflow.debug.variableInspect.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.debug.variableInspect.emptyTip')).toBeInTheDocument()
|
||||
expect(link).toHaveAttribute('href', resolveDocLink('/use-dify/debug/variable-inspect'))
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(mockDocLink).toHaveBeenCalledWith('/use-dify/debug/variable-inspect')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,131 @@
|
||||
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import { BlockEnum, VarType } from '../../types'
|
||||
import Group from '../group'
|
||||
|
||||
const mockUseToolIcon = vi.fn(() => '')
|
||||
|
||||
vi.mock('../../hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../hooks')>()
|
||||
return {
|
||||
...actual,
|
||||
useToolIcon: () => mockUseToolIcon(),
|
||||
}
|
||||
})
|
||||
|
||||
const createVar = (overrides: Partial<VarInInspect> = {}): VarInInspect => ({
|
||||
id: 'var-1',
|
||||
type: VarInInspectType.node,
|
||||
name: 'message',
|
||||
description: '',
|
||||
selector: ['node-1', 'message'],
|
||||
value_type: VarType.string,
|
||||
value: 'hello',
|
||||
edited: false,
|
||||
visible: true,
|
||||
is_truncated: false,
|
||||
full_content: {
|
||||
size_bytes: 0,
|
||||
download_url: '',
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createNodeData = (overrides: Partial<NodeWithVar> = {}): NodeWithVar => ({
|
||||
nodeId: 'node-1',
|
||||
nodePayload: {
|
||||
type: BlockEnum.Code,
|
||||
title: 'Code',
|
||||
desc: '',
|
||||
},
|
||||
nodeType: BlockEnum.Code,
|
||||
title: 'Code',
|
||||
vars: [],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('VariableInspect Group', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should mask secret environment variables before selecting them', () => {
|
||||
const handleSelect = vi.fn()
|
||||
|
||||
render(
|
||||
<Group
|
||||
varType={VarInInspectType.environment}
|
||||
varList={[
|
||||
createVar({
|
||||
id: 'env-secret',
|
||||
type: VarInInspectType.environment,
|
||||
name: 'API_KEY',
|
||||
value_type: VarType.secret,
|
||||
value: 'plain-secret',
|
||||
}),
|
||||
]}
|
||||
handleSelect={handleSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('API_KEY'))
|
||||
|
||||
expect(screen.getByText('workflow.debug.variableInspect.envNode')).toBeInTheDocument()
|
||||
expect(handleSelect).toHaveBeenCalledWith({
|
||||
nodeId: VarInInspectType.environment,
|
||||
nodeType: VarInInspectType.environment,
|
||||
title: VarInInspectType.environment,
|
||||
var: expect.objectContaining({
|
||||
id: 'env-secret',
|
||||
type: VarInInspectType.environment,
|
||||
value: '******************',
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it('should hide invisible variables and collapse the list when the group header is clicked', () => {
|
||||
render(
|
||||
<Group
|
||||
nodeData={createNodeData()}
|
||||
varType={VarInInspectType.node}
|
||||
varList={[
|
||||
createVar({ id: 'visible-var', name: 'visible_var' }),
|
||||
createVar({ id: 'hidden-var', name: 'hidden_var', visible: false }),
|
||||
]}
|
||||
handleSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('visible_var')).toBeInTheDocument()
|
||||
expect(screen.queryByText('hidden_var')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('Code'))
|
||||
|
||||
expect(screen.queryByText('visible_var')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose node view and clear actions for node groups', () => {
|
||||
const handleView = vi.fn()
|
||||
const handleClear = vi.fn()
|
||||
|
||||
render(
|
||||
<Group
|
||||
nodeData={createNodeData()}
|
||||
varType={VarInInspectType.node}
|
||||
varList={[createVar()]}
|
||||
handleSelect={vi.fn()}
|
||||
handleView={handleView}
|
||||
handleClear={handleClear}
|
||||
/>,
|
||||
)
|
||||
|
||||
const actionButtons = screen.getAllByRole('button')
|
||||
|
||||
fireEvent.click(actionButtons[0])
|
||||
fireEvent.click(actionButtons[1])
|
||||
|
||||
expect(handleView).toHaveBeenCalledTimes(1)
|
||||
expect(handleClear).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,19 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import LargeDataAlert from '../large-data-alert'
|
||||
|
||||
describe('LargeDataAlert', () => {
|
||||
it('should render the default message and export action when a download URL exists', () => {
|
||||
const { container } = render(<LargeDataAlert downloadUrl="https://example.com/export.json" className="extra-alert" />)
|
||||
|
||||
expect(screen.getByText('workflow.debug.variableInspect.largeData')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.debug.variableInspect.export')).toBeInTheDocument()
|
||||
expect(container.firstChild).toHaveClass('extra-alert')
|
||||
})
|
||||
|
||||
it('should render the no-export message and omit the export action when the URL is missing', () => {
|
||||
render(<LargeDataAlert textHasNoExport />)
|
||||
|
||||
expect(screen.getByText('workflow.debug.variableInspect.largeDataNoExport')).toBeInTheDocument()
|
||||
expect(screen.queryByText('workflow.debug.variableInspect.export')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,173 @@
|
||||
import type { EnvironmentVariable } from '../../types'
|
||||
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWorkflowFlowComponent } from '../../__tests__/workflow-test-env'
|
||||
import { BlockEnum } from '../../types'
|
||||
import Panel from '../panel'
|
||||
import { EVENT_WORKFLOW_STOP } from '../types'
|
||||
|
||||
type InspectVarsState = {
|
||||
conversationVars: VarInInspect[]
|
||||
systemVars: VarInInspect[]
|
||||
nodesWithInspectVars: NodeWithVar[]
|
||||
}
|
||||
|
||||
const {
|
||||
mockEditInspectVarValue,
|
||||
mockEmit,
|
||||
mockFetchInspectVarValue,
|
||||
mockHandleNodeSelect,
|
||||
mockResetConversationVar,
|
||||
mockResetToLastRunVar,
|
||||
mockSetInputs,
|
||||
} = vi.hoisted(() => ({
|
||||
mockEditInspectVarValue: vi.fn(),
|
||||
mockEmit: vi.fn(),
|
||||
mockFetchInspectVarValue: vi.fn(),
|
||||
mockHandleNodeSelect: vi.fn(),
|
||||
mockResetConversationVar: vi.fn(),
|
||||
mockResetToLastRunVar: vi.fn(),
|
||||
mockSetInputs: vi.fn(),
|
||||
}))
|
||||
|
||||
let inspectVarsState: InspectVarsState
|
||||
|
||||
vi.mock('../../hooks/use-inspect-vars-crud', () => ({
|
||||
default: () => ({
|
||||
...inspectVarsState,
|
||||
deleteAllInspectorVars: vi.fn(),
|
||||
deleteNodeInspectorVars: vi.fn(),
|
||||
editInspectVarValue: mockEditInspectVarValue,
|
||||
fetchInspectVarValue: mockFetchInspectVarValue,
|
||||
resetConversationVar: mockResetConversationVar,
|
||||
resetToLastRunVar: mockResetToLastRunVar,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../nodes/_base/components/variable/use-match-schema-type', () => ({
|
||||
default: () => ({
|
||||
isLoading: false,
|
||||
schemaTypeDefinitions: {},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks/use-nodes-interactions', () => ({
|
||||
useNodesInteractions: () => ({
|
||||
handleNodeSelect: mockHandleNodeSelect,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../hooks')>()
|
||||
return {
|
||||
...actual,
|
||||
useNodesInteractions: () => ({
|
||||
handleNodeSelect: mockHandleNodeSelect,
|
||||
}),
|
||||
useToolIcon: () => '',
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../nodes/_base/hooks/use-node-crud', () => ({
|
||||
default: () => ({
|
||||
setInputs: mockSetInputs,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../nodes/_base/hooks/use-node-info', () => ({
|
||||
default: () => ({
|
||||
node: undefined,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks-store', () => ({
|
||||
useHooksStore: <T,>(selector: (state: { configsMap?: { flowId: string } }) => T) =>
|
||||
selector({
|
||||
configsMap: {
|
||||
flowId: 'flow-1',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({
|
||||
eventEmitter: {
|
||||
emit: mockEmit,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
const createEnvironmentVariable = (overrides: Partial<EnvironmentVariable> = {}): EnvironmentVariable => ({
|
||||
id: 'env-1',
|
||||
name: 'API_KEY',
|
||||
value: 'env-value',
|
||||
value_type: 'string',
|
||||
description: '',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const renderPanel = (initialStoreState: Record<string, unknown> = {}) => {
|
||||
return renderWorkflowFlowComponent(
|
||||
<Panel />,
|
||||
{
|
||||
nodes: [],
|
||||
edges: [],
|
||||
initialStoreState,
|
||||
historyStore: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
describe('VariableInspect Panel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
inspectVarsState = {
|
||||
conversationVars: [],
|
||||
systemVars: [],
|
||||
nodesWithInspectVars: [],
|
||||
}
|
||||
})
|
||||
|
||||
it('should render the listening state and stop the workflow on demand', () => {
|
||||
renderPanel({
|
||||
isListening: true,
|
||||
listeningTriggerType: BlockEnum.TriggerWebhook,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'workflow.debug.variableInspect.listening.stopButton' }))
|
||||
|
||||
expect(screen.getByText('workflow.debug.variableInspect.listening.title')).toBeInTheDocument()
|
||||
expect(mockEmit).toHaveBeenCalledWith({
|
||||
type: EVENT_WORKFLOW_STOP,
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the empty state and close the panel from the header action', () => {
|
||||
const { store } = renderPanel({
|
||||
showVariableInspectPanel: true,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getAllByRole('button')[0])
|
||||
|
||||
expect(screen.getByText('workflow.debug.variableInspect.emptyTip')).toBeInTheDocument()
|
||||
expect(store.getState().showVariableInspectPanel).toBe(false)
|
||||
})
|
||||
|
||||
it('should select an environment variable and show its details in the right panel', async () => {
|
||||
renderPanel({
|
||||
environmentVariables: [createEnvironmentVariable()],
|
||||
bottomPanelWidth: 560,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('API_KEY'))
|
||||
|
||||
await waitFor(() => expect(screen.getAllByText('API_KEY').length).toBeGreaterThan(1))
|
||||
|
||||
expect(screen.getByText('workflow.debug.variableInspect.envNode')).toBeInTheDocument()
|
||||
expect(screen.getAllByText('string').length).toBeGreaterThan(0)
|
||||
expect(screen.getByText('env-value')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,153 @@
|
||||
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import { createNode } from '../../__tests__/fixtures'
|
||||
import { baseRunningData, renderWorkflowFlowComponent } from '../../__tests__/workflow-test-env'
|
||||
import { BlockEnum, NodeRunningStatus, VarType, WorkflowRunningStatus } from '../../types'
|
||||
import VariableInspectTrigger from '../trigger'
|
||||
|
||||
type InspectVarsState = {
|
||||
conversationVars: VarInInspect[]
|
||||
systemVars: VarInInspect[]
|
||||
nodesWithInspectVars: NodeWithVar[]
|
||||
}
|
||||
|
||||
const {
|
||||
mockDeleteAllInspectorVars,
|
||||
mockEmit,
|
||||
} = vi.hoisted(() => ({
|
||||
mockDeleteAllInspectorVars: vi.fn(),
|
||||
mockEmit: vi.fn(),
|
||||
}))
|
||||
|
||||
let inspectVarsState: InspectVarsState
|
||||
|
||||
vi.mock('../../hooks/use-inspect-vars-crud', () => ({
|
||||
default: () => ({
|
||||
...inspectVarsState,
|
||||
deleteAllInspectorVars: mockDeleteAllInspectorVars,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({
|
||||
eventEmitter: {
|
||||
emit: mockEmit,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
const createVariable = (overrides: Partial<VarInInspect> = {}): VarInInspect => ({
|
||||
id: 'var-1',
|
||||
type: VarInInspectType.node,
|
||||
name: 'result',
|
||||
description: '',
|
||||
selector: ['node-1', 'result'],
|
||||
value_type: VarType.string,
|
||||
value: 'cached',
|
||||
edited: false,
|
||||
visible: true,
|
||||
is_truncated: false,
|
||||
full_content: {
|
||||
size_bytes: 0,
|
||||
download_url: '',
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const renderTrigger = ({
|
||||
nodes = [createNode()],
|
||||
initialStoreState = {},
|
||||
}: {
|
||||
nodes?: Array<ReturnType<typeof createNode>>
|
||||
initialStoreState?: Record<string, unknown>
|
||||
} = {}) => {
|
||||
return renderWorkflowFlowComponent(<VariableInspectTrigger />, { nodes, edges: [], initialStoreState })
|
||||
}
|
||||
|
||||
describe('VariableInspectTrigger', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
inspectVarsState = {
|
||||
conversationVars: [],
|
||||
systemVars: [],
|
||||
nodesWithInspectVars: [],
|
||||
}
|
||||
})
|
||||
|
||||
it('should stay hidden when the variable-inspect panel is already open', () => {
|
||||
renderTrigger({
|
||||
initialStoreState: {
|
||||
showVariableInspectPanel: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.queryByText('workflow.debug.variableInspect.trigger.normal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open the panel from the normal trigger state', () => {
|
||||
const { store } = renderTrigger()
|
||||
|
||||
fireEvent.click(screen.getByText('workflow.debug.variableInspect.trigger.normal'))
|
||||
|
||||
expect(store.getState().showVariableInspectPanel).toBe(true)
|
||||
})
|
||||
|
||||
it('should block opening while the workflow is read only', () => {
|
||||
const { store } = renderTrigger({
|
||||
initialStoreState: {
|
||||
isRestoring: true,
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('workflow.debug.variableInspect.trigger.normal'))
|
||||
|
||||
expect(store.getState().showVariableInspectPanel).toBe(false)
|
||||
})
|
||||
|
||||
it('should clear cached variables and reset the focused node', () => {
|
||||
inspectVarsState = {
|
||||
conversationVars: [createVariable({
|
||||
id: 'conversation-var',
|
||||
type: VarInInspectType.conversation,
|
||||
})],
|
||||
systemVars: [],
|
||||
nodesWithInspectVars: [],
|
||||
}
|
||||
|
||||
const { store } = renderTrigger({
|
||||
initialStoreState: {
|
||||
currentFocusNodeId: 'node-2',
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('workflow.debug.variableInspect.trigger.clear'))
|
||||
|
||||
expect(screen.getByText('workflow.debug.variableInspect.trigger.cached')).toBeInTheDocument()
|
||||
expect(mockDeleteAllInspectorVars).toHaveBeenCalledTimes(1)
|
||||
expect(store.getState().currentFocusNodeId).toBe('')
|
||||
})
|
||||
|
||||
it('should show the running state and open the panel while running', () => {
|
||||
const { store } = renderTrigger({
|
||||
nodes: [createNode({
|
||||
data: {
|
||||
type: BlockEnum.Code,
|
||||
title: 'Code',
|
||||
desc: '',
|
||||
_singleRunningStatus: NodeRunningStatus.Running,
|
||||
},
|
||||
})],
|
||||
initialStoreState: {
|
||||
workflowRunningData: baseRunningData({
|
||||
result: { status: WorkflowRunningStatus.Running },
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('workflow.debug.variableInspect.trigger.running'))
|
||||
|
||||
expect(screen.queryByText('workflow.debug.variableInspect.trigger.clear')).not.toBeInTheDocument()
|
||||
expect(store.getState().showVariableInspectPanel).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user