Files
dify/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-message-sender.spec.ts
yyh 30b9295156 Merge remote-tracking branch 'origin/build/feat/hitl' into wip/hitl-merge-web-conflicts-20260206-175434
# Conflicts:
#	api/.env.example
#	api/core/app/apps/advanced_chat/app_generator.py
#	api/core/app/apps/advanced_chat/app_runner.py
#	api/core/app/apps/advanced_chat/generate_task_pipeline.py
#	api/core/app/apps/workflow/app_generator.py
#	api/core/app/apps/workflow/app_runner.py
#	api/core/app/entities/queue_entities.py
#	api/core/workflow/node_events/__init__.py
#	api/core/workflow/runtime/graph_runtime_state.py
#	api/fields/message_fields.py
#	api/services/workflow_service.py
#	web/app/components/app/app-publisher/index.tsx
#	web/app/components/base/chat/chat/answer/index.tsx
#	web/app/components/base/chat/chat/hooks.ts
#	web/app/components/base/chat/chat/type.ts
#	web/app/components/base/prompt-editor/index.tsx
#	web/app/components/rag-pipeline/hooks/use-available-nodes-meta-data.ts
#	web/app/components/share/text-generation/result/header.tsx
#	web/app/components/workflow-app/components/workflow-header/features-trigger.tsx
#	web/app/components/workflow-app/hooks/use-workflow-run.ts
#	web/app/components/workflow/hooks/use-checklist.ts
#	web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts
#	web/app/components/workflow/hooks/use-workflow.ts
#	web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx
#	web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx
#	web/app/components/workflow/nodes/_base/node.tsx
#	web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/components/placeholder.tsx
#	web/app/components/workflow/panel/debug-and-preview/hooks.ts
#	web/app/components/workflow/panel/workflow-preview.tsx
#	web/app/components/workflow/store/workflow/workflow-slice.ts
#	web/eslint-suppressions.json
#	web/i18n/en-US/common.json
#	web/i18n/zh-Hans/common.json
#	web/i18n/zh-Hant/common.json
#	web/service/workflow.ts
2026-02-06 19:13:51 +08:00

229 lines
6.0 KiB
TypeScript

