Files
dify/web/service/debug.spec.ts
Crazywoola 7865ffd429 chore: Improve workflow generator accessibility and API contracts (#38838)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-13 09:15:54 +00:00

242 lines
7.5 KiB
TypeScript

import type { AppModeEnum } from '@/types/app'
// service/base is the dependency we're mocking in this test; the
// no-restricted-imports rule targets production imports, not test
// instrumentation — mirrors sibling service specs (annotation.spec.ts etc.).
// oxlint-disable-next-line no-restricted-imports
import { get, post, ssePost } from './base'
import { consoleClient, streamWorkflowGeneration } from './client'
import {
fetchConversationMessages,
fetchPromptTemplate,
fetchSuggestedQuestions,
fetchTextGenerationMessage,
generateBasicAppFirstTimeRule,
generateRule,
sendCompletionMessage,
stopChatMessageResponding,
} from './debug'
import {
fetchWorkflowInstructionSuggestions,
generateWorkflow,
generateWorkflowStream,
} from './workflow-generator'
// Stub the shared `post` wrapper so tests verify only what `generateWorkflow`
// composes on top of it — URL, body, and the typed response surface.
vi.mock('./base', () => ({
post: vi.fn(),
get: vi.fn(),
ssePost: vi.fn(),
}))
vi.mock('./client', () => ({
streamWorkflowGeneration: vi.fn(),
consoleClient: {
workflowGenerate: {
post: vi.fn(),
suggestions: {
post: vi.fn(),
},
},
},
}))
describe('debug service — generateWorkflow', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// The new endpoint lives at /workflow-generate; the controller mirrors
// /rule-generate so the body must flow through unchanged.
it('should call the generated workflow contract with the body verbatim', () => {
const body = {
mode: 'workflow' as const,
instruction: 'Summarize a URL',
ideal_output: 'A 3-sentence summary.',
model_config: {
provider: 'openai',
name: 'gpt-4o',
mode: 'chat' as const,
completion_params: {},
},
}
generateWorkflow(body)
expect(consoleClient.workflowGenerate.post).toHaveBeenCalledWith(
{ body },
{ signal: expect.any(AbortSignal) },
)
})
// The optional fields must still POST cleanly — `ideal_output` defaulting
// server-side requires the helper to forward the body as-is, not augment it.
it('should pass the body through even when ideal_output is omitted', () => {
const body = {
mode: 'advanced-chat' as const,
instruction: 'Friendly support bot',
model_config: { provider: 'openai', name: 'gpt-4o', mode: 'chat' as const },
}
generateWorkflow(body)
expect(consoleClient.workflowGenerate.post).toHaveBeenCalledWith(
{ body },
{ signal: expect.any(AbortSignal) },
)
})
it('should expose the controller whose signal is passed to the generated client', () => {
const body = {
mode: 'workflow' as const,
instruction: 'Long-running generation',
model_config: { provider: 'openai', name: 'gpt-4o', mode: 'chat' as const },
}
const getAbortController = vi.fn()
generateWorkflow(body, { getAbortController })
const controller = getAbortController.mock.calls[0]![0]
expect(consoleClient.workflowGenerate.post).toHaveBeenCalledWith(
{ body },
{ signal: controller.signal },
)
})
it('should create an internal abort signal when no callback is provided', () => {
const body = {
mode: 'workflow' as const,
instruction: 'Plain call',
model_config: { provider: 'openai', name: 'gpt-4o', mode: 'chat' as const },
}
generateWorkflow(body)
expect(consoleClient.workflowGenerate.post).toHaveBeenCalledWith(
{ body },
{ signal: expect.any(AbortSignal) },
)
})
describe('other endpoints', () => {
it('stopChatMessageResponding', async () => {
await stopChatMessageResponding('app-1', 'task-1')
expect(post).toHaveBeenCalledWith('apps/app-1/chat-messages/task-1/stop')
})
it('sendCompletionMessage', async () => {
const callbacks = {
onData: vi.fn(),
onCompleted: vi.fn(),
onError: vi.fn(),
onMessageReplace: vi.fn(),
}
await sendCompletionMessage('app-1', { text: 'hello' }, callbacks)
expect(ssePost).toHaveBeenCalledWith(
'apps/app-1/completion-messages',
{
body: { text: 'hello', response_mode: 'streaming' },
},
callbacks,
)
})
it('fetchSuggestedQuestions', async () => {
const getAbortController = vi.fn()
await fetchSuggestedQuestions('app-1', 'msg-1', getAbortController)
expect(get).toHaveBeenCalledWith(
'apps/app-1/chat-messages/msg-1/suggested-questions',
{},
{ getAbortController },
)
})
it('fetchConversationMessages', async () => {
const getAbortController = vi.fn()
await fetchConversationMessages('app-1', 'conv-1', getAbortController)
expect(get).toHaveBeenCalledWith(
'apps/app-1/chat-messages',
{ params: { conversation_id: 'conv-1' } },
{ getAbortController },
)
})
it('generateBasicAppFirstTimeRule', async () => {
await generateBasicAppFirstTimeRule({ mode: 'chat' })
expect(post).toHaveBeenCalledWith('/rule-generate', { body: { mode: 'chat' } })
})
it('generateRule', async () => {
await generateRule({ mode: 'chat' })
expect(post).toHaveBeenCalledWith('/instruction-generate', { body: { mode: 'chat' } })
})
it('generateWorkflowStream', async () => {
const body = {
mode: 'workflow' as const,
instruction: 'test',
model_config: { provider: 'test', name: 'test', mode: 'chat' as const },
}
const callbacks = {
onPlan: vi.fn(),
onResult: vi.fn(),
onError: vi.fn(),
onCompleted: vi.fn(),
getAbortController: vi.fn(),
}
vi.mocked(streamWorkflowGeneration).mockImplementation((_url, _body, options) => {
options?.onPlan?.({ title: 'plan' })
options?.onResult?.({ graph: { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } } })
return Promise.resolve()
})
await generateWorkflowStream(body, callbacks)
expect(streamWorkflowGeneration).toHaveBeenCalled()
expect(callbacks.onPlan).toHaveBeenCalled()
expect(callbacks.onResult).toHaveBeenCalled()
})
it('fetchWorkflowInstructionSuggestions without getAbortController', async () => {
await fetchWorkflowInstructionSuggestions({ mode: 'workflow' })
expect(consoleClient.workflowGenerate.suggestions.post).toHaveBeenCalledWith(
{ body: { mode: 'workflow' } },
{ signal: expect.any(AbortSignal) },
)
})
it('fetchWorkflowInstructionSuggestions with getAbortController', async () => {
const getAbortController = vi.fn()
await fetchWorkflowInstructionSuggestions({ mode: 'workflow' }, { getAbortController })
const controller = getAbortController.mock.calls[0]![0]
expect(consoleClient.workflowGenerate.suggestions.post).toHaveBeenCalledWith(
{ body: { mode: 'workflow' } },
{ signal: controller.signal },
)
})
it('fetchPromptTemplate', async () => {
await fetchPromptTemplate({
appMode: 'chat' as AppModeEnum,
mode: 'chat',
modelName: 'gpt-4',
hasSetDataSet: true,
})
expect(get).toHaveBeenCalledWith('/app/prompt-templates', {
params: {
app_mode: 'chat',
model_mode: 'chat',
model_name: 'gpt-4',
has_context: true,
},
})
})
it('fetchTextGenerationMessage', async () => {
await fetchTextGenerationMessage({ appId: 'app-1', messageId: 'msg-1' })
expect(get).toHaveBeenCalledWith('/apps/app-1/messages/msg-1')
})
})
})