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:
Novice
2026-03-23 14:20:06 +08:00
1671 changed files with 124822 additions and 22302 deletions

View File

@ -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')
})
})

View File

@ -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)
})
})

View File

@ -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()
})
})

View File

@ -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()
})
})

View File

@ -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)
})
})