import { act, renderHook } from '@testing-library/react'
import { vi } from 'vitest'
import { WorkflowRunningStatus } from '../../../types'
import { useChatMessageSender } from './use-chat-message-sender'
const {
mockSseGet,
mockSubmitHumanInputForm,
mockHandleRun,
mockFetchInspectVars,
mockInvalidAllLastRun,
mockInvalidateSandboxFiles,
} = vi.hoisted(() => ({
mockSseGet: vi.fn(),
mockSubmitHumanInputForm: vi.fn(),
mockHandleRun: vi.fn(),
mockFetchInspectVars: vi.fn(),
mockInvalidAllLastRun: vi.fn(),
mockInvalidateSandboxFiles: vi.fn(),
}))
type ChatTreeNode = {
id: string
children?: ChatTreeNode[]
workflowProcess?: {
status: WorkflowRunningStatus
tracing: unknown[]
}
[key: string]: unknown
}
type ChatPreviewStoreState = {
chatTree: ChatTreeNode[]
updateChatTree: (updater: (current: ChatTreeNode[]) => ChatTreeNode[]) => void
setConversationId: ReturnType<typeof vi.fn>
setTargetMessageId: ReturnType<typeof vi.fn>
setSuggestedQuestions: ReturnType<typeof vi.fn>
setActiveTaskId: ReturnType<typeof vi.fn>
setHasStopResponded: ReturnType<typeof vi.fn>
setSuggestedQuestionsAbortController: ReturnType<typeof vi.fn>
setWorkflowEventsAbortController: ReturnType<typeof vi.fn>
startRun: ReturnType<typeof vi.fn>
}
const mockStoreState: ChatPreviewStoreState = {
chatTree: [],
updateChatTree: () => {},
setConversationId: vi.fn(),
setTargetMessageId: vi.fn(),
setSuggestedQuestions: vi.fn(),
setActiveTaskId: vi.fn(),
setHasStopResponded: vi.fn(),
setSuggestedQuestionsAbortController: vi.fn(),
setWorkflowEventsAbortController: vi.fn(),
startRun: vi.fn(() => 1),
}
const mockWorkflowStore = {
getState: vi.fn(() => ({
isResponding: false,
activeRunId: 1,
suggestedQuestionsAbortController: null,
workflowEventsAbortController: null as AbortController | null,
workflowRunningData: {
result: {
status: WorkflowRunningStatus.Running,
},
},
hasStopResponded: false,
})),
}
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
vi.mock('@/app/components/base/toast', () => ({
useToastContext: () => ({
notify: vi.fn(),
}),
}))
vi.mock('@/service/base', () => ({
sseGet: (...args: unknown[]) => mockSseGet(...args),
}))
vi.mock('@/service/workflow', () => ({
submitHumanInputForm: (...args: unknown[]) => mockSubmitHumanInputForm(...args),
}))
vi.mock('@/service/use-workflow', () => ({
useInvalidAllLastRun: () => mockInvalidAllLastRun,
}))
vi.mock('@/service/use-sandbox-file', () => ({
useInvalidateSandboxFiles: () => mockInvalidateSandboxFiles,
}))
vi.mock('../../../hooks', () => ({
useWorkflowRun: () => ({
handleRun: mockHandleRun,
}),
useSetWorkflowVarsWithValue: () => ({
fetchInspectVars: mockFetchInspectVars,
}),
}))
vi.mock('../../../hooks-store', () => ({
useHooksStore: () => ({
configsMap: {
flowType: 'workflow',
flowId: 'flow-1',
},
}),
}))
vi.mock('../../../store', () => ({
useStore: (selector: (state: ChatPreviewStoreState) => unknown) => selector(mockStoreState),
useWorkflowStore: () => mockWorkflowStore,
}))
describe('useChatMessageSender HITL regression', () => {
beforeEach(() => {
vi.clearAllMocks()
mockStoreState.chatTree = [
{
id: 'question-1',
isAnswer: false,
children: [
{
id: 'answer-1',
isAnswer: true,
content: '',
workflowProcess: {
status: WorkflowRunningStatus.Running,
tracing: [],
},
children: [],
},
],
},
]
mockStoreState.updateChatTree = (updater) => {
mockStoreState.chatTree = updater(mockStoreState.chatTree)
}
mockSseGet.mockImplementation(() => undefined)
mockWorkflowStore.getState.mockReturnValue({
isResponding: false,
activeRunId: 1,
suggestedQuestionsAbortController: null,
workflowEventsAbortController: null,
workflowRunningData: {
result: {
status: WorkflowRunningStatus.Running,
},
},
hasStopResponded: false,
})
})
it('should subscribe with include_state_snapshot and re-subscribe on workflow paused event', () => {
let callCount = 0
mockSseGet.mockImplementation((url: string, _params: unknown, otherOptions: {
getAbortController?: (abortController: AbortController) => void
onWorkflowPaused?: (event: { data: { workflow_run_id: string } }) => void
}) => {
callCount += 1
otherOptions.getAbortController?.(new AbortController())
if (callCount === 1) {
otherOptions.onWorkflowPaused?.({
data: {
workflow_run_id: 'workflow-run-2',
},
})
}
return undefined
})
const { result } = renderHook(() => useChatMessageSender({
threadMessages: [],
config: undefined,
formSettings: undefined,
handleResponding: vi.fn(),
updateCurrentQAOnTree: vi.fn(),
}))
act(() => {
result.current.handleResume('answer-1', 'workflow-run-1', {})
})
expect(mockSseGet).toHaveBeenNthCalledWith(
1,
'/workflow/workflow-run-1/events?include_state_snapshot=true',
{},
expect.any(Object),
)
expect(mockSseGet).toHaveBeenNthCalledWith(
2,
'/workflow/workflow-run-2/events',
{},
expect.any(Object),
)
})
it('should forward human input form submission to workflow service', async () => {
const { result } = renderHook(() => useChatMessageSender({
threadMessages: [],
config: undefined,
formSettings: undefined,
handleResponding: vi.fn(),
updateCurrentQAOnTree: vi.fn(),
}))
await act(async () => {
await result.current.handleSubmitHumanInputForm('form-token-1', {
inputs: { foo: 'bar' },
action: 'submit',
})
})
expect(mockSubmitHumanInputForm).toHaveBeenCalledWith('form-token-1', {
inputs: { foo: 'bar' },
action: 'submit',
})
})
})