Skip to content
This repository was archived by the owner on Jul 4, 2025. It is now read-only.

Commit 4763c1c

Browse files
Van-QAlouis-janVan-QA
authored
feat: Add testcase for Cortex CLI (#689)
Co-authored-by: Louis <louis@jan.ai> Co-authored-by: Van-QA <van@jan.ai>
1 parent 0cbbf08 commit 4763c1c

File tree

6 files changed

+236
-76
lines changed

6 files changed

+236
-76
lines changed

cortex-js/.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,11 @@ module.exports = {
2121
'@typescript-eslint/explicit-function-return-type': 'off',
2222
'@typescript-eslint/explicit-module-boundary-types': 'off',
2323
'@typescript-eslint/no-explicit-any': 'off',
24+
"prettier/prettier": [
25+
"error",
26+
{
27+
"endOfLine": "auto"
28+
},
29+
],
2430
},
2531
};

cortex-js/src/infrastructure/commanders/serve.command.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,22 @@ export class ServeCommand extends CommandRunner {
2020
const host = options?.host || defaultCortexJsHost;
2121
const port = options?.port || defaultCortexJsPort;
2222

23-
spawn('node', [join(__dirname, '../../main.js')], {
24-
env: {
25-
...process.env,
26-
CORTEX_JS_HOST: host,
27-
CORTEX_JS_PORT: port.toString(),
28-
NODE_ENV: 'production',
23+
spawn(
24+
'node',
25+
process.env.TEST
26+
? [join(__dirname, '../../../dist/src/main.js')]
27+
: [join(__dirname, '../../main.js')],
28+
{
29+
env: {
30+
...process.env,
31+
CORTEX_JS_HOST: host,
32+
CORTEX_JS_PORT: port.toString(),
33+
NODE_ENV: 'production',
34+
},
35+
stdio: 'inherit',
36+
detached: false,
2937
},
30-
stdio: 'inherit',
31-
detached: false,
32-
});
38+
);
3339
}
3440

3541
@Option({
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { TestingModule } from '@nestjs/testing';
2+
import { spy, Stub, stubMethod } from 'hanbi';
3+
import { CommandTestFactory } from 'nest-commander-testing';
4+
import { CommandModule } from '@/command.module';
5+
import { LogService } from '@/infrastructure/commanders/test/log.service';
6+
import axios from 'axios';
7+
8+
let commandInstance: TestingModule,
9+
exitSpy: Stub<typeof process.exit>,
10+
stdoutSpy: Stub<typeof process.stdout.write>,
11+
stderrSpy: Stub<typeof process.stderr.write>;
12+
export const timeout = 500000;
13+
14+
beforeEach(
15+
() =>
16+
new Promise<void>(async (res) => {
17+
stubMethod(process.stderr, 'write');
18+
exitSpy = stubMethod(process, 'exit');
19+
stdoutSpy = stubMethod(process.stdout, 'write');
20+
stderrSpy = stubMethod(process.stderr, 'write');
21+
commandInstance = await CommandTestFactory.createTestingCommand({
22+
imports: [CommandModule],
23+
})
24+
.overrideProvider(LogService)
25+
.useValue({ log: spy().handler })
26+
.compile();
27+
res();
28+
stdoutSpy.reset();
29+
stderrSpy.reset();
30+
}),
31+
);
32+
33+
describe('Helper commands', () => {
34+
test(
35+
'Init with hardware auto detection',
36+
async () => {
37+
await CommandTestFactory.run(commandInstance, ['init', '-s']);
38+
39+
// Wait for a brief period to allow the command to execute
40+
await new Promise((resolve) => setTimeout(resolve, 1000));
41+
42+
expect(stdoutSpy.firstCall?.args.length).toBeGreaterThan(0);
43+
},
44+
timeout,
45+
);
46+
47+
test('Chat with option -m', async () => {
48+
const logMock = stubMethod(console, 'log');
49+
50+
await CommandTestFactory.run(commandInstance, [
51+
'chat',
52+
// '-m',
53+
// 'hello',
54+
// '>output.txt',
55+
]);
56+
expect(logMock.firstCall?.args[0]).toBe("Inorder to exit, type 'exit()'.");
57+
// expect(exitSpy.callCount).toBe(1);
58+
// expect(exitSpy.firstCall?.args[0]).toBe(1);
59+
});
60+
61+
test('Show / kill running models', async () => {
62+
const tableMock = stubMethod(console, 'table');
63+
64+
const logMock = stubMethod(console, 'log');
65+
await CommandTestFactory.run(commandInstance, ['kill']);
66+
await CommandTestFactory.run(commandInstance, ['ps']);
67+
68+
expect(logMock.firstCall?.args[0]).toEqual({
69+
message: 'Cortex stopped successfully',
70+
status: 'success',
71+
});
72+
expect(tableMock.firstCall?.args[0]).toBeInstanceOf(Array);
73+
expect(tableMock.firstCall?.args[0].length).toEqual(0);
74+
});
75+
76+
test('Help command return guideline to users', async () => {
77+
await CommandTestFactory.run(commandInstance, ['-h']);
78+
expect(stdoutSpy.firstCall?.args).toBeInstanceOf(Array);
79+
expect(stdoutSpy.firstCall?.args.length).toBe(1);
80+
expect(stdoutSpy.firstCall?.args[0]).toContain('display help for command');
81+
82+
expect(exitSpy.callCount).toBeGreaterThan(1);
83+
expect(exitSpy.firstCall?.args[0]).toBe(0);
84+
});
85+
86+
test('Should handle missing command', async () => {
87+
await CommandTestFactory.run(commandInstance, ['--unknown']);
88+
expect(stderrSpy.firstCall?.args[0]).toContain('error: unknown option');
89+
expect(stderrSpy.firstCall?.args[0]).toContain('--unknown');
90+
expect(exitSpy.callCount).toBe(1);
91+
expect(exitSpy.firstCall?.args[0]).toBe(1);
92+
});
93+
94+
test('Local API server via localhost:1337/api', async () => {
95+
await CommandTestFactory.run(commandInstance, ['serve']);
96+
97+
// Add a delay of 1000 milliseconds (1 second)
98+
return new Promise<void>(async (resolve) => {
99+
setTimeout(async () => {
100+
// Send a request to the API server to check if it's running
101+
const response = await axios.get('http://localhost:1337/api');
102+
expect(response.status).toBe(200);
103+
resolve();
104+
}, 1000);
105+
});
106+
});
107+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
@Injectable()
4+
export class LogService {
5+
log(...args: any[]): void {
6+
console.log(...args);
7+
}
8+
}

cortex-js/src/infrastructure/commanders/test/model-list.command.spec.ts

Lines changed: 0 additions & 67 deletions
This file was deleted.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { TestingModule } from '@nestjs/testing';
2+
import { stubMethod } from 'hanbi';
3+
import { CommandTestFactory } from 'nest-commander-testing';
4+
import { CommandModule } from '@/command.module';
5+
import { join } from 'path';
6+
import { rmSync } from 'fs';
7+
import { timeout } from '@/infrastructure/commanders/test/helpers.command.spec';
8+
9+
let commandInstance: TestingModule;
10+
11+
beforeEach(
12+
() =>
13+
new Promise<void>(async (res) => {
14+
commandInstance = await CommandTestFactory.createTestingCommand({
15+
imports: [CommandModule],
16+
})
17+
// .overrideProvider(LogService)
18+
// .useValue({})
19+
.compile();
20+
res();
21+
}),
22+
);
23+
24+
afterEach(
25+
() =>
26+
new Promise<void>(async (res) => {
27+
// Attempt to clean test folder
28+
rmSync(join(__dirname, 'test_data'), {
29+
recursive: true,
30+
force: true,
31+
});
32+
res();
33+
}),
34+
);
35+
36+
export const modelName = 'tinyllama';
37+
describe('Models list returns array of models', () => {
38+
test('Init with CPU', async () => {
39+
const logMock = stubMethod(console, 'log');
40+
41+
logMock.passThrough();
42+
CommandTestFactory.setAnswers(['CPU', '', 'AVX2']);
43+
44+
await CommandTestFactory.run(commandInstance, ['init']);
45+
expect(logMock.firstCall?.args[0]).toBe(
46+
'Downloading engine file windows-amd64-avx2.tar.gz',
47+
);
48+
}, 50000);
49+
50+
test('Empty model list', async () => {
51+
const logMock = stubMethod(console, 'table');
52+
53+
await CommandTestFactory.run(commandInstance, ['models', 'list']);
54+
expect(logMock.firstCall?.args[0]).toBeInstanceOf(Array);
55+
expect(logMock.firstCall?.args[0].length).toBe(0);
56+
});
57+
58+
test(
59+
'Run model and check with cortex ps',
60+
async () => {
61+
const logMock = stubMethod(console, 'log');
62+
63+
await CommandTestFactory.run(commandInstance, ['run', modelName]);
64+
expect(logMock.lastCall?.args[0]).toBe("Inorder to exit, type 'exit()'.");
65+
66+
const tableMock = stubMethod(console, 'table');
67+
await CommandTestFactory.run(commandInstance, ['ps']);
68+
expect(tableMock.firstCall?.args[0].length).toBeGreaterThan(0);
69+
},
70+
timeout,
71+
);
72+
73+
test('Get model', async () => {
74+
const logMock = stubMethod(console, 'log');
75+
76+
await CommandTestFactory.run(commandInstance, ['models', 'get', modelName]);
77+
expect(logMock.firstCall?.args[0]).toBeInstanceOf(Object);
78+
expect(logMock.firstCall?.args[0].files.length).toBe(1);
79+
});
80+
81+
test('Many models in the list', async () => {
82+
const logMock = stubMethod(console, 'table');
83+
await CommandTestFactory.run(commandInstance, ['models', 'list']);
84+
expect(logMock.firstCall?.args[0]).toBeInstanceOf(Array);
85+
expect(logMock.firstCall?.args[0].length).toBe(1);
86+
expect(logMock.firstCall?.args[0][0].id).toBe(modelName);
87+
});
88+
89+
test(
90+
'Model already exists',
91+
async () => {
92+
const stdoutSpy = stubMethod(process.stdout, 'write');
93+
const exitSpy = stubMethod(process, 'exit');
94+
await CommandTestFactory.run(commandInstance, ['pull', modelName]);
95+
expect(stdoutSpy.firstCall?.args[0]).toContain('Model already exists');
96+
expect(exitSpy.firstCall?.args[0]).toBe(1);
97+
},
98+
timeout,
99+
);
100+
});

0 commit comments

Comments
 (0)