mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
fix(skill): guard file collaboration initialization
This commit is contained in:
parent
de62fd15bd
commit
e13100098a
@ -0,0 +1,75 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { useSkillMarkdownCollaboration } from '../use-skill-markdown-collaboration'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
openFile: vi.fn<(appId: string, fileId: string, initialContent: string) => void>(),
|
||||
setActiveFile: vi.fn<(appId: string, fileId: string, isActive: boolean) => void>(),
|
||||
subscribe: vi.fn<(fileId: string, callback: (text: string, source: 'remote') => void) => () => void>(() => vi.fn()),
|
||||
onSyncRequest: vi.fn<(fileId: string, callback: () => void) => () => void>(() => vi.fn()),
|
||||
updateText: vi.fn<(fileId: string, text: string) => void>(),
|
||||
isLeader: vi.fn<(fileId: string) => boolean>(() => false),
|
||||
getState: vi.fn(() => ({
|
||||
clearDraftContent: vi.fn(),
|
||||
setDraftContent: vi.fn(),
|
||||
pinTab: vi.fn(),
|
||||
})),
|
||||
emit: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: mocks.getState,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({
|
||||
eventEmitter: {
|
||||
emit: mocks.emit,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../skill-collaboration-manager', () => ({
|
||||
skillCollaborationManager: {
|
||||
openFile: (appId: string, fileId: string, initialContent: string) => mocks.openFile(appId, fileId, initialContent),
|
||||
setActiveFile: (appId: string, fileId: string, isActive: boolean) => mocks.setActiveFile(appId, fileId, isActive),
|
||||
subscribe: (fileId: string, callback: (text: string, source: 'remote') => void) => mocks.subscribe(fileId, callback),
|
||||
onSyncRequest: (fileId: string, callback: () => void) => mocks.onSyncRequest(fileId, callback),
|
||||
updateText: (fileId: string, text: string) => mocks.updateText(fileId, text),
|
||||
isLeader: (fileId: string) => mocks.isLeader(fileId),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useSkillMarkdownCollaboration', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should degrade gracefully when collaboration initialization fails', async () => {
|
||||
const error = new TypeError('Cannot read properties of undefined (reading "lorodoc_new")')
|
||||
mocks.openFile.mockImplementation(() => {
|
||||
throw error
|
||||
})
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
renderHook(() => useSkillMarkdownCollaboration({
|
||||
appId: 'app-1',
|
||||
fileId: 'file-1',
|
||||
enabled: true,
|
||||
initialContent: 'hello',
|
||||
baselineContent: 'hello',
|
||||
onLocalChange: vi.fn(),
|
||||
onLeaderSync: vi.fn(),
|
||||
}))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to initialize skill collaboration:', error)
|
||||
})
|
||||
expect(mocks.setActiveFile).not.toHaveBeenCalled()
|
||||
expect(mocks.subscribe).not.toHaveBeenCalled()
|
||||
expect(mocks.onSyncRequest).not.toHaveBeenCalled()
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
@ -49,7 +49,14 @@ export const useSkillCodeCollaboration = ({
|
||||
if (!enabled || !fileId)
|
||||
return
|
||||
|
||||
skillCollaborationManager.openFile(appId, fileId, initialContent)
|
||||
try {
|
||||
skillCollaborationManager.openFile(appId, fileId, initialContent)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to initialize skill collaboration:', error)
|
||||
return
|
||||
}
|
||||
|
||||
skillCollaborationManager.setActiveFile(appId, fileId, true)
|
||||
|
||||
const unsubscribe = skillCollaborationManager.subscribe(fileId, (nextText) => {
|
||||
|
||||
@ -52,7 +52,14 @@ export const useSkillMarkdownCollaboration = ({
|
||||
if (!enabled || !fileId)
|
||||
return
|
||||
|
||||
skillCollaborationManager.openFile(appId, fileId, initialContent)
|
||||
try {
|
||||
skillCollaborationManager.openFile(appId, fileId, initialContent)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to initialize skill collaboration:', error)
|
||||
return
|
||||
}
|
||||
|
||||
skillCollaborationManager.setActiveFile(appId, fileId, true)
|
||||
|
||||
const unsubscribe = skillCollaborationManager.subscribe(fileId, (nextText) => {
|
||||
|
||||
@ -12,6 +12,12 @@ type AppStoreState = {
|
||||
} | null
|
||||
}
|
||||
|
||||
type GlobalPublicState = {
|
||||
systemFeatures: {
|
||||
enable_collaboration_mode: boolean
|
||||
}
|
||||
}
|
||||
|
||||
type WorkflowStoreState = {
|
||||
activeTabId: string | null
|
||||
editorAutoFocusFileId: string | null
|
||||
@ -127,6 +133,11 @@ const mocks = vi.hoisted(() => ({
|
||||
id: 'app-1',
|
||||
},
|
||||
} as AppStoreState,
|
||||
globalPublicState: {
|
||||
systemFeatures: {
|
||||
enable_collaboration_mode: true,
|
||||
},
|
||||
} as GlobalPublicState,
|
||||
workflowState: {
|
||||
activeTabId: 'file-1',
|
||||
editorAutoFocusFileId: null,
|
||||
@ -228,6 +239,10 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
useGlobalPublicStore: (selector: (state: GlobalPublicState) => unknown) => selector(mocks.globalPublicState),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: mocks.appTheme }),
|
||||
}))
|
||||
@ -389,6 +404,7 @@ describe('FileContentPanel', () => {
|
||||
mocks.appState.appDetail = { id: 'app-1' }
|
||||
mocks.workflowState.activeTabId = 'file-1'
|
||||
mocks.workflowState.editorAutoFocusFileId = null
|
||||
mocks.globalPublicState.systemFeatures.enable_collaboration_mode = true
|
||||
mocks.workflowState.dirtyContents = new Map<string, string>()
|
||||
mocks.workflowState.fileMetadata = new Map<string, Record<string, unknown>>()
|
||||
mocks.workflowState.dirtyMetadataIds = new Set<string>()
|
||||
@ -564,6 +580,22 @@ describe('FileContentPanel', () => {
|
||||
expect(mocks.saveFile).toHaveBeenCalledWith('file-1')
|
||||
})
|
||||
|
||||
it('should disable skill collaboration when system collaboration feature is off', async () => {
|
||||
// Arrange
|
||||
mocks.globalPublicState.systemFeatures.enable_collaboration_mode = false
|
||||
|
||||
// Act
|
||||
render(<FileContentPanel />)
|
||||
await screen.findByTestId('code-editor')
|
||||
|
||||
// Assert
|
||||
expect(mocks.useSkillCodeCollaboration).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
enabled: false,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should ignore editor content updates when file is not editable', async () => {
|
||||
// Arrange
|
||||
mocks.fileTypeInfo = {
|
||||
|
||||
@ -6,6 +6,7 @@ import isDeepEqual from 'fast-deep-equal'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useSkillCodeCollaboration } from '../../../../collaboration/skills/use-skill-code-collaboration'
|
||||
import { useSkillMarkdownCollaboration } from '../../../../collaboration/skills/use-skill-markdown-collaboration'
|
||||
import { START_TAB_ID } from '../../../constants'
|
||||
@ -21,6 +22,7 @@ import { extractFileReferenceIds } from './utils'
|
||||
export const useFileContentController = (): FileContentControllerState => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appId = appDetail?.id || ''
|
||||
const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
|
||||
const activeTabId = useStore(s => s.activeTabId)
|
||||
const editorAutoFocusFileId = useStore(s => s.editorAutoFocusFileId)
|
||||
const storeApi = useWorkflowStore()
|
||||
@ -67,7 +69,7 @@ export const useFileContentController = (): FileContentControllerState => {
|
||||
const originalContent = fileContent?.content ?? ''
|
||||
const currentContent = draftContent !== undefined ? draftContent : originalContent
|
||||
const initialContentRegistryRef = useRef<Map<string, string>>(new Map())
|
||||
const canInitCollaboration = Boolean(appId && fileTabId && isEditable && !isLoading && !error)
|
||||
const canInitCollaboration = Boolean(isCollaborationEnabled && appId && fileTabId && isEditable && !isLoading && !error)
|
||||
|
||||
if (canInitCollaboration && fileTabId && !initialContentRegistryRef.current.has(fileTabId))
|
||||
initialContentRegistryRef.current.set(fileTabId, currentContent)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user