test: add core modules test cases (#3498)

* chore: add core module test cases

* chore: fix tests

* chore: add code coverage report

* chore: split coverage step

* chore: split coverage step

* Update jan-electron-linter-and-test.yml

* Update jan-electron-linter-and-test.yml

* Update jan-electron-linter-and-test.yml

* chore: update tests

* chore: add web utils test cases

* chore: add restful and helper tests

* chore: add tests
This commit is contained in:
Louis 2024-09-06 11:14:28 +07:00 committed by GitHub
parent f759fae55f
commit 846efb3126
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1194 additions and 19 deletions

View File

@ -37,6 +37,30 @@ on:
- '!README.md'
jobs:
base_branch_cov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.base_ref }}
- name: Use Node.js v20.9.0
uses: actions/setup-node@v3
with:
node-version: v20.9.0
- name: Install dependencies
run: yarn
- name: Run test coverage
run: yarn test:coverage
- name: Upload code coverage for ref branch
uses: actions/upload-artifact@v3
with:
name: ref-lcov.info
path: ./coverage/lcov.info
test-on-macos:
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
runs-on: [self-hosted, macOS, macos-desktop]
@ -292,6 +316,56 @@ jobs:
TURBO_TEAM: 'linux'
TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}'
coverage-check:
runs-on: [self-hosted, Linux, ubuntu-desktop]
needs: base_branch_cov
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps:
- name: Getting the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Installing node
uses: actions/setup-node@v3
with:
node-version: 20
- name: 'Cleanup cache'
continue-on-error: true
run: |
rm -rf ~/jan
make clean
- name: Download code coverage report from base branch
uses: actions/download-artifact@v3
with:
name: ref-lcov.info
- name: Linter and test coverage
run: |
export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
echo -e "Display ID: $DISPLAY"
npm config set registry ${{ secrets.NPM_PROXY }} --global
yarn config set registry ${{ secrets.NPM_PROXY }} --global
make lint
yarn build:test
yarn test:coverage
env:
TURBO_API: '${{ secrets.TURBO_API }}'
TURBO_TEAM: 'linux'
TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}'
- name: Generate Code Coverage report
id: code-coverage
uses: barecheck/code-coverage-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
lcov-file: "./coverage/lcov.info"
base-lcov-file: "./lcov.info"
send-summary-comment: true
show-annotations: "warning"
test-on-ubuntu-pr-target:
runs-on: [self-hosted, Linux, ubuntu-desktop]
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository

2
.gitignore vendored
View File

