[Manager] Add registry search fallback with gateway pattern (#4187)

This commit is contained in:
Christian Byrne
2025-06-15 17:22:05 -07:00
committed by GitHub
parent d5ecfb2c99
commit 75077fe9ed
14 changed files with 1687 additions and 260 deletions

View 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])
})
})
})

View 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
})
})
})

View 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' }
])
})
})
})