From dc06abb364172fd6b8b67f37e6fb488187096926 Mon Sep 17 00:00:00 2001 From: DeclineThyself <235079501+DeclineThyself@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:20:25 +0000 Subject: [PATCH] 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 Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com> --- tests/mock-server.test.js | 34 +++++++++++++ tests/util/mock-server.js | 101 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 tests/mock-server.test.js create mode 100644 tests/util/mock-server.js diff --git a/tests/mock-server.test.js b/tests/mock-server.test.js new file mode 100644 index 000000000..eba5b9926 --- /dev/null +++ b/tests/mock-server.test.js @@ -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); + }); +}); diff --git a/tests/util/mock-server.js b/tests/util/mock-server.js new file mode 100644 index 000000000..564ac04d3 --- /dev/null +++ b/tests/util/mock-server.js @@ -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} + */ + 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} + */ + 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(); + }); + }); + } +}