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
|
coverage
|
||||||
.yarn
|
.yarn
|
||||||
.yarnrc
|
.yarnrc
|
||||||
|
test_results.html
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
|
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'@/(.*)': '<rootDir>/src/$1',
|
'@/(.*)': '<rootDir>/src/$1',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
import { BaseExtension } from './extension'
|
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 {
|
class TestBaseExtension extends BaseExtension {
|
||||||
onLoad(): void {}
|
onLoad(): void {}
|
||||||
@ -44,3 +49,103 @@ describe('BaseExtension', () => {
|
|||||||
// Add your assertions here
|
// 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 { lastValueFrom, Observable } from 'rxjs'
|
||||||
import { requestInference } from './sse'
|
import { requestInference } from './sse'
|
||||||
|
|
||||||
|
import { ReadableStream } from 'stream/web';
|
||||||
describe('requestInference', () => {
|
describe('requestInference', () => {
|
||||||
it('should send a request to the inference server and return an Observable', () => {
|
it('should send a request to the inference server and return an Observable', () => {
|
||||||
// Mock the fetch function
|
// Mock the fetch function
|
||||||
@ -58,3 +59,66 @@ describe('requestInference', () => {
|
|||||||
expect(lastValueFrom(result)).rejects.toEqual({ message: 'Wrong API Key', code: 'invalid_api_key' })
|
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) {
|
for (const source of model.sources) {
|
||||||
const rq = request({ url: source, strictSSL, proxy })
|
const rq = request({ url: source, strictSSL, proxy })
|
||||||
progress(rq, {})
|
progress(rq, {})
|
||||||
.on('progress', function (state: any) {
|
?.on('progress', function (state: any) {
|
||||||
console.debug('progress', JSON.stringify(state, null, 2))
|
console.debug('progress', JSON.stringify(state, null, 2))
|
||||||
})
|
})
|
||||||
.on('error', function (err: Error) {
|
?.on('error', function (err: Error) {
|
||||||
console.error('error', err)
|
console.error('error', err)
|
||||||
})
|
})
|
||||||
.on('end', function () {
|
?.on('end', function () {
|
||||||
console.debug('end')
|
console.debug('end')
|
||||||
})
|
})
|
||||||
.pipe(createWriteStream(modelBinaryPath))
|
.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',
|
testEnvironment: 'node',
|
||||||
roots: ['<rootDir>/src'],
|
roots: ['<rootDir>/src'],
|
||||||
testMatch: ['**/*.test.*'],
|
testMatch: ['**/*.test.*'],
|
||||||
|
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
|
||||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "yarn workspace jan lint && yarn workspace @janhq/web lint",
|
"lint": "yarn workspace jan lint && yarn workspace @janhq/web lint",
|
||||||
"test:unit": "jest",
|
"test:unit": "jest",
|
||||||
"test:coverage": "jest --coverage --collectCoverageFrom='src/**/*.{ts,tsx}'",
|
"test:coverage": "jest --coverage",
|
||||||
"test": "yarn workspace jan test:e2e",
|
"test": "yarn workspace jan test:e2e",
|
||||||
"test-local": "yarn lint && yarn build:test && yarn test",
|
"test-local": "yarn lint && yarn build:test && yarn test",
|
||||||
"pre-install:darwin": "find extensions -type f -path \"**/*.tgz\" -exec cp {} pre-install \\;",
|
"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
|
url: string
|
||||||
|
|
||||||
/** @type {boolean} Whether the extension is activated or not. */
|
/** @type {boolean} Whether the extension is activated or not. */
|
||||||
active
|
active?: boolean
|
||||||
|
|
||||||
/** @type {string} Extension's description. */
|
/** @type {string} Extension's description. */
|
||||||
description
|
description?: string
|
||||||
|
|
||||||
/** @type {string} Extension's version. */
|
/** @type {string} Extension's version. */
|
||||||
version
|
version?: string
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
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
|
// Add any custom config to be passed to Jest
|
||||||
const config = {
|
const config = {
|
||||||
coverageProvider: 'v8',
|
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.(ts|tsx)$': 'ts-jest',
|
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||||
@ -17,6 +16,8 @@ const config = {
|
|||||||
},
|
},
|
||||||
// Add more setup options before each test is run
|
// Add more setup options before each test is run
|
||||||
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||||
|
runner: './testRunner.js',
|
||||||
|
collectCoverageFrom: ['./**/*.{ts,tsx}'],
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/a/72926763/5078746
|
// https://stackoverflow.com/a/72926763/5078746
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
@ -10,7 +14,6 @@ class ResizeObserverMock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
global.ResizeObserver = ResizeObserverMock
|
global.ResizeObserver = ResizeObserverMock
|
||||||
// @ts-ignore
|
|
||||||
global.window.core = {
|
global.window.core = {
|
||||||
api: {
|
api: {
|
||||||
getAppConfigurations: () => jest.fn(),
|
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', () => {
|
describe('normalizeValue', () => {
|
||||||
it('should normalize ctx_len correctly', () => {
|
it('should normalize ctx_len correctly', () => {
|
||||||
expect(normalizeValue('ctx_len', 100.5)).toBe(100)
|
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