mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-05 05:32:02 +00:00
[Manager] Add registry search fallback with gateway pattern (#4187)
This commit is contained in:
445
tests-ui/tests/services/registrySearchGateway.test.ts
Normal file
445
tests-ui/tests/services/registrySearchGateway.test.ts
Normal file
@@ -0,0 +1,445 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useRegistrySearchGateway } from '@/services/gateway/registrySearchGateway'
|
||||
import { useAlgoliaSearchProvider } from '@/services/providers/algoliaSearchProvider'
|
||||
import { useComfyRegistrySearchProvider } from '@/services/providers/registrySearchProvider'
|
||||
|
||||
// Mock the provider modules to control their behavior
|
||||
vi.mock('@/services/providers/algoliaSearchProvider')
|
||||
vi.mock('@/services/providers/registrySearchProvider')
|
||||
|
||||
describe('useRegistrySearchGateway', () => {
|
||||
let consoleWarnSpy: any
|
||||
let consoleInfoSpy: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
consoleWarnSpy.mockRestore()
|
||||
consoleInfoSpy.mockRestore()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('Provider initialization', () => {
|
||||
it('should initialize with both providers', () => {
|
||||
const mockAlgoliaProvider = {
|
||||
searchPacks: vi.fn(),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi.fn(),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
|
||||
expect(useAlgoliaSearchProvider).toHaveBeenCalled()
|
||||
expect(useComfyRegistrySearchProvider).toHaveBeenCalled()
|
||||
expect(gateway).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle Algolia initialization failure gracefully', () => {
|
||||
vi.mocked(useAlgoliaSearchProvider).mockImplementation(() => {
|
||||
throw new Error('Algolia init failed')
|
||||
})
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ nodePacks: [], querySuggestions: [] }),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
|
||||
// Gateway should still work with just the Registry provider
|
||||
expect(gateway).toBeDefined()
|
||||
expect(typeof gateway.searchPacks).toBe('function')
|
||||
|
||||
// Verify it can still search using the fallback provider
|
||||
return expect(
|
||||
gateway.searchPacks('test', { pageSize: 10, pageNumber: 0 })
|
||||
).resolves.toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Search functionality', () => {
|
||||
it('should use Algolia provider by default and fallback on failure', async () => {
|
||||
const algoliaResult = {
|
||||
nodePacks: [{ id: 'algolia-1', name: 'Algolia Pack' }],
|
||||
querySuggestions: []
|
||||
}
|
||||
const registryResult = {
|
||||
nodePacks: [{ id: 'registry-1', name: 'Registry Pack' }],
|
||||
querySuggestions: []
|
||||
}
|
||||
|
||||
const mockAlgoliaProvider = {
|
||||
searchPacks: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(algoliaResult)
|
||||
.mockRejectedValueOnce(new Error('Algolia failed')),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi.fn().mockResolvedValue(registryResult),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
|
||||
// First call should use Algolia
|
||||
const result1 = await gateway.searchPacks('test', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
expect(result1.nodePacks[0].name).toBe('Algolia Pack')
|
||||
|
||||
// Second call should fallback to Registry when Algolia fails
|
||||
const result2 = await gateway.searchPacks('test', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
expect(result2.nodePacks[0].name).toBe('Registry Pack')
|
||||
})
|
||||
|
||||
it('should throw error when all providers fail', async () => {
|
||||
const mockAlgoliaProvider = {
|
||||
searchPacks: vi.fn().mockRejectedValue(new Error('Algolia failed')),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi.fn().mockRejectedValue(new Error('Registry failed')),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
|
||||
await expect(
|
||||
gateway.searchPacks('test', { pageSize: 10, pageNumber: 0 })
|
||||
).rejects.toThrow('All search providers failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Circuit breaker functionality', () => {
|
||||
it('should switch to fallback provider after failure and log warnings', async () => {
|
||||
const registryResult = {
|
||||
nodePacks: [{ id: 'registry-1', name: 'Registry Pack' }],
|
||||
querySuggestions: []
|
||||
}
|
||||
|
||||
// Create mock that fails
|
||||
const mockAlgoliaProvider = {
|
||||
searchPacks: vi.fn().mockRejectedValue(new Error('Algolia failed')),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi.fn().mockResolvedValue(registryResult),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
|
||||
// First call should try Algolia, fail, and use Registry
|
||||
const result = await gateway.searchPacks('test', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
|
||||
expect(mockAlgoliaProvider.searchPacks).toHaveBeenCalledTimes(1)
|
||||
expect(mockRegistryProvider.searchPacks).toHaveBeenCalledTimes(1)
|
||||
expect(result.nodePacks[0].name).toBe('Registry Pack')
|
||||
|
||||
// Circuit breaker behavior is internal implementation detail
|
||||
// We only test the observable behavior (fallback works)
|
||||
})
|
||||
|
||||
it('should have circuit breaker timeout mechanism', () => {
|
||||
// This test verifies that the constants exist for circuit breaker behavior
|
||||
// The actual circuit breaker logic is tested in integration with real provider behavior
|
||||
expect(typeof useRegistrySearchGateway).toBe('function')
|
||||
|
||||
// We can test that the gateway logs circuit breaker behavior
|
||||
const mockAlgoliaProvider = {
|
||||
searchPacks: vi.fn().mockRejectedValue(new Error('Persistent failure')),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ nodePacks: [], querySuggestions: [] }),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
expect(gateway).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cache management', () => {
|
||||
it('should clear cache for all providers', () => {
|
||||
const mockAlgoliaProvider = {
|
||||
searchPacks: vi.fn(),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi.fn(),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
gateway.clearSearchCache()
|
||||
|
||||
expect(mockAlgoliaProvider.clearSearchCache).toHaveBeenCalled()
|
||||
expect(mockRegistryProvider.clearSearchCache).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle cache clear failures gracefully', () => {
|
||||
const mockAlgoliaProvider = {
|
||||
searchPacks: vi.fn(),
|
||||
clearSearchCache: vi.fn().mockImplementation(() => {
|
||||
throw new Error('Cache clear failed')
|
||||
}),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi.fn(),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
|
||||
// Should not throw when clearing cache even if one provider fails
|
||||
expect(() => gateway.clearSearchCache()).not.toThrow()
|
||||
|
||||
// Should still attempt to clear cache for all providers
|
||||
expect(mockAlgoliaProvider.clearSearchCache).toHaveBeenCalled()
|
||||
expect(mockRegistryProvider.clearSearchCache).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Sort functionality', () => {
|
||||
it('should use sort fields from active provider', () => {
|
||||
const algoliaFields = [
|
||||
{ id: 'downloads', label: 'Downloads', direction: 'desc' }
|
||||
]
|
||||
|
||||
const mockAlgoliaProvider = {
|
||||
searchPacks: vi.fn(),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue(algoliaFields)
|
||||
}
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi.fn(),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
const sortFields = gateway.getSortableFields()
|
||||
|
||||
expect(sortFields).toEqual(algoliaFields)
|
||||
})
|
||||
|
||||
it('should switch sort fields when provider changes', async () => {
|
||||
const algoliaFields = [
|
||||
{ id: 'downloads', label: 'Downloads', direction: 'desc' }
|
||||
]
|
||||
const registryFields = [{ id: 'name', label: 'Name', direction: 'asc' }]
|
||||
|
||||
const mockAlgoliaProvider = {
|
||||
searchPacks: vi.fn().mockRejectedValue(new Error('Algolia failed')),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue(algoliaFields)
|
||||
}
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ nodePacks: [], querySuggestions: [] }),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue(registryFields)
|
||||
}
|
||||
|
||||
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
|
||||
// Initially should use Algolia's sort fields
|
||||
expect(gateway.getSortableFields()).toEqual(algoliaFields)
|
||||
|
||||
// Force a search to trigger provider switch
|
||||
await gateway.searchPacks('test', { pageSize: 10, pageNumber: 0 })
|
||||
|
||||
// Now should use Registry's sort fields
|
||||
expect(gateway.getSortableFields()).toEqual(registryFields)
|
||||
})
|
||||
|
||||
it('should delegate getSortValue to active provider', () => {
|
||||
const mockAlgoliaProvider = {
|
||||
searchPacks: vi.fn(),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn().mockReturnValue(100),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi.fn(),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
const pack = { id: '1', name: 'Test Pack' }
|
||||
|
||||
const value = gateway.getSortValue(pack, 'downloads')
|
||||
|
||||
expect(mockAlgoliaProvider.getSortValue).toHaveBeenCalledWith(
|
||||
pack,
|
||||
'downloads'
|
||||
)
|
||||
expect(value).toBe(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Provider recovery', () => {
|
||||
it('should use fallback provider when primary fails', async () => {
|
||||
const algoliaError = new Error('Algolia service unavailable')
|
||||
const registryResult = {
|
||||
nodePacks: [{ id: 'registry-1', name: 'Registry Pack' }],
|
||||
querySuggestions: []
|
||||
}
|
||||
|
||||
const mockAlgoliaProvider = {
|
||||
searchPacks: vi.fn().mockRejectedValue(algoliaError),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
const mockRegistryProvider = {
|
||||
searchPacks: vi.fn().mockResolvedValue(registryResult),
|
||||
clearSearchCache: vi.fn(),
|
||||
getSortValue: vi.fn(),
|
||||
getSortableFields: vi.fn().mockReturnValue([])
|
||||
}
|
||||
|
||||
vi.mocked(useAlgoliaSearchProvider).mockReturnValue(mockAlgoliaProvider)
|
||||
vi.mocked(useComfyRegistrySearchProvider).mockReturnValue(
|
||||
mockRegistryProvider
|
||||
)
|
||||
|
||||
const gateway = useRegistrySearchGateway()
|
||||
|
||||
// Should fallback to Registry when Algolia fails
|
||||
const result = await gateway.searchPacks('test', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
|
||||
expect(result.nodePacks[0].name).toBe('Registry Pack')
|
||||
expect(mockAlgoliaProvider.searchPacks).toHaveBeenCalledTimes(1)
|
||||
expect(mockRegistryProvider.searchPacks).toHaveBeenCalledTimes(1)
|
||||
|
||||
// The gateway successfully handled the failure and returned results
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user