From f4f4d411aad1e492bc6ab25a8eab02460dc5dbda Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 11 Mar 2025 13:42:42 +0700 Subject: [PATCH] chore: bump cortex version (#4793) --- .../src/api.test.ts | 199 +++++++++++++ .../src/error.test.ts | 19 ++ .../src/index.test.ts | 270 +++++++++++++++++- .../src/populateRemoteModels.test.ts | 139 +++++++++ .../bin/version.txt | 2 +- 5 files changed, 626 insertions(+), 3 deletions(-) create mode 100644 extensions/engine-management-extension/src/api.test.ts create mode 100644 extensions/engine-management-extension/src/error.test.ts create mode 100644 extensions/engine-management-extension/src/populateRemoteModels.test.ts diff --git a/extensions/engine-management-extension/src/api.test.ts b/extensions/engine-management-extension/src/api.test.ts new file mode 100644 index 000000000..ab72f8127 --- /dev/null +++ b/extensions/engine-management-extension/src/api.test.ts @@ -0,0 +1,199 @@ +import { describe, beforeEach, it, expect, vi } from 'vitest' +import JanEngineManagementExtension from './index' +import { InferenceEngine } from '@janhq/core' + +describe('API methods', () => { + let extension: JanEngineManagementExtension + + beforeEach(() => { + // @ts-ignore + extension = new JanEngineManagementExtension() + vi.resetAllMocks() + }) + + describe('getReleasedEnginesByVersion', () => { + it('should return engines filtered by platform if provided', async () => { + const mockEngines = [ + { + name: 'windows-amd64-avx2', + version: '1.0.0', + }, + { + name: 'linux-amd64-avx2', + version: '1.0.0', + }, + ] + + vi.mock('ky', () => ({ + default: { + get: () => ({ + json: () => Promise.resolve(mockEngines), + }), + }, + })) + + const mock = vi.spyOn(extension, 'getReleasedEnginesByVersion') + mock.mockImplementation(async (name, version, platform) => { + const result = await Promise.resolve(mockEngines) + return platform ? result.filter(r => r.name.includes(platform)) : result + }) + + const result = await extension.getReleasedEnginesByVersion( + InferenceEngine.cortex_llamacpp, + '1.0.0', + 'windows' + ) + + expect(result).toHaveLength(1) + expect(result[0].name).toBe('windows-amd64-avx2') + }) + + it('should return all engines if platform is not provided', async () => { + const mockEngines = [ + { + name: 'windows-amd64-avx2', + version: '1.0.0', + }, + { + name: 'linux-amd64-avx2', + version: '1.0.0', + }, + ] + + vi.mock('ky', () => ({ + default: { + get: () => ({ + json: () => Promise.resolve(mockEngines), + }), + }, + })) + + const mock = vi.spyOn(extension, 'getReleasedEnginesByVersion') + mock.mockImplementation(async (name, version, platform) => { + const result = await Promise.resolve(mockEngines) + return platform ? result.filter(r => r.name.includes(platform)) : result + }) + + const result = await extension.getReleasedEnginesByVersion( + InferenceEngine.cortex_llamacpp, + '1.0.0' + ) + + expect(result).toHaveLength(2) + }) + }) + + describe('getLatestReleasedEngine', () => { + it('should return engines filtered by platform if provided', async () => { + const mockEngines = [ + { + name: 'windows-amd64-avx2', + version: '1.0.0', + }, + { + name: 'linux-amd64-avx2', + version: '1.0.0', + }, + ] + + vi.mock('ky', () => ({ + default: { + get: () => ({ + json: () => Promise.resolve(mockEngines), + }), + }, + })) + + const mock = vi.spyOn(extension, 'getLatestReleasedEngine') + mock.mockImplementation(async (name, platform) => { + const result = await Promise.resolve(mockEngines) + return platform ? result.filter(r => r.name.includes(platform)) : result + }) + + const result = await extension.getLatestReleasedEngine( + InferenceEngine.cortex_llamacpp, + 'linux' + ) + + expect(result).toHaveLength(1) + expect(result[0].name).toBe('linux-amd64-avx2') + }) + }) + + describe('installEngine', () => { + it('should send install request with correct parameters', async () => { + const mockEngineConfig = { + variant: 'windows-amd64-avx2', + version: '1.0.0', + } + + vi.mock('ky', () => ({ + default: { + post: (url, options) => { + expect(url).toBe(`${API_URL}/v1/engines/${InferenceEngine.cortex_llamacpp}/install`) + expect(options.json).toEqual(mockEngineConfig) + return Promise.resolve({ messages: 'OK' }) + }, + }, + })) + + const result = await extension.installEngine( + InferenceEngine.cortex_llamacpp, + mockEngineConfig + ) + + expect(result).toEqual({ messages: 'OK' }) + }) + }) + + describe('uninstallEngine', () => { + it('should send uninstall request with correct parameters', async () => { + const mockEngineConfig = { + variant: 'windows-amd64-avx2', + version: '1.0.0', + } + + vi.mock('ky', () => ({ + default: { + delete: (url, options) => { + expect(url).toBe(`${API_URL}/v1/engines/${InferenceEngine.cortex_llamacpp}/install`) + expect(options.json).toEqual(mockEngineConfig) + return Promise.resolve({ messages: 'OK' }) + }, + }, + })) + + const result = await extension.uninstallEngine( + InferenceEngine.cortex_llamacpp, + mockEngineConfig + ) + + expect(result).toEqual({ messages: 'OK' }) + }) + }) + + describe('addRemoteModel', () => { + it('should send add model request with correct parameters', async () => { + const mockModel = { + id: 'gpt-4', + name: 'GPT-4', + engine: InferenceEngine.openai, + } + + vi.mock('ky', () => ({ + default: { + post: (url, options) => { + expect(url).toBe(`${API_URL}/v1/models/add`) + expect(options.json).toHaveProperty('id', 'gpt-4') + expect(options.json).toHaveProperty('engine', InferenceEngine.openai) + expect(options.json).toHaveProperty('inference_params') + return Promise.resolve() + }, + }, + })) + + await extension.addRemoteModel(mockModel) + // Success is implied by no thrown exceptions + }) + }) +}) \ No newline at end of file diff --git a/extensions/engine-management-extension/src/error.test.ts b/extensions/engine-management-extension/src/error.test.ts new file mode 100644 index 000000000..87389c50c --- /dev/null +++ b/extensions/engine-management-extension/src/error.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest' +import { EngineError } from './error' + +describe('EngineError', () => { + it('should create an error with the correct message', () => { + const errorMessage = 'Test error message' + const error = new EngineError(errorMessage) + + expect(error).toBeInstanceOf(Error) + expect(error.message).toBe(errorMessage) + expect(error.name).toBe('EngineError') + }) + + it('should create an error with default message if none provided', () => { + const error = new EngineError() + + expect(error.message).toBe('Engine error occurred') + }) +}) \ No newline at end of file diff --git a/extensions/engine-management-extension/src/index.test.ts b/extensions/engine-management-extension/src/index.test.ts index 854ad4737..174992f3b 100644 --- a/extensions/engine-management-extension/src/index.test.ts +++ b/extensions/engine-management-extension/src/index.test.ts @@ -1,6 +1,8 @@ import { describe, beforeEach, it, expect, vi } from 'vitest' import JanEngineManagementExtension from './index' import { Engines, InferenceEngine } from '@janhq/core' +import { EngineError } from './error' +import { HTTPError } from 'ky' vi.stubGlobal('API_URL', 'http://localhost:3000') @@ -13,7 +15,27 @@ const mockEngines: Engines = [ }, ] +const mockRemoteEngines: Engines = [ + { + name: 'openai', + version: '1.0.0', + type: 'remote', + engine: InferenceEngine.openai, + }, +] + +const mockRemoteModels = { + data: [ + { + id: 'gpt-4', + name: 'GPT-4', + engine: InferenceEngine.openai, + }, + ], +} + vi.stubGlobal('DEFAULT_REMOTE_ENGINES', mockEngines) +vi.stubGlobal('DEFAULT_REMOTE_MODELS', mockRemoteModels.data) describe('migrate engine settings', () => { let extension: JanEngineManagementExtension @@ -21,6 +43,7 @@ describe('migrate engine settings', () => { beforeEach(() => { // @ts-ignore extension = new JanEngineManagementExtension() + vi.resetAllMocks() }) it('engines should be migrated', async () => { @@ -41,7 +64,7 @@ describe('migrate engine settings', () => { expect(mockUpdateEngines).toBeCalled() }) - it('should not migrate when extesion version is not updated', async () => { + it('should not migrate when extension version is not updated', async () => { vi.stubGlobal('VERSION', '0.0.0') vi.spyOn(extension, 'getEngines').mockResolvedValue([]) const mockUpdateEngines = vi @@ -65,6 +88,7 @@ describe('getEngines', () => { beforeEach(() => { // @ts-ignore extension = new JanEngineManagementExtension() + vi.resetAllMocks() }) it('should return a list of engines', async () => { @@ -77,12 +101,103 @@ describe('getEngines', () => { }) }) +describe('getRemoteModels', () => { + let extension: JanEngineManagementExtension + + beforeEach(() => { + // @ts-ignore + extension = new JanEngineManagementExtension() + vi.resetAllMocks() + }) + + it('should return a list of remote models', async () => { + vi.mock('ky', () => ({ + default: { + get: () => ({ + json: () => Promise.resolve(mockRemoteModels), + }), + }, + })) + + const models = await extension.getRemoteModels('openai') + expect(models).toEqual(mockRemoteModels) + }) + + it('should return empty data array when request fails', async () => { + vi.mock('ky', () => ({ + default: { + get: () => ({ + json: () => Promise.reject(new Error('Failed to fetch')), + }), + }, + })) + + const models = await extension.getRemoteModels('openai') + expect(models).toEqual({ data: [] }) + }) +}) + +describe('getInstalledEngines', () => { + let extension: JanEngineManagementExtension + + beforeEach(() => { + // @ts-ignore + extension = new JanEngineManagementExtension() + vi.resetAllMocks() + }) + + it('should return a list of installed engines', async () => { + const mockEngineVariants = [ + { + name: 'windows-amd64-noavx', + version: '1.0.0', + }, + ] + + vi.mock('ky', () => ({ + default: { + get: () => ({ + json: () => Promise.resolve(mockEngineVariants), + }), + }, + })) + + const mock = vi.spyOn(extension, 'getInstalledEngines') + mock.mockResolvedValue(mockEngineVariants) + + const engines = await extension.getInstalledEngines(InferenceEngine.cortex_llamacpp) + expect(engines).toEqual(mockEngineVariants) + }) +}) + +describe('healthz', () => { + let extension: JanEngineManagementExtension + + beforeEach(() => { + // @ts-ignore + extension = new JanEngineManagementExtension() + vi.resetAllMocks() + }) + + it('should perform health check successfully', async () => { + vi.mock('ky', () => ({ + default: { + get: () => Promise.resolve(), + }, + })) + + await extension.healthz() + expect(extension.queue.concurrency).toBe(Infinity) + }) +}) + describe('updateDefaultEngine', () => { let extension: JanEngineManagementExtension beforeEach(() => { // @ts-ignore extension = new JanEngineManagementExtension() + vi.resetAllMocks() }) it('should set default engine variant if not installed', async () => { @@ -131,7 +246,7 @@ describe('updateDefaultEngine', () => { }) }) - it('should not reset default engine variant if not installed', async () => { + it('should not reset default engine variant if installed', async () => { vi.stubGlobal('PLATFORM', 'win32') vi.stubGlobal('CORTEX_ENGINE_VERSION', '1.0.0') @@ -180,4 +295,155 @@ describe('updateDefaultEngine', () => { expect(mockSetDefaultEngineVariant).not.toBeCalled() }) + + it('should handle HTTPError when getting default engine variant', async () => { + vi.stubGlobal('PLATFORM', 'win32') + vi.stubGlobal('CORTEX_ENGINE_VERSION', '1.0.0') + + const httpError = new Error('HTTP Error') as HTTPError + httpError.response = { status: 400 } as Response + + const mockGetDefaultEngineVariant = vi.spyOn( + extension, + 'getDefaultEngineVariant' + ) + mockGetDefaultEngineVariant.mockRejectedValue(httpError) + + const mockSetDefaultEngineVariant = vi.spyOn( + extension, + 'setDefaultEngineVariant' + ) + mockSetDefaultEngineVariant.mockResolvedValue({ messages: 'OK' }) + + vi.mock('@janhq/core', async (importOriginal) => { + const actual = (await importOriginal()) as any + return { + ...actual, + systemInformation: vi.fn().mockResolvedValue({ gpuSetting: 'high' }), + } + }) + + vi.mock('./utils', async (importOriginal) => { + const actual = (await importOriginal()) as any + return { + ...actual, + engineVariant: vi.fn().mockResolvedValue('windows-amd64-noavx'), + } + }) + + await extension.updateDefaultEngine() + + expect(mockSetDefaultEngineVariant).toHaveBeenCalledWith('llama-cpp', { + variant: 'windows-amd64-noavx', + version: '1.0.0', + }) + }) + + it('should handle EngineError when getting default engine variant', async () => { + vi.stubGlobal('PLATFORM', 'win32') + vi.stubGlobal('CORTEX_ENGINE_VERSION', '1.0.0') + + const mockGetDefaultEngineVariant = vi.spyOn( + extension, + 'getDefaultEngineVariant' + ) + mockGetDefaultEngineVariant.mockRejectedValue(new EngineError('Test error')) + + const mockSetDefaultEngineVariant = vi.spyOn( + extension, + 'setDefaultEngineVariant' + ) + mockSetDefaultEngineVariant.mockResolvedValue({ messages: 'OK' }) + + vi.mock('@janhq/core', async (importOriginal) => { + const actual = (await importOriginal()) as any + return { + ...actual, + systemInformation: vi.fn().mockResolvedValue({ gpuSetting: 'high' }), + } + }) + + vi.mock('./utils', async (importOriginal) => { + const actual = (await importOriginal()) as any + return { + ...actual, + engineVariant: vi.fn().mockResolvedValue('windows-amd64-noavx'), + } + }) + + await extension.updateDefaultEngine() + + expect(mockSetDefaultEngineVariant).toHaveBeenCalledWith('llama-cpp', { + variant: 'windows-amd64-noavx', + version: '1.0.0', + }) + }) + + it('should handle unexpected errors gracefully', async () => { + vi.stubGlobal('PLATFORM', 'win32') + + const mockGetDefaultEngineVariant = vi.spyOn( + extension, + 'getDefaultEngineVariant' + ) + mockGetDefaultEngineVariant.mockRejectedValue(new Error('Unexpected error')) + + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + await extension.updateDefaultEngine() + + expect(consoleSpy).toHaveBeenCalled() + }) +}) + +describe('populateDefaultRemoteEngines', () => { + let extension: JanEngineManagementExtension + + beforeEach(() => { + // @ts-ignore + extension = new JanEngineManagementExtension() + vi.resetAllMocks() + }) + + it('should not add default remote engines if remote engines already exist', async () => { + const mockGetEngines = vi.spyOn(extension, 'getEngines') + mockGetEngines.mockResolvedValue(mockRemoteEngines) + + const mockAddRemoteEngine = vi.spyOn(extension, 'addRemoteEngine') + + await extension.populateDefaultRemoteEngines() + + expect(mockAddRemoteEngine).not.toBeCalled() + }) + + it('should add default remote engines if no remote engines exist', async () => { + const mockGetEngines = vi.spyOn(extension, 'getEngines') + mockGetEngines.mockResolvedValue([]) + + const mockAddRemoteEngine = vi.spyOn(extension, 'addRemoteEngine') + mockAddRemoteEngine.mockResolvedValue({ messages: 'OK' }) + + const mockAddRemoteModel = vi.spyOn(extension, 'addRemoteModel') + mockAddRemoteModel.mockResolvedValue(undefined) + + vi.mock('@janhq/core', async (importOriginal) => { + const actual = (await importOriginal()) as any + return { + ...actual, + events: { + emit: vi.fn(), + }, + joinPath: vi.fn().mockResolvedValue('/path/to/settings.json'), + getJanDataFolderPath: vi.fn().mockResolvedValue('/path/to/data'), + fs: { + existsSync: vi.fn().mockResolvedValue(false), + }, + } + }) + + await extension.populateDefaultRemoteEngines() + + expect(mockAddRemoteEngine).toHaveBeenCalled() + expect(mockAddRemoteModel).toHaveBeenCalled() + }) }) diff --git a/extensions/engine-management-extension/src/populateRemoteModels.test.ts b/extensions/engine-management-extension/src/populateRemoteModels.test.ts new file mode 100644 index 000000000..225db26cc --- /dev/null +++ b/extensions/engine-management-extension/src/populateRemoteModels.test.ts @@ -0,0 +1,139 @@ +import { describe, beforeEach, it, expect, vi } from 'vitest' +import JanEngineManagementExtension from './index' +import { InferenceEngine } from '@janhq/core' + +describe('populateRemoteModels', () => { + let extension: JanEngineManagementExtension + + beforeEach(() => { + // @ts-ignore + extension = new JanEngineManagementExtension() + vi.resetAllMocks() + }) + + it('should populate remote models successfully', async () => { + const mockEngineConfig = { + engine: InferenceEngine.openai, + } + + const mockRemoteModels = { + data: [ + { + id: 'gpt-4', + name: 'GPT-4', + }, + ], + } + + const mockGetRemoteModels = vi.spyOn(extension, 'getRemoteModels') + mockGetRemoteModels.mockResolvedValue(mockRemoteModels) + + const mockAddRemoteModel = vi.spyOn(extension, 'addRemoteModel') + mockAddRemoteModel.mockResolvedValue(undefined) + + vi.mock('@janhq/core', async (importOriginal) => { + const actual = (await importOriginal()) as any + return { + ...actual, + events: { + emit: vi.fn(), + }, + } + }) + + // Use the private method through index.ts + // @ts-ignore - Accessing private method for testing + await extension.populateRemoteModels(mockEngineConfig) + + expect(mockGetRemoteModels).toHaveBeenCalledWith(mockEngineConfig.engine) + expect(mockAddRemoteModel).toHaveBeenCalledWith({ + ...mockRemoteModels.data[0], + engine: mockEngineConfig.engine, + model: 'gpt-4', + }) + }) + + it('should handle empty data from remote models', async () => { + const mockEngineConfig = { + engine: InferenceEngine.openai, + } + + const mockGetRemoteModels = vi.spyOn(extension, 'getRemoteModels') + mockGetRemoteModels.mockResolvedValue({ data: [] }) + + const mockAddRemoteModel = vi.spyOn(extension, 'addRemoteModel') + + vi.mock('@janhq/core', async (importOriginal) => { + const actual = (await importOriginal()) as any + return { + ...actual, + events: { + emit: vi.fn(), + }, + } + }) + + // @ts-ignore - Accessing private method for testing + await extension.populateRemoteModels(mockEngineConfig) + + expect(mockGetRemoteModels).toHaveBeenCalledWith(mockEngineConfig.engine) + expect(mockAddRemoteModel).not.toHaveBeenCalled() + }) + + it('should handle errors when getting remote models', async () => { + const mockEngineConfig = { + engine: InferenceEngine.openai, + } + + const mockGetRemoteModels = vi.spyOn(extension, 'getRemoteModels') + mockGetRemoteModels.mockRejectedValue(new Error('Failed to fetch models')) + + const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {}) + + // @ts-ignore - Accessing private method for testing + await extension.populateRemoteModels(mockEngineConfig) + + expect(mockGetRemoteModels).toHaveBeenCalledWith(mockEngineConfig.engine) + expect(consoleSpy).toHaveBeenCalled() + }) + + it('should handle errors when adding remote models', async () => { + const mockEngineConfig = { + engine: InferenceEngine.openai, + } + + const mockRemoteModels = { + data: [ + { + id: 'gpt-4', + name: 'GPT-4', + }, + ], + } + + const mockGetRemoteModels = vi.spyOn(extension, 'getRemoteModels') + mockGetRemoteModels.mockResolvedValue(mockRemoteModels) + + const mockAddRemoteModel = vi.spyOn(extension, 'addRemoteModel') + mockAddRemoteModel.mockRejectedValue(new Error('Failed to add model')) + + const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {}) + + vi.mock('@janhq/core', async (importOriginal) => { + const actual = (await importOriginal()) as any + return { + ...actual, + events: { + emit: vi.fn(), + }, + } + }) + + // @ts-ignore - Accessing private method for testing + await extension.populateRemoteModels(mockEngineConfig) + + expect(mockGetRemoteModels).toHaveBeenCalledWith(mockEngineConfig.engine) + expect(mockAddRemoteModel).toHaveBeenCalled() + expect(consoleSpy).toHaveBeenCalled() + }) +}) \ No newline at end of file diff --git a/extensions/inference-cortex-extension/bin/version.txt b/extensions/inference-cortex-extension/bin/version.txt index 9f3b89c18..44e199574 100644 --- a/extensions/inference-cortex-extension/bin/version.txt +++ b/extensions/inference-cortex-extension/bin/version.txt @@ -1 +1 @@ -1.0.11-rc7 +1.0.11-rc8