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:
Louis 2024-09-20 14:24:51 +07:00 committed by GitHub
parent 1aefb8f7ab
commit 302b73ae73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 1786 additions and 9 deletions

1
.gitignore vendored
View File

@ -45,4 +45,5 @@ core/test_results.html
coverage coverage
.yarn .yarn
.yarnrc .yarnrc
test_results.html
*.tsbuildinfo *.tsbuildinfo

View File

@ -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',
}, },

View File

@ -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)
)
})
})

View 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)
})
})

View 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()
})
})

View 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, {})
})
})

View 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)
})
})

View 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({})
})
})

View File

@ -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');
});

View File

@ -0,0 +1,6 @@
import { expect } from '@jest/globals';
it('should re-export all exports from ./AIEngine', () => {
expect(require('./index')).toHaveProperty('AIEngine');
});

View 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();
});
});

View 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)
})
})

View 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)
})

View 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))
})
})
})

View File

@ -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))

View 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),
})
})
})

View 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')
})

View 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');
});

View File

@ -0,0 +1,9 @@
import { AppConfigurationEventName } from './appConfigEvent';
describe('AppConfigurationEventName', () => {
it('should have the correct value for OnConfigurationUpdate', () => {
expect(AppConfigurationEventName.OnConfigurationUpdate).toBe('OnConfigurationUpdate');
});
});

View 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',
]);
});

View File

@ -0,0 +1,8 @@
import * as huggingfaceEntity from './huggingfaceEntity';
import * as index from './index';
test('test_exports_from_huggingfaceEntity', () => {
expect(index).toEqual(huggingfaceEntity);
});

View 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();
});

View 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);
});

View File

@ -0,0 +1,7 @@
import { InferenceEvent } from './inferenceEvent';
test('testInferenceEventEnumContainsOnInferenceStopped', () => {
expect(InferenceEvent.OnInferenceStopped).toBe('OnInferenceStopped');
});

View File

@ -0,0 +1,7 @@
import { MessageEvent } from './messageEvent';
test('testOnMessageSentValue', () => {
expect(MessageEvent.OnMessageSent).toBe('OnMessageSent');
});

View File

@ -0,0 +1,7 @@
import { MessageRequestType } from './messageRequestType';
test('testMessageRequestTypeEnumContainsThread', () => {
expect(MessageRequestType.Thread).toBe('Thread');
});

View 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);
});

View File

@ -0,0 +1,7 @@
import { ModelEvent } from './modelEvent';
test('testOnModelInit', () => {
expect(ModelEvent.OnModelInit).toBe('OnModelInit');
});

View File

@ -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',
} }

View File

@ -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
View 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

View 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)
})
})

View 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);
});

View File

@ -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,

View 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)
})
})

View File

@ -0,0 +1,9 @@
import { extensionManager } from './index';
describe('index', () => {
it('should export extensionManager from ExtensionManager', () => {
expect(extensionManager).toBeDefined();
});
});

View 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' },
]);
});

View 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);
});

View File

@ -0,0 +1,7 @@
import { hostAtom } from './AppConfig.atom';
test('hostAtom default value', () => {
const result = hostAtom.init;
expect(result).toBe('http://localhost:1337/');
});

View 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);
});

View 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 });
});

View 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();
});

View File

@ -0,0 +1,7 @@
import { serverEnabledAtom } from './LocalServer.atom';
test('serverEnabledAtom_initialValue', () => {
const result = serverEnabledAtom.init;
expect(result).toBe(false);
});

View 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');
});

View File

@ -0,0 +1,6 @@
import { activeTabThreadRightPanelAtom } from './ThreadRightPanel.atom';
test('activeTabThreadRightPanelAtom can be imported', () => {
expect(activeTabThreadRightPanelAtom).toBeDefined();
});

View 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))
})
})

View File

@ -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

View File

@ -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(),

View 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();
});

View 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);
});

View 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);
});

View 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
View 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();
});
});

View File

@ -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)

View 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([])
})
})