@ -41,3 +41,5 @@ extensions/*-extension/bin/vulkaninfo
.turbo
electron/test-data
electron/test-results
core/test_results.html
coverage

View File

@ -104,7 +104,7 @@ endif
# Testing
test: lint
yarn build:test
yarn test:unit
yarn test:coverage
yarn test
# Builds and publishes the app

View File

@ -4,4 +4,5 @@ module.exports = {
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
},
runner: './testRunner.js',
}

View File

@ -46,6 +46,8 @@
"eslint": "8.57.0",
"eslint-plugin-jest": "^27.9.0",
"jest": "^29.7.0",
"jest-junit": "^16.0.0",
"jest-runner": "^29.7.0",
"rimraf": "^3.0.2",
"rollup": "^2.38.5",
"rollup-plugin-commonjs": "^9.1.8",
@ -53,7 +55,7 @@
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.36.0",
"ts-jest": "^29.1.2",
"ts-jest": "^29.2.5",
"tslib": "^2.6.2",
"typescript": "^5.3.3"
},

View File

@ -0,0 +1,98 @@
import { openExternalUrl } from './core';
import { joinPath } from './core';
import { openFileExplorer } from './core';
import { getJanDataFolderPath } from './core';
import { abortDownload } from './core';
import { getFileSize } from './core';
import { executeOnMain } from './core';
it('should open external url', async () => {
const url = 'http://example.com';
globalThis.core = {
api: {
openExternalUrl: jest.fn().mockResolvedValue('opened')
}
};
const result = await openExternalUrl(url);
expect(globalThis.core.api.openExternalUrl).toHaveBeenCalledWith(url);
expect(result).toBe('opened');
});
it('should join paths', async () => {
const paths = ['/path/one', '/path/two'];
globalThis.core = {
api: {
joinPath: jest.fn().mockResolvedValue('/path/one/path/two')
}
};
const result = await joinPath(paths);
expect(globalThis.core.api.joinPath).toHaveBeenCalledWith(paths);
expect(result).toBe('/path/one/path/two');
});
it('should open file explorer', async () => {
const path = '/path/to/open';
globalThis.core = {
api: {
openFileExplorer: jest.fn().mockResolvedValue('opened')
}
};
const result = await openFileExplorer(path);
expect(globalThis.core.api.openFileExplorer).toHaveBeenCalledWith(path);
expect(result).toBe('opened');
});
it('should get jan data folder path', async () => {
globalThis.core = {
api: {
getJanDataFolderPath: jest.fn().mockResolvedValue('/path/to/jan/data')
}
};
const result = await getJanDataFolderPath();
expect(globalThis.core.api.getJanDataFolderPath).toHaveBeenCalled();
expect(result).toBe('/path/to/jan/data');
});
it('should abort download', async () => {
const fileName = 'testFile';
globalThis.core = {
api: {
abortDownload: jest.fn().mockResolvedValue('aborted')
}
};
const result = await abortDownload(fileName);
expect(globalThis.core.api.abortDownload).toHaveBeenCalledWith(fileName);
expect(result).toBe('aborted');
});
it('should get file size', async () => {
const url = 'http://example.com/file';
globalThis.core = {
api: {
getFileSize: jest.fn().mockResolvedValue(1024)
}
};
const result = await getFileSize(url);
expect(globalThis.core.api.getFileSize).toHaveBeenCalledWith(url);
expect(result).toBe(1024);
});
it('should execute function on main process', async () => {
const extension = 'testExtension';
const method = 'testMethod';
const args = ['arg1', 'arg2'];
globalThis.core = {
api: {
invokeExtensionFunc: jest.fn().mockResolvedValue('result')
}
};
const result = await executeOnMain(extension, method, ...args);
expect(globalThis.core.api.invokeExtensionFunc).toHaveBeenCalledWith(extension, method, ...args);
expect(result).toBe('result');
});

View File

@ -0,0 +1,37 @@
import { events } from './events';
import { jest } from '@jest/globals';
it('should emit an event', () => {
const mockObject = { key: 'value' };
globalThis.core = {
events: {
emit: jest.fn()
}
};
events.emit('testEvent', mockObject);
expect(globalThis.core.events.emit).toHaveBeenCalledWith('testEvent', mockObject);
});
it('should remove an observer for an event', () => {
const mockHandler = jest.fn();
globalThis.core = {
events: {
off: jest.fn()
}
};
events.off('testEvent', mockHandler);
expect(globalThis.core.events.off).toHaveBeenCalledWith('testEvent', mockHandler);
});
it('should add an observer for an event', () => {
const mockHandler = jest.fn();
globalThis.core = {
events: {
on: jest.fn()
}
};
events.on('testEvent', mockHandler);
expect(globalThis.core.events.on).toHaveBeenCalledWith('testEvent', mockHandler);
});

View File

@ -0,0 +1,46 @@
import { BaseExtension } from './extension'
class TestBaseExtension extends BaseExtension {
onLoad(): void {}
onUnload(): void {}
}
describe('BaseExtension', () => {
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
})
})

View File

@ -0,0 +1,60 @@
import { lastValueFrom, Observable } from 'rxjs'
import { requestInference } from './sse'
describe('requestInference', () => {
it('should send a request to the inference server and return an Observable', () => {
// 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',
// Add other required properties here
})
)
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 } }
// Call the function
const result = requestInference(inferenceUrl, requestBody, model)
// Assert the expected behavior
expect(result).toBeInstanceOf(Observable)
expect(lastValueFrom(result)).resolves.toEqual('Generated response')
})
it('returns 401 error', () => {
// Mock the fetch function
const mockFetch: any = jest.fn(() =>
Promise.resolve({
ok: false,
json: () => Promise.resolve({ error: { message: 'Wrong API Key', code: 'invalid_api_key' } }),
headers: new Headers(),
redirected: false,
status: 401,
statusText: 'invalid_api_key',
// Add other required properties here
})
)
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 } }
// Call the function
const result = requestInference(inferenceUrl, requestBody, model)
// Assert the expected behavior
expect(result).toBeInstanceOf(Observable)
expect(lastValueFrom(result)).rejects.toEqual({ message: 'Wrong API Key', code: 'invalid_api_key' })
})
})

View File

@ -0,0 +1,32 @@
import * as Core from './core';
import * as Events from './events';
import * as FileSystem from './fs';
import * as Extension from './extension';
import * as Extensions from './extensions';
import * as Tools from './tools';
describe('Module Tests', () => {
it('should export Core module', () => {
expect(Core).toBeDefined();
});
it('should export Event module', () => {
expect(Events).toBeDefined();
});
it('should export Filesystem module', () => {
expect(FileSystem).toBeDefined();
});
it('should export Extension module', () => {
expect(Extension).toBeDefined();
});
it('should export all base extensions', () => {
expect(Extensions).toBeDefined();
});
it('should export all base tools', () => {
expect(Tools).toBeDefined();
});
});

View File

@ -0,0 +1,10 @@
import { RequestAdapter } from './adapter';
it('should return undefined for unknown route', () => {
const adapter = new RequestAdapter();
const route = 'unknownRoute';
const result = adapter.process(route, 'arg1', 'arg2');
expect(result).toBeUndefined();
});

View File

@ -0,0 +1,25 @@
import { CoreRoutes } from '../../../types/api';
import { RequestHandler } from './handler';
import { RequestAdapter } from './adapter';
it('should not call handler if CoreRoutes is empty', () => {
const mockHandler = jest.fn();
const mockObserver = jest.fn();
const requestHandler = new RequestHandler(mockHandler, mockObserver);
CoreRoutes.length = 0; // Ensure CoreRoutes is empty
requestHandler.handle();
expect(mockHandler).not.toHaveBeenCalled();
});
it('should initialize handler and adapter correctly', () => {
const mockHandler = jest.fn();
const mockObserver = jest.fn();
const requestHandler = new RequestHandler(mockHandler, mockObserver);
expect(requestHandler.handler).toBe(mockHandler);
expect(requestHandler.adapter).toBeInstanceOf(RequestAdapter);
});

View File

@ -0,0 +1,40 @@
import { App } from './app';
it('should call stopServer', () => {
const app = new App();
const stopServerMock = jest.fn().mockResolvedValue('Server stopped');
jest.mock('@janhq/server', () => ({
stopServer: stopServerMock
}));
const result = app.stopServer();
expect(stopServerMock).toHaveBeenCalled();
});
it('should correctly retrieve basename', () => {
const app = new App();
const result = app.baseName('/path/to/file.txt');
expect(result).toBe('file.txt');
});
it('should correctly identify subdirectories', () => {
const app = new App();
const basePath = process.platform === 'win32' ? 'C:\\path\\to' : '/path/to';
const subPath = process.platform === 'win32' ? 'C:\\path\\to\\subdir' : '/path/to/subdir';
const result = app.isSubdirectory(basePath, subPath);
expect(result).toBe(true);
});
it('should correctly join multiple paths', () => {
const app = new App();
const result = app.joinPath(['path', 'to', 'file']);
const expectedPath = process.platform === 'win32' ? 'path\\to\\file' : 'path/to/file';
expect(result).toBe(expectedPath);
});
it('should call correct function with provided arguments using process method', () => {
const app = new App();
const mockFunc = jest.fn();
app.joinPath = mockFunc;
app.process('joinPath', ['path1', 'path2']);
expect(mockFunc).toHaveBeenCalledWith(['path1', 'path2']);
});

View File

@ -0,0 +1,59 @@
import { Downloader } from './download';
import { DownloadEvent } from '../../../types/api';
import { DownloadManager } from '../../helper/download';
it('should handle getFileSize errors correctly', async () => {
const observer = jest.fn();
const url = 'http://example.com/file';
const downloader = new Downloader(observer);
const requestMock = jest.fn((options, callback) => {
callback(new Error('Test error'), null);
});
jest.mock('request', () => requestMock);
await expect(downloader.getFileSize(observer, url)).rejects.toThrow('Test error');
});
it('should pause download correctly', () => {
const observer = jest.fn();
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file';
const downloader = new Downloader(observer);
const pauseMock = jest.fn();
DownloadManager.instance.networkRequests[fileName] = { pause: pauseMock };
downloader.pauseDownload(observer, fileName);
expect(pauseMock).toHaveBeenCalled();
});
it('should resume download correctly', () => {
const observer = jest.fn();
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file';
const downloader = new Downloader(observer);
const resumeMock = jest.fn();
DownloadManager.instance.networkRequests[fileName] = { resume: resumeMock };
downloader.resumeDownload(observer, fileName);
expect(resumeMock).toHaveBeenCalled();
});
it('should handle aborting a download correctly', () => {
const observer = jest.fn();
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file';
const downloader = new Downloader(observer);
const abortMock = jest.fn();
DownloadManager.instance.networkRequests[fileName] = { abort: abortMock };
downloader.abortDownload(observer, fileName);
expect(abortMock).toHaveBeenCalled();
expect(observer).toHaveBeenCalledWith(DownloadEvent.onFileDownloadError, expect.objectContaining({
error: 'aborted'
}));
});

View File

@ -0,0 +1,9 @@
import { Extension } from './extension';
it('should call function associated with key in process method', () => {
const mockFunc = jest.fn();
const extension = new Extension();
(extension as any).testKey = mockFunc;
extension.process('testKey', 'arg1', 'arg2');
expect(mockFunc).toHaveBeenCalledWith('arg1', 'arg2');
});

View File

@ -0,0 +1,18 @@
import { FileSystem } from './fs';
it('should throw an error when the route does not exist in process', async () => {
const fileSystem = new FileSystem();
await expect(fileSystem.process('nonExistentRoute', 'arg1')).rejects.toThrow();
});
it('should throw an error for invalid argument in mkdir', async () => {
const fileSystem = new FileSystem();
expect(() => fileSystem.mkdir(123)).toThrow('mkdir error: Invalid argument [123]');
});
it('should throw an error for invalid argument in rm', async () => {
const fileSystem = new FileSystem();
expect(() => fileSystem.rm(123)).toThrow('rm error: Invalid argument [123]');
});

View File

@ -0,0 +1,34 @@
import { FSExt } from './fsExt';
import { defaultAppConfig } from '../../helper';
it('should handle errors in writeBlob', () => {
const fsExt = new FSExt();
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
fsExt.writeBlob('invalid-path', 'data');
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
it('should call correct function in process method', () => {
const fsExt = new FSExt();
const mockFunction = jest.fn();
(fsExt as any).mockFunction = mockFunction;
fsExt.process('mockFunction', 'arg1', 'arg2');
expect(mockFunction).toHaveBeenCalledWith('arg1', 'arg2');
});
it('should return correct user home path', () => {
const fsExt = new FSExt();
const userHomePath = fsExt.getUserHomePath();
expect(userHomePath).toBe(defaultAppConfig().data_folder);
});
it('should return empty array when no files are provided', async () => {
const fsExt = new FSExt();
const result = await fsExt.getGgufFiles([]);
expect(result.supportedFiles).toEqual([]);
expect(result.unsupportedFiles).toEqual([]);
});

View File

@ -0,0 +1,62 @@
import { HttpServer } from '../../HttpServer'
import { DownloadManager } from '../../../helper/download'
describe('downloadRouter', () => {
let app: HttpServer
beforeEach(() => {
app = {
register: jest.fn(),
post: jest.fn(),
get: jest.fn(),
patch: jest.fn(),
put: jest.fn(),
delete: jest.fn(),
}
})
it('should return download progress for a given modelId', async () => {
const modelId = '123'
const downloadProgress = { progress: 50 }
DownloadManager.instance.downloadProgressMap[modelId] = downloadProgress as any
const req = { params: { modelId } }
const res = {
status: jest.fn(),
send: jest.fn(),
}
jest.spyOn(app, 'get').mockImplementation((path, handler) => {
if (path === `/download/getDownloadProgress/${modelId}`) {
res.status(200)
res.send(downloadProgress)
}
})
app.get(`/download/getDownloadProgress/${modelId}`, req as any)
expect(res.status).toHaveBeenCalledWith(200)
expect(res.send).toHaveBeenCalledWith(downloadProgress)
})
it('should return 404 if download progress is not found', async () => {
const modelId = '123'
const req = { params: { modelId } }
const res = {
status: jest.fn(),
send: jest.fn(),
}
jest.spyOn(app, 'get').mockImplementation((path, handler) => {
if (path === `/download/getDownloadProgress/${modelId}`) {
res.status(404)
res.send({ message: 'Download progress not found' })
}
})
app.get(`/download/getDownloadProgress/${modelId}`, req as any)
expect(res.status).toHaveBeenCalledWith(404)
expect(res.send).toHaveBeenCalledWith({ message: 'Download progress not found' })
})
})

View File

@ -0,0 +1,16 @@
//
import { jest } from '@jest/globals';
import { HttpServer } from '../../HttpServer';
import { handleRequests } from './handlers';
import { Handler, RequestHandler } from '../../common/handler';
it('should initialize RequestHandler and call handle', () => {
const mockHandle = jest.fn();
jest.spyOn(RequestHandler.prototype, 'handle').mockImplementation(mockHandle);
const mockApp = { post: jest.fn() };
handleRequests(mockApp as unknown as HttpServer);
expect(mockHandle).toHaveBeenCalled();
});

View File

@ -0,0 +1,21 @@
import { commonRouter } from './common';
import { JanApiRouteConfiguration } from './helper/configuration';
test('commonRouter sets up routes for each key in JanApiRouteConfiguration', async () => {
const mockHttpServer = {
get: jest.fn(),
post: jest.fn(),
patch: jest.fn(),
put: jest.fn(),
delete: jest.fn(),
};
await commonRouter(mockHttpServer as any);
const expectedRoutes = Object.keys(JanApiRouteConfiguration);
expectedRoutes.forEach((key) => {
expect(mockHttpServer.get).toHaveBeenCalledWith(`/${key}`, expect.any(Function));
expect(mockHttpServer.get).toHaveBeenCalledWith(`/${key}/:id`, expect.any(Function));
expect(mockHttpServer.delete).toHaveBeenCalledWith(`/${key}/:id`, expect.any(Function));
});
});

View File

@ -0,0 +1,24 @@
import { JanApiRouteConfiguration } from './configuration'
describe('JanApiRouteConfiguration', () => {
it('should have the correct models configuration', () => {
const modelsConfig = JanApiRouteConfiguration.models;
expect(modelsConfig.dirName).toBe('models');
expect(modelsConfig.metadataFileName).toBe('model.json');
expect(modelsConfig.delete.object).toBe('model');
});
it('should have the correct assistants configuration', () => {
const assistantsConfig = JanApiRouteConfiguration.assistants;
expect(assistantsConfig.dirName).toBe('assistants');
expect(assistantsConfig.metadataFileName).toBe('assistant.json');
expect(assistantsConfig.delete.object).toBe('assistant');
});
it('should have the correct threads configuration', () => {
const threadsConfig = JanApiRouteConfiguration.threads;
expect(threadsConfig.dirName).toBe('threads');
expect(threadsConfig.metadataFileName).toBe('thread.json');
expect(threadsConfig.delete.object).toBe('thread');
});
});

View File

@ -0,0 +1,16 @@
import { v1Router } from './v1';
import { commonRouter } from './common';
test('should define v1Router function', () => {
expect(v1Router).toBeDefined();
});
test('should register commonRouter', () => {
const mockApp = {
register: jest.fn(),
};
v1Router(mockApp as any);
expect(mockApp.register).toHaveBeenCalledWith(commonRouter);
});

View File

@ -0,0 +1,122 @@
import Extension from './extension';
import { join } from 'path';
import 'pacote';
it('should set active and call emitUpdate', () => {
const extension = new Extension();
extension.emitUpdate = jest.fn();
extension.setActive(true);
expect(extension._active).toBe(true);
expect(extension.emitUpdate).toHaveBeenCalled();
});
it('should return correct specifier', () => {
const origin = 'test-origin';
const options = { version: '1.0.0' };
const extension = new Extension(origin, options);
expect(extension.specifier).toBe('test-origin@1.0.0');
});
it('should set origin and installOptions in constructor', () => {
const origin = 'test-origin';
const options = { someOption: true };
const extension = new Extension(origin, options);
expect(extension.origin).toBe(origin);
expect(extension.installOptions.someOption).toBe(true);
expect(extension.installOptions.fullMetadata).toBe(true); // default option
});
it('should install extension and set url', async () => {
const origin = 'test-origin';
const options = {};
const extension = new Extension(origin, options);
const mockManifest = {
name: 'test-name',
productName: 'Test Product',
version: '1.0.0',
main: 'index.js',
description: 'Test description'
};
jest.mock('pacote', () => ({
manifest: jest.fn().mockResolvedValue(mockManifest),
extract: jest.fn().mockResolvedValue(null)
}));
extension.emitUpdate = jest.fn();
await extension._install();
expect(extension.url).toBe('extension://test-name/index.js');
expect(extension.emitUpdate).toHaveBeenCalled();
});
it('should call all listeners in emitUpdate', () => {
const extension = new Extension();
const callback1 = jest.fn();
const callback2 = jest.fn();
extension.subscribe('listener1', callback1);
extension.subscribe('listener2', callback2);
extension.emitUpdate();
expect(callback1).toHaveBeenCalledWith(extension);
expect(callback2).toHaveBeenCalledWith(extension);
});
it('should remove listener in unsubscribe', () => {
const extension = new Extension();
const callback = jest.fn();
extension.subscribe('testListener', callback);
extension.unsubscribe('testListener');
expect(extension.listeners['testListener']).toBeUndefined();
});
it('should add listener in subscribe', () => {
const extension = new Extension();
const callback = jest.fn();
extension.subscribe('testListener', callback);
expect(extension.listeners['testListener']).toBe(callback);
});
it('should set properties from manifest', async () => {
const origin = 'test-origin';
const options = {};
const extension = new Extension(origin, options);
const mockManifest = {
name: 'test-name',
productName: 'Test Product',
version: '1.0.0',
main: 'index.js',
description: 'Test description'
};
jest.mock('pacote', () => ({
manifest: jest.fn().mockResolvedValue(mockManifest)
}));
await extension.getManifest();
expect(extension.name).toBe('test-name');
expect(extension.productName).toBe('Test Product');
expect(extension.version).toBe('1.0.0');
expect(extension.main).toBe('index.js');
expect(extension.description).toBe('Test description');
});

View File

@ -0,0 +1,28 @@
import * as fs from 'fs';
import { join } from 'path';
import { ExtensionManager } from './manager';
it('should throw an error when an invalid path is provided', () => {
const manager = new ExtensionManager();
jest.spyOn(fs, 'existsSync').mockReturnValue(false);
expect(() => manager.setExtensionsPath('')).toThrow('Invalid path provided to the extensions folder');
});
it('should return an empty string when extensionsPath is not set', () => {
const manager = new ExtensionManager();
expect(manager.getExtensionsFile()).toBe(join('', 'extensions.json'));
});
it('should return undefined if no path is set', () => {
const manager = new ExtensionManager();
expect(manager.getExtensionsPath()).toBeUndefined();
});
it('should return the singleton instance', () => {
const instance1 = new ExtensionManager();
const instance2 = new ExtensionManager();
expect(instance1).toBe(instance2);
});

View File

@ -0,0 +1,43 @@
import { getAllExtensions } from './store';
import { getActiveExtensions } from './store';
import { getExtension } from './store';
test('should return empty array when no extensions added', () => {
expect(getAllExtensions()).toEqual([]);
});
test('should throw error when extension does not exist', () => {
expect(() => getExtension('nonExistentExtension')).toThrow('Extension nonExistentExtension does not exist');
});
import { addExtension } from './store';
import Extension from './extension';
test('should return all extensions when multiple extensions added', () => {
const ext1 = new Extension('ext1');
ext1.name = 'ext1';
const ext2 = new Extension('ext2');
ext2.name = 'ext2';
addExtension(ext1, false);
addExtension(ext2, false);
expect(getAllExtensions()).toEqual([ext1, ext2]);
});
test('should return only active extensions', () => {
const ext1 = new Extension('ext1');
ext1.name = 'ext1';
ext1.setActive(true);
const ext2 = new Extension('ext2');
ext2.name = 'ext2';
ext2.setActive(false);
addExtension(ext1, false);
addExtension(ext2, false);
expect(getActiveExtensions()).toEqual([ext1]);
});

View File

@ -0,0 +1,14 @@
import { getEngineConfiguration } from './config';
import { getAppConfigurations, defaultAppConfig } from './config';
it('should return undefined for invalid engine ID', async () => {
const config = await getEngineConfiguration('invalid_engine');
expect(config).toBeUndefined();
});
it('should return default config when CI is e2e', () => {
process.env.CI = 'e2e';
const config = getAppConfigurations();
expect(config).toEqual(defaultAppConfig());
});

View File

@ -0,0 +1,11 @@
import { DownloadManager } from './download';
it('should set a network request for a specific file', () => {
const downloadManager = new DownloadManager();
const fileName = 'testFile';
const request = { url: 'http://example.com' };
downloadManager.setRequest(fileName, request);
expect(downloadManager.networkRequests[fileName]).toEqual(request);
});

View File

@ -0,0 +1,47 @@
import { Logger, LoggerManager } from './logger';
it('should flush queued logs to registered loggers', () => {
class TestLogger extends Logger {
name = 'testLogger';
log(args: any): void {
console.log(args);
}
}
const loggerManager = new LoggerManager();
const testLogger = new TestLogger();
loggerManager.register(testLogger);
const logSpy = jest.spyOn(testLogger, 'log');
loggerManager.log('test log');
expect(logSpy).toHaveBeenCalledWith('test log');
});
it('should unregister a logger', () => {
class TestLogger extends Logger {
name = 'testLogger';
log(args: any): void {
console.log(args);
}
}
const loggerManager = new LoggerManager();
const testLogger = new TestLogger();
loggerManager.register(testLogger);
loggerManager.unregister('testLogger');
const retrievedLogger = loggerManager.get('testLogger');
expect(retrievedLogger).toBeUndefined();
});
it('should register and retrieve a logger', () => {
class TestLogger extends Logger {
name = 'testLogger';
log(args: any): void {
console.log(args);
}
}
const loggerManager = new LoggerManager();
const testLogger = new TestLogger();
loggerManager.register(testLogger);
const retrievedLogger = loggerManager.get('testLogger');
expect(retrievedLogger).toBe(testLogger);
});

View File

@ -0,0 +1,23 @@
import { ModuleManager } from './module';
it('should clear all imported modules', () => {
const moduleManager = new ModuleManager();
moduleManager.setModule('module1', { key: 'value1' });
moduleManager.setModule('module2', { key: 'value2' });
moduleManager.clearImportedModules();
expect(moduleManager.requiredModules).toEqual({});
});
it('should set a module correctly', () => {
const moduleManager = new ModuleManager();
moduleManager.setModule('testModule', { key: 'value' });
expect(moduleManager.requiredModules['testModule']).toEqual({ key: 'value' });
});
it('should return the singleton instance', () => {
const instance1 = new ModuleManager();
const instance2 = new ModuleManager();
expect(instance1).toBe(instance2);
});

View File

@ -0,0 +1,29 @@
import { normalizeFilePath } from './path'
import { jest } from '@jest/globals'
describe('Test file normalize', () => {
test('returns no file protocol prefix on Unix', async () => {
expect(normalizeFilePath('file://test.txt')).toBe('test.txt')
expect(normalizeFilePath('file:/test.txt')).toBe('test.txt')
})
test('returns no file protocol prefix on Windows', async () => {
expect(normalizeFilePath('file:\\\\test.txt')).toBe('test.txt')
expect(normalizeFilePath('file:\\test.txt')).toBe('test.txt')
})
test('returns correct path when Electron is available and app is not packaged', () => {
const electronMock = {
app: {
getAppPath: jest.fn().mockReturnValue('/mocked/path'),
isPackaged: false,
},
protocol: {},
}
jest.mock('electron', () => electronMock)
const { appResourcePath } = require('./path')
const expectedPath = process.platform === 'win32' ? '\\mocked\\path' : '/mocked/path'
expect(appResourcePath()).toBe(expectedPath)
})
})

View File

@ -0,0 +1,15 @@
import { getSystemResourceInfo } from './resource';
it('should return the correct system resource information with a valid CPU count', async () => {
const mockCpuCount = 4;
jest.spyOn(require('./config'), 'physicalCpuCount').mockResolvedValue(mockCpuCount);
const logSpy = jest.spyOn(require('./logger'), 'log').mockImplementation(() => {});
const result = await getSystemResourceInfo();
expect(result).toEqual({
numCpuPhysicalCore: mockCpuCount,
memAvailable: 0,
});
expect(logSpy).toHaveBeenCalledWith(`[CORTEX]::CPU information - ${mockCpuCount}`);
});

View File

@ -0,0 +1,7 @@
import { AssistantEvent } from './assistantEvent';
it('dummy test', () => { expect(true).toBe(true); });
it('should contain OnAssistantsUpdate event', () => {
expect(AssistantEvent.OnAssistantsUpdate).toBe('OnAssistantsUpdate');
});

View File

@ -16,7 +16,7 @@ export type DownloadState = {
error?: string
extensionId?: string
downloadType?: DownloadType
downloadType?: DownloadType | string
localPath?: string
}
@ -40,7 +40,7 @@ export type DownloadRequest = {
*/
extensionId?: string
downloadType?: DownloadType
downloadType?: DownloadType | string
}
type DownloadTime = {

10
core/testRunner.js Normal file
View File

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

View File

@ -1,12 +0,0 @@
import { normalizeFilePath } from "../../src/node/helper/path";
describe("Test file normalize", () => {
test("returns no file protocol prefix on Unix", async () => {
expect(normalizeFilePath("file://test.txt")).toBe("test.txt");
expect(normalizeFilePath("file:/test.txt")).toBe("test.txt");
});
test("returns no file protocol prefix on Windows", async () => {
expect(normalizeFilePath("file:\\\\test.txt")).toBe("test.txt");
expect(normalizeFilePath("file:\\test.txt")).toBe("test.txt");
});
});

View File

@ -16,4 +16,5 @@
"types": ["@types/jest"],
},
"include": ["src"],
"exclude": ["**/*.test.ts"]
}

