test: add web helpers, services, utils tests (#3669)
* test: add web helpers tests * fix: coverage report * test: add more tests * test: add more generated tests * chore: add more tests * test: add more tests
This commit is contained in:
parent
1aefb8f7ab
commit
302b73ae73
1
.gitignore
vendored
1
.gitignore
vendored
@ -45,4 +45,5 @@ core/test_results.html
|
||||
coverage
|
||||
.yarn
|
||||
.yarnrc
|
||||
test_results.html
|
||||
*.tsbuildinfo
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
|
||||
moduleNameMapper: {
|
||||
'@/(.*)': '<rootDir>/src/$1',
|
||||
},
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import { BaseExtension } from './extension'
|
||||
import { SettingComponentProps } from '../types'
|
||||
import { getJanDataFolderPath, joinPath } from './core'
|
||||
import { fs } from './fs'
|
||||
jest.mock('./core')
|
||||
jest.mock('./fs')
|
||||
|
||||
class TestBaseExtension extends BaseExtension {
|
||||
onLoad(): void {}
|
||||
@ -44,3 +49,103 @@ describe('BaseExtension', () => {
|
||||
// Add your assertions here
|
||||
})
|
||||
})
|
||||
|
||||
describe('BaseExtension', () => {
|
||||
class TestBaseExtension extends BaseExtension {
|
||||
onLoad(): void {}
|
||||
onUnload(): void {}
|
||||
}
|
||||
|
||||
let baseExtension: TestBaseExtension
|
||||
|
||||
beforeEach(() => {
|
||||
baseExtension = new TestBaseExtension('https://example.com', 'TestExtension')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it('should have the correct properties', () => {
|
||||
expect(baseExtension.name).toBe('TestExtension')
|
||||
expect(baseExtension.productName).toBeUndefined()
|
||||
expect(baseExtension.url).toBe('https://example.com')
|
||||
expect(baseExtension.active).toBeUndefined()
|
||||
expect(baseExtension.description).toBeUndefined()
|
||||
expect(baseExtension.version).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for type()', () => {
|
||||
expect(baseExtension.type()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should have abstract methods onLoad() and onUnload()', () => {
|
||||
expect(baseExtension.onLoad).toBeDefined()
|
||||
expect(baseExtension.onUnload).toBeDefined()
|
||||
})
|
||||
|
||||
it('should have installationState() return "NotRequired"', async () => {
|
||||
const installationState = await baseExtension.installationState()
|
||||
expect(installationState).toBe('NotRequired')
|
||||
})
|
||||
|
||||
it('should install the extension', async () => {
|
||||
await baseExtension.install()
|
||||
// Add your assertions here
|
||||
})
|
||||
|
||||
it('should register settings', async () => {
|
||||
const settings: SettingComponentProps[] = [
|
||||
{ key: 'setting1', controllerProps: { value: 'value1' } } as any,
|
||||
{ key: 'setting2', controllerProps: { value: 'value2' } } as any,
|
||||
]
|
||||
|
||||
;(getJanDataFolderPath as jest.Mock).mockResolvedValue('/data')
|
||||
;(joinPath as jest.Mock).mockResolvedValue('/data/settings/TestExtension')
|
||||
;(fs.existsSync as jest.Mock).mockResolvedValue(false)
|
||||
;(fs.mkdir as jest.Mock).mockResolvedValue(undefined)
|
||||
;(fs.writeFileSync as jest.Mock).mockResolvedValue(undefined)
|
||||
|
||||
await baseExtension.registerSettings(settings)
|
||||
|
||||
expect(fs.mkdir).toHaveBeenCalledWith('/data/settings/TestExtension')
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
'/data/settings/TestExtension',
|
||||
JSON.stringify(settings, null, 2)
|
||||
)
|
||||
})
|
||||
|
||||
it('should get setting with default value', async () => {
|
||||
const settings: SettingComponentProps[] = [
|
||||
{ key: 'setting1', controllerProps: { value: 'value1' } } as any,
|
||||
]
|
||||
|
||||
jest.spyOn(baseExtension, 'getSettings').mockResolvedValue(settings)
|
||||
|
||||
const value = await baseExtension.getSetting('setting1', 'defaultValue')
|
||||
expect(value).toBe('value1')
|
||||
|
||||
const defaultValue = await baseExtension.getSetting('setting2', 'defaultValue')
|
||||
expect(defaultValue).toBe('defaultValue')
|
||||
})
|
||||
|
||||
it('should update settings', async () => {
|
||||
const settings: SettingComponentProps[] = [
|
||||
{ key: 'setting1', controllerProps: { value: 'value1' } } as any,
|
||||
]
|
||||
|
||||
jest.spyOn(baseExtension, 'getSettings').mockResolvedValue(settings)
|
||||
;(getJanDataFolderPath as jest.Mock).mockResolvedValue('/data')
|
||||
;(joinPath as jest.Mock).mockResolvedValue('/data/settings/TestExtension/settings.json')
|
||||
;(fs.writeFileSync as jest.Mock).mockResolvedValue(undefined)
|
||||
|
||||
await baseExtension.updateSettings([
|
||||
{ key: 'setting1', controllerProps: { value: 'newValue' } } as any,
|
||||
])
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
'/data/settings/TestExtension/settings.json',
|
||||
JSON.stringify([{ key: 'setting1', controllerProps: { value: 'newValue' } }], null, 2)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
59
core/src/browser/extensions/engines/AIEngine.test.ts
Normal file
59
core/src/browser/extensions/engines/AIEngine.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { AIEngine } from './AIEngine'
|
||||
import { events } from '../../events'
|
||||
import { ModelEvent, Model, ModelFile, InferenceEngine } from '../../../types'
|
||||
import { EngineManager } from './EngineManager'
|
||||
import { fs } from '../../fs'
|
||||
|
||||
jest.mock('../../events')
|
||||
jest.mock('./EngineManager')
|
||||
jest.mock('../../fs')
|
||||
|
||||
class TestAIEngine extends AIEngine {
|
||||
onUnload(): void {}
|
||||
provider = 'test-provider'
|
||||
|
||||
inference(data: any) {}
|
||||
|
||||
stopInference() {}
|
||||
}
|
||||
|
||||
describe('AIEngine', () => {
|
||||
let engine: TestAIEngine
|
||||
|
||||
beforeEach(() => {
|
||||
engine = new TestAIEngine('', '')
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should load model if provider matches', async () => {
|
||||
const model: ModelFile = { id: 'model1', engine: 'test-provider' } as any
|
||||
|
||||
await engine.loadModel(model)
|
||||
|
||||
expect(events.emit).toHaveBeenCalledWith(ModelEvent.OnModelReady, model)
|
||||
})
|
||||
|
||||
it('should not load model if provider does not match', async () => {
|
||||
const model: ModelFile = { id: 'model1', engine: 'other-provider' } as any
|
||||
|
||||
await engine.loadModel(model)
|
||||
|
||||
expect(events.emit).not.toHaveBeenCalledWith(ModelEvent.OnModelReady, model)
|
||||
})
|
||||
|
||||
it('should unload model if provider matches', async () => {
|
||||
const model: Model = { id: 'model1', version: '1.0', engine: 'test-provider' } as any
|
||||
|
||||
await engine.unloadModel(model)
|
||||
|
||||
expect(events.emit).toHaveBeenCalledWith(ModelEvent.OnModelStopped, model)
|
||||
})
|
||||
|
||||
it('should not unload model if provider does not match', async () => {
|
||||
const model: Model = { id: 'model1', version: '1.0', engine: 'other-provider' } as any
|
||||
|
||||
await engine.unloadModel(model)
|
||||
|
||||
expect(events.emit).not.toHaveBeenCalledWith(ModelEvent.OnModelStopped, model)
|
||||
})
|
||||
})
|
||||
43
core/src/browser/extensions/engines/EngineManager.test.ts
Normal file
43
core/src/browser/extensions/engines/EngineManager.test.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { EngineManager } from './EngineManager'
|
||||
import { AIEngine } from './AIEngine'
|
||||
|
||||
// @ts-ignore
|
||||
class MockAIEngine implements AIEngine {
|
||||
provider: string
|
||||
constructor(provider: string) {
|
||||
this.provider = provider
|
||||
}
|
||||
}
|
||||
|
||||
describe('EngineManager', () => {
|
||||
let engineManager: EngineManager
|
||||
|
||||
beforeEach(() => {
|
||||
engineManager = new EngineManager()
|
||||
})
|
||||
|
||||
test('should register an engine', () => {
|
||||
const engine = new MockAIEngine('testProvider')
|
||||
// @ts-ignore
|
||||
engineManager.register(engine)
|
||||
expect(engineManager.engines.get('testProvider')).toBe(engine)
|
||||
})
|
||||
|
||||
test('should retrieve a registered engine by provider', () => {
|
||||
const engine = new MockAIEngine('testProvider')
|
||||
// @ts-ignore
|
||||
engineManager.register(engine)
|
||||
// @ts-ignore
|
||||
const retrievedEngine = engineManager.get<MockAIEngine>('testProvider')
|
||||
expect(retrievedEngine).toBe(engine)
|
||||
})
|
||||
|
||||
test('should return undefined for an unregistered provider', () => {
|
||||
// @ts-ignore
|
||||
const retrievedEngine = engineManager.get<MockAIEngine>('nonExistentProvider')
|
||||
expect(retrievedEngine).toBeUndefined()
|
||||
})
|
||||
})
|
||||
100
core/src/browser/extensions/engines/LocalOAIEngine.test.ts
Normal file
100
core/src/browser/extensions/engines/LocalOAIEngine.test.ts
Normal file
@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { LocalOAIEngine } from './LocalOAIEngine'
|
||||
import { events } from '../../events'
|
||||
import { ModelEvent, ModelFile, Model } from '../../../types'
|
||||
import { executeOnMain, systemInformation, dirName } from '../../core'
|
||||
|
||||
jest.mock('../../core', () => ({
|
||||
executeOnMain: jest.fn(),
|
||||
systemInformation: jest.fn(),
|
||||
dirName: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('../../events', () => ({
|
||||
events: {
|
||||
on: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
class TestLocalOAIEngine extends LocalOAIEngine {
|
||||
inferenceUrl = ''
|
||||
nodeModule = 'testNodeModule'
|
||||
provider = 'testProvider'
|
||||
}
|
||||
|
||||
describe('LocalOAIEngine', () => {
|
||||
let engine: TestLocalOAIEngine
|
||||
|
||||
beforeEach(() => {
|
||||
engine = new TestLocalOAIEngine('', '')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should subscribe to events on load', () => {
|
||||
engine.onLoad()
|
||||
expect(events.on).toHaveBeenCalledWith(ModelEvent.OnModelInit, expect.any(Function))
|
||||
expect(events.on).toHaveBeenCalledWith(ModelEvent.OnModelStop, expect.any(Function))
|
||||
})
|
||||
|
||||
it('should load model correctly', async () => {
|
||||
const model: ModelFile = { engine: 'testProvider', file_path: 'path/to/model' } as any
|
||||
const modelFolder = 'path/to'
|
||||
const systemInfo = { os: 'testOS' }
|
||||
const res = { error: null }
|
||||
|
||||
;(dirName as jest.Mock).mockResolvedValue(modelFolder)
|
||||
;(systemInformation as jest.Mock).mockResolvedValue(systemInfo)
|
||||
;(executeOnMain as jest.Mock).mockResolvedValue(res)
|
||||
|
||||
await engine.loadModel(model)
|
||||
|
||||
expect(dirName).toHaveBeenCalledWith(model.file_path)
|
||||
expect(systemInformation).toHaveBeenCalled()
|
||||
expect(executeOnMain).toHaveBeenCalledWith(
|
||||
engine.nodeModule,
|
||||
engine.loadModelFunctionName,
|
||||
{ modelFolder, model },
|
||||
systemInfo
|
||||
)
|
||||
expect(events.emit).toHaveBeenCalledWith(ModelEvent.OnModelReady, model)
|
||||
})
|
||||
|
||||
it('should handle load model error', async () => {
|
||||
const model: ModelFile = { engine: 'testProvider', file_path: 'path/to/model' } as any
|
||||
const modelFolder = 'path/to'
|
||||
const systemInfo = { os: 'testOS' }
|
||||
const res = { error: 'load error' }
|
||||
|
||||
;(dirName as jest.Mock).mockResolvedValue(modelFolder)
|
||||
;(systemInformation as jest.Mock).mockResolvedValue(systemInfo)
|
||||
;(executeOnMain as jest.Mock).mockResolvedValue(res)
|
||||
|
||||
await expect(engine.loadModel(model)).rejects.toEqual('load error')
|
||||
|
||||
expect(events.emit).toHaveBeenCalledWith(ModelEvent.OnModelFail, { error: res.error })
|
||||
})
|
||||
|
||||
it('should unload model correctly', async () => {
|
||||
const model: Model = { engine: 'testProvider' } as any
|
||||
|
||||
await engine.unloadModel(model)
|
||||
|
||||
expect(executeOnMain).toHaveBeenCalledWith(engine.nodeModule, engine.unloadModelFunctionName)
|
||||
expect(events.emit).toHaveBeenCalledWith(ModelEvent.OnModelStopped, {})
|
||||
})
|
||||
|
||||
it('should not unload model if engine does not match', async () => {
|
||||
const model: Model = { engine: 'otherProvider' } as any
|
||||
|
||||
await engine.unloadModel(model)
|
||||
|
||||
expect(executeOnMain).not.toHaveBeenCalled()
|
||||
expect(events.emit).not.toHaveBeenCalledWith(ModelEvent.OnModelStopped, {})
|
||||
})
|
||||
})
|
||||
119
core/src/browser/extensions/engines/OAIEngine.test.ts
Normal file
119
core/src/browser/extensions/engines/OAIEngine.test.ts
Normal file
@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { OAIEngine } from './OAIEngine'
|
||||
import { events } from '../../events'
|
||||
import {
|
||||
MessageEvent,
|
||||
InferenceEvent,
|
||||
MessageRequest,
|
||||
MessageRequestType,
|
||||
MessageStatus,
|
||||
ChatCompletionRole,
|
||||
ContentType,
|
||||
} from '../../../types'
|
||||
import { requestInference } from './helpers/sse'
|
||||
import { ulid } from 'ulidx'
|
||||
|
||||
jest.mock('./helpers/sse')
|
||||
jest.mock('ulidx')
|
||||
jest.mock('../../events')
|
||||
|
||||
class TestOAIEngine extends OAIEngine {
|
||||
inferenceUrl = 'http://test-inference-url'
|
||||
provider = 'test-provider'
|
||||
|
||||
async headers() {
|
||||
return { Authorization: 'Bearer test-token' }
|
||||
}
|
||||
}
|
||||
|
||||
describe('OAIEngine', () => {
|
||||
let engine: TestOAIEngine
|
||||
|
||||
beforeEach(() => {
|
||||
engine = new TestOAIEngine('', '')
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should subscribe to events on load', () => {
|
||||
engine.onLoad()
|
||||
expect(events.on).toHaveBeenCalledWith(MessageEvent.OnMessageSent, expect.any(Function))
|
||||
expect(events.on).toHaveBeenCalledWith(InferenceEvent.OnInferenceStopped, expect.any(Function))
|
||||
})
|
||||
|
||||
it('should handle inference request', async () => {
|
||||
const data: MessageRequest = {
|
||||
model: { engine: 'test-provider', id: 'test-model' } as any,
|
||||
threadId: 'test-thread',
|
||||
type: MessageRequestType.Thread,
|
||||
assistantId: 'test-assistant',
|
||||
messages: [{ role: ChatCompletionRole.User, content: 'Hello' }],
|
||||
}
|
||||
|
||||
;(ulid as jest.Mock).mockReturnValue('test-id')
|
||||
;(requestInference as jest.Mock).mockReturnValue({
|
||||
subscribe: ({ next, complete }: any) => {
|
||||
next('test response')
|
||||
complete()
|
||||
},
|
||||
})
|
||||
|
||||
await engine.inference(data)
|
||||
|
||||
expect(requestInference).toHaveBeenCalledWith(
|
||||
'http://test-inference-url',
|
||||
expect.objectContaining({ model: 'test-model' }),
|
||||
expect.any(Object),
|
||||
expect.any(AbortController),
|
||||
{ Authorization: 'Bearer test-token' },
|
||||
undefined
|
||||
)
|
||||
|
||||
expect(events.emit).toHaveBeenCalledWith(
|
||||
MessageEvent.OnMessageResponse,
|
||||
expect.objectContaining({ id: 'test-id' })
|
||||
)
|
||||
expect(events.emit).toHaveBeenCalledWith(
|
||||
MessageEvent.OnMessageUpdate,
|
||||
expect.objectContaining({
|
||||
content: [{ type: ContentType.Text, text: { value: 'test response', annotations: [] } }],
|
||||
status: MessageStatus.Ready,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle inference error', async () => {
|
||||
const data: MessageRequest = {
|
||||
model: { engine: 'test-provider', id: 'test-model' } as any,
|
||||
threadId: 'test-thread',
|
||||
type: MessageRequestType.Thread,
|
||||
assistantId: 'test-assistant',
|
||||
messages: [{ role: ChatCompletionRole.User, content: 'Hello' }],
|
||||
}
|
||||
|
||||
;(ulid as jest.Mock).mockReturnValue('test-id')
|
||||
;(requestInference as jest.Mock).mockReturnValue({
|
||||
subscribe: ({ error }: any) => {
|
||||
error({ message: 'test error', code: 500 })
|
||||
},
|
||||
})
|
||||
|
||||
await engine.inference(data)
|
||||
|
||||
expect(events.emit).toHaveBeenCalledWith(
|
||||
MessageEvent.OnMessageUpdate,
|
||||
expect.objectContaining({
|
||||
content: [{ type: ContentType.Text, text: { value: 'test error', annotations: [] } }],
|
||||
status: MessageStatus.Error,
|
||||
error_code: 500,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should stop inference', () => {
|
||||
engine.stopInference()
|
||||
expect(engine.isCancelled).toBe(true)
|
||||
expect(engine.controller.signal.aborted).toBe(true)
|
||||
})
|
||||
})
|
||||
43
core/src/browser/extensions/engines/RemoteOAIEngine.test.ts
Normal file
43
core/src/browser/extensions/engines/RemoteOAIEngine.test.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { RemoteOAIEngine } from './'
|
||||
|
||||
class TestRemoteOAIEngine extends RemoteOAIEngine {
|
||||
inferenceUrl: string = ''
|
||||
provider: string = 'TestRemoteOAIEngine'
|
||||
}
|
||||
|
||||
describe('RemoteOAIEngine', () => {
|
||||
let engine: TestRemoteOAIEngine
|
||||
|
||||
beforeEach(() => {
|
||||
engine = new TestRemoteOAIEngine('', '')
|
||||
})
|
||||
|
||||
test('should call onLoad and super.onLoad', () => {
|
||||
const onLoadSpy = jest.spyOn(engine, 'onLoad')
|
||||
const superOnLoadSpy = jest.spyOn(Object.getPrototypeOf(RemoteOAIEngine.prototype), 'onLoad')
|
||||
engine.onLoad()
|
||||
|
||||
expect(onLoadSpy).toHaveBeenCalled()
|
||||
expect(superOnLoadSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should return headers with apiKey', async () => {
|
||||
engine.apiKey = 'test-api-key'
|
||||
const headers = await engine.headers()
|
||||
|
||||
expect(headers).toEqual({
|
||||
'Authorization': 'Bearer test-api-key',
|
||||
'api-key': 'test-api-key',
|
||||
})
|
||||
})
|
||||
|
||||
test('should return empty headers when apiKey is not set', async () => {
|
||||
engine.apiKey = undefined
|
||||
const headers = await engine.headers()
|
||||
|
||||
expect(headers).toEqual({})
|
||||
})
|
||||
})
|
||||
@ -1,6 +1,7 @@
|
||||
import { lastValueFrom, Observable } from 'rxjs'
|
||||
import { requestInference } from './sse'
|
||||
|
||||
import { ReadableStream } from 'stream/web';
|
||||
describe('requestInference', () => {
|
||||
it('should send a request to the inference server and return an Observable', () => {
|
||||
// Mock the fetch function
|
||||
@ -58,3 +59,66 @@ describe('requestInference', () => {
|
||||
expect(lastValueFrom(result)).rejects.toEqual({ message: 'Wrong API Key', code: 'invalid_api_key' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle a successful response with a transformResponse function', () => {
|
||||
// Mock the fetch function
|
||||
const mockFetch: any = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ choices: [{ message: { content: 'Generated response' } }] }),
|
||||
headers: new Headers(),
|
||||
redirected: false,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
})
|
||||
)
|
||||
jest.spyOn(global, 'fetch').mockImplementation(mockFetch)
|
||||
|
||||
// Define the test inputs
|
||||
const inferenceUrl = 'https://inference-server.com'
|
||||
const requestBody = { message: 'Hello' }
|
||||
const model = { id: 'model-id', parameters: { stream: false } }
|
||||
const transformResponse = (data: any) => data.choices[0].message.content.toUpperCase()
|
||||
|
||||
// Call the function
|
||||
const result = requestInference(inferenceUrl, requestBody, model, undefined, undefined, transformResponse)
|
||||
|
||||
// Assert the expected behavior
|
||||
expect(result).toBeInstanceOf(Observable)
|
||||
expect(lastValueFrom(result)).resolves.toEqual('GENERATED RESPONSE')
|
||||
})
|
||||
|
||||
|
||||
it('should handle a successful response with streaming enabled', () => {
|
||||
// Mock the fetch function
|
||||
const mockFetch: any = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode('data: {"choices": [{"delta": {"content": "Streamed"}}]}'));
|
||||
controller.enqueue(new TextEncoder().encode('data: [DONE]'));
|
||||
controller.close();
|
||||
}
|
||||
}),
|
||||
headers: new Headers(),
|
||||
redirected: false,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
})
|
||||
);
|
||||
jest.spyOn(global, 'fetch').mockImplementation(mockFetch);
|
||||
|
||||
// Define the test inputs
|
||||
const inferenceUrl = 'https://inference-server.com';
|
||||
const requestBody = { message: 'Hello' };
|
||||
const model = { id: 'model-id', parameters: { stream: true } };
|
||||
|
||||
// Call the function
|
||||
const result = requestInference(inferenceUrl, requestBody, model);
|
||||
|
||||
// Assert the expected behavior
|
||||
expect(result).toBeInstanceOf(Observable);
|
||||
expect(lastValueFrom(result)).resolves.toEqual('Streamed');
|
||||
});
|
||||
|
||||
|
||||
6
core/src/browser/extensions/engines/index.test.ts
Normal file
6
core/src/browser/extensions/engines/index.test.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
import { expect } from '@jest/globals';
|
||||
|
||||
it('should re-export all exports from ./AIEngine', () => {
|
||||
expect(require('./index')).toHaveProperty('AIEngine');
|
||||
});
|
||||
32
core/src/browser/extensions/index.test.ts
Normal file
32
core/src/browser/extensions/index.test.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { ConversationalExtension } from './index';
|
||||
import { InferenceExtension } from './index';
|
||||
import { MonitoringExtension } from './index';
|
||||
import { AssistantExtension } from './index';
|
||||
import { ModelExtension } from './index';
|
||||
import * as Engines from './index';
|
||||
|
||||
describe('index.ts exports', () => {
|
||||
test('should export ConversationalExtension', () => {
|
||||
expect(ConversationalExtension).toBeDefined();
|
||||
});
|
||||
|
||||
test('should export InferenceExtension', () => {
|
||||
expect(InferenceExtension).toBeDefined();
|
||||
});
|
||||
|
||||
test('should export MonitoringExtension', () => {
|
||||
expect(MonitoringExtension).toBeDefined();
|
||||
});
|
||||
|
||||
test('should export AssistantExtension', () => {
|
||||
expect(AssistantExtension).toBeDefined();
|
||||
});
|
||||
|
||||
test('should export ModelExtension', () => {
|
||||
expect(ModelExtension).toBeDefined();
|
||||
});
|
||||
|
||||
test('should export Engines', () => {
|
||||
expect(Engines).toBeDefined();
|
||||
});
|
||||
});
|
||||
97
core/src/browser/fs.test.ts
Normal file
97
core/src/browser/fs.test.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { fs } from './fs'
|
||||
|
||||
describe('fs module', () => {
|
||||
beforeEach(() => {
|
||||
globalThis.core = {
|
||||
api: {
|
||||
writeFileSync: jest.fn(),
|
||||
writeBlob: jest.fn(),
|
||||
readFileSync: jest.fn(),
|
||||
existsSync: jest.fn(),
|
||||
readdirSync: jest.fn(),
|
||||
mkdir: jest.fn(),
|
||||
rm: jest.fn(),
|
||||
unlinkSync: jest.fn(),
|
||||
appendFileSync: jest.fn(),
|
||||
copyFile: jest.fn(),
|
||||
getGgufFiles: jest.fn(),
|
||||
fileStat: jest.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('should call writeFileSync with correct arguments', () => {
|
||||
const args = ['path/to/file', 'data']
|
||||
fs.writeFileSync(...args)
|
||||
expect(globalThis.core.api.writeFileSync).toHaveBeenCalledWith(...args)
|
||||
})
|
||||
|
||||
it('should call writeBlob with correct arguments', async () => {
|
||||
const path = 'path/to/file'
|
||||
const data = 'blob data'
|
||||
await fs.writeBlob(path, data)
|
||||
expect(globalThis.core.api.writeBlob).toHaveBeenCalledWith(path, data)
|
||||
})
|
||||
|
||||
it('should call readFileSync with correct arguments', () => {
|
||||
const args = ['path/to/file']
|
||||
fs.readFileSync(...args)
|
||||
expect(globalThis.core.api.readFileSync).toHaveBeenCalledWith(...args)
|
||||
})
|
||||
|
||||
it('should call existsSync with correct arguments', () => {
|
||||
const args = ['path/to/file']
|
||||
fs.existsSync(...args)
|
||||
expect(globalThis.core.api.existsSync).toHaveBeenCalledWith(...args)
|
||||
})
|
||||
|
||||
it('should call readdirSync with correct arguments', () => {
|
||||
const args = ['path/to/directory']
|
||||
fs.readdirSync(...args)
|
||||
expect(globalThis.core.api.readdirSync).toHaveBeenCalledWith(...args)
|
||||
})
|
||||
|
||||
it('should call mkdir with correct arguments', () => {
|
||||
const args = ['path/to/directory']
|
||||
fs.mkdir(...args)
|
||||
expect(globalThis.core.api.mkdir).toHaveBeenCalledWith(...args)
|
||||
})
|
||||
|
||||
it('should call rm with correct arguments', () => {
|
||||
const args = ['path/to/directory']
|
||||
fs.rm(...args)
|
||||
expect(globalThis.core.api.rm).toHaveBeenCalledWith(...args, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('should call unlinkSync with correct arguments', () => {
|
||||
const args = ['path/to/file']
|
||||
fs.unlinkSync(...args)
|
||||
expect(globalThis.core.api.unlinkSync).toHaveBeenCalledWith(...args)
|
||||
})
|
||||
|
||||
it('should call appendFileSync with correct arguments', () => {
|
||||
const args = ['path/to/file', 'data']
|
||||
fs.appendFileSync(...args)
|
||||
expect(globalThis.core.api.appendFileSync).toHaveBeenCalledWith(...args)
|
||||
})
|
||||
|
||||
it('should call copyFile with correct arguments', async () => {
|
||||
const src = 'path/to/src'
|
||||
const dest = 'path/to/dest'
|
||||
await fs.copyFile(src, dest)
|
||||
expect(globalThis.core.api.copyFile).toHaveBeenCalledWith(src, dest)
|
||||
})
|
||||
|
||||
it('should call getGgufFiles with correct arguments', async () => {
|
||||
const paths = ['path/to/file1', 'path/to/file2']
|
||||
await fs.getGgufFiles(paths)
|
||||
expect(globalThis.core.api.getGgufFiles).toHaveBeenCalledWith(paths)
|
||||
})
|
||||
|
||||
it('should call fileStat with correct arguments', async () => {
|
||||
const path = 'path/to/file'
|
||||
const outsideJanDataFolder = true
|
||||
await fs.fileStat(path, outsideJanDataFolder)
|
||||
expect(globalThis.core.api.fileStat).toHaveBeenCalledWith(path, outsideJanDataFolder)
|
||||
})
|
||||
})
|
||||
55
core/src/browser/tools/tool.test.ts
Normal file
55
core/src/browser/tools/tool.test.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { ToolManager } from '../../browser/tools/manager'
|
||||
import { InferenceTool } from '../../browser/tools/tool'
|
||||
import { AssistantTool, MessageRequest } from '../../types'
|
||||
|
||||
class MockInferenceTool implements InferenceTool {
|
||||
name = 'mockTool'
|
||||
process(request: MessageRequest, tool: AssistantTool): Promise<MessageRequest> {
|
||||
return Promise.resolve(request)
|
||||
}
|
||||
}
|
||||
|
||||
it('should register a tool', () => {
|
||||
const manager = new ToolManager()
|
||||
const tool = new MockInferenceTool()
|
||||
manager.register(tool)
|
||||
expect(manager.get(tool.name)).toBe(tool)
|
||||
})
|
||||
|
||||
it('should retrieve a tool by its name', () => {
|
||||
const manager = new ToolManager()
|
||||
const tool = new MockInferenceTool()
|
||||
manager.register(tool)
|
||||
const retrievedTool = manager.get(tool.name)
|
||||
expect(retrievedTool).toBe(tool)
|
||||
})
|
||||
|
||||
it('should return undefined for a non-existent tool', () => {
|
||||
const manager = new ToolManager()
|
||||
const retrievedTool = manager.get('nonExistentTool')
|
||||
expect(retrievedTool).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should process the message request with enabled tools', async () => {
|
||||
const manager = new ToolManager()
|
||||
const tool = new MockInferenceTool()
|
||||
manager.register(tool)
|
||||
|
||||
const request: MessageRequest = { message: 'test' } as any
|
||||
const tools: AssistantTool[] = [{ type: 'mockTool', enabled: true }] as any
|
||||
|
||||
const result = await manager.process(request, tools)
|
||||
expect(result).toBe(request)
|
||||
})
|
||||
|
||||
it('should skip processing for disabled tools', async () => {
|
||||
const manager = new ToolManager()
|
||||
const tool = new MockInferenceTool()
|
||||
manager.register(tool)
|
||||
|
||||
const request: MessageRequest = { message: 'test' } as any
|
||||
const tools: AssistantTool[] = [{ type: 'mockTool', enabled: false }] as any
|
||||
|
||||
const result = await manager.process(request, tools)
|
||||
expect(result).toBe(request)
|
||||
})
|
||||
264
core/src/node/api/restful/helper/builder.test.ts
Normal file
264
core/src/node/api/restful/helper/builder.test.ts
Normal file
@ -0,0 +1,264 @@
|
||||
import {
|
||||
existsSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
mkdirSync,
|
||||
appendFileSync,
|
||||
rmdirSync,
|
||||
} from 'fs'
|
||||
import { join } from 'path'
|
||||
import {
|
||||
getBuilder,
|
||||
retrieveBuilder,
|
||||
deleteBuilder,
|
||||
getMessages,
|
||||
retrieveMessage,
|
||||
createThread,
|
||||
updateThread,
|
||||
createMessage,
|
||||
downloadModel,
|
||||
chatCompletions,
|
||||
} from './builder'
|
||||
import { RouteConfiguration } from './configuration'
|
||||
|
||||
jest.mock('fs')
|
||||
jest.mock('path')
|
||||
jest.mock('../../../helper', () => ({
|
||||
getEngineConfiguration: jest.fn(),
|
||||
getJanDataFolderPath: jest.fn().mockReturnValue('/mock/path'),
|
||||
}))
|
||||
jest.mock('request')
|
||||
jest.mock('request-progress')
|
||||
jest.mock('node-fetch')
|
||||
|
||||
describe('builder helper functions', () => {
|
||||
const mockConfiguration: RouteConfiguration = {
|
||||
dirName: 'mockDir',
|
||||
metadataFileName: 'metadata.json',
|
||||
delete: {
|
||||
object: 'mockObject',
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('getBuilder', () => {
|
||||
it('should return an empty array if directory does not exist', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(false)
|
||||
const result = await getBuilder(mockConfiguration)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should return model data if directory exists', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['file1'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ id: 'model1' }))
|
||||
|
||||
const result = await getBuilder(mockConfiguration)
|
||||
expect(result).toEqual([{ id: 'model1' }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('retrieveBuilder', () => {
|
||||
it('should return undefined if no data matches the id', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['file1'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ id: 'model1' }))
|
||||
|
||||
const result = await retrieveBuilder(mockConfiguration, 'nonexistentId')
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return the matching data', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['file1'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ id: 'model1' }))
|
||||
|
||||
const result = await retrieveBuilder(mockConfiguration, 'model1')
|
||||
expect(result).toEqual({ id: 'model1' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteBuilder', () => {
|
||||
it('should return a message if trying to delete Jan assistant', async () => {
|
||||
const result = await deleteBuilder({ ...mockConfiguration, dirName: 'assistants' }, 'jan')
|
||||
expect(result).toEqual({ message: 'Cannot delete Jan assistant' })
|
||||
})
|
||||
|
||||
it('should return a message if data is not found', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['file1'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ id: 'model1' }))
|
||||
|
||||
const result = await deleteBuilder(mockConfiguration, 'nonexistentId')
|
||||
expect(result).toEqual({ message: 'Not found' })
|
||||
})
|
||||
|
||||
it('should delete the directory and return success message', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['file1'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ id: 'model1' }))
|
||||
|
||||
const result = await deleteBuilder(mockConfiguration, 'model1')
|
||||
expect(rmdirSync).toHaveBeenCalledWith(join('/mock/path', 'mockDir', 'model1'), {
|
||||
recursive: true,
|
||||
})
|
||||
expect(result).toEqual({ id: 'model1', object: 'mockObject', deleted: true })
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMessages', () => {
|
||||
it('should return an empty array if message file does not exist', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(false)
|
||||
|
||||
const result = await getMessages('thread1')
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should return messages if message file exists', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['messages.jsonl'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue('{"id":"msg1"}\n{"id":"msg2"}\n')
|
||||
|
||||
const result = await getMessages('thread1')
|
||||
expect(result).toEqual([{ id: 'msg1' }, { id: 'msg2' }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('retrieveMessage', () => {
|
||||
it('should return a message if no messages match the id', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['messages.jsonl'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue('{"id":"msg1"}\n')
|
||||
|
||||
const result = await retrieveMessage('thread1', 'nonexistentId')
|
||||
expect(result).toEqual({ message: 'Not found' })
|
||||
})
|
||||
|
||||
it('should return the matching message', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['messages.jsonl'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue('{"id":"msg1"}\n')
|
||||
|
||||
const result = await retrieveMessage('thread1', 'msg1')
|
||||
expect(result).toEqual({ id: 'msg1' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('createThread', () => {
|
||||
it('should return a message if thread has no assistants', async () => {
|
||||
const result = await createThread({})
|
||||
expect(result).toEqual({ message: 'Thread must have at least one assistant' })
|
||||
})
|
||||
|
||||
it('should create a thread and return the updated thread', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(false)
|
||||
|
||||
const thread = { assistants: [{ assistant_id: 'assistant1' }] }
|
||||
const result = await createThread(thread)
|
||||
expect(mkdirSync).toHaveBeenCalled()
|
||||
expect(writeFileSync).toHaveBeenCalled()
|
||||
expect(result.id).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateThread', () => {
|
||||
it('should return a message if thread is not found', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['file1'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ id: 'model1' }))
|
||||
|
||||
const result = await updateThread('nonexistentId', {})
|
||||
expect(result).toEqual({ message: 'Thread not found' })
|
||||
})
|
||||
|
||||
it('should update the thread and return the updated thread', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['file1'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ id: 'model1' }))
|
||||
|
||||
const result = await updateThread('model1', { name: 'updatedName' })
|
||||
expect(writeFileSync).toHaveBeenCalled()
|
||||
expect(result.name).toEqual('updatedName')
|
||||
})
|
||||
})
|
||||
|
||||
describe('createMessage', () => {
|
||||
it('should create a message and return the created message', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(false)
|
||||
const message = { role: 'user', content: 'Hello' }
|
||||
|
||||
const result = (await createMessage('thread1', message)) as any
|
||||
expect(mkdirSync).toHaveBeenCalled()
|
||||
expect(appendFileSync).toHaveBeenCalled()
|
||||
expect(result.id).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('downloadModel', () => {
|
||||
it('should return a message if model is not found', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['file1'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ id: 'model1' }))
|
||||
|
||||
const result = await downloadModel('nonexistentId')
|
||||
expect(result).toEqual({ message: 'Model not found' })
|
||||
})
|
||||
|
||||
it('should start downloading the model', async () => {
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['file1'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue(
|
||||
JSON.stringify({ id: 'model1', object: 'model', sources: ['http://example.com'] })
|
||||
)
|
||||
const result = await downloadModel('model1')
|
||||
expect(result).toEqual({ message: 'Starting download model1' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('chatCompletions', () => {
|
||||
it('should return an error if model is not found', async () => {
|
||||
const request = { body: { model: 'nonexistentModel' } }
|
||||
const reply = { code: jest.fn().mockReturnThis(), send: jest.fn() }
|
||||
|
||||
await chatCompletions(request, reply)
|
||||
expect(reply.code).toHaveBeenCalledWith(404)
|
||||
expect(reply.send).toHaveBeenCalledWith({
|
||||
error: {
|
||||
message: 'The model nonexistentModel does not exist',
|
||||
type: 'invalid_request_error',
|
||||
param: null,
|
||||
code: 'model_not_found',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should return the chat completions', async () => {
|
||||
const request = { body: { model: 'model1' } }
|
||||
const reply = {
|
||||
code: jest.fn().mockReturnThis(),
|
||||
send: jest.fn(),
|
||||
raw: { writeHead: jest.fn(), pipe: jest.fn() },
|
||||
}
|
||||
|
||||
;(existsSync as jest.Mock).mockReturnValue(true)
|
||||
;(readdirSync as jest.Mock).mockReturnValue(['file1'])
|
||||
;(readFileSync as jest.Mock).mockReturnValue(
|
||||
JSON.stringify({ id: 'model1', engine: 'openai' })
|
||||
)
|
||||
|
||||
// Mock fetch
|
||||
const fetch = require('node-fetch')
|
||||
fetch.mockResolvedValue({
|
||||
status: 200,
|
||||
body: { pipe: jest.fn() },
|
||||
json: jest.fn().mockResolvedValue({ completions: ['completion1'] }),
|
||||
})
|
||||
await chatCompletions(request, reply)
|
||||
expect(reply.raw.writeHead).toHaveBeenCalledWith(200, expect.any(Object))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -280,13 +280,13 @@ export const downloadModel = async (
|
||||
for (const source of model.sources) {
|
||||
const rq = request({ url: source, strictSSL, proxy })
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
?.on('progress', function (state: any) {
|
||||
console.debug('progress', JSON.stringify(state, null, 2))
|
||||
})
|
||||
.on('error', function (err: Error) {
|
||||
?.on('error', function (err: Error) {
|
||||
console.error('error', err)
|
||||
})
|
||||
.on('end', function () {
|
||||
?.on('end', function () {
|
||||
console.debug('end')
|
||||
})
|
||||
.pipe(createWriteStream(modelBinaryPath))
|
||||
|
||||
16
core/src/node/api/restful/helper/startStopModel.test.ts
Normal file
16
core/src/node/api/restful/helper/startStopModel.test.ts
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
import { startModel } from './startStopModel'
|
||||
|
||||
describe('startModel', () => {
|
||||
it('test_startModel_error', async () => {
|
||||
const modelId = 'testModelId'
|
||||
const settingParams = undefined
|
||||
|
||||
const result = await startModel(modelId, settingParams)
|
||||
|
||||
expect(result).toEqual({
|
||||
error: expect.any(Error),
|
||||
})
|
||||
})
|
||||
})
|
||||
7
core/src/node/extension/index.test.ts
Normal file
7
core/src/node/extension/index.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
import { useExtensions } from './index'
|
||||
|
||||
test('testUseExtensionsMissingPath', () => {
|
||||
expect(() => useExtensions(undefined as any)).toThrowError('A path to the extensions folder is required to use extensions')
|
||||
})
|
||||
24
core/src/types/api/index.test.ts
Normal file
24
core/src/types/api/index.test.ts
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
|
||||
import { NativeRoute } from '../index';
|
||||
|
||||
test('testNativeRouteEnum', () => {
|
||||
expect(NativeRoute.openExternalUrl).toBe('openExternalUrl');
|
||||
expect(NativeRoute.openAppDirectory).toBe('openAppDirectory');
|
||||
expect(NativeRoute.openFileExplore).toBe('openFileExplorer');
|
||||
expect(NativeRoute.selectDirectory).toBe('selectDirectory');
|
||||
expect(NativeRoute.selectFiles).toBe('selectFiles');
|
||||
expect(NativeRoute.relaunch).toBe('relaunch');
|
||||
expect(NativeRoute.setNativeThemeLight).toBe('setNativeThemeLight');
|
||||
expect(NativeRoute.setNativeThemeDark).toBe('setNativeThemeDark');
|
||||
expect(NativeRoute.setMinimizeApp).toBe('setMinimizeApp');
|
||||
expect(NativeRoute.setCloseApp).toBe('setCloseApp');
|
||||
expect(NativeRoute.setMaximizeApp).toBe('setMaximizeApp');
|
||||
expect(NativeRoute.showOpenMenu).toBe('showOpenMenu');
|
||||
expect(NativeRoute.hideQuickAskWindow).toBe('hideQuickAskWindow');
|
||||
expect(NativeRoute.sendQuickAskInput).toBe('sendQuickAskInput');
|
||||
expect(NativeRoute.hideMainWindow).toBe('hideMainWindow');
|
||||
expect(NativeRoute.showMainWindow).toBe('showMainWindow');
|
||||
expect(NativeRoute.quickAskSizeUpdated).toBe('quickAskSizeUpdated');
|
||||
expect(NativeRoute.ackDeepLink).toBe('ackDeepLink');
|
||||
});
|
||||
9
core/src/types/config/appConfigEvent.test.ts
Normal file
9
core/src/types/config/appConfigEvent.test.ts
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
import { AppConfigurationEventName } from './appConfigEvent';
|
||||
|
||||
describe('AppConfigurationEventName', () => {
|
||||
it('should have the correct value for OnConfigurationUpdate', () => {
|
||||
expect(AppConfigurationEventName.OnConfigurationUpdate).toBe('OnConfigurationUpdate');
|
||||
});
|
||||
});
|
||||
28
core/src/types/huggingface/huggingfaceEntity.test.ts
Normal file
28
core/src/types/huggingface/huggingfaceEntity.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
|
||||
import { AllQuantizations } from './huggingfaceEntity';
|
||||
|
||||
test('testAllQuantizationsArray', () => {
|
||||
expect(AllQuantizations).toEqual([
|
||||
'Q3_K_S',
|
||||
'Q3_K_M',
|
||||
'Q3_K_L',
|
||||
'Q4_K_S',
|
||||
'Q4_K_M',
|
||||
'Q5_K_S',
|
||||
'Q5_K_M',
|
||||
'Q4_0',
|
||||
'Q4_1',
|
||||
'Q5_0',
|
||||
'Q5_1',
|
||||
'IQ2_XXS',
|
||||
'IQ2_XS',
|
||||
'Q2_K',
|
||||
'Q2_K_S',
|
||||
'Q6_K',
|
||||
'Q8_0',
|
||||
'F16',
|
||||
'F32',
|
||||
'COPY',
|
||||
]);
|
||||
});
|
||||
8
core/src/types/huggingface/index.test.ts
Normal file
8
core/src/types/huggingface/index.test.ts
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
import * as huggingfaceEntity from './huggingfaceEntity';
|
||||
import * as index from './index';
|
||||
|
||||
test('test_exports_from_huggingfaceEntity', () => {
|
||||
expect(index).toEqual(huggingfaceEntity);
|
||||
});
|
||||
28
core/src/types/index.test.ts
Normal file
28
core/src/types/index.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
import * as assistant from './assistant';
|
||||
import * as model from './model';
|
||||
import * as thread from './thread';
|
||||
import * as message from './message';
|
||||
import * as inference from './inference';
|
||||
import * as monitoring from './monitoring';
|
||||
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';
|
||||
|
||||
test('test_module_exports', () => {
|
||||
expect(assistant).toBeDefined();
|
||||
expect(model).toBeDefined();
|
||||
expect(thread).toBeDefined();
|
||||
expect(message).toBeDefined();
|
||||
expect(inference).toBeDefined();
|
||||
expect(monitoring).toBeDefined();
|
||||
expect(file).toBeDefined();
|
||||
expect(config).toBeDefined();
|
||||
expect(huggingface).toBeDefined();
|
||||
expect(miscellaneous).toBeDefined();
|
||||
expect(api).toBeDefined();
|
||||
expect(setting).toBeDefined();
|
||||
});
|
||||
13
core/src/types/inference/inferenceEntity.test.ts
Normal file
13
core/src/types/inference/inferenceEntity.test.ts
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
import { ChatCompletionMessage, ChatCompletionRole } from './inferenceEntity';
|
||||
|
||||
test('test_chatCompletionMessage_withStringContent_andSystemRole', () => {
|
||||
const message: ChatCompletionMessage = {
|
||||
content: 'Hello, world!',
|
||||
role: ChatCompletionRole.System,
|
||||
};
|
||||
|
||||
expect(message.content).toBe('Hello, world!');
|
||||
expect(message.role).toBe(ChatCompletionRole.System);
|
||||
});
|
||||
7
core/src/types/inference/inferenceEvent.test.ts
Normal file
7
core/src/types/inference/inferenceEvent.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
import { InferenceEvent } from './inferenceEvent';
|
||||
|
||||
test('testInferenceEventEnumContainsOnInferenceStopped', () => {
|
||||
expect(InferenceEvent.OnInferenceStopped).toBe('OnInferenceStopped');
|
||||
});
|
||||
7
core/src/types/message/messageEvent.test.ts
Normal file
7
core/src/types/message/messageEvent.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
import { MessageEvent } from './messageEvent';
|
||||
|
||||
test('testOnMessageSentValue', () => {
|
||||
expect(MessageEvent.OnMessageSent).toBe('OnMessageSent');
|
||||
});
|
||||
7
core/src/types/message/messageRequestType.test.ts
Normal file
7
core/src/types/message/messageRequestType.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
import { MessageRequestType } from './messageRequestType';
|
||||
|
||||
test('testMessageRequestTypeEnumContainsThread', () => {
|
||||
expect(MessageRequestType.Thread).toBe('Thread');
|
||||
});
|
||||
30
core/src/types/model/modelEntity.test.ts
Normal file
30
core/src/types/model/modelEntity.test.ts
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
|
||||
import { Model, ModelSettingParams, ModelRuntimeParams, InferenceEngine } from '../model'
|
||||
|
||||
test('testValidModelCreation', () => {
|
||||
const model: Model = {
|
||||
object: 'model',
|
||||
version: '1.0',
|
||||
format: 'format1',
|
||||
sources: [{ filename: 'model.bin', url: 'http://example.com/model.bin' }],
|
||||
id: 'model1',
|
||||
name: 'Test Model',
|
||||
created: Date.now(),
|
||||
description: 'A cool model from Huggingface',
|
||||
settings: { ctx_len: 100, ngl: 50, embedding: true },
|
||||
parameters: { temperature: 0.5, token_limit: 100, top_k: 10 },
|
||||
metadata: { author: 'Author', tags: ['tag1', 'tag2'], size: 100 },
|
||||
engine: InferenceEngine.anthropic
|
||||
};
|
||||
|
||||
expect(model).toBeDefined();
|
||||
expect(model.object).toBe('model');
|
||||
expect(model.version).toBe('1.0');
|
||||
expect(model.sources).toHaveLength(1);
|
||||
expect(model.sources[0].filename).toBe('model.bin');
|
||||
expect(model.settings).toBeDefined();
|
||||
expect(model.parameters).toBeDefined();
|
||||
expect(model.metadata).toBeDefined();
|
||||
expect(model.engine).toBe(InferenceEngine.anthropic);
|
||||
});
|
||||
7
core/src/types/model/modelEvent.test.ts
Normal file
7
core/src/types/model/modelEvent.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
import { ModelEvent } from './modelEvent';
|
||||
|
||||
test('testOnModelInit', () => {
|
||||
expect(ModelEvent.OnModelInit).toBe('OnModelInit');
|
||||
});
|
||||
@ -3,6 +3,7 @@ module.exports = {
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/*.test.*'],
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
testEnvironment: 'jsdom',
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
"scripts": {
|
||||
"lint": "yarn workspace jan lint && yarn workspace @janhq/web lint",
|
||||
"test:unit": "jest",
|
||||
"test:coverage": "jest --coverage --collectCoverageFrom='src/**/*.{ts,tsx}'",
|
||||
"test:coverage": "jest --coverage",
|
||||
"test": "yarn workspace jan test:e2e",
|
||||
"test-local": "yarn lint && yarn build:test && yarn test",
|
||||
"pre-install:darwin": "find extensions -type f -path \"**/*.tgz\" -exec cp {} pre-install \\;",
|
||||
|
||||
19
testRunner.js
Normal file
19
testRunner.js
Normal file
@ -0,0 +1,19 @@
|
||||
const jestRunner = require('jest-runner')
|
||||
|
||||
class EmptyTestFileRunner extends jestRunner.default {
|
||||
async runTests(tests, watcher, onStart, onResult, onFailure, options) {
|
||||
const nonEmptyTests = tests.filter(
|
||||
(test) => test.context.hasteFS.getSize(test.path) > 0
|
||||
)
|
||||
return super.runTests(
|
||||
nonEmptyTests,
|
||||
watcher,
|
||||
onStart,
|
||||
onResult,
|
||||
onFailure,
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EmptyTestFileRunner
|
||||
23
web/containers/Loader/Loader.test.tsx
Normal file
23
web/containers/Loader/Loader.test.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
// Loader.test.tsx
|
||||
import '@testing-library/jest-dom';
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import Loader from './index'
|
||||
|
||||
describe('Loader Component', () => {
|
||||
it('renders without crashing', () => {
|
||||
render(<Loader description="Loading..." />)
|
||||
})
|
||||
|
||||
it('displays the correct description', () => {
|
||||
const descriptionText = 'Loading...'
|
||||
render(<Loader description={descriptionText} />)
|
||||
expect(screen.getByText(descriptionText)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the correct number of loader elements', () => {
|
||||
const { container } = render(<Loader description="Loading..." />)
|
||||
const loaderElements = container.querySelectorAll('label')
|
||||
expect(loaderElements).toHaveLength(6)
|
||||
})
|
||||
})
|
||||
19
web/extension/Extension.test.ts
Normal file
19
web/extension/Extension.test.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import Extension from "./Extension";
|
||||
|
||||
test('should create an Extension instance with all properties', () => {
|
||||
const url = 'https://example.com';
|
||||
const name = 'Test Extension';
|
||||
const productName = 'Test Product';
|
||||
const active = true;
|
||||
const description = 'Test Description';
|
||||
const version = '1.0.0';
|
||||
|
||||
const extension = new Extension(url, name, productName, active, description, version);
|
||||
|
||||
expect(extension.url).toBe(url);
|
||||
expect(extension.name).toBe(name);
|
||||
expect(extension.productName).toBe(productName);
|
||||
expect(extension.active).toBe(active);
|
||||
expect(extension.description).toBe(description);
|
||||
expect(extension.version).toBe(version);
|
||||
});
|
||||
@ -12,13 +12,13 @@ class Extension {
|
||||
url: string
|
||||
|
||||
/** @type {boolean} Whether the extension is activated or not. */
|
||||
active
|
||||
active?: boolean
|
||||
|
||||
/** @type {string} Extension's description. */
|
||||
description
|
||||
description?: string
|
||||
|
||||
/** @type {string} Extension's version. */
|
||||
version
|
||||
version?: string
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
|
||||
131
web/extension/ExtensionManager.test.ts
Normal file
131
web/extension/ExtensionManager.test.ts
Normal file
@ -0,0 +1,131 @@
|
||||
// ExtensionManager.test.ts
|
||||
import { AIEngine, BaseExtension, ExtensionTypeEnum } from '@janhq/core'
|
||||
import { ExtensionManager } from './ExtensionManager'
|
||||
import Extension from './Extension'
|
||||
|
||||
class TestExtension extends BaseExtension {
|
||||
onLoad(): void {}
|
||||
onUnload(): void {}
|
||||
}
|
||||
class TestEngine extends AIEngine {
|
||||
provider: string = 'testEngine'
|
||||
onUnload(): void {}
|
||||
}
|
||||
|
||||
describe('ExtensionManager', () => {
|
||||
let manager: ExtensionManager
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ExtensionManager()
|
||||
})
|
||||
|
||||
it('should register an extension', () => {
|
||||
const extension = new TestExtension('', '')
|
||||
manager.register('testExtension', extension)
|
||||
expect(manager.getByName('testExtension')).toBe(extension)
|
||||
})
|
||||
|
||||
it('should register an AI engine', () => {
|
||||
const extension = { provider: 'testEngine' } as unknown as BaseExtension
|
||||
manager.register('testExtension', extension)
|
||||
expect(manager.getEngine('testEngine')).toBe(extension)
|
||||
})
|
||||
|
||||
it('should retrieve an extension by type', () => {
|
||||
const extension = new TestExtension('', '')
|
||||
jest.spyOn(extension, 'type').mockReturnValue(ExtensionTypeEnum.Assistant)
|
||||
manager.register('testExtension', extension)
|
||||
expect(manager.get(ExtensionTypeEnum.Assistant)).toBe(extension)
|
||||
})
|
||||
|
||||
it('should retrieve an extension by name', () => {
|
||||
const extension = new TestExtension('', '')
|
||||
manager.register('testExtension', extension)
|
||||
expect(manager.getByName('testExtension')).toBe(extension)
|
||||
})
|
||||
|
||||
it('should retrieve all extensions', () => {
|
||||
const extension1 = new TestExtension('', '')
|
||||
const extension2 = new TestExtension('', '')
|
||||
manager.register('testExtension1', extension1)
|
||||
manager.register('testExtension2', extension2)
|
||||
expect(manager.getAll()).toEqual([extension1, extension2])
|
||||
})
|
||||
|
||||
it('should retrieve an engine by name', () => {
|
||||
const engine = new TestEngine('', '')
|
||||
manager.register('anything', engine)
|
||||
expect(manager.getEngine('testEngine')).toBe(engine)
|
||||
})
|
||||
|
||||
it('should load all extensions', () => {
|
||||
const extension = new TestExtension('', '')
|
||||
jest.spyOn(extension, 'onLoad')
|
||||
manager.register('testExtension', extension)
|
||||
manager.load()
|
||||
expect(extension.onLoad).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should unload all extensions', () => {
|
||||
const extension = new TestExtension('', '')
|
||||
jest.spyOn(extension, 'onUnload')
|
||||
manager.register('testExtension', extension)
|
||||
manager.unload()
|
||||
expect(extension.onUnload).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should list all extensions', () => {
|
||||
const extension1 = new TestExtension('', '')
|
||||
const extension2 = new TestExtension('', '')
|
||||
manager.register('testExtension1', extension1)
|
||||
manager.register('testExtension2', extension2)
|
||||
expect(manager.listExtensions()).toEqual([extension1, extension2])
|
||||
})
|
||||
|
||||
it('should retrieve active extensions', async () => {
|
||||
const extension = new Extension(
|
||||
'url',
|
||||
'name',
|
||||
'productName',
|
||||
true,
|
||||
'description',
|
||||
'version'
|
||||
)
|
||||
window.core = {
|
||||
api: {
|
||||
getActiveExtensions: jest.fn(),
|
||||
},
|
||||
}
|
||||
jest
|
||||
.spyOn(window.core.api, 'getActiveExtensions')
|
||||
.mockResolvedValue([extension])
|
||||
const activeExtensions = await manager.getActive()
|
||||
expect(activeExtensions).toEqual([extension])
|
||||
})
|
||||
|
||||
it('should register all active extensions', async () => {
|
||||
const extension = new Extension(
|
||||
'url',
|
||||
'name',
|
||||
'productName',
|
||||
true,
|
||||
'description',
|
||||
'version'
|
||||
)
|
||||
jest.spyOn(manager, 'getActive').mockResolvedValue([extension])
|
||||
jest.spyOn(manager, 'activateExtension').mockResolvedValue()
|
||||
await manager.registerActive()
|
||||
expect(manager.activateExtension).toHaveBeenCalledWith(extension)
|
||||
})
|
||||
|
||||
it('should uninstall extensions', async () => {
|
||||
window.core = {
|
||||
api: {
|
||||
uninstallExtension: jest.fn(),
|
||||
},
|
||||
}
|
||||
jest.spyOn(window.core.api, 'uninstallExtension').mockResolvedValue(true)
|
||||
const result = await manager.uninstall(['testExtension'])
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
9
web/extension/index.test.ts
Normal file
9
web/extension/index.test.ts
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
import { extensionManager } from './index';
|
||||
|
||||
describe('index', () => {
|
||||
it('should export extensionManager from ExtensionManager', () => {
|
||||
expect(extensionManager).toBeDefined();
|
||||
});
|
||||
});
|
||||
9
web/helpers/atoms/ApiServer.atom.test.ts
Normal file
9
web/helpers/atoms/ApiServer.atom.test.ts
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
import { hostOptions } from './ApiServer.atom';
|
||||
|
||||
test('hostOptions correct values', () => {
|
||||
expect(hostOptions).toEqual([
|
||||
{ name: '127.0.0.1', value: '127.0.0.1' },
|
||||
{ name: '0.0.0.0', value: '0.0.0.0' },
|
||||
]);
|
||||
});
|
||||
8
web/helpers/atoms/App.atom.test.ts
Normal file
8
web/helpers/atoms/App.atom.test.ts
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
import { mainViewStateAtom } from './App.atom';
|
||||
import { MainViewState } from '@/constants/screens';
|
||||
|
||||
test('mainViewStateAtom initializes with Thread', () => {
|
||||
const result = mainViewStateAtom.init;
|
||||
expect(result).toBe(MainViewState.Thread);
|
||||
});
|
||||
7
web/helpers/atoms/AppConfig.atom.test.ts
Normal file
7
web/helpers/atoms/AppConfig.atom.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
import { hostAtom } from './AppConfig.atom';
|
||||
|
||||
test('hostAtom default value', () => {
|
||||
const result = hostAtom.init;
|
||||
expect(result).toBe('http://localhost:1337/');
|
||||
});
|
||||
8
web/helpers/atoms/Assistant.atom.test.ts
Normal file
8
web/helpers/atoms/Assistant.atom.test.ts
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
import { assistantsAtom } from './Assistant.atom';
|
||||
|
||||
test('assistantsAtom initializes as an empty array', () => {
|
||||
const initialValue = assistantsAtom.init;
|
||||
expect(Array.isArray(initialValue)).toBe(true);
|
||||
expect(initialValue).toHaveLength(0);
|
||||
});
|
||||
32
web/helpers/atoms/ChatMessage.atom.test.ts
Normal file
32
web/helpers/atoms/ChatMessage.atom.test.ts
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
import { getCurrentChatMessagesAtom } from './ChatMessage.atom';
|
||||
import { setConvoMessagesAtom, chatMessages, readyThreadsMessagesAtom } from './ChatMessage.atom';
|
||||
|
||||
test('getCurrentChatMessagesAtom returns empty array when no active thread ID', () => {
|
||||
const getMock = jest.fn().mockReturnValue(undefined);
|
||||
expect(getCurrentChatMessagesAtom.read(getMock)).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
test('getCurrentChatMessagesAtom returns empty array when activeThreadId is undefined', () => {
|
||||
const getMock = jest.fn().mockReturnValue({
|
||||
activeThreadId: undefined,
|
||||
chatMessages: {
|
||||
threadId: [{ id: 1, content: 'message' }],
|
||||
},
|
||||
});
|
||||
expect(getCurrentChatMessagesAtom.read(getMock)).toEqual([]);
|
||||
});
|
||||
|
||||
test('setConvoMessagesAtom updates chatMessages and readyThreadsMessagesAtom', () => {
|
||||
const getMock = jest.fn().mockReturnValue({});
|
||||
const setMock = jest.fn();
|
||||
const threadId = 'thread1';
|
||||
const messages = [{ id: '1', content: 'Hello' }];
|
||||
|
||||
setConvoMessagesAtom.write(getMock, setMock, threadId, messages);
|
||||
|
||||
expect(setMock).toHaveBeenCalledWith(chatMessages, { [threadId]: messages });
|
||||
expect(setMock).toHaveBeenCalledWith(readyThreadsMessagesAtom, { [threadId]: true });
|
||||
});
|
||||
|
||||
14
web/helpers/atoms/HuggingFace.atom.test.ts
Normal file
14
web/helpers/atoms/HuggingFace.atom.test.ts
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
import { importHuggingFaceModelStageAtom } from './HuggingFace.atom';
|
||||
import { importingHuggingFaceRepoDataAtom } from './HuggingFace.atom';
|
||||
|
||||
test('importHuggingFaceModelStageAtom should have initial value of NONE', () => {
|
||||
const result = importHuggingFaceModelStageAtom.init;
|
||||
expect(result).toBe('NONE');
|
||||
});
|
||||
|
||||
|
||||
test('importingHuggingFaceRepoDataAtom should have initial value of undefined', () => {
|
||||
const result = importingHuggingFaceRepoDataAtom.init;
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
7
web/helpers/atoms/LocalServer.atom.test.ts
Normal file
7
web/helpers/atoms/LocalServer.atom.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
import { serverEnabledAtom } from './LocalServer.atom';
|
||||
|
||||
test('serverEnabledAtom_initialValue', () => {
|
||||
const result = serverEnabledAtom.init;
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
7
web/helpers/atoms/Setting.atom.test.ts
Normal file
7
web/helpers/atoms/Setting.atom.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
import { selectedSettingAtom } from './Setting.atom';
|
||||
|
||||
test('selectedSettingAtom has correct initial value', () => {
|
||||
const result = selectedSettingAtom.init;
|
||||
expect(result).toBe('My Models');
|
||||
});
|
||||
6
web/helpers/atoms/ThreadRightPanel.atom.test.ts
Normal file
6
web/helpers/atoms/ThreadRightPanel.atom.test.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
import { activeTabThreadRightPanelAtom } from './ThreadRightPanel.atom';
|
||||
|
||||
test('activeTabThreadRightPanelAtom can be imported', () => {
|
||||
expect(activeTabThreadRightPanelAtom).toBeDefined();
|
||||
});
|
||||
109
web/hooks/useDownloadState.test.ts
Normal file
109
web/hooks/useDownloadState.test.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import {
|
||||
setDownloadStateAtom,
|
||||
modelDownloadStateAtom,
|
||||
} from './useDownloadState'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('jotai', () => ({
|
||||
atom: jest.fn(),
|
||||
useAtom: jest.fn(),
|
||||
}))
|
||||
jest.mock('@/containers/Toast', () => ({
|
||||
toaster: jest.fn(),
|
||||
}))
|
||||
jest.mock('@/helpers/atoms/Model.atom', () => ({
|
||||
configuredModelsAtom: jest.fn(),
|
||||
downloadedModelsAtom: jest.fn(),
|
||||
removeDownloadingModelAtom: jest.fn(),
|
||||
}))
|
||||
|
||||
describe('setDownloadStateAtom', () => {
|
||||
let get: jest.Mock
|
||||
let set: jest.Mock
|
||||
|
||||
beforeEach(() => {
|
||||
get = jest.fn()
|
||||
set = jest.fn()
|
||||
})
|
||||
|
||||
it('should handle download completion', () => {
|
||||
const state = {
|
||||
downloadState: 'end',
|
||||
modelId: 'model1',
|
||||
fileName: 'file1',
|
||||
children: [],
|
||||
}
|
||||
const currentState = {
|
||||
model1: {
|
||||
children: [state],
|
||||
},
|
||||
}
|
||||
get.mockReturnValueOnce(currentState)
|
||||
get.mockReturnValueOnce([{ id: 'model1' }])
|
||||
|
||||
set(setDownloadStateAtom, state)
|
||||
|
||||
expect(set).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({ modelId: expect.stringContaining('model1') })
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle download error', () => {
|
||||
const state = {
|
||||
downloadState: 'error',
|
||||
modelId: 'model1',
|
||||
error: 'some error',
|
||||
}
|
||||
const currentState = {
|
||||
model1: {},
|
||||
}
|
||||
get.mockReturnValueOnce(currentState)
|
||||
|
||||
set(setDownloadStateAtom, state)
|
||||
|
||||
expect(set).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({ modelId: 'model1' })
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle download error with certificate issue', () => {
|
||||
const state = {
|
||||
downloadState: 'error',
|
||||
modelId: 'model1',
|
||||
error: 'certificate error',
|
||||
}
|
||||
const currentState = {
|
||||
model1: {},
|
||||
}
|
||||
get.mockReturnValueOnce(currentState)
|
||||
|
||||
set(setDownloadStateAtom, state)
|
||||
|
||||
expect(set).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({ modelId: 'model1' })
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle download in progress', () => {
|
||||
const state = {
|
||||
downloadState: 'progress',
|
||||
modelId: 'model1',
|
||||
fileName: 'file1',
|
||||
size: { total: 100, transferred: 50 },
|
||||
}
|
||||
const currentState = {
|
||||
model1: {
|
||||
children: [],
|
||||
size: { total: 0, transferred: 0 },
|
||||
},
|
||||
}
|
||||
get.mockReturnValueOnce(currentState)
|
||||
|
||||
set(setDownloadStateAtom, state)
|
||||
|
||||
expect(set).toHaveBeenCalledWith(modelDownloadStateAtom, expect.any(Object))
|
||||
})
|
||||
})
|
||||
@ -5,7 +5,6 @@ const createJestConfig = nextJest({})
|
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
const config = {
|
||||
coverageProvider: 'v8',
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||
@ -17,6 +16,8 @@ const config = {
|
||||
},
|
||||
// Add more setup options before each test is run
|
||||
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||
runner: './testRunner.js',
|
||||
collectCoverageFrom: ['./**/*.{ts,tsx}'],
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/72926763/5078746
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
@ -10,7 +14,6 @@ class ResizeObserverMock {
|
||||
}
|
||||
|
||||
global.ResizeObserver = ResizeObserverMock
|
||||
// @ts-ignore
|
||||
global.window.core = {
|
||||
api: {
|
||||
getAppConfigurations: () => jest.fn(),
|
||||
|
||||
30
web/services/appService.test.ts
Normal file
30
web/services/appService.test.ts
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
import { ExtensionTypeEnum, extensionManager } from '@/extension';
|
||||
import { appService } from './appService';
|
||||
|
||||
test('should return correct system information when monitoring extension is found', async () => {
|
||||
const mockGpuSetting = { name: 'NVIDIA GeForce GTX 1080', memory: 8192 };
|
||||
const mockOsInfo = { platform: 'win32', release: '10.0.19041' };
|
||||
const mockMonitoringExtension = {
|
||||
getGpuSetting: jest.fn().mockResolvedValue(mockGpuSetting),
|
||||
getOsInfo: jest.fn().mockResolvedValue(mockOsInfo),
|
||||
};
|
||||
extensionManager.get = jest.fn().mockReturnValue(mockMonitoringExtension);
|
||||
|
||||
const result = await appService.systemInformation();
|
||||
|
||||
expect(mockMonitoringExtension.getGpuSetting).toHaveBeenCalled();
|
||||
expect(mockMonitoringExtension.getOsInfo).toHaveBeenCalled();
|
||||
expect(result).toEqual({ gpuSetting: mockGpuSetting, osInfo: mockOsInfo });
|
||||
});
|
||||
|
||||
|
||||
test('should log a warning when monitoring extension is not found', async () => {
|
||||
const consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
extensionManager.get = jest.fn().mockReturnValue(undefined);
|
||||
|
||||
await appService.systemInformation();
|
||||
|
||||
expect(consoleWarnMock).toHaveBeenCalledWith('System monitoring extension not found');
|
||||
consoleWarnMock.mockRestore();
|
||||
});
|
||||
47
web/services/eventsService.test.ts
Normal file
47
web/services/eventsService.test.ts
Normal file
@ -0,0 +1,47 @@
|
||||
|
||||
import { EventEmitter } from './eventsService';
|
||||
|
||||
test('should do nothing when no handlers for event', () => {
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
expect(() => {
|
||||
emitter.emit('nonExistentEvent', 'test data');
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
|
||||
test('should call all handlers for event', () => {
|
||||
const emitter = new EventEmitter();
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
|
||||
emitter.on('testEvent', handler1);
|
||||
emitter.on('testEvent', handler2);
|
||||
|
||||
emitter.emit('testEvent', 'test data');
|
||||
|
||||
expect(handler1).toHaveBeenCalledWith('test data');
|
||||
expect(handler2).toHaveBeenCalledWith('test data');
|
||||
});
|
||||
|
||||
|
||||
test('should remove handler for event', () => {
|
||||
const emitter = new EventEmitter();
|
||||
const handler = jest.fn();
|
||||
|
||||
emitter.on('testEvent', handler);
|
||||
emitter.off('testEvent', handler);
|
||||
|
||||
expect(emitter['handlers'].get('testEvent')).not.toContain(handler);
|
||||
});
|
||||
|
||||
|
||||
test('should add handler for event', () => {
|
||||
const emitter = new EventEmitter();
|
||||
const handler = jest.fn();
|
||||
|
||||
emitter.on('testEvent', handler);
|
||||
|
||||
expect(emitter['handlers'].has('testEvent')).toBe(true);
|
||||
expect(emitter['handlers'].get('testEvent')).toContain(handler);
|
||||
});
|
||||
35
web/services/extensionService.test.ts
Normal file
35
web/services/extensionService.test.ts
Normal file
@ -0,0 +1,35 @@
|
||||
|
||||
import { extensionManager } from '@/extension/ExtensionManager';
|
||||
import { ExtensionTypeEnum } from '@janhq/core';
|
||||
import { isCoreExtensionInstalled } from './extensionService';
|
||||
|
||||
test('isCoreExtensionInstalled returns true when both extensions are installed', () => {
|
||||
jest.spyOn(extensionManager, 'get').mockImplementation((type) => {
|
||||
if (type === ExtensionTypeEnum.Conversational || type === ExtensionTypeEnum.Model) return {};
|
||||
return undefined;
|
||||
});
|
||||
|
||||
expect(isCoreExtensionInstalled()).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
test('isCoreExtensionInstalled returns false when Model extension is not installed', () => {
|
||||
jest.spyOn(extensionManager, 'get').mockImplementation((type) => {
|
||||
if (type === ExtensionTypeEnum.Conversational) return {};
|
||||
if (type === ExtensionTypeEnum.Model) return undefined;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
expect(isCoreExtensionInstalled()).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
test('isCoreExtensionInstalled returns false when Conversational extension is not installed', () => {
|
||||
jest.spyOn(extensionManager, 'get').mockImplementation((type) => {
|
||||
if (type === ExtensionTypeEnum.Conversational) return undefined;
|
||||
if (type === ExtensionTypeEnum.Model) return {};
|
||||
return undefined;
|
||||
});
|
||||
|
||||
expect(isCoreExtensionInstalled()).toBe(false);
|
||||
});
|
||||
15
web/services/restService.test.ts
Normal file
15
web/services/restService.test.ts
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
test('restAPI.baseApiUrl set correctly', () => {
|
||||
const originalEnv = process.env.API_BASE_URL;
|
||||
process.env.API_BASE_URL = 'http://test-api.com';
|
||||
|
||||
// Re-import to get the updated value
|
||||
jest.resetModules();
|
||||
const { restAPI } = require('./restService');
|
||||
|
||||
expect(restAPI.baseApiUrl).toBe('http://test-api.com');
|
||||
|
||||
// Clean up
|
||||
process.env.API_BASE_URL = originalEnv;
|
||||
});
|
||||
22
web/utils/json.test.ts
Normal file
22
web/utils/json.test.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// json.test.ts
|
||||
import { safeJsonParse } from './json';
|
||||
|
||||
describe('safeJsonParse', () => {
|
||||
it('should correctly parse a valid JSON string', () => {
|
||||
const jsonString = '{"name": "John", "age": 30}';
|
||||
const result = safeJsonParse<{ name: string; age: number }>(jsonString);
|
||||
expect(result).toEqual({ name: 'John', age: 30 });
|
||||
});
|
||||
|
||||
it('should return undefined for an invalid JSON string', () => {
|
||||
const jsonString = '{"name": "John", "age": 30';
|
||||
const result = safeJsonParse<{ name: string; age: number }>(jsonString);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for an empty string', () => {
|
||||
const jsonString = '';
|
||||
const result = safeJsonParse<unknown>(jsonString);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@ -149,6 +149,14 @@ describe('validationRules', () => {
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
it('should normalize invalid values for keys not listed in validationRules', () => {
|
||||
expect(normalizeValue('invalid_key', 'invalid')).toBe('invalid')
|
||||
expect(normalizeValue('invalid_key', 123)).toBe(123)
|
||||
expect(normalizeValue('invalid_key', true)).toBe(true)
|
||||
expect(normalizeValue('invalid_key', false)).toBe(false)
|
||||
})
|
||||
|
||||
describe('normalizeValue', () => {
|
||||
it('should normalize ctx_len correctly', () => {
|
||||
expect(normalizeValue('ctx_len', 100.5)).toBe(100)
|
||||
|
||||
27
web/utils/threadMessageBuilder.test.ts
Normal file
27
web/utils/threadMessageBuilder.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
|
||||
import { ChatCompletionRole, MessageStatus } from '@janhq/core'
|
||||
|
||||
import { ThreadMessageBuilder } from './threadMessageBuilder'
|
||||
import { MessageRequestBuilder } from './messageRequestBuilder'
|
||||
|
||||
describe('ThreadMessageBuilder', () => {
|
||||
it('testBuildMethod', () => {
|
||||
const msgRequest = new MessageRequestBuilder(
|
||||
'type',
|
||||
{ model: 'model' },
|
||||
{ id: 'thread-id' },
|
||||
[]
|
||||
)
|
||||
const builder = new ThreadMessageBuilder(msgRequest)
|
||||
const result = builder.build()
|
||||
|
||||
expect(result.id).toBe(msgRequest.msgId)
|
||||
expect(result.thread_id).toBe(msgRequest.thread.id)
|
||||
expect(result.role).toBe(ChatCompletionRole.User)
|
||||
expect(result.status).toBe(MessageStatus.Ready)
|
||||
expect(result.created).toBeDefined()
|
||||
expect(result.updated).toBeDefined()
|
||||
expect(result.object).toBe('thread.message')
|
||||
expect(result.content).toEqual([])
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user