mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com>
452 lines
13 KiB
TypeScript
452 lines
13 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import type { AppDetailResponse } from '@/models/app'
|
|
import type { AppSSO } from '@/types/app'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
import { act, renderHook } from '@testing-library/react'
|
|
import * as React from 'react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { AppModeEnum } from '@/types/app'
|
|
import { useMCPServiceCardState } from './use-mcp-service-card'
|
|
|
|
// Mutable mock data for MCP server detail
|
|
let mockMCPServerDetailData: {
|
|
id: string
|
|
status: string
|
|
server_code: string
|
|
description: string
|
|
parameters: Record<string, unknown>
|
|
} | undefined = {
|
|
id: 'server-123',
|
|
status: 'active',
|
|
server_code: 'abc123',
|
|
description: 'Test server',
|
|
parameters: {},
|
|
}
|
|
|
|
// Mock service hooks
|
|
vi.mock('@/service/use-tools', () => ({
|
|
useUpdateMCPServer: () => ({
|
|
mutateAsync: vi.fn().mockResolvedValue({}),
|
|
}),
|
|
useRefreshMCPServerCode: () => ({
|
|
mutateAsync: vi.fn().mockResolvedValue({}),
|
|
isPending: false,
|
|
}),
|
|
useMCPServerDetail: () => ({
|
|
data: mockMCPServerDetailData,
|
|
}),
|
|
useInvalidateMCPServerDetail: () => vi.fn(),
|
|
}))
|
|
|
|
// Mock workflow hook
|
|
vi.mock('@/service/use-workflow', () => ({
|
|
useAppWorkflow: (appId: string) => ({
|
|
data: appId
|
|
? {
|
|
graph: {
|
|
nodes: [
|
|
{ data: { type: 'start', variables: [{ variable: 'input', label: 'Input' }] } },
|
|
],
|
|
},
|
|
}
|
|
: undefined,
|
|
}),
|
|
}))
|
|
|
|
// Mock app context
|
|
vi.mock('@/context/app-context', () => ({
|
|
useAppContext: () => ({
|
|
isCurrentWorkspaceManager: true,
|
|
isCurrentWorkspaceEditor: true,
|
|
}),
|
|
}))
|
|
|
|
// Mock apps service
|
|
vi.mock('@/service/apps', () => ({
|
|
fetchAppDetail: vi.fn().mockResolvedValue({
|
|
model_config: {
|
|
updated_at: '2024-01-01',
|
|
user_input_form: [],
|
|
},
|
|
}),
|
|
}))
|
|
|
|
describe('useMCPServiceCardState', () => {
|
|
const createWrapper = () => {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
},
|
|
},
|
|
})
|
|
return ({ children }: { children: ReactNode }) =>
|
|
React.createElement(QueryClientProvider, { client: queryClient }, children)
|
|
}
|
|
|
|
const createMockAppInfo = (mode: AppModeEnum = AppModeEnum.CHAT): AppDetailResponse & Partial<AppSSO> => ({
|
|
id: 'app-123',
|
|
name: 'Test App',
|
|
mode,
|
|
api_base_url: 'https://api.example.com/v1',
|
|
} as AppDetailResponse & Partial<AppSSO>)
|
|
|
|
beforeEach(() => {
|
|
// Reset mock data to default (published server)
|
|
mockMCPServerDetailData = {
|
|
id: 'server-123',
|
|
status: 'active',
|
|
server_code: 'abc123',
|
|
description: 'Test server',
|
|
parameters: {},
|
|
}
|
|
})
|
|
|
|
describe('Initialization', () => {
|
|
it('should initialize with correct default values for basic app', () => {
|
|
const appInfo = createMockAppInfo(AppModeEnum.CHAT)
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.serverPublished).toBe(true)
|
|
expect(result.current.serverActivated).toBe(true)
|
|
expect(result.current.showConfirmDelete).toBe(false)
|
|
expect(result.current.showMCPServerModal).toBe(false)
|
|
})
|
|
|
|
it('should initialize with correct values for workflow app', () => {
|
|
const appInfo = createMockAppInfo(AppModeEnum.WORKFLOW)
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.isLoading).toBe(false)
|
|
})
|
|
|
|
it('should initialize with correct values for advanced chat app', () => {
|
|
const appInfo = createMockAppInfo(AppModeEnum.ADVANCED_CHAT)
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.isLoading).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Server URL Generation', () => {
|
|
it('should generate correct server URL when published', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.serverURL).toBe('https://api.example.com/mcp/server/abc123/mcp')
|
|
})
|
|
})
|
|
|
|
describe('Permission Flags', () => {
|
|
it('should have isCurrentWorkspaceManager as true', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.isCurrentWorkspaceManager).toBe(true)
|
|
})
|
|
|
|
it('should have toggleDisabled false when editor has permissions', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Toggle is not disabled when user has permissions and app is published
|
|
expect(typeof result.current.toggleDisabled).toBe('boolean')
|
|
})
|
|
|
|
it('should have toggleDisabled true when triggerModeDisabled is true', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, true),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.toggleDisabled).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('UI State Actions', () => {
|
|
it('should open confirm delete modal', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.showConfirmDelete).toBe(false)
|
|
|
|
act(() => {
|
|
result.current.openConfirmDelete()
|
|
})
|
|
|
|
expect(result.current.showConfirmDelete).toBe(true)
|
|
})
|
|
|
|
it('should close confirm delete modal', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.openConfirmDelete()
|
|
})
|
|
expect(result.current.showConfirmDelete).toBe(true)
|
|
|
|
act(() => {
|
|
result.current.closeConfirmDelete()
|
|
})
|
|
expect(result.current.showConfirmDelete).toBe(false)
|
|
})
|
|
|
|
it('should open server modal', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.showMCPServerModal).toBe(false)
|
|
|
|
act(() => {
|
|
result.current.openServerModal()
|
|
})
|
|
|
|
expect(result.current.showMCPServerModal).toBe(true)
|
|
})
|
|
|
|
it('should handle server modal hide', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.openServerModal()
|
|
})
|
|
expect(result.current.showMCPServerModal).toBe(true)
|
|
|
|
let hideResult: { shouldDeactivate: boolean } | undefined
|
|
act(() => {
|
|
hideResult = result.current.handleServerModalHide(false)
|
|
})
|
|
|
|
expect(result.current.showMCPServerModal).toBe(false)
|
|
expect(hideResult?.shouldDeactivate).toBe(true)
|
|
})
|
|
|
|
it('should not deactivate when wasActivated is true', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
let hideResult: { shouldDeactivate: boolean } | undefined
|
|
act(() => {
|
|
hideResult = result.current.handleServerModalHide(true)
|
|
})
|
|
|
|
expect(hideResult?.shouldDeactivate).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Handler Functions', () => {
|
|
it('should have handleGenCode function', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(typeof result.current.handleGenCode).toBe('function')
|
|
})
|
|
|
|
it('should call handleGenCode and invalidate server detail', async () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
await act(async () => {
|
|
await result.current.handleGenCode()
|
|
})
|
|
|
|
// handleGenCode should complete without error
|
|
expect(result.current.genLoading).toBe(false)
|
|
})
|
|
|
|
it('should have handleStatusChange function', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(typeof result.current.handleStatusChange).toBe('function')
|
|
})
|
|
|
|
it('should have invalidateBasicAppConfig function', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(typeof result.current.invalidateBasicAppConfig).toBe('function')
|
|
})
|
|
|
|
it('should call invalidateBasicAppConfig', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Call the function - should not throw
|
|
act(() => {
|
|
result.current.invalidateBasicAppConfig()
|
|
})
|
|
|
|
// Function should exist and be callable
|
|
expect(typeof result.current.invalidateBasicAppConfig).toBe('function')
|
|
})
|
|
})
|
|
|
|
describe('Status Change', () => {
|
|
it('should return activated state when status change succeeds', async () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
let statusResult: { activated: boolean } | undefined
|
|
await act(async () => {
|
|
statusResult = await result.current.handleStatusChange(true)
|
|
})
|
|
|
|
expect(statusResult?.activated).toBe(true)
|
|
})
|
|
|
|
it('should return deactivated state when disabling', async () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
let statusResult: { activated: boolean } | undefined
|
|
await act(async () => {
|
|
statusResult = await result.current.handleStatusChange(false)
|
|
})
|
|
|
|
expect(statusResult?.activated).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Unpublished Server', () => {
|
|
it('should open modal and return not activated when enabling unpublished server', async () => {
|
|
// Set mock to return undefined (unpublished server)
|
|
mockMCPServerDetailData = undefined
|
|
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Verify server is not published
|
|
expect(result.current.serverPublished).toBe(false)
|
|
|
|
let statusResult: { activated: boolean } | undefined
|
|
await act(async () => {
|
|
statusResult = await result.current.handleStatusChange(true)
|
|
})
|
|
|
|
// Should open modal and return not activated
|
|
expect(result.current.showMCPServerModal).toBe(true)
|
|
expect(statusResult?.activated).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Loading States', () => {
|
|
it('should have genLoading state', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(typeof result.current.genLoading).toBe('boolean')
|
|
})
|
|
|
|
it('should have isLoading state for basic app', () => {
|
|
const appInfo = createMockAppInfo(AppModeEnum.CHAT)
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Basic app doesn't need workflow, so isLoading should be false
|
|
expect(result.current.isLoading).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Detail Data', () => {
|
|
it('should return detail data when available', () => {
|
|
const appInfo = createMockAppInfo()
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.detail).toBeDefined()
|
|
expect(result.current.detail?.id).toBe('server-123')
|
|
expect(result.current.detail?.status).toBe('active')
|
|
})
|
|
})
|
|
|
|
describe('Latest Params', () => {
|
|
it('should return latestParams for workflow app', () => {
|
|
const appInfo = createMockAppInfo(AppModeEnum.WORKFLOW)
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(Array.isArray(result.current.latestParams)).toBe(true)
|
|
})
|
|
|
|
it('should return latestParams for basic app', () => {
|
|
const appInfo = createMockAppInfo(AppModeEnum.CHAT)
|
|
const { result } = renderHook(
|
|
() => useMCPServiceCardState(appInfo, false),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(Array.isArray(result.current.latestParams)).toBe(true)
|
|
})
|
|
})
|
|
})
|