View File

@ -1,3 +1,3 @@
module.exports = {
projects: ['<rootDir>/core', '<rootDir>/web'],
projects: ['<rootDir>/core', '<rootDir>/web', '<rootDir>/joi'],
}

35
web/utils/Stack.test.ts Normal file
View File

@ -0,0 +1,35 @@
import { Stack } from './Stack';
it('should return elements in reverse order', () => {
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
stack.push(3);
const reversedOutput = stack.reverseOutput();
expect(reversedOutput).toEqual([1, 2, 3]);
});
it('should pop an element from the stack', () => {
const stack = new Stack<number>();
stack.push(1);
const poppedElement = stack.pop();
expect(poppedElement).toBe(1);
expect(stack.isEmpty()).toBe(true);
});
it('should push an element to the stack', () => {
const stack = new Stack<number>();
stack.push(1);
expect(stack.isEmpty()).toBe(false);
expect(stack.size()).toBe(1);
expect(stack.peek()).toBe(1);
});
it('should initialize as empty', () => {
const stack = new Stack<number>();
expect(stack.isEmpty()).toBe(true);
});

View File

@ -0,0 +1,22 @@
import { getConfigurationsData } from './componentSettings';
it('should process checkbox setting', () => {
const settings = { embedding: true };
const result = getConfigurationsData(settings);
expect(result[0].controllerProps.value).toBe(true);
});
it('should process input setting and handle array input', () => {
const settings = { prompt_template: ['Hello', 'World', ''] };
const result = getConfigurationsData(settings);
expect(result[0].controllerProps.value).toBe('Hello World ');
});
it('should return an empty array when settings object is empty', () => {
const settings = {};
const result = getConfigurationsData(settings);
expect(result).toEqual([]);
});

