mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-22 23:39:45 +00:00
[Manager] Add registry search fallback with gateway pattern (#4187)
This commit is contained in:
365
tests-ui/tests/services/algoliaSearchProvider.test.ts
Normal file
365
tests-ui/tests/services/algoliaSearchProvider.test.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useAlgoliaSearchProvider } from '@/services/providers/algoliaSearchProvider'
|
||||
import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
|
||||
|
||||
// Mock global Algolia constants
|
||||
|
||||
;(global as any).__ALGOLIA_APP_ID__ = 'test-app-id'
|
||||
;(global as any).__ALGOLIA_API_KEY__ = 'test-api-key'
|
||||
|
||||
// Mock algoliasearch
|
||||
vi.mock('algoliasearch/dist/lite/builds/browser', () => ({
|
||||
liteClient: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useAlgoliaSearchProvider', () => {
|
||||
let mockSearchClient: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Create mock search client
|
||||
mockSearchClient = {
|
||||
search: vi.fn()
|
||||
}
|
||||
|
||||
vi.mocked(algoliasearch).mockReturnValue(mockSearchClient)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Clear the module-level cache between tests
|
||||
const provider = useAlgoliaSearchProvider()
|
||||
provider.clearSearchCache()
|
||||
})
|
||||
|
||||
describe('searchPacks', () => {
|
||||
it('should search for packs and convert results', async () => {
|
||||
const mockAlgoliaResults = {
|
||||
results: [
|
||||
{
|
||||
hits: [
|
||||
{
|
||||
objectID: 'algolia-1',
|
||||
id: 'pack-1',
|
||||
name: 'Test Pack',
|
||||
description: 'A test pack',
|
||||
publisher_id: 'publisher-1',
|
||||
total_install: 500,
|
||||
create_time: '2024-01-01T00:00:00Z',
|
||||
update_time: '2024-01-15T00:00:00Z',
|
||||
repository_url: 'https://github.com/test/pack',
|
||||
license: 'MIT',
|
||||
status: 'active',
|
||||
latest_version: '1.0.0',
|
||||
latest_version_status: 'published',
|
||||
icon_url: 'https://example.com/icon.png',
|
||||
comfy_nodes: ['LoadImage', 'SaveImage']
|
||||
}
|
||||
]
|
||||
},
|
||||
{ hits: [] } // Query suggestions
|
||||
]
|
||||
}
|
||||
|
||||
mockSearchClient.search.mockResolvedValue(mockAlgoliaResults)
|
||||
|
||||
const provider = useAlgoliaSearchProvider()
|
||||
const result = await provider.searchPacks('test', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
|
||||
expect(mockSearchClient.search).toHaveBeenCalledWith({
|
||||
requests: [
|
||||
{
|
||||
query: 'test',
|
||||
indexName: 'nodes_index',
|
||||
attributesToRetrieve: expect.any(Array),
|
||||
hitsPerPage: 10,
|
||||
page: 0
|
||||
},
|
||||
{
|
||||
query: 'test',
|
||||
indexName: 'nodes_index_query_suggestions'
|
||||
}
|
||||
],
|
||||
strategy: 'none'
|
||||
})
|
||||
|
||||
expect(result.nodePacks).toHaveLength(1)
|
||||
expect(result.nodePacks[0]).toEqual({
|
||||
id: 'pack-1',
|
||||
name: 'Test Pack',
|
||||
description: 'A test pack',
|
||||
repository: 'https://github.com/test/pack',
|
||||
license: 'MIT',
|
||||
downloads: 500,
|
||||
status: 'active',
|
||||
icon: 'https://example.com/icon.png',
|
||||
latest_version: {
|
||||
version: '1.0.0',
|
||||
createdAt: '2024-01-15T00:00:00Z',
|
||||
status: 'published',
|
||||
comfy_node_extract_status: undefined
|
||||
},
|
||||
publisher: {
|
||||
id: 'publisher-1',
|
||||
name: 'publisher-1'
|
||||
},
|
||||
create_time: '2024-01-01T00:00:00Z',
|
||||
comfy_nodes: ['LoadImage', 'SaveImage']
|
||||
})
|
||||
})
|
||||
|
||||
it('should include query suggestions when query is long enough', async () => {
|
||||
const mockAlgoliaResults = {
|
||||
results: [
|
||||
{ hits: [] }, // Main results
|
||||
{
|
||||
hits: [
|
||||
{ query: 'test query', popularity: 10 },
|
||||
{ query: 'test pack', popularity: 5 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
mockSearchClient.search.mockResolvedValue(mockAlgoliaResults)
|
||||
|
||||
const provider = useAlgoliaSearchProvider()
|
||||
const result = await provider.searchPacks('test', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
|
||||
// Should make 2 requests (main + suggestions)
|
||||
expect(mockSearchClient.search).toHaveBeenCalledWith({
|
||||
requests: [
|
||||
expect.objectContaining({ indexName: 'nodes_index' }),
|
||||
expect.objectContaining({
|
||||
indexName: 'nodes_index_query_suggestions'
|
||||
})
|
||||
],
|
||||
strategy: 'none'
|
||||
})
|
||||
|
||||
expect(result.querySuggestions).toEqual([
|
||||
{ query: 'test query', popularity: 10 },
|
||||
{ query: 'test pack', popularity: 5 }
|
||||
])
|
||||
})
|
||||
|
||||
it('should not query suggestions for short queries', async () => {
|
||||
mockSearchClient.search.mockResolvedValue({
|
||||
results: [{ hits: [] }]
|
||||
})
|
||||
|
||||
const provider = useAlgoliaSearchProvider()
|
||||
await provider.searchPacks('a', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
|
||||
// Should only make 1 request (no suggestions)
|
||||
expect(mockSearchClient.search).toHaveBeenCalledWith({
|
||||
requests: [expect.objectContaining({ indexName: 'nodes_index' })],
|
||||
strategy: 'none'
|
||||
})
|
||||
})
|
||||
|
||||
it('should cache search results', async () => {
|
||||
mockSearchClient.search.mockResolvedValue({
|
||||
results: [{ hits: [] }, { hits: [] }]
|
||||
})
|
||||
|
||||
const provider = useAlgoliaSearchProvider()
|
||||
const params = { pageSize: 10, pageNumber: 0 }
|
||||
|
||||
// First call
|
||||
await provider.searchPacks('test', params)
|
||||
expect(mockSearchClient.search).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Second call with same params should use cache
|
||||
await provider.searchPacks('test', params)
|
||||
expect(mockSearchClient.search).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Different params should make new request
|
||||
await provider.searchPacks('test', { ...params, pageNumber: 1 })
|
||||
expect(mockSearchClient.search).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should handle missing objectID by using id field', async () => {
|
||||
const mockAlgoliaResults = {
|
||||
results: [
|
||||
{
|
||||
hits: [
|
||||
{
|
||||
id: 'pack-id-only',
|
||||
name: 'Pack without objectID',
|
||||
// ... other required fields
|
||||
publisher_id: 'pub',
|
||||
total_install: 0,
|
||||
comfy_nodes: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{ hits: [] }
|
||||
]
|
||||
}
|
||||
|
||||
mockSearchClient.search.mockResolvedValue(mockAlgoliaResults)
|
||||
|
||||
const provider = useAlgoliaSearchProvider()
|
||||
const result = await provider.searchPacks('test', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
|
||||
expect(result.nodePacks[0].id).toBe('pack-id-only')
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearSearchCache', () => {
|
||||
it('should clear the cache', async () => {
|
||||
mockSearchClient.search.mockResolvedValue({
|
||||
results: [{ hits: [] }, { hits: [] }]
|
||||
})
|
||||
|
||||
const provider = useAlgoliaSearchProvider()
|
||||
const params = { pageSize: 10, pageNumber: 0 }
|
||||
|
||||
// Populate cache
|
||||
await provider.searchPacks('test', params)
|
||||
expect(mockSearchClient.search).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Clear cache
|
||||
provider.clearSearchCache()
|
||||
|
||||
// Same search should hit API again
|
||||
await provider.searchPacks('test', params)
|
||||
expect(mockSearchClient.search).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSortValue', () => {
|
||||
const testPack = {
|
||||
id: '1',
|
||||
name: 'Test Pack',
|
||||
downloads: 100,
|
||||
publisher: { id: 'pub1', name: 'Publisher One' },
|
||||
latest_version: {
|
||||
version: '1.0.0',
|
||||
createdAt: '2024-01-15T10:00:00Z'
|
||||
},
|
||||
create_time: '2024-01-01T10:00:00Z'
|
||||
}
|
||||
|
||||
it('should return correct values for each sort field', () => {
|
||||
const provider = useAlgoliaSearchProvider()
|
||||
|
||||
expect(
|
||||
provider.getSortValue(testPack, SortableAlgoliaField.Downloads)
|
||||
).toBe(100)
|
||||
expect(provider.getSortValue(testPack, SortableAlgoliaField.Name)).toBe(
|
||||
'Test Pack'
|
||||
)
|
||||
expect(
|
||||
provider.getSortValue(testPack, SortableAlgoliaField.Publisher)
|
||||
).toBe('Publisher One')
|
||||
|
||||
const createdTimestamp = new Date('2024-01-01T10:00:00Z').getTime()
|
||||
expect(
|
||||
provider.getSortValue(testPack as any, SortableAlgoliaField.Created)
|
||||
).toBe(createdTimestamp)
|
||||
|
||||
const updatedTimestamp = new Date('2024-01-15T10:00:00Z').getTime()
|
||||
expect(
|
||||
provider.getSortValue(testPack, SortableAlgoliaField.Updated)
|
||||
).toBe(updatedTimestamp)
|
||||
})
|
||||
|
||||
it('should handle missing values', () => {
|
||||
const incompletePack = { id: '1', name: 'Incomplete' }
|
||||
const provider = useAlgoliaSearchProvider()
|
||||
|
||||
expect(
|
||||
provider.getSortValue(incompletePack, SortableAlgoliaField.Downloads)
|
||||
).toBe(0)
|
||||
expect(
|
||||
provider.getSortValue(incompletePack, SortableAlgoliaField.Publisher)
|
||||
).toBe('')
|
||||
expect(
|
||||
provider.getSortValue(
|
||||
incompletePack as any,
|
||||
SortableAlgoliaField.Created
|
||||
)
|
||||
).toBe(0)
|
||||
expect(
|
||||
provider.getSortValue(incompletePack, SortableAlgoliaField.Updated)
|
||||
).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSortableFields', () => {
|
||||
it('should return all Algolia sort fields', () => {
|
||||
const provider = useAlgoliaSearchProvider()
|
||||
const fields = provider.getSortableFields()
|
||||
|
||||
expect(fields).toEqual([
|
||||
{
|
||||
id: SortableAlgoliaField.Downloads,
|
||||
label: 'Downloads',
|
||||
direction: 'desc'
|
||||
},
|
||||
{
|
||||
id: SortableAlgoliaField.Created,
|
||||
label: 'Created',
|
||||
direction: 'desc'
|
||||
},
|
||||
{
|
||||
id: SortableAlgoliaField.Updated,
|
||||
label: 'Updated',
|
||||
direction: 'desc'
|
||||
},
|
||||
{
|
||||
id: SortableAlgoliaField.Publisher,
|
||||
label: 'Publisher',
|
||||
direction: 'asc'
|
||||
},
|
||||
{ id: SortableAlgoliaField.Name, label: 'Name', direction: 'asc' }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('memoization', () => {
|
||||
it('should memoize toRegistryPack conversions', async () => {
|
||||
const mockHit = {
|
||||
objectID: 'algolia-1',
|
||||
id: 'pack-1',
|
||||
name: 'Test Pack',
|
||||
publisher_id: 'pub1',
|
||||
total_install: 100,
|
||||
comfy_nodes: []
|
||||
}
|
||||
|
||||
mockSearchClient.search.mockResolvedValue({
|
||||
results: [
|
||||
{ hits: [mockHit, mockHit, mockHit] }, // Same object 3 times
|
||||
{ hits: [] }
|
||||
]
|
||||
})
|
||||
|
||||
const provider = useAlgoliaSearchProvider()
|
||||
const result = await provider.searchPacks('test', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
|
||||
// All 3 results should be the same object reference due to memoization
|
||||
expect(result.nodePacks[0]).toBe(result.nodePacks[1])
|
||||
expect(result.nodePacks[1]).toBe(result.nodePacks[2])
|
||||
})
|
||||
})
|
||||
})
|
||||
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
|
||||
})
|
||||
})
|
||||
})
|
||||
186
tests-ui/tests/services/registrySearchProvider.test.ts
Normal file
186
tests-ui/tests/services/registrySearchProvider.test.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useComfyRegistrySearchProvider } from '@/services/providers/registrySearchProvider'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
|
||||
// Mock the store
|
||||
vi.mock('@/stores/comfyRegistryStore', () => ({
|
||||
useComfyRegistryStore: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useComfyRegistrySearchProvider', () => {
|
||||
const mockSearchCall = vi.fn()
|
||||
const mockSearchClear = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Setup store mock
|
||||
vi.mocked(useComfyRegistryStore).mockReturnValue({
|
||||
search: {
|
||||
call: mockSearchCall,
|
||||
clear: mockSearchClear
|
||||
}
|
||||
} as any)
|
||||
})
|
||||
|
||||
describe('searchPacks', () => {
|
||||
it('should search for packs by name', async () => {
|
||||
const mockResults = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'Test Pack 1' },
|
||||
{ id: '2', name: 'Test Pack 2' }
|
||||
]
|
||||
}
|
||||
mockSearchCall.mockResolvedValue(mockResults)
|
||||
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
const result = await provider.searchPacks('test', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0,
|
||||
restrictSearchableAttributes: ['name', 'description']
|
||||
})
|
||||
|
||||
expect(mockSearchCall).toHaveBeenCalledWith({
|
||||
search: 'test',
|
||||
comfy_node_search: undefined,
|
||||
limit: 10,
|
||||
offset: 0
|
||||
})
|
||||
expect(result.nodePacks).toEqual(mockResults.nodes)
|
||||
expect(result.querySuggestions).toEqual([])
|
||||
})
|
||||
|
||||
it('should search for packs by node names', async () => {
|
||||
const mockResults = {
|
||||
nodes: [{ id: '1', name: 'Pack with LoadImage node' }]
|
||||
}
|
||||
mockSearchCall.mockResolvedValue(mockResults)
|
||||
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
const result = await provider.searchPacks('LoadImage', {
|
||||
pageSize: 20,
|
||||
pageNumber: 1,
|
||||
restrictSearchableAttributes: ['comfy_nodes']
|
||||
})
|
||||
|
||||
expect(mockSearchCall).toHaveBeenCalledWith({
|
||||
search: undefined,
|
||||
comfy_node_search: 'LoadImage',
|
||||
limit: 20,
|
||||
offset: 20
|
||||
})
|
||||
expect(result.nodePacks).toEqual(mockResults.nodes)
|
||||
})
|
||||
|
||||
it('should handle empty results', async () => {
|
||||
mockSearchCall.mockResolvedValue({ nodes: [] })
|
||||
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
const result = await provider.searchPacks('nonexistent', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
|
||||
expect(result.nodePacks).toEqual([])
|
||||
expect(result.querySuggestions).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle null results', async () => {
|
||||
mockSearchCall.mockResolvedValue(null)
|
||||
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
const result = await provider.searchPacks('test', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
|
||||
expect(result.nodePacks).toEqual([])
|
||||
expect(result.querySuggestions).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle results without nodes property', async () => {
|
||||
mockSearchCall.mockResolvedValue({})
|
||||
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
const result = await provider.searchPacks('test', {
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
|
||||
expect(result.nodePacks).toEqual([])
|
||||
expect(result.querySuggestions).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearSearchCache', () => {
|
||||
it('should delegate to store search.clear', () => {
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
provider.clearSearchCache()
|
||||
|
||||
expect(mockSearchClear).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSortValue', () => {
|
||||
const testPack = {
|
||||
id: '1',
|
||||
name: 'Test Pack',
|
||||
downloads: 100,
|
||||
publisher: { id: 'pub1', name: 'Publisher One' },
|
||||
latest_version: {
|
||||
version: '1.0.0',
|
||||
createdAt: '2024-01-15T10:00:00Z'
|
||||
}
|
||||
}
|
||||
|
||||
it('should return download count for downloads field', () => {
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
expect(provider.getSortValue(testPack, 'downloads')).toBe(100)
|
||||
})
|
||||
|
||||
it('should return pack name for name field', () => {
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
expect(provider.getSortValue(testPack, 'name')).toBe('Test Pack')
|
||||
})
|
||||
|
||||
it('should return publisher name for publisher field', () => {
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
expect(provider.getSortValue(testPack, 'publisher')).toBe('Publisher One')
|
||||
})
|
||||
|
||||
it('should return timestamp for updated field', () => {
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
const timestamp = new Date('2024-01-15T10:00:00Z').getTime()
|
||||
expect(provider.getSortValue(testPack, 'updated')).toBe(timestamp)
|
||||
})
|
||||
|
||||
it('should handle missing values gracefully', () => {
|
||||
const incompletePack = { id: '1', name: 'Incomplete' }
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
|
||||
expect(provider.getSortValue(incompletePack, 'downloads')).toBe(0)
|
||||
expect(provider.getSortValue(incompletePack, 'publisher')).toBe('')
|
||||
expect(provider.getSortValue(incompletePack, 'updated')).toBe(0)
|
||||
})
|
||||
|
||||
it('should return 0 for unknown sort fields', () => {
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
expect(provider.getSortValue(testPack, 'unknown')).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSortableFields', () => {
|
||||
it('should return supported sort fields', () => {
|
||||
const provider = useComfyRegistrySearchProvider()
|
||||
const fields = provider.getSortableFields()
|
||||
|
||||
expect(fields).toEqual([
|
||||
{ id: 'downloads', label: 'Downloads', direction: 'desc' },
|
||||
{ id: 'name', label: 'Name', direction: 'asc' },
|
||||
{ id: 'publisher', label: 'Publisher', direction: 'asc' },
|
||||
{ id: 'updated', label: 'Updated', direction: 'desc' }
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user