Added MockServer class for tests. (#4843)
* Added a mock OpenAI-compatible endpoint at /v1/chat/completions. Disabled by default. Intended for debugging and e2e tests. * Fixed empty prompts. * Add mock server and example test * Improve test * Added `eslint-plugin-playwright` * Removed `Date.now()` for reproducible responses. * Ignore ERR_SERVER_NOT_RUNNING on close. * Use MockServer in mock-openai.js * Fixed error check. * Removed mock server. * Fix test name --------- Co-authored-by: user <user@exmaple.com> Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
|
||||
import { MockServer } from './util/mock-server.js';
|
||||
|
||||
describe('MockServer tests', () => {
|
||||
/** @type {MockServer} */
|
||||
const mockServer = new MockServer({ port: 3000, host: '127.0.0.1' });
|
||||
|
||||
beforeAll(async () => {
|
||||
await mockServer.start();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mockServer.stop();
|
||||
});
|
||||
|
||||
test('should provide OpenAI-compatible endpoint', async () => {
|
||||
const requestBody = {
|
||||
model: 'gpt-4o',
|
||||
max_tokens: 400,
|
||||
messages: [
|
||||
{ role: 'user', content: 'Hello, world!' },
|
||||
],
|
||||
};
|
||||
const response = await fetch('http://127.0.0.1:3000/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
const expectedResponse = { 'choices': [{ 'finish_reason': 'stop', 'index': 0, 'message': { 'role': 'assistant', 'reasoning_content': 'gpt-4o\n1\n400', 'content': 'Hello, world!' } }], 'created': 0, 'model': 'gpt-4o' };
|
||||
expect(response.status).toBe(200);
|
||||
const json = await response.json();
|
||||
expect(json).toEqual(expectedResponse);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import http from 'node:http';
|
||||
import { readAllChunks, tryParse } from '../../src/util.js';
|
||||
|
||||
export class MockServer {
|
||||
/** @type {string} */
|
||||
host;
|
||||
/** @type {number} */
|
||||
port;
|
||||
/** @type {import('http').Server} */
|
||||
server;
|
||||
|
||||
/**
|
||||
* Creates an instance of MockServer.
|
||||
* @param {object} [param] Options object.
|
||||
* @param {string} [param.host] The hostname or IP address to bind the server to.
|
||||
* @param {number} [param.port] The port number to listen on.
|
||||
*/
|
||||
constructor({ host, port } = {}) {
|
||||
this.host = host ?? '127.0.0.1';
|
||||
this.port = port ?? 3000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Chat Completions requests.
|
||||
* @param {object} jsonBody The parsed JSON body from the request.
|
||||
* @returns {object} Mock response object.
|
||||
*/
|
||||
handleChatCompletions(jsonBody) {
|
||||
const messages = jsonBody?.messages;
|
||||
const lastMessage = messages?.[messages.length - 1];
|
||||
const mockResponse = {
|
||||
choices: [
|
||||
{
|
||||
finish_reason: 'stop',
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
reasoning_content: `${jsonBody?.model}\n${messages?.length}\n${jsonBody?.max_tokens}`,
|
||||
content: String(lastMessage?.content ?? 'No prompt messages.'),
|
||||
},
|
||||
},
|
||||
],
|
||||
created: 0,
|
||||
model: jsonBody?.model,
|
||||
};
|
||||
return mockResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the mock server.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async start() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const body = await readAllChunks(req);
|
||||
const jsonBody = tryParse(body.toString());
|
||||
if (req.method === 'POST' && req.url === '/v1/chat/completions') {
|
||||
const mockResponse = this.handleChatCompletions(jsonBody);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(mockResponse));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
} catch (error) {
|
||||
res.writeHead(500);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
this.server.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.server.listen(this.port, this.host, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the mock server.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async stop() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.server) {
|
||||
return reject(new Error('Server is not running.'));
|
||||
}
|
||||
this.server.closeAllConnections();
|
||||
this.server.close(( /** @type {NodeJS.ErrnoException|undefined} */ err) => {
|
||||
if (err && (err?.code !== 'ERR_SERVER_NOT_RUNNING')) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user