mirror of
https://github.com/langgenius/dify.git
synced 2026-07-24 13:08:34 +08:00
fix: text generation web app keeps loading after timeout (#39268)
This commit is contained in:
parent
605c0fe02b
commit
bbc31a3403
@ -11,37 +11,49 @@ const {
|
||||
notifyMock,
|
||||
sendCompletionMessageMock,
|
||||
sendWorkflowMessageMock,
|
||||
sleepMock,
|
||||
stopChatMessageRespondingMock,
|
||||
textGenerationResPropsSpy,
|
||||
} = vi.hoisted(() => ({
|
||||
notifyMock: vi.fn(),
|
||||
sendCompletionMessageMock: vi.fn(),
|
||||
sendWorkflowMessageMock: vi.fn(),
|
||||
sleepMock: vi.fn(),
|
||||
stopChatMessageRespondingMock: vi.fn(),
|
||||
textGenerationResPropsSpy: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('i18next', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('i18next')>()
|
||||
const { createI18nextMock } = await import('@/test/i18n-mock')
|
||||
|
||||
return {
|
||||
...actual,
|
||||
...createI18nextMock(),
|
||||
t: () => '',
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('react-i18next', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('react-i18next')>()
|
||||
const { createReactI18nextMock } = await import('@/test/i18n-mock')
|
||||
|
||||
return {
|
||||
...actual,
|
||||
...createReactI18nextMock({
|
||||
'operation.stopResponding': 'operation.stopResponding',
|
||||
'warningMessage.timeoutExceeded': 'Translated timeout warning',
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
default: {
|
||||
notify: notifyMock,
|
||||
},
|
||||
toast: (...args: unknown[]) => notifyMock(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/utils')>('@/utils')
|
||||
return {
|
||||
...actual,
|
||||
sleep: () => new Promise<void>(() => {}),
|
||||
sleep: (...args: Parameters<typeof actual.sleep>) => sleepMock(...args),
|
||||
}
|
||||
})
|
||||
|
||||
@ -120,6 +132,7 @@ const baseProps = {
|
||||
describe('Result', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
sleepMock.mockImplementation(() => new Promise<void>(() => {}))
|
||||
stopChatMessageRespondingMock.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
@ -271,6 +284,77 @@ describe('Result', () => {
|
||||
expect(onCompleted).toHaveBeenCalledWith('{"answer":"Hello"}', undefined, true)
|
||||
})
|
||||
|
||||
it('should finish a timed-out workflow and show a translated warning', async () => {
|
||||
let resolveTimeout!: () => void
|
||||
let workflowHandlers: IOtherOptions | null = null
|
||||
sleepMock.mockImplementation(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveTimeout = resolve
|
||||
}),
|
||||
)
|
||||
sendWorkflowMessageMock.mockImplementation(async (_data, handlers) => {
|
||||
workflowHandlers = handlers
|
||||
})
|
||||
|
||||
const onCompleted = vi.fn()
|
||||
const { rerender } = render(<Result {...baseProps} isWorkflow onCompleted={onCompleted} />)
|
||||
|
||||
rerender(<Result {...baseProps} isWorkflow controlSend={1} onCompleted={onCompleted} />)
|
||||
|
||||
await act(async () => {
|
||||
workflowHandlers?.onWorkflowStarted?.({
|
||||
workflow_run_id: 'run-timeout',
|
||||
task_id: 'task-timeout',
|
||||
event: 'workflow_started',
|
||||
data: {
|
||||
id: 'run-timeout',
|
||||
workflow_id: 'wf-1',
|
||||
created_at: 0,
|
||||
},
|
||||
})
|
||||
resolveTimeout()
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
workflowHandlers?.onWorkflowFinished?.({
|
||||
task_id: 'task-timeout',
|
||||
workflow_run_id: 'run-timeout',
|
||||
event: 'workflow_finished',
|
||||
data: {
|
||||
id: 'run-timeout',
|
||||
workflow_id: 'wf-1',
|
||||
status: 'succeeded',
|
||||
outputs: { answer: 'Late result' },
|
||||
error: '',
|
||||
elapsed_time: 61,
|
||||
total_tokens: 0,
|
||||
total_steps: 0,
|
||||
created_at: 0,
|
||||
created_by: {
|
||||
id: 'user-1',
|
||||
name: 'User',
|
||||
email: 'user@example.com',
|
||||
},
|
||||
finished_at: 61,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
expect(textGenerationResPropsSpy).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
workflowProcessData: expect.objectContaining({
|
||||
resultText: '',
|
||||
status: 'succeeded',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(notifyMock).toHaveBeenCalledWith('Translated timeout warning', { type: 'warning' })
|
||||
expect(onCompleted).toHaveBeenCalledWith('', undefined, false)
|
||||
expect(onCompleted).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should render batch task ids for both short and long indexes', () => {
|
||||
const { rerender } = render(<Result {...baseProps} isCallBatchAPI taskId={3} />)
|
||||
|
||||
|
||||
@ -637,7 +637,7 @@ describe('createWorkflowStreamHandlers', () => {
|
||||
expect(setup.resetRunState).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle timeout and workflow failures', () => {
|
||||
it('should finish timed-out workflow state and warn without applying late outputs', () => {
|
||||
const timeoutSetup = setupHandlers({
|
||||
isTimedOut: () => true,
|
||||
})
|
||||
@ -654,7 +654,7 @@ describe('createWorkflowStreamHandlers', () => {
|
||||
id: 'run-1',
|
||||
workflow_id: 'wf-1',
|
||||
status: WorkflowRunningStatus.Succeeded,
|
||||
outputs: null,
|
||||
outputs: { answer: 'Late result' },
|
||||
error: '',
|
||||
elapsed_time: 0,
|
||||
total_tokens: 0,
|
||||
@ -674,7 +674,18 @@ describe('createWorkflowStreamHandlers', () => {
|
||||
type: 'warning',
|
||||
message: 'warningMessage.timeoutExceeded',
|
||||
})
|
||||
expect(timeoutSetup.workflowProcessData()).toEqual(
|
||||
expect.objectContaining({
|
||||
resultText: '',
|
||||
status: WorkflowRunningStatus.Succeeded,
|
||||
}),
|
||||
)
|
||||
expect(timeoutSetup.onCompleted).not.toHaveBeenCalled()
|
||||
expect(timeoutSetup.setRespondingFalse).not.toHaveBeenCalled()
|
||||
expect(timeoutSetup.resetRunState).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle workflow failures', () => {
|
||||
const failureSetup = setupHandlers()
|
||||
const failureHandlers = failureSetup.handlers as Required<
|
||||
Pick<IOtherOptions, 'onWorkflowStarted' | 'onWorkflowFinished'>
|
||||
|
||||
@ -7,19 +7,15 @@ import type { AppSourceType } from '@/service/share'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { t } from 'i18next'
|
||||
import { useCallback } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import TextGenerationRes from '@/app/components/app/text-generate/item'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import NoData from '@/app/components/share/text-generation/no-data'
|
||||
import { useResultRunState } from './hooks/use-result-run-state'
|
||||
import { useResultSender } from './hooks/use-result-sender'
|
||||
|
||||
const translateResultKey: TextGenerationTranslate = (selector, options) => {
|
||||
return t(selector, options)
|
||||
}
|
||||
|
||||
type IResultProps = {
|
||||
isWorkflow: boolean
|
||||
isCallBatchAPI: boolean
|
||||
@ -77,6 +73,11 @@ const Result: FC<IResultProps> = ({
|
||||
onRunControlChange,
|
||||
hideInlineStopButton = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const translateResultKey = useCallback<TextGenerationTranslate>(
|
||||
(selector, options) => t(selector, options),
|
||||
[t],
|
||||
)
|
||||
const notify = useCallback(
|
||||
({ type, message }: { type: 'error' | 'info' | 'success' | 'warning'; message: string }) => {
|
||||
toast(message, { type })
|
||||
|
||||
@ -341,15 +341,25 @@ export const createWorkflowStreamHandlers = ({
|
||||
setWorkflowProcessData(finishWorkflowNode(getWorkflowProcessData(), data))
|
||||
},
|
||||
onWorkflowFinished: ({ data }) => {
|
||||
const workflowStatus = data.status as WorkflowRunningStatus | undefined
|
||||
if (isTimedOut()) {
|
||||
const finishedStatus =
|
||||
workflowStatus === WorkflowRunningStatus.Stopped
|
||||
? WorkflowRunningStatus.Stopped
|
||||
: workflowStatus === WorkflowRunningStatus.Failed || data.error
|
||||
? WorkflowRunningStatus.Failed
|
||||
: WorkflowRunningStatus.Succeeded
|
||||
setWorkflowProcessData(
|
||||
applyWorkflowFinishedState(getWorkflowProcessData(), finishedStatus, data.error),
|
||||
)
|
||||
notify({
|
||||
type: 'warning',
|
||||
message: t(($) => $['warningMessage.timeoutExceeded'], { ns: 'appDebug' }),
|
||||
})
|
||||
markEnded()
|
||||
return
|
||||
}
|
||||
|
||||
const workflowStatus = data.status as WorkflowRunningStatus | undefined
|
||||
if (workflowStatus === WorkflowRunningStatus.Stopped) {
|
||||
setWorkflowProcessData(
|
||||
applyWorkflowFinishedState(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user