View File

@ -0,0 +1,27 @@
import { displayDate } from './datetime';
import { isToday } from './datetime';
test('should return only time for today\'s timestamp', () => {
const today = new Date();
const timestamp = today.getTime();
const expectedTime = today.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true,
});
expect(displayDate(timestamp)).toBe(expectedTime);
});
test('should return N/A for undefined timestamp', () => {
expect(displayDate()).toBe('N/A');
});
test('should return true for today\'s timestamp', () => {
const today = new Date();
const timestamp = today.setHours(0, 0, 0, 0);
expect(isToday(timestamp)).toBe(true);
});

View File

@ -0,0 +1,17 @@
import cssVars from './jsonToCssVariables';
test('should convert nested JSON object to CSS variables', () => {
const input = { theme: { color: 'blue', font: { size: '14px', weight: 'bold' } } };
const expectedOutput = '--theme-color: blue;--theme-font-size: 14px;--theme-font-weight: bold;';
const result = cssVars(input);
expect(result).toBe(expectedOutput);
});
test('should convert simple JSON object to CSS variables', () => {
const input = { color: 'red', fontSize: '16px' };
const expectedOutput = '--color: red;--fontSize: 16px;';
const result = cssVars(input);
expect(result).toBe(expectedOutput);
});

View File

@ -1,5 +1,3 @@
// @auto-generated
import { utilizedMemory } from './memory'
test('test_utilizedMemory_arbitraryValues', () => {

View File

@ -0,0 +1,18 @@
import { presetConfiguration } from './predefinedComponent';
it('should have correct configuration for prompt_template', () => {
const config = presetConfiguration['prompt_template'];
expect(config).toEqual({
key: 'prompt_template',
title: 'Prompt template',
description: `A predefined text or framework that guides the AI model's response generation. It includes placeholders or instructions for the model to fill in or expand upon.`,
controllerType: 'input',
controllerProps: {
placeholder: 'Prompt template',
value: '',
},
requireModelReload: true,
configType: 'setting',
});
});

9
web/utils/thread.test.ts Normal file
View File

@ -0,0 +1,9 @@
import { generateThreadId } from './thread';
test('shouldGenerateThreadIdWithCorrectFormat', () => {
const assistantId = 'assistant123';
const threadId = generateThreadId(assistantId);
const regex = /^assistant123_\d{10}$/;
expect(threadId).toMatch(regex);
});

View File

@ -0,0 +1,25 @@
import { openFileTitle } from './titleUtils';
test('should return "Open Containing Folder" when neither isMac nor isWindows is true', () => {
(global as any).isMac = false;
(global as any).isWindows = false;
const result = openFileTitle();
expect(result).toBe('Open Containing Folder');
});
test('should return "Show in File Explorer" when isWindows is true', () => {
(global as any).isMac = false;
(global as any).isWindows = true;
const result = openFileTitle();
expect(result).toBe('Show in File Explorer');
});
test('should return "Show in Finder" when isMac is true', () => {
(global as any).isMac = true;
(global as any).isWindows = false;
const result = openFileTitle();
expect(result).toBe('Show in Finder');
});