387 lines
11 KiB
TypeScript
387 lines
11 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { renderHook, act } from '@testing-library/react'
|
|
import { useModelSources } from '../useModelSources'
|
|
import type { CatalogModel } from '@/services/models'
|
|
|
|
// Mock constants
|
|
vi.mock('@/constants/localStorage', () => ({
|
|
localStorageKey: {
|
|
modelSources: 'model-sources-settings',
|
|
},
|
|
}))
|
|
|
|
// Mock zustand persist
|
|
vi.mock('zustand/middleware', () => ({
|
|
persist: (fn: any) => fn,
|
|
createJSONStorage: () => ({
|
|
getItem: vi.fn(),
|
|
setItem: vi.fn(),
|
|
removeItem: vi.fn(),
|
|
}),
|
|
}))
|
|
|
|
// Mock the fetchModelCatalog service
|
|
vi.mock('@/services/models', () => ({
|
|
fetchModelCatalog: vi.fn(),
|
|
}))
|
|
|
|
// Mock the sanitizeModelId function
|
|
vi.mock('@/lib/utils', () => ({
|
|
sanitizeModelId: vi.fn((id: string) => id),
|
|
}))
|
|
|
|
describe('useModelSources', () => {
|
|
let mockFetchModelCatalog: any
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks()
|
|
// Get the mocked function
|
|
const { fetchModelCatalog } = await import('@/services/models')
|
|
mockFetchModelCatalog = fetchModelCatalog as any
|
|
|
|
// Reset store state to defaults
|
|
useModelSources.setState({
|
|
sources: [],
|
|
error: null,
|
|
loading: false,
|
|
})
|
|
})
|
|
|
|
it('should initialize with default values', () => {
|
|
const { result } = renderHook(() => useModelSources())
|
|
|
|
expect(result.current.sources).toEqual([])
|
|
expect(result.current.error).toBe(null)
|
|
expect(result.current.loading).toBe(false)
|
|
expect(typeof result.current.fetchSources).toBe('function')
|
|
})
|
|
|
|
describe('fetchSources', () => {
|
|
it('should fetch sources successfully', async () => {
|
|
const mockSources: CatalogModel[] = [
|
|
{
|
|
model_name: 'model-1',
|
|
description: 'First model',
|
|
developer: 'provider-1',
|
|
downloads: 100,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }],
|
|
},
|
|
{
|
|
model_name: 'model-2',
|
|
description: 'Second model',
|
|
developer: 'provider-2',
|
|
downloads: 200,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'model-2-q4', path: '/path/2', file_size: '2GB' }],
|
|
},
|
|
]
|
|
|
|
mockFetchModelCatalog.mockResolvedValueOnce(mockSources)
|
|
|
|
const { result } = renderHook(() => useModelSources())
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(mockFetchModelCatalog).toHaveBeenCalledOnce()
|
|
expect(result.current.sources).toEqual(mockSources)
|
|
expect(result.current.loading).toBe(false)
|
|
expect(result.current.error).toBe(null)
|
|
})
|
|
|
|
it('should handle fetch errors', async () => {
|
|
const mockError = new Error('Network error')
|
|
mockFetchModelCatalog.mockRejectedValueOnce(mockError)
|
|
|
|
const { result } = renderHook(() => useModelSources())
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(result.current.loading).toBe(false)
|
|
expect(result.current.error).toBe(mockError)
|
|
expect(result.current.sources).toEqual([])
|
|
})
|
|
|
|
it('should not merge new sources with existing ones', async () => {
|
|
const existingSources: CatalogModel[] = [
|
|
{
|
|
model_name: 'existing-model',
|
|
description: 'Existing model',
|
|
developer: 'existing-provider',
|
|
downloads: 50,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'existing-model-q4', path: '/path/existing', file_size: '1GB' }],
|
|
},
|
|
]
|
|
|
|
const newSources: CatalogModel[] = [
|
|
{
|
|
model_name: 'new-model',
|
|
description: 'New model',
|
|
developer: 'new-provider',
|
|
downloads: 150,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'new-model-q4', path: '/path/new', file_size: '2GB' }],
|
|
},
|
|
]
|
|
|
|
// Set initial state with existing sources
|
|
useModelSources.setState({
|
|
sources: existingSources,
|
|
error: null,
|
|
loading: false,
|
|
})
|
|
|
|
mockFetchModelCatalog.mockResolvedValueOnce(newSources)
|
|
|
|
const { result } = renderHook(() => useModelSources())
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(result.current.sources).toEqual(newSources)
|
|
})
|
|
|
|
it('should not duplicate models with same model_name', async () => {
|
|
const existingSources: CatalogModel[] = [
|
|
{
|
|
model_name: 'duplicate-model',
|
|
description: 'Old version',
|
|
developer: 'old-provider',
|
|
downloads: 100,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'duplicate-model-q4', path: '/path/old', file_size: '1GB' }],
|
|
},
|
|
{
|
|
model_name: 'unique-model',
|
|
description: 'Unique model',
|
|
developer: 'provider',
|
|
downloads: 75,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'unique-model-q4', path: '/path/unique', file_size: '1GB' }],
|
|
},
|
|
]
|
|
|
|
const newSources: CatalogModel[] = [
|
|
{
|
|
model_name: 'duplicate-model',
|
|
description: 'New version',
|
|
developer: 'new-provider',
|
|
downloads: 200,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'duplicate-model-q4-new', path: '/path/new', file_size: '2GB' }],
|
|
},
|
|
]
|
|
|
|
// Set initial state with existing sources
|
|
useModelSources.setState({
|
|
sources: existingSources,
|
|
error: null,
|
|
loading: false,
|
|
})
|
|
|
|
mockFetchModelCatalog.mockResolvedValueOnce(newSources)
|
|
|
|
const { result } = renderHook(() => useModelSources())
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(result.current.sources).toEqual(newSources)
|
|
})
|
|
|
|
it('should handle empty sources response', async () => {
|
|
mockFetchModelCatalog.mockResolvedValueOnce([])
|
|
|
|
const { result } = renderHook(() => useModelSources())
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(result.current.sources).toEqual([])
|
|
expect(result.current.loading).toBe(false)
|
|
expect(result.current.error).toBe(null)
|
|
})
|
|
|
|
it('should clear previous error on successful fetch', async () => {
|
|
const { result } = renderHook(() => useModelSources())
|
|
|
|
// First request fails
|
|
mockFetchModelCatalog.mockRejectedValueOnce(new Error('First error'))
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(result.current.error).toBeInstanceOf(Error)
|
|
|
|
// Second request succeeds
|
|
const mockSources: CatalogModel[] = [
|
|
{
|
|
model_name: 'model-1',
|
|
description: 'Model 1',
|
|
developer: 'provider-1',
|
|
downloads: 100,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }],
|
|
},
|
|
]
|
|
|
|
mockFetchModelCatalog.mockResolvedValueOnce(mockSources)
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(result.current.error).toBe(null)
|
|
expect(result.current.sources).toEqual(mockSources)
|
|
})
|
|
})
|
|
|
|
describe('state management', () => {
|
|
it('should maintain state across multiple hook instances', () => {
|
|
const { result: result1 } = renderHook(() => useModelSources())
|
|
const { result: result2 } = renderHook(() => useModelSources())
|
|
|
|
expect(result1.current.sources).toBe(result2.current.sources)
|
|
expect(result1.current.loading).toBe(result2.current.loading)
|
|
expect(result1.current.error).toBe(result2.current.error)
|
|
})
|
|
|
|
it('should update state across multiple hook instances', async () => {
|
|
const mockSources: CatalogModel[] = [
|
|
{
|
|
model_name: 'shared-model',
|
|
description: 'Shared model',
|
|
developer: 'shared-provider',
|
|
downloads: 100,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'shared-model-q4', path: '/path/shared', file_size: '1GB' }],
|
|
},
|
|
]
|
|
|
|
mockFetchModelCatalog.mockResolvedValueOnce(mockSources)
|
|
|
|
const { result: result1 } = renderHook(() => useModelSources())
|
|
const { result: result2 } = renderHook(() => useModelSources())
|
|
|
|
await act(async () => {
|
|
await result1.current.fetchSources()
|
|
})
|
|
|
|
expect(result2.current.sources).toEqual(mockSources)
|
|
})
|
|
})
|
|
|
|
describe('error handling', () => {
|
|
it('should handle different error types', async () => {
|
|
const errors = [
|
|
new Error('Network error'),
|
|
new TypeError('Type error'),
|
|
new ReferenceError('Reference error'),
|
|
]
|
|
|
|
const { result } = renderHook(() => useModelSources())
|
|
|
|
for (const error of errors) {
|
|
mockFetchModelCatalog.mockRejectedValueOnce(error)
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(result.current.error).toBe(error)
|
|
expect(result.current.loading).toBe(false)
|
|
expect(result.current.sources).toEqual([])
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('complex scenarios', () => {
|
|
it('should handle multiple fetch operations', async () => {
|
|
const { result } = renderHook(() => useModelSources())
|
|
|
|
const sources1: CatalogModel[] = [
|
|
{
|
|
model_name: 'model-1',
|
|
description: 'First batch',
|
|
developer: 'provider-1',
|
|
downloads: 100,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }],
|
|
},
|
|
]
|
|
|
|
const sources2: CatalogModel[] = [
|
|
{
|
|
model_name: 'model-2',
|
|
description: 'Second batch',
|
|
developer: 'provider-2',
|
|
downloads: 200,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'model-2-q4', path: '/path/2', file_size: '2GB' }],
|
|
},
|
|
]
|
|
|
|
// First fetch
|
|
mockFetchModelCatalog.mockResolvedValueOnce(sources1)
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(result.current.sources).toEqual(sources1)
|
|
|
|
// Second fetch
|
|
mockFetchModelCatalog.mockResolvedValueOnce(sources2)
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(result.current.sources).toEqual(sources2)
|
|
})
|
|
|
|
it('should handle fetch after error', async () => {
|
|
const { result } = renderHook(() => useModelSources())
|
|
|
|
// First request fails
|
|
mockFetchModelCatalog.mockRejectedValueOnce(new Error('Network error'))
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(result.current.error).toBeInstanceOf(Error)
|
|
|
|
// Second request succeeds
|
|
const mockSources: CatalogModel[] = [
|
|
{
|
|
model_name: 'recovery-model',
|
|
description: 'Recovery model',
|
|
developer: 'recovery-provider',
|
|
downloads: 100,
|
|
num_quants: 1,
|
|
quants: [{ model_id: 'recovery-model-q4', path: '/path/recovery', file_size: '1GB' }],
|
|
},
|
|
]
|
|
|
|
mockFetchModelCatalog.mockResolvedValueOnce(mockSources)
|
|
|
|
await act(async () => {
|
|
await result.current.fetchSources()
|
|
})
|
|
|
|
expect(result.current.error).toBe(null)
|
|
expect(result.current.sources).toEqual(mockSources)
|
|
})
|
|
})
|
|
})
|