test: fix failed tests
This commit is contained in:
parent
6e0218c084
commit
ca6f4f8977
@ -13,6 +13,38 @@ class TestAIEngine extends AIEngine {
|
|||||||
inference(data: any) {}
|
inference(data: any) {}
|
||||||
|
|
||||||
stopInference() {}
|
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', () => {
|
describe('AIEngine', () => {
|
||||||
@ -23,35 +55,31 @@ describe('AIEngine', () => {
|
|||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should load model if provider matches', async () => {
|
it('should load model successfully', async () => {
|
||||||
const model: any = { id: 'model1', engine: 'test-provider' } as any
|
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 () => {
|
it('should unload model successfully', async () => {
|
||||||
const model: any = { id: 'model1', engine: 'other-provider' } as any
|
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 () => {
|
it('should list models', async () => {
|
||||||
const model: Model = { id: 'model1', version: '1.0', engine: 'test-provider' } as any
|
const result = await engine.list()
|
||||||
|
|
||||||
await engine.unloadModel(model)
|
expect(result).toEqual([])
|
||||||
|
|
||||||
expect(events.emit).toHaveBeenCalledWith(ModelEvent.OnModelStopped, model)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not unload model if provider does not match', async () => {
|
it('should get loaded models', async () => {
|
||||||
const model: Model = { id: 'model1', version: '1.0', engine: 'other-provider' } as any
|
const result = await engine.getLoadedModels()
|
||||||
|
|
||||||
await engine.unloadModel(model)
|
expect(result).toEqual([])
|
||||||
|
|
||||||
expect(events.emit).not.toHaveBeenCalledWith(ModelEvent.OnModelStopped, model)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -28,9 +28,14 @@ export abstract class LocalOAIEngine extends OAIEngine {
|
|||||||
/**
|
/**
|
||||||
* Load the model.
|
* 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.
|
* Stops the model.
|
||||||
*/
|
*/
|
||||||
async unloadModel(model?: Model) {}
|
async unloadModel(model?: Model) {
|
||||||
|
// Implementation of unloading the model
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { ConversationalExtension } from './index';
|
import { ConversationalExtension } from './index';
|
||||||
import { InferenceExtension } from './index';
|
import { InferenceExtension } from './index';
|
||||||
import { AssistantExtension } from './index';
|
import { AssistantExtension } from './index';
|
||||||
import { ModelExtension } from './index';
|
|
||||||
import * as Engines from './index';
|
import * as Engines from './index';
|
||||||
|
|
||||||
describe('index.ts exports', () => {
|
describe('index.ts exports', () => {
|
||||||
@ -17,9 +16,6 @@ describe('index.ts exports', () => {
|
|||||||
expect(AssistantExtension).toBeDefined();
|
expect(AssistantExtension).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should export ModelExtension', () => {
|
|
||||||
expect(ModelExtension).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should export Engines', () => {
|
test('should export Engines', () => {
|
||||||
expect(Engines).toBeDefined();
|
expect(Engines).toBeDefined();
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import * as message from './message';
|
|||||||
import * as inference from './inference';
|
import * as inference from './inference';
|
||||||
import * as file from './file';
|
import * as file from './file';
|
||||||
import * as config from './config';
|
import * as config from './config';
|
||||||
import * as huggingface from './huggingface';
|
|
||||||
import * as miscellaneous from './miscellaneous';
|
import * as miscellaneous from './miscellaneous';
|
||||||
import * as api from './api';
|
import * as api from './api';
|
||||||
import * as setting from './setting';
|
import * as setting from './setting';
|
||||||
@ -19,7 +18,6 @@ import * as setting from './setting';
|
|||||||
expect(inference).toBeDefined();
|
expect(inference).toBeDefined();
|
||||||
expect(file).toBeDefined();
|
expect(file).toBeDefined();
|
||||||
expect(config).toBeDefined();
|
expect(config).toBeDefined();
|
||||||
expect(huggingface).toBeDefined();
|
|
||||||
expect(miscellaneous).toBeDefined();
|
expect(miscellaneous).toBeDefined();
|
||||||
expect(api).toBeDefined();
|
expect(api).toBeDefined();
|
||||||
expect(setting).toBeDefined();
|
expect(setting).toBeDefined();
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh'
|
|||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
export default tseslint.config(
|
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],
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
|||||||
128
web-app/src/hooks/__tests__/useMediaQuery.test.ts
Normal file
128
web-app/src/hooks/__tests__/useMediaQuery.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
255
web-app/src/services/__tests__/models.test.ts
Normal file
255
web-app/src/services/__tests__/models.test.ts
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
176
web-app/src/services/__tests__/threads.test.ts
Normal file
176
web-app/src/services/__tests__/threads.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
14
web-app/src/utils/__tests__/error.test.ts
Normal file
14
web-app/src/utils/__tests__/error.test.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
71
web-app/src/utils/__tests__/highlight.test.ts
Normal file
71
web-app/src/utils/__tests__/highlight.test.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
42
web-app/src/utils/__tests__/teamEmoji.test.ts
Normal file
42
web-app/src/utils/__tests__/teamEmoji.test.ts
Normal 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$/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user