diff --git a/cortex-js/.eslintrc.js b/cortex-js/.eslintrc.js index 259de13c7..448a2d910 100644 --- a/cortex-js/.eslintrc.js +++ b/cortex-js/.eslintrc.js @@ -21,5 +21,11 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', + "prettier/prettier": [ + "error", + { + "endOfLine": "auto" + }, + ], }, }; diff --git a/cortex-js/src/infrastructure/commanders/serve.command.ts b/cortex-js/src/infrastructure/commanders/serve.command.ts index f839b4a0b..99575bcb2 100644 --- a/cortex-js/src/infrastructure/commanders/serve.command.ts +++ b/cortex-js/src/infrastructure/commanders/serve.command.ts @@ -20,16 +20,22 @@ export class ServeCommand extends CommandRunner { const host = options?.host || defaultCortexJsHost; const port = options?.port || defaultCortexJsPort; - spawn('node', [join(__dirname, '../../main.js')], { - env: { - ...process.env, - CORTEX_JS_HOST: host, - CORTEX_JS_PORT: port.toString(), - NODE_ENV: 'production', + spawn( + 'node', + process.env.TEST + ? [join(__dirname, '../../../dist/src/main.js')] + : [join(__dirname, '../../main.js')], + { + env: { + ...process.env, + CORTEX_JS_HOST: host, + CORTEX_JS_PORT: port.toString(), + NODE_ENV: 'production', + }, + stdio: 'inherit', + detached: false, }, - stdio: 'inherit', - detached: false, - }); + ); } @Option({ diff --git a/cortex-js/src/infrastructure/commanders/test/helpers.command.spec.ts b/cortex-js/src/infrastructure/commanders/test/helpers.command.spec.ts new file mode 100644 index 000000000..05d9562d6 --- /dev/null +++ b/cortex-js/src/infrastructure/commanders/test/helpers.command.spec.ts @@ -0,0 +1,107 @@ +import { TestingModule } from '@nestjs/testing'; +import { spy, Stub, stubMethod } from 'hanbi'; +import { CommandTestFactory } from 'nest-commander-testing'; +import { CommandModule } from '@/command.module'; +import { LogService } from '@/infrastructure/commanders/test/log.service'; +import axios from 'axios'; + +let commandInstance: TestingModule, + exitSpy: Stub, + stdoutSpy: Stub, + stderrSpy: Stub; +export const timeout = 500000; + +beforeEach( + () => + new Promise(async (res) => { + stubMethod(process.stderr, 'write'); + exitSpy = stubMethod(process, 'exit'); + stdoutSpy = stubMethod(process.stdout, 'write'); + stderrSpy = stubMethod(process.stderr, 'write'); + commandInstance = await CommandTestFactory.createTestingCommand({ + imports: [CommandModule], + }) + .overrideProvider(LogService) + .useValue({ log: spy().handler }) + .compile(); + res(); + stdoutSpy.reset(); + stderrSpy.reset(); + }), +); + +describe('Helper commands', () => { + test( + 'Init with hardware auto detection', + async () => { + await CommandTestFactory.run(commandInstance, ['init', '-s']); + + // Wait for a brief period to allow the command to execute + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect(stdoutSpy.firstCall?.args.length).toBeGreaterThan(0); + }, + timeout, + ); + + test('Chat with option -m', async () => { + const logMock = stubMethod(console, 'log'); + + await CommandTestFactory.run(commandInstance, [ + 'chat', + // '-m', + // 'hello', + // '>output.txt', + ]); + expect(logMock.firstCall?.args[0]).toBe("Inorder to exit, type 'exit()'."); + // expect(exitSpy.callCount).toBe(1); + // expect(exitSpy.firstCall?.args[0]).toBe(1); + }); + + test('Show / kill running models', async () => { + const tableMock = stubMethod(console, 'table'); + + const logMock = stubMethod(console, 'log'); + await CommandTestFactory.run(commandInstance, ['kill']); + await CommandTestFactory.run(commandInstance, ['ps']); + + expect(logMock.firstCall?.args[0]).toEqual({ + message: 'Cortex stopped successfully', + status: 'success', + }); + expect(tableMock.firstCall?.args[0]).toBeInstanceOf(Array); + expect(tableMock.firstCall?.args[0].length).toEqual(0); + }); + + test('Help command return guideline to users', async () => { + await CommandTestFactory.run(commandInstance, ['-h']); + expect(stdoutSpy.firstCall?.args).toBeInstanceOf(Array); + expect(stdoutSpy.firstCall?.args.length).toBe(1); + expect(stdoutSpy.firstCall?.args[0]).toContain('display help for command'); + + expect(exitSpy.callCount).toBeGreaterThan(1); + expect(exitSpy.firstCall?.args[0]).toBe(0); + }); + + test('Should handle missing command', async () => { + await CommandTestFactory.run(commandInstance, ['--unknown']); + expect(stderrSpy.firstCall?.args[0]).toContain('error: unknown option'); + expect(stderrSpy.firstCall?.args[0]).toContain('--unknown'); + expect(exitSpy.callCount).toBe(1); + expect(exitSpy.firstCall?.args[0]).toBe(1); + }); + + test('Local API server via localhost:1337/api', async () => { + await CommandTestFactory.run(commandInstance, ['serve']); + + // Add a delay of 1000 milliseconds (1 second) + return new Promise(async (resolve) => { + setTimeout(async () => { + // Send a request to the API server to check if it's running + const response = await axios.get('http://localhost:1337/api'); + expect(response.status).toBe(200); + resolve(); + }, 1000); + }); + }); +}); diff --git a/cortex-js/src/infrastructure/commanders/test/log.service.ts b/cortex-js/src/infrastructure/commanders/test/log.service.ts new file mode 100644 index 000000000..1151f5fb5 --- /dev/null +++ b/cortex-js/src/infrastructure/commanders/test/log.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class LogService { + log(...args: any[]): void { + console.log(...args); + } +} diff --git a/cortex-js/src/infrastructure/commanders/test/model-list.command.spec.ts b/cortex-js/src/infrastructure/commanders/test/model-list.command.spec.ts deleted file mode 100644 index b90b26006..000000000 --- a/cortex-js/src/infrastructure/commanders/test/model-list.command.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { TestingModule } from '@nestjs/testing'; -import { stubMethod } from 'hanbi'; -import { CommandTestFactory } from 'nest-commander-testing'; -import { CommandModule } from '@/command.module'; -import { FileManagerService } from '@/file-manager/file-manager.service'; -import { join } from 'path'; -import { mkdirSync, rmSync, writeFileSync } from 'fs'; - -let commandInstance: TestingModule; - -beforeEach( - () => - new Promise(async (res) => { - commandInstance = await CommandTestFactory.createTestingCommand({ - imports: [CommandModule], - }) - // .overrideProvider(LogService) - // .useValue({}) - .compile(); - const fileService = - await commandInstance.resolve(FileManagerService); - - // Attempt to create test folder - await fileService.writeConfigFile({ - dataFolderPath: join(__dirname, 'test_data'), - }); - res(); - }), -); - -afterEach( - () => - new Promise(async (res) => { - // Attempt to clean test folder - rmSync(join(__dirname, 'test_data'), { - recursive: true, - force: true, - }); - res(); - }), -); - -describe('models list returns array of models', () => { - test('empty model list', async () => { - const logMock = stubMethod(console, 'table'); - - await CommandTestFactory.run(commandInstance, ['models', 'list']); - expect(logMock.firstCall?.args[0]).toBeInstanceOf(Array); - expect(logMock.firstCall?.args[0].length).toBe(0); - }); - - test('many models in the list', async () => { - const logMock = stubMethod(console, 'table'); - - mkdirSync(join(__dirname, 'test_data', 'models'), { recursive: true }); - writeFileSync( - join(__dirname, 'test_data', 'models', 'test.yaml'), - 'model: test', - 'utf8', - ); - - await CommandTestFactory.run(commandInstance, ['models', 'list']); - expect(logMock.firstCall?.args[0]).toBeInstanceOf(Array); - expect(logMock.firstCall?.args[0].length).toBe(1); - expect(logMock.firstCall?.args[0][0].id).toBe('test'); - }); -}); diff --git a/cortex-js/src/infrastructure/commanders/test/models.command.spec.ts b/cortex-js/src/infrastructure/commanders/test/models.command.spec.ts new file mode 100644 index 000000000..7d512d2be --- /dev/null +++ b/cortex-js/src/infrastructure/commanders/test/models.command.spec.ts @@ -0,0 +1,100 @@ +import { TestingModule } from '@nestjs/testing'; +import { stubMethod } from 'hanbi'; +import { CommandTestFactory } from 'nest-commander-testing'; +import { CommandModule } from '@/command.module'; +import { join } from 'path'; +import { rmSync } from 'fs'; +import { timeout } from '@/infrastructure/commanders/test/helpers.command.spec'; + +let commandInstance: TestingModule; + +beforeEach( + () => + new Promise(async (res) => { + commandInstance = await CommandTestFactory.createTestingCommand({ + imports: [CommandModule], + }) + // .overrideProvider(LogService) + // .useValue({}) + .compile(); + res(); + }), +); + +afterEach( + () => + new Promise(async (res) => { + // Attempt to clean test folder + rmSync(join(__dirname, 'test_data'), { + recursive: true, + force: true, + }); + res(); + }), +); + +export const modelName = 'tinyllama'; +describe('Models list returns array of models', () => { + test('Init with CPU', async () => { + const logMock = stubMethod(console, 'log'); + + logMock.passThrough(); + CommandTestFactory.setAnswers(['CPU', '', 'AVX2']); + + await CommandTestFactory.run(commandInstance, ['init']); + expect(logMock.firstCall?.args[0]).toBe( + 'Downloading engine file windows-amd64-avx2.tar.gz', + ); + }, 50000); + + test('Empty model list', async () => { + const logMock = stubMethod(console, 'table'); + + await CommandTestFactory.run(commandInstance, ['models', 'list']); + expect(logMock.firstCall?.args[0]).toBeInstanceOf(Array); + expect(logMock.firstCall?.args[0].length).toBe(0); + }); + + test( + 'Run model and check with cortex ps', + async () => { + const logMock = stubMethod(console, 'log'); + + await CommandTestFactory.run(commandInstance, ['run', modelName]); + expect(logMock.lastCall?.args[0]).toBe("Inorder to exit, type 'exit()'."); + + const tableMock = stubMethod(console, 'table'); + await CommandTestFactory.run(commandInstance, ['ps']); + expect(tableMock.firstCall?.args[0].length).toBeGreaterThan(0); + }, + timeout, + ); + + test('Get model', async () => { + const logMock = stubMethod(console, 'log'); + + await CommandTestFactory.run(commandInstance, ['models', 'get', modelName]); + expect(logMock.firstCall?.args[0]).toBeInstanceOf(Object); + expect(logMock.firstCall?.args[0].files.length).toBe(1); + }); + + test('Many models in the list', async () => { + const logMock = stubMethod(console, 'table'); + await CommandTestFactory.run(commandInstance, ['models', 'list']); + expect(logMock.firstCall?.args[0]).toBeInstanceOf(Array); + expect(logMock.firstCall?.args[0].length).toBe(1); + expect(logMock.firstCall?.args[0][0].id).toBe(modelName); + }); + + test( + 'Model already exists', + async () => { + const stdoutSpy = stubMethod(process.stdout, 'write'); + const exitSpy = stubMethod(process, 'exit'); + await CommandTestFactory.run(commandInstance, ['pull', modelName]); + expect(stdoutSpy.firstCall?.args[0]).toContain('Model already exists'); + expect(exitSpy.firstCall?.args[0]).toBe(1); + }, + timeout, + ); +});