stabilize MCP service card collaboration subscription

This commit is contained in:
hjlarry 2026-04-13 17:51:43 +08:00
parent f0cd2e8465
commit 5e0e0982bc
2 changed files with 54 additions and 23 deletions

View File

@ -26,6 +26,23 @@ const mockHandleGenCode = vi.fn()
const mockOpenConfirmDelete = vi.fn()
const mockCloseConfirmDelete = vi.fn()
const mockOpenServerModal = vi.fn()
const mockOnMcpServerUpdate = vi.hoisted(() => vi.fn())
const mockUnsubscribeMcpServerUpdate = vi.hoisted(() => vi.fn())
const invalidateMCPServerDetailFns = vi.hoisted(() => [] as Array<ReturnType<typeof vi.fn>>)
vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', () => ({
collaborationManager: {
onMcpServerUpdate: mockOnMcpServerUpdate,
},
}))
vi.mock('@/service/use-tools', () => ({
useInvalidateMCPServerDetail: () => {
const invalidateFn = vi.fn()
invalidateMCPServerDetailFns.push(invalidateFn)
return invalidateFn
},
}))
type MockHookState = {
genLoading: boolean
@ -106,12 +123,15 @@ describe('MCPServiceCard', () => {
beforeEach(() => {
mockHookState = createDefaultHookState()
invalidateMCPServerDetailFns.length = 0
mockHandleStatusChange.mockClear().mockResolvedValue({ activated: true })
mockHandleServerModalHide.mockClear().mockReturnValue({ shouldDeactivate: false })
mockHandleGenCode.mockClear()
mockOpenConfirmDelete.mockClear()
mockCloseConfirmDelete.mockClear()
mockOpenServerModal.mockClear()
mockUnsubscribeMcpServerUpdate.mockClear()
mockOnMcpServerUpdate.mockReset().mockReturnValue(mockUnsubscribeMcpServerUpdate)
})
describe('Rendering', () => {
@ -431,4 +451,26 @@ describe('MCPServiceCard', () => {
expect(screen.getByRole('switch')).toBeInTheDocument()
})
})
describe('Collaboration Sync', () => {
it('should keep a stable MCP update subscription across rerenders and invalidate with the latest callback', async () => {
let mcpUpdateHandler: ((payload: unknown) => void) | undefined
mockOnMcpServerUpdate.mockImplementation((handler: (payload: unknown) => void) => {
mcpUpdateHandler = handler
return mockUnsubscribeMcpServerUpdate
})
const { rerender } = render(<MCPServiceCard appInfo={createMockAppInfo()} />, { wrapper: createWrapper() })
rerender(<MCPServiceCard appInfo={createMockAppInfo()} />, { wrapper: createWrapper() })
expect(mockOnMcpServerUpdate).toHaveBeenCalledTimes(1)
expect(invalidateMCPServerDetailFns).toHaveLength(2)
mcpUpdateHandler?.({ type: 'mcp_server_update' })
expect(invalidateMCPServerDetailFns[0]).not.toHaveBeenCalled()
expect(invalidateMCPServerDetailFns[1]).toHaveBeenCalledWith('app-123')
})
})
})

View File

@ -5,7 +5,7 @@ import type { CollaborationUpdate } from '@/app/components/workflow/collaboratio
import type { AppDetailResponse } from '@/models/app'
import type { AppSSO } from '@/types/app'
import { RiEditLine, RiLoopLeftLine } from '@remixicon/react'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
@ -16,6 +16,7 @@ import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal'
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
import { useDocLink } from '@/context/i18n'
import { useInvalidateMCPServerDetail } from '@/service/use-tools'
import { cn } from '@/utils/classnames'
@ -166,6 +167,11 @@ const MCPServiceCard: FC<IAppCardProps> = ({
const docLink = useDocLink()
const appId = appInfo.id
const invalidateMCPServerDetail = useInvalidateMCPServerDetail()
const invalidateMCPServerDetailRef = useRef(invalidateMCPServerDetail)
useEffect(() => {
invalidateMCPServerDetailRef.current = invalidateMCPServerDetail
}, [invalidateMCPServerDetail])
const {
genLoading,
@ -258,34 +264,17 @@ const MCPServiceCard: FC<IAppCardProps> = ({
if (!appId)
return
let unsubscribe: (() => void) | undefined
let cancelled = false
void (async () => {
const unsubscribe = collaborationManager.onMcpServerUpdate((_update: CollaborationUpdate) => {
try {
const { collaborationManager } = await import('@/app/components/workflow/collaboration/core/collaboration-manager')
if (cancelled)
return
unsubscribe = collaborationManager.onMcpServerUpdate((_update: CollaborationUpdate) => {
try {
invalidateMCPServerDetail(appId)
}
catch (error) {
console.error('MCP server update failed:', error)
}
})
invalidateMCPServerDetailRef.current(appId)
}
catch (error) {
console.error('MCP collaboration subscription failed:', error)
console.error('MCP server update failed:', error)
}
})
return () => {
cancelled = true
unsubscribe?.()
}
}, [appId, invalidateMCPServerDetail])
return unsubscribe
}, [appId])
if (isLoading)
return null