test: fix failed tests

This commit is contained in:
Louis 2025-07-10 16:03:56 +07:00
parent 6e0218c084
commit ca6f4f8977
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
11 changed files with 740 additions and 27 deletions

View File

@ -13,6 +13,38 @@ class TestAIEngine extends AIEngine {
inference(data: any) {}
stopInference() {}
async list(): Promise<any[]> {
return []
}
async load(modelId: string): Promise<any> {
return { pid: 1, port: 8080, model_id: modelId, model_path: '', api_key: '' }
}
async unload(sessionId: string): Promise<any> {
return { success: true }
}
async chat(opts: any): Promise<any> {
return { id: 'test', object: 'chat.completion', created: Date.now(), model: 'test', choices: [] }
}
async delete(modelId: string): Promise<void> {
return
}
async import(modelId: string, opts: any): Promise<void> {
return
}
async abortImport(modelId: string): Promise<void> {
return
}
async getLoadedModels(): Promise<string[]> {
return []
}
}
describe('AIEngine', () => {
@ -23,35 +55,31 @@ describe('AIEngine', () => {
jest.clearAllMocks()
})
it('should load model if provider matches', async () => {
const model: any = { id: 'model1', engine: 'test-provider' } as any
it('should load model successfully', async () => {
const modelId = 'model1'
await engine.loadModel(model)
const result = await engine.load(modelId)
expect(events.emit).toHaveBeenCalledWith(ModelEvent.OnModelReady, model)
expect(result).toEqual({ pid: 1, port: 8080, model_id: modelId, model_path: '', api_key: '' })
})
it('should not load model if provider does not match', async () => {
const model: any = { id: 'model1', engine: 'other-provider' } as any
it('should unload model successfully', async () => {
const sessionId = 'session1'
await engine.loadModel(model)
const result = await engine.unload(sessionId)
expect(events.emit).not.toHaveBeenCalledWith(ModelEvent.OnModelReady, model)
expect(result).toEqual({ success: true })
})
it('should unload model if provider matches', async () => {
const model: Model = { id: 'model1', version: '1.0', engine: 'test-provider' } as any
it('should list models', async () => {
const result = await engine.list()
await engine.unloadModel(model)
expect(events.emit).toHaveBeenCalledWith(ModelEvent.OnModelStopped, model)
expect(result).toEqual([])
})
it('should not unload model if provider does not match', async () => {
const model: Model = { id: 'model1', version: '1.0', engine: 'other-provider' } as any
it('should get loaded models', async () => {
const result = await engine.getLoadedModels()
await engine.unloadModel(model)
expect(events.emit).not.toHaveBeenCalledWith(ModelEvent.OnModelStopped, model)
expect(result).toEqual([])
})
})

View File

@ -28,9 +28,14 @@ export abstract class LocalOAIEngine extends OAIEngine {
/**
* Load the model.
*/
async loadModel(model: Model & { file_path?: string }): Promise<void> {}
async loadModel(model: Model & { file_path?: string }): Promise<void> {
// Implementation of loading the model
}
/**
* Stops the model.
*/
async unloadModel(model?: Model) {}
async unloadModel(model?: Model) {
// Implementation of unloading the model
}
}

View File

@ -1,7 +1,6 @@
import { ConversationalExtension } from './index';
import { InferenceExtension } from './index';
import { AssistantExtension } from './index';
import { ModelExtension } from './index';
import * as Engines from './index';
describe('index.ts exports', () => {
@ -17,9 +16,6 @@ describe('index.ts exports', () => {
expect(AssistantExtension).toBeDefined();
});
test('should export ModelExtension', () => {
expect(ModelExtension).toBeDefined();
});
test('should export Engines', () => {
expect(Engines).toBeDefined();

View File

@ -6,7 +6,6 @@ import * as message from './message';
import * as inference from './inference';
import * as file from './file';
import * as config from './config';
import * as huggingface from './huggingface';
import * as miscellaneous from './miscellaneous';
import * as api from './api';
import * as setting from './setting';
@ -19,7 +18,6 @@ import * as setting from './setting';
expect(inference).toBeDefined();
expect(file).toBeDefined();
expect(config).toBeDefined();
expect(huggingface).toBeDefined();
expect(miscellaneous).toBeDefined();
expect(api).toBeDefined();
expect(setting).toBeDefined();

View File

@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{ ignores: ['dist', 'coverage', '**/__tests__/**', '**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],

View File

@ -0,0 +1,128 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useMediaQuery } from '../useMediaQuery'
// Mock window.matchMedia
const mockMatchMedia = vi.fn()
beforeEach(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: mockMatchMedia,
})
})
afterEach(() => {
vi.clearAllMocks()
})
describe('useMediaQuery hook', () => {
it('should return initial match value', () => {
const mockMediaQueryList = {
matches: true,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
mockMatchMedia.mockReturnValue(mockMediaQueryList)
const { result } = renderHook(() => useMediaQuery('(min-width: 768px)'))
expect(result.current).toBe(true)
expect(mockMatchMedia).toHaveBeenCalledWith('(min-width: 768px)')
})
it('should return false when media query does not match', () => {
const mockMediaQueryList = {
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
mockMatchMedia.mockReturnValue(mockMediaQueryList)
const { result } = renderHook(() => useMediaQuery('(max-width: 767px)'))
expect(result.current).toBe(false)
})
it('should update when media query changes', () => {
const mockMediaQueryList = {
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
mockMatchMedia.mockReturnValue(mockMediaQueryList)
const { result } = renderHook(() => useMediaQuery('(min-width: 768px)'))
expect(result.current).toBe(false)
// Simulate media query change
const changeHandler = mockMediaQueryList.addEventListener.mock.calls[0][1]
act(() => {
changeHandler({ matches: true })
})
expect(result.current).toBe(true)
})
it('should add event listener on mount', () => {
const mockMediaQueryList = {
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
mockMatchMedia.mockReturnValue(mockMediaQueryList)
renderHook(() => useMediaQuery('(min-width: 768px)'))
expect(mockMediaQueryList.addEventListener).toHaveBeenCalledWith('change', expect.any(Function))
})
it('should remove event listener on unmount', () => {
const mockMediaQueryList = {
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
mockMatchMedia.mockReturnValue(mockMediaQueryList)
const { unmount } = renderHook(() => useMediaQuery('(min-width: 768px)'))
unmount()
expect(mockMediaQueryList.removeEventListener).toHaveBeenCalledWith('change', expect.any(Function))
})
it('should handle different media queries', () => {
const mockMediaQueryList = {
matches: true,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
mockMatchMedia.mockReturnValue(mockMediaQueryList)
const { result: result1 } = renderHook(() => useMediaQuery('(min-width: 768px)'))
const { result: result2 } = renderHook(() => useMediaQuery('(max-width: 1024px)'))
expect(result1.current).toBe(true)
expect(result2.current).toBe(true)
expect(mockMatchMedia).toHaveBeenCalledWith('(min-width: 768px)')
expect(mockMatchMedia).toHaveBeenCalledWith('(max-width: 1024px)')
})
it('should handle matchMedia not being available', () => {
// @ts-ignore
delete window.matchMedia
const { result } = renderHook(() => useMediaQuery('(min-width: 768px)'))
expect(result.current).toBe(false)
})
})

View File

@ -0,0 +1,255 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
fetchModels,
fetchModelCatalog,
updateModel,
pullModel,
abortDownload,
deleteModel,
getActiveModels,
stopModel,
stopAllModels,
startModel,
configurePullOptions,
} from '../models'
import { EngineManager } from '@janhq/core'
// Mock EngineManager
vi.mock('@janhq/core', () => ({
EngineManager: {
instance: vi.fn(),
},
}))
// Mock fetch
global.fetch = vi.fn()
// Mock MODEL_CATALOG_URL
Object.defineProperty(global, 'MODEL_CATALOG_URL', {
value: 'https://example.com/models',
writable: true,
configurable: true,
})
describe('models service', () => {
const mockEngine = {
list: vi.fn(),
updateSettings: vi.fn(),
import: vi.fn(),
abortImport: vi.fn(),
delete: vi.fn(),
getLoadedModels: vi.fn(),
unload: vi.fn(),
load: vi.fn(),
}
const mockEngineManager = {
get: vi.fn().mockReturnValue(mockEngine),
}
beforeEach(() => {
vi.clearAllMocks()
;(EngineManager.instance as any).mockReturnValue(mockEngineManager)
})
describe('fetchModels', () => {
it('should fetch models successfully', async () => {
const mockModels = [
{ id: 'model1', name: 'Model 1' },
{ id: 'model2', name: 'Model 2' },
]
mockEngine.list.mockResolvedValue(mockModels)
const result = await fetchModels()
expect(result).toEqual(mockModels)
expect(mockEngine.list).toHaveBeenCalled()
})
})
describe('fetchModelCatalog', () => {
it('should fetch model catalog successfully', async () => {
const mockCatalog = [
{
model_name: 'GPT-4',
description: 'Large language model',
developer: 'OpenAI',
downloads: 1000,
num_quants: 5,
quants: [],
},
]
;(fetch as any).mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockCatalog),
})
const result = await fetchModelCatalog()
expect(result).toEqual(mockCatalog)
})
it('should handle fetch error', async () => {
;(fetch as any).mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found',
})
await expect(fetchModelCatalog()).rejects.toThrow('Failed to fetch model catalog: 404 Not Found')
})
it('should handle network error', async () => {
;(fetch as any).mockRejectedValue(new Error('Network error'))
await expect(fetchModelCatalog()).rejects.toThrow('Failed to fetch model catalog: Network error')
})
})
describe('updateModel', () => {
it('should update model settings', async () => {
const model = {
id: 'model1',
settings: [{ key: 'temperature', value: 0.7 }],
}
await updateModel(model)
expect(mockEngine.updateSettings).toHaveBeenCalledWith(model.settings)
})
it('should handle model without settings', async () => {
const model = { id: 'model1' }
await updateModel(model)
expect(mockEngine.updateSettings).not.toHaveBeenCalled()
})
})
describe('pullModel', () => {
it('should pull model successfully', async () => {
const id = 'model1'
const modelPath = '/path/to/model'
await pullModel(id, modelPath)
expect(mockEngine.import).toHaveBeenCalledWith(id, { modelPath })
})
})
describe('abortDownload', () => {
it('should abort download successfully', async () => {
const id = 'model1'
await abortDownload(id)
expect(mockEngine.abortImport).toHaveBeenCalledWith(id)
})
})
describe('deleteModel', () => {
it('should delete model successfully', async () => {
const id = 'model1'
await deleteModel(id)
expect(mockEngine.delete).toHaveBeenCalledWith(id)
})
})
describe('getActiveModels', () => {
it('should get active models successfully', async () => {
const mockActiveModels = ['model1', 'model2']
mockEngine.getLoadedModels.mockResolvedValue(mockActiveModels)
const result = await getActiveModels()
expect(result).toEqual(mockActiveModels)
expect(mockEngine.getLoadedModels).toHaveBeenCalled()
})
})
describe('stopModel', () => {
it('should stop model successfully', async () => {
const model = 'model1'
const provider = 'openai'
await stopModel(model, provider)
expect(mockEngine.unload).toHaveBeenCalledWith(model)
})
})
describe('stopAllModels', () => {
it('should stop all active models', async () => {
const mockActiveModels = ['model1', 'model2']
mockEngine.getLoadedModels.mockResolvedValue(mockActiveModels)
await stopAllModels()
expect(mockEngine.unload).toHaveBeenCalledTimes(2)
expect(mockEngine.unload).toHaveBeenCalledWith('model1')
expect(mockEngine.unload).toHaveBeenCalledWith('model2')
})
it('should handle empty active models', async () => {
mockEngine.getLoadedModels.mockResolvedValue(null)
await stopAllModels()
expect(mockEngine.unload).not.toHaveBeenCalled()
})
})
describe('startModel', () => {
it('should start model successfully', async () => {
const provider = { provider: 'openai', models: [] } as ProviderObject
const model = 'model1'
const mockSession = { id: 'session1' }
mockEngine.load.mockResolvedValue(mockSession)
const result = await startModel(provider, model)
expect(result).toEqual(mockSession)
expect(mockEngine.load).toHaveBeenCalledWith(model)
})
it('should handle start model error', async () => {
const provider = { provider: 'openai', models: [] } as ProviderObject
const model = 'model1'
const error = new Error('Failed to start model')
mockEngine.load.mockRejectedValue(error)
await expect(startModel(provider, model)).rejects.toThrow(error)
})
})
describe('configurePullOptions', () => {
it('should configure proxy options', async () => {
const proxyOptions = {
proxyEnabled: true,
proxyUrl: 'http://proxy.com',
proxyUsername: 'user',
proxyPassword: 'pass',
proxyIgnoreSSL: false,
verifyProxySSL: true,
verifyProxyHostSSL: true,
verifyPeerSSL: true,
verifyHostSSL: true,
noProxy: '',
}
// Mock console.log to avoid output during tests
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await configurePullOptions(proxyOptions)
expect(consoleSpy).toHaveBeenCalledWith('Configuring proxy options:', proxyOptions)
consoleSpy.mockRestore()
})
})
})

View File

@ -0,0 +1,176 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { fetchThreads, createThread, updateThread, deleteThread } from '../threads'
import { ExtensionManager } from '@/lib/extension'
import { ConversationalExtension, ExtensionTypeEnum } from '@janhq/core'
import { defaultAssistant } from '@/hooks/useAssistant'
// Mock ExtensionManager
vi.mock('@/lib/extension', () => ({
ExtensionManager: {
getInstance: vi.fn(),
},
}))
vi.mock('@/hooks/useAssistant', () => ({
defaultAssistant: {
id: 'jan',
name: 'Jan',
instructions: 'You are a helpful assistant.',
},
}))
describe('threads service', () => {
const mockConversationalExtension = {
listThreads: vi.fn(),
createThread: vi.fn(),
modifyThread: vi.fn(),
deleteThread: vi.fn(),
}
const mockExtensionManager = {
get: vi.fn().mockReturnValue(mockConversationalExtension),
}
beforeEach(() => {
vi.clearAllMocks()
;(ExtensionManager.getInstance as any).mockReturnValue(mockExtensionManager)
})
describe('fetchThreads', () => {
it('should fetch and transform threads successfully', async () => {
const mockThreads = [
{
id: '1',
title: 'Test Thread',
updated: 1234567890,
metadata: { order: 1, is_favorite: true },
assistants: [{ model: { id: 'gpt-4', engine: 'openai' } }],
},
]
mockConversationalExtension.listThreads.mockResolvedValue(mockThreads)
const result = await fetchThreads()
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
id: '1',
title: 'Test Thread',
updated: 1234567890,
order: 1,
isFavorite: true,
model: { id: 'gpt-4', provider: 'openai' },
assistants: [{ model: { id: 'gpt-4', engine: 'openai' } }],
})
})
it('should handle empty threads array', async () => {
mockConversationalExtension.listThreads.mockResolvedValue([])
const result = await fetchThreads()
expect(result).toEqual([])
})
it('should handle error and return empty array', async () => {
mockConversationalExtension.listThreads.mockRejectedValue(new Error('API Error'))
const result = await fetchThreads()
expect(result).toEqual([])
})
it('should handle null/undefined response', async () => {
mockConversationalExtension.listThreads.mockResolvedValue(null)
const result = await fetchThreads()
expect(result).toEqual([])
})
})
describe('createThread', () => {
it('should create thread successfully', async () => {
const inputThread = {
id: '1',
title: 'New Thread',
model: { id: 'gpt-4', provider: 'openai' },
assistants: [defaultAssistant],
order: 1,
}
const mockCreatedThread = {
id: '1',
title: 'New Thread',
updated: 1234567890,
assistants: [{ model: { id: 'gpt-4', engine: 'openai' } }],
metadata: { order: 1 },
}
mockConversationalExtension.createThread.mockResolvedValue(mockCreatedThread)
const result = await createThread(inputThread as Thread)
expect(result).toMatchObject({
id: '1',
title: 'New Thread',
updated: 1234567890,
model: { id: 'gpt-4', provider: 'openai' },
order: 1,
assistants: [{ model: { id: 'gpt-4', engine: 'openai' } }],
})
})
it('should handle creation error and return original thread', async () => {
const inputThread = {
id: '1',
title: 'New Thread',
model: { id: 'gpt-4', provider: 'openai' },
}
mockConversationalExtension.createThread.mockRejectedValue(new Error('Creation failed'))
const result = await createThread(inputThread as Thread)
expect(result).toEqual(inputThread)
})
})
describe('updateThread', () => {
it('should update thread successfully', async () => {
const thread = {
id: '1',
title: 'Updated Thread',
model: { id: 'gpt-4', provider: 'openai' },
assistants: [defaultAssistant],
isFavorite: true,
order: 2,
}
const result = updateThread(thread as Thread)
expect(mockConversationalExtension.modifyThread).toHaveBeenCalledWith(
expect.objectContaining({
id: '1',
title: 'Updated Thread',
assistants: expect.arrayContaining([
expect.objectContaining({
model: { id: 'gpt-4', engine: 'openai' },
}),
]),
metadata: { is_favorite: true, order: 2 },
})
)
})
})
describe('deleteThread', () => {
it('should delete thread successfully', () => {
const threadId = '1'
deleteThread(threadId)
expect(mockConversationalExtension.deleteThread).toHaveBeenCalledWith(threadId)
})
})
})

View File

@ -0,0 +1,14 @@
import { describe, it, expect } from 'vitest'
import { OUT_OF_CONTEXT_SIZE } from '../error'
describe('error utilities', () => {
describe('OUT_OF_CONTEXT_SIZE', () => {
it('should have correct error message', () => {
expect(OUT_OF_CONTEXT_SIZE).toBe('the request exceeds the available context size.')
})
it('should be a string', () => {
expect(typeof OUT_OF_CONTEXT_SIZE).toBe('string')
})
})
})

View File

@ -0,0 +1,71 @@
import { describe, it, expect } from 'vitest'
import { highlightFzfMatch } from '../highlight'
describe('highlight utility', () => {
describe('highlightFzfMatch', () => {
it('should highlight characters at specified positions', () => {
const text = 'Hello World'
const positions = [0, 6]
const result = highlightFzfMatch(text, positions)
expect(result).toBe('<span class="search-highlight">H</span>ello <span class="search-highlight">W</span>orld')
})
it('should handle empty positions array', () => {
const text = 'Hello World'
const positions: number[] = []
const result = highlightFzfMatch(text, positions)
expect(result).toBe('Hello World')
})
it('should handle empty text', () => {
const text = ''
const positions = [0, 1]
const result = highlightFzfMatch(text, positions)
expect(result).toBe('')
})
it('should handle positions out of bounds', () => {
const text = 'Hello'
const positions = [0, 10]
const result = highlightFzfMatch(text, positions)
expect(result).toBe('<span class="search-highlight">H</span>ello')
})
it('should handle custom highlight class', () => {
const text = 'Hello World'
const positions = [0]
const result = highlightFzfMatch(text, positions, 'custom-highlight')
expect(result).toBe('<span class="custom-highlight">H</span>ello World')
})
it('should sort positions automatically', () => {
const text = 'Hello World'
const positions = [6, 0]
const result = highlightFzfMatch(text, positions)
expect(result).toBe('<span class="search-highlight">H</span>ello <span class="search-highlight">W</span>orld')
})
it('should handle multiple consecutive positions', () => {
const text = 'Hello'
const positions = [0, 1, 2]
const result = highlightFzfMatch(text, positions)
expect(result).toBe('<span class="search-highlight">H</span><span class="search-highlight">e</span><span class="search-highlight">l</span>lo')
})
it('should handle null or undefined positions', () => {
const text = 'Hello World'
const result1 = highlightFzfMatch(text, null as any)
const result2 = highlightFzfMatch(text, undefined as any)
expect(result1).toBe('Hello World')
expect(result2).toBe('Hello World')
})
})
})

View File

@ -0,0 +1,42 @@
import { describe, it, expect } from 'vitest'
import { teamEmoji } from '../teamEmoji'
describe('teamEmoji utility', () => {
describe('teamEmoji', () => {
it('should contain team member data', () => {
expect(teamEmoji).toBeInstanceOf(Array)
expect(teamEmoji.length).toBeGreaterThan(0)
})
it('should have correct structure for team members', () => {
const member = teamEmoji[0]
expect(member).toHaveProperty('names')
expect(member).toHaveProperty('imgUrl')
expect(member).toHaveProperty('id')
expect(Array.isArray(member.names)).toBe(true)
expect(typeof member.imgUrl).toBe('string')
expect(typeof member.id).toBe('string')
})
it('should contain expected team members', () => {
const memberIds = teamEmoji.map(m => m.id)
expect(memberIds).toContain('louis')
expect(memberIds).toContain('emre')
expect(memberIds).toContain('alex')
expect(memberIds).toContain('daniel')
expect(memberIds).toContain('bach')
})
it('should have unique IDs', () => {
const ids = teamEmoji.map(m => m.id)
const uniqueIds = [...new Set(ids)]
expect(ids.length).toBe(uniqueIds.length)
})
it('should have valid image URLs', () => {
teamEmoji.forEach(member => {
expect(member.imgUrl).toMatch(/^\/images\/emoji\/.*\.png$/)
})
})
})
})