diff --git a/core/jest.config.js b/core/jest.config.js index 2f652dd39..9b1dd2ade 100644 --- a/core/jest.config.js +++ b/core/jest.config.js @@ -6,4 +6,12 @@ module.exports = { '@/(.*)': '/src/$1', }, runner: './testRunner.js', + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + diagnostics: false, + }, + ], + }, } diff --git a/core/src/browser/extensions/assistant.test.ts b/core/src/browser/extensions/assistant.test.ts new file mode 100644 index 000000000..ae81b0985 --- /dev/null +++ b/core/src/browser/extensions/assistant.test.ts @@ -0,0 +1,8 @@ + +import { AssistantExtension } from './assistant'; +import { ExtensionTypeEnum } from '../extension'; + +it('should return the correct type', () => { + const extension = new AssistantExtension(); + expect(extension.type()).toBe(ExtensionTypeEnum.Assistant); +}); diff --git a/core/src/browser/extensions/inference.test.ts b/core/src/browser/extensions/inference.test.ts new file mode 100644 index 000000000..45ec9d172 --- /dev/null +++ b/core/src/browser/extensions/inference.test.ts @@ -0,0 +1,45 @@ +import { MessageRequest, ThreadMessage } from '../../types' +import { BaseExtension, ExtensionTypeEnum } from '../extension' +import { InferenceExtension } from './' + +// Mock the MessageRequest and ThreadMessage types +type MockMessageRequest = { + text: string +} + +type MockThreadMessage = { + text: string + userId: string +} + +// Mock the BaseExtension class +class MockBaseExtension extends BaseExtension { + type(): ExtensionTypeEnum | undefined { + return ExtensionTypeEnum.Base + } +} + +// Create a mock implementation of InferenceExtension +class MockInferenceExtension extends InferenceExtension { + async inference(data: MessageRequest): Promise { + return { text: 'Mock response', userId: '123' } as unknown as ThreadMessage + } +} + +describe('InferenceExtension', () => { + let inferenceExtension: InferenceExtension + + beforeEach(() => { + inferenceExtension = new MockInferenceExtension() + }) + + it('should have the correct type', () => { + expect(inferenceExtension.type()).toBe(ExtensionTypeEnum.Inference) + }) + + it('should implement the inference method', async () => { + const messageRequest: MessageRequest = { text: 'Hello' } as unknown as MessageRequest + const result = await inferenceExtension.inference(messageRequest) + expect(result).toEqual({ text: 'Mock response', userId: '123' } as unknown as ThreadMessage) + }) +}) diff --git a/core/src/browser/extensions/monitoring.test.ts b/core/src/browser/extensions/monitoring.test.ts new file mode 100644 index 000000000..9bba89a8c --- /dev/null +++ b/core/src/browser/extensions/monitoring.test.ts @@ -0,0 +1,42 @@ + +import { ExtensionTypeEnum } from '../extension'; +import { MonitoringExtension } from './monitoring'; + +it('should have the correct type', () => { + class TestMonitoringExtension extends MonitoringExtension { + getGpuSetting(): Promise { + throw new Error('Method not implemented.'); + } + getResourcesInfo(): Promise { + throw new Error('Method not implemented.'); + } + getCurrentLoad(): Promise { + throw new Error('Method not implemented.'); + } + getOsInfo(): Promise { + throw new Error('Method not implemented.'); + } + } + const monitoringExtension = new TestMonitoringExtension(); + expect(monitoringExtension.type()).toBe(ExtensionTypeEnum.SystemMonitoring); +}); + + +it('should create an instance of MonitoringExtension', () => { + class TestMonitoringExtension extends MonitoringExtension { + getGpuSetting(): Promise { + throw new Error('Method not implemented.'); + } + getResourcesInfo(): Promise { + throw new Error('Method not implemented.'); + } + getCurrentLoad(): Promise { + throw new Error('Method not implemented.'); + } + getOsInfo(): Promise { + throw new Error('Method not implemented.'); + } + } + const monitoringExtension = new TestMonitoringExtension(); + expect(monitoringExtension).toBeInstanceOf(MonitoringExtension); +}); diff --git a/core/src/browser/tools/index.test.ts b/core/src/browser/tools/index.test.ts new file mode 100644 index 000000000..8a24d3bb6 --- /dev/null +++ b/core/src/browser/tools/index.test.ts @@ -0,0 +1,5 @@ + + +it('should not throw any errors when imported', () => { + expect(() => require('./index')).not.toThrow(); +}) diff --git a/core/src/browser/tools/tool.test.ts b/core/src/browser/tools/tool.test.ts index ba918a3cb..dcb478478 100644 --- a/core/src/browser/tools/tool.test.ts +++ b/core/src/browser/tools/tool.test.ts @@ -52,4 +52,12 @@ it('should skip processing for disabled tools', async () => { const result = await manager.process(request, tools) expect(result).toBe(request) -}) \ No newline at end of file +}) + +it('should throw an error when process is called without implementation', () => { + class TestTool extends InferenceTool { + name = 'testTool' + } + const tool = new TestTool() + expect(() => tool.process({} as MessageRequest)).toThrowError() +}) diff --git a/core/src/index.test.ts b/core/src/index.test.ts new file mode 100644 index 000000000..a1bd7c6b9 --- /dev/null +++ b/core/src/index.test.ts @@ -0,0 +1,7 @@ + + +it('should declare global object core when importing the module and then deleting it', () => { + import('./index'); + delete globalThis.core; + expect(typeof globalThis.core).toBe('undefined'); +}); diff --git a/core/src/node/api/index.test.ts b/core/src/node/api/index.test.ts new file mode 100644 index 000000000..c35d6e792 --- /dev/null +++ b/core/src/node/api/index.test.ts @@ -0,0 +1,7 @@ + +import * as restfulV1 from './restful/v1'; + +it('should re-export from restful/v1', () => { + const restfulV1Exports = require('./restful/v1'); + expect(restfulV1Exports).toBeDefined(); +}) diff --git a/core/src/node/api/processors/Processor.test.ts b/core/src/node/api/processors/Processor.test.ts new file mode 100644 index 000000000..fd913c481 --- /dev/null +++ b/core/src/node/api/processors/Processor.test.ts @@ -0,0 +1,6 @@ + +import { Processor } from './Processor'; + +it('should be defined', () => { + expect(Processor).toBeDefined(); +}); diff --git a/core/src/node/api/processors/extension.test.ts b/core/src/node/api/processors/extension.test.ts index 917883499..2067c5c42 100644 --- a/core/src/node/api/processors/extension.test.ts +++ b/core/src/node/api/processors/extension.test.ts @@ -7,3 +7,34 @@ it('should call function associated with key in process method', () => { extension.process('testKey', 'arg1', 'arg2'); expect(mockFunc).toHaveBeenCalledWith('arg1', 'arg2'); }); + + +it('should_handle_empty_extension_list_for_install', async () => { + jest.mock('../../extension/store', () => ({ + installExtensions: jest.fn(() => Promise.resolve([])), + })); + const extension = new Extension(); + const result = await extension.installExtension([]); + expect(result).toEqual([]); +}); + + +it('should_handle_empty_extension_list_for_update', async () => { + jest.mock('../../extension/store', () => ({ + getExtension: jest.fn(() => ({ update: jest.fn(() => Promise.resolve(true)) })), + })); + const extension = new Extension(); + const result = await extension.updateExtension([]); + expect(result).toEqual([]); +}); + + +it('should_handle_empty_extension_list', async () => { + jest.mock('../../extension/store', () => ({ + getExtension: jest.fn(() => ({ uninstall: jest.fn(() => Promise.resolve(true)) })), + removeExtension: jest.fn(), + })); + const extension = new Extension(); + const result = await extension.uninstallExtension([]); + expect(result).toBe(true); +}); diff --git a/core/src/node/api/restful/helper/consts.test.ts b/core/src/node/api/restful/helper/consts.test.ts new file mode 100644 index 000000000..34d42dcf0 --- /dev/null +++ b/core/src/node/api/restful/helper/consts.test.ts @@ -0,0 +1,6 @@ + +import { NITRO_DEFAULT_PORT } from './consts'; + +it('should test NITRO_DEFAULT_PORT', () => { + expect(NITRO_DEFAULT_PORT).toBe(3928); +}); diff --git a/core/src/node/helper/config.test.ts b/core/src/node/helper/config.test.ts index 201a98141..d46750d5f 100644 --- a/core/src/node/helper/config.test.ts +++ b/core/src/node/helper/config.test.ts @@ -1,6 +1,8 @@ import { getEngineConfiguration } from './config'; import { getAppConfigurations, defaultAppConfig } from './config'; +import { getJanExtensionsPath } from './config'; +import { getJanDataFolderPath } from './config'; it('should return undefined for invalid engine ID', async () => { const config = await getEngineConfiguration('invalid_engine'); expect(config).toBeUndefined(); @@ -12,3 +14,15 @@ it('should return default config when CI is e2e', () => { const config = getAppConfigurations(); expect(config).toEqual(defaultAppConfig()); }); + + +it('should return extensions path when retrieved successfully', () => { + const extensionsPath = getJanExtensionsPath(); + expect(extensionsPath).not.toBeUndefined(); +}); + + +it('should return data folder path when retrieved successfully', () => { + const dataFolderPath = getJanDataFolderPath(); + expect(dataFolderPath).not.toBeUndefined(); +}); diff --git a/core/src/types/message/messageEntity.test.ts b/core/src/types/message/messageEntity.test.ts new file mode 100644 index 000000000..1d41d129a --- /dev/null +++ b/core/src/types/message/messageEntity.test.ts @@ -0,0 +1,9 @@ + +import { MessageStatus } from './messageEntity'; + +it('should have correct values', () => { + expect(MessageStatus.Ready).toBe('ready'); + expect(MessageStatus.Pending).toBe('pending'); + expect(MessageStatus.Error).toBe('error'); + expect(MessageStatus.Stopped).toBe('stopped'); +}) diff --git a/core/src/types/miscellaneous/systemResourceInfo.test.ts b/core/src/types/miscellaneous/systemResourceInfo.test.ts new file mode 100644 index 000000000..35a459f0e --- /dev/null +++ b/core/src/types/miscellaneous/systemResourceInfo.test.ts @@ -0,0 +1,6 @@ + +import { SupportedPlatforms } from './systemResourceInfo'; + +it('should contain the correct values', () => { + expect(SupportedPlatforms).toEqual(['win32', 'linux', 'darwin']); +}); diff --git a/core/src/types/monitoring/index.test.ts b/core/src/types/monitoring/index.test.ts new file mode 100644 index 000000000..010fcb97a --- /dev/null +++ b/core/src/types/monitoring/index.test.ts @@ -0,0 +1,16 @@ + +import * as monitoringInterface from './monitoringInterface'; +import * as resourceInfo from './resourceInfo'; + + import * as index from './index'; + import * as monitoringInterface from './monitoringInterface'; + import * as resourceInfo from './resourceInfo'; + + it('should re-export all symbols from monitoringInterface and resourceInfo', () => { + for (const key in monitoringInterface) { + expect(index[key]).toBe(monitoringInterface[key]); + } + for (const key in resourceInfo) { + expect(index[key]).toBe(resourceInfo[key]); + } + }); diff --git a/core/src/types/setting/index.test.ts b/core/src/types/setting/index.test.ts new file mode 100644 index 000000000..699adfe4f --- /dev/null +++ b/core/src/types/setting/index.test.ts @@ -0,0 +1,5 @@ + + +it('should not throw any errors', () => { + expect(() => require('./index')).not.toThrow(); +}); diff --git a/core/src/types/setting/settingComponent.test.ts b/core/src/types/setting/settingComponent.test.ts new file mode 100644 index 000000000..c56550e19 --- /dev/null +++ b/core/src/types/setting/settingComponent.test.ts @@ -0,0 +1,19 @@ + +import { createSettingComponent } from './settingComponent'; + + it('should throw an error when creating a setting component with invalid controller type', () => { + const props: SettingComponentProps = { + key: 'invalidControllerKey', + title: 'Invalid Controller Title', + description: 'Invalid Controller Description', + controllerType: 'invalid' as any, + controllerProps: { + placeholder: 'Enter text', + value: 'Initial Value', + type: 'text', + textAlign: 'left', + inputActions: ['unobscure'], + }, + }; + expect(() => createSettingComponent(props)).toThrowError(); + }); diff --git a/core/src/types/thread/threadEvent.test.ts b/core/src/types/thread/threadEvent.test.ts new file mode 100644 index 000000000..f892f1050 --- /dev/null +++ b/core/src/types/thread/threadEvent.test.ts @@ -0,0 +1,6 @@ + +import { ThreadEvent } from './threadEvent'; + +it('should have the correct values', () => { + expect(ThreadEvent.OnThreadStarted).toBe('OnThreadStarted'); +}); diff --git a/electron/jest.config.js b/electron/jest.config.js new file mode 100644 index 000000000..ec5968ccd --- /dev/null +++ b/electron/jest.config.js @@ -0,0 +1,18 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + collectCoverageFrom: ['src/**/*.{ts,tsx}'], + modulePathIgnorePatterns: ['/tests'], + moduleNameMapper: { + '@/(.*)': '/src/$1', + }, + runner: './testRunner.js', + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + diagnostics: false, + }, + ], + }, +} diff --git a/electron/testRunner.js b/electron/testRunner.js new file mode 100644 index 000000000..b0d108160 --- /dev/null +++ b/electron/testRunner.js @@ -0,0 +1,10 @@ +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; \ No newline at end of file diff --git a/web/jest.config.js b/web/jest.config.js index 8b2683e78..7d2bee9ee 100644 --- a/web/jest.config.js +++ b/web/jest.config.js @@ -18,6 +18,14 @@ const config = { // setupFilesAfterEnv: ['/jest.setup.ts'], runner: './testRunner.js', collectCoverageFrom: ['./**/*.{ts,tsx}'], + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + diagnostics: false, + }, + ], + }, } // https://stackoverflow.com/a/72926763/5078746 diff --git a/web/utils/base64.test.ts b/web/utils/base64.test.ts new file mode 100644 index 000000000..1067970d4 --- /dev/null +++ b/web/utils/base64.test.ts @@ -0,0 +1,8 @@ + +import { getBase64 } from './base64'; + +test('getBase64_converts_file_to_base64', async () => { + const file = new File(['test'], 'test.txt', { type: 'text/plain' }); + const base64String = await getBase64(file); + expect(base64String).toBe('data:text/plain;base64,dGVzdA=='); +}); diff --git a/web/utils/converter.test.ts b/web/utils/converter.test.ts new file mode 100644 index 000000000..e86923b30 --- /dev/null +++ b/web/utils/converter.test.ts @@ -0,0 +1,33 @@ + +import { formatDownloadSpeed } from './converter'; +import { formatExtensionsName } from './converter'; +import { formatTwoDigits } from './converter'; + + test('formatDownloadSpeed_should_return_correct_output_when_input_is_undefined', () => { + expect(formatDownloadSpeed(undefined)).toBe('0B/s'); + }); + + + test('formatExtensionsName_should_return_correct_output_for_string_with_janhq_and_dash', () => { + expect(formatExtensionsName('@janhq/extension-name')).toBe('extension name'); + }); + + + test('formatTwoDigits_should_return_correct_output_for_single_digit_number', () => { + expect(formatTwoDigits(5)).toBe('5.00'); + }); + + + test('formatDownloadSpeed_should_return_correct_output_for_gigabytes', () => { + expect(formatDownloadSpeed(1500000000)).toBe('1.40GB/s'); + }); + + + test('formatDownloadSpeed_should_return_correct_output_for_megabytes', () => { + expect(formatDownloadSpeed(1500000)).toBe('1.43MB/s'); + }); + + + test('formatDownloadSpeed_should_return_correct_output_for_kilobytes', () => { + expect(formatDownloadSpeed(1500)).toBe('1.46KB/s'); + }); diff --git a/web/utils/modelParam.test.ts b/web/utils/modelParam.test.ts index 994a5bd57..97325d277 100644 --- a/web/utils/modelParam.test.ts +++ b/web/utils/modelParam.test.ts @@ -1,5 +1,7 @@ // web/utils/modelParam.test.ts import { normalizeValue, validationRules } from './modelParam' +import { extractModelLoadParams } from './modelParam'; +import { extractInferenceParams } from './modelParam'; describe('validationRules', () => { it('should validate temperature correctly', () => { @@ -189,3 +191,20 @@ describe('normalizeValue', () => { expect(normalizeValue('cpu_threads', 0)).toBe(0) }) }) + + + it('should handle invalid values correctly by falling back to originParams', () => { + const modelParams = { temperature: 'invalid', token_limit: -1 }; + const originParams = { temperature: 0.5, token_limit: 100 }; + expect(extractInferenceParams(modelParams, originParams)).toEqual(originParams); + }); + + + it('should return an empty object when no modelParams are provided', () => { + expect(extractModelLoadParams()).toEqual({}); + }); + + + it('should return an empty object when no modelParams are provided', () => { + expect(extractInferenceParams()).toEqual({}); + }); diff --git a/web/utils/threadMessageBuilder.test.ts b/web/utils/threadMessageBuilder.test.ts index d938a2e03..cc192a5c1 100644 --- a/web/utils/threadMessageBuilder.test.ts +++ b/web/utils/threadMessageBuilder.test.ts @@ -4,6 +4,7 @@ import { ChatCompletionRole, MessageStatus } from '@janhq/core' import { ThreadMessageBuilder } from './threadMessageBuilder' import { MessageRequestBuilder } from './messageRequestBuilder' +import { ContentType } from '@janhq/core'; describe('ThreadMessageBuilder', () => { it('testBuildMethod', () => { const msgRequest = new MessageRequestBuilder( @@ -25,3 +26,75 @@ import { ChatCompletionRole, MessageStatus } from '@janhq/core' expect(result.content).toEqual([]) }) }) + + it('testPushMessageWithPromptOnly', () => { + const msgRequest = new MessageRequestBuilder( + 'type', + { model: 'model' }, + { id: 'thread-id' }, + [] + ); + const builder = new ThreadMessageBuilder(msgRequest); + const prompt = 'test prompt'; + builder.pushMessage(prompt, undefined, []); + expect(builder.content).toEqual([ + { + type: ContentType.Text, + text: { + value: prompt, + annotations: [], + }, + }, + ]); + }); + + + it('testPushMessageWithPdf', () => { + const msgRequest = new MessageRequestBuilder( + 'type', + { model: 'model' }, + { id: 'thread-id' }, + [] + ); + const builder = new ThreadMessageBuilder(msgRequest); + const prompt = 'test prompt'; + const base64 = 'test base64'; + const fileUpload = [{ type: 'pdf', file: { name: 'test.pdf', size: 1000 } }]; + builder.pushMessage(prompt, base64, fileUpload); + expect(builder.content).toEqual([ + { + type: ContentType.Pdf, + text: { + value: prompt, + annotations: [base64], + name: fileUpload[0].file.name, + size: fileUpload[0].file.size, + }, + }, + ]); + }); + + + it('testPushMessageWithImage', () => { + const msgRequest = new MessageRequestBuilder( + 'type', + { model: 'model' }, + { id: 'thread-id' }, + [] + ); + const builder = new ThreadMessageBuilder(msgRequest); + const prompt = 'test prompt'; + const base64 = 'test base64'; + const fileUpload = [{ type: 'image', file: { name: 'test.jpg', size: 1000 } }]; + builder.pushMessage(prompt, base64, fileUpload); + expect(builder.content).toEqual([ + { + type: ContentType.Image, + text: { + value: prompt, + annotations: [base64], + }, + }, + ]); + }); +