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>

dc06abb364172fd6b8b67f37e6fb488187096926

DeclineThyself <235079501+DeclineThyself@users.noreply.github.com>

Signed
2 files changed, +135 -0Showing whitespace changes
tests/mock-server.test.js+34 -0
@@ -0,0 +1,34 @@
1+import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
2+import { MockServer } from './util/mock-server.js';
3+
4+describe('MockServer tests', () => {
5+ /** @type {MockServer} */
6+ const mockServer = new MockServer({ port: 3000, host: '127.0.0.1' });
7+
8+ beforeAll(async () => {
9+ await mockServer.start();
10+ });
11+
12+ afterAll(async () => {
13+ await mockServer.stop();
14+ });
15+
16+ test('should provide OpenAI-compatible endpoint', async () => {
17+ const requestBody = {
18+ model: 'gpt-4o',
19+ max_tokens: 400,
20+ messages: [
21+ { role: 'user', content: 'Hello, world!' },
22+ ],
23+ };
24+ const response = await fetch('http://127.0.0.1:3000/v1/chat/completions', {
25+ method: 'POST',
26+ headers: { 'Content-Type': 'application/json' },
27+ body: JSON.stringify(requestBody),
28+ });
29+ 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' };
30+ expect(response.status).toBe(200);
31+ const json = await response.json();
32+ expect(json).toEqual(expectedResponse);
33+ });
34+});
tests/util/mock-server.js+101 -0
@@ -0,0 +1,101 @@
1+import http from 'node:http';
2+import { readAllChunks, tryParse } from '../../src/util.js';
3+
4+export class MockServer {
5+ /** @type {string} */
6+ host;
7+ /** @type {number} */
8+ port;
9+ /** @type {import('http').Server} */
10+ server;
11+
12+ /**
13+ * Creates an instance of MockServer.
14+ * @param {object} [param] Options object.
15+ * @param {string} [param.host] The hostname or IP address to bind the server to.
16+ * @param {number} [param.port] The port number to listen on.
17+ */
18+ constructor({ host, port } = {}) {
19+ this.host = host ?? '127.0.0.1';
20+ this.port = port ?? 3000;
21+ }
22+
23+ /**
24+ * Handles Chat Completions requests.
25+ * @param {object} jsonBody The parsed JSON body from the request.
26+ * @returns {object} Mock response object.
27+ */
28+ handleChatCompletions(jsonBody) {
29+ const messages = jsonBody?.messages;
30+ const lastMessage = messages?.[messages.length - 1];
31+ const mockResponse = {
32+ choices: [
33+ {
34+ finish_reason: 'stop',
35+ index: 0,
36+ message: {
37+ role: 'assistant',
38+ reasoning_content: `${jsonBody?.model}\n${messages?.length}\n${jsonBody?.max_tokens}`,
39+ content: String(lastMessage?.content ?? 'No prompt messages.'),
40+ },
41+ },
42+ ],
43+ created: 0,
44+ model: jsonBody?.model,
45+ };
46+ return mockResponse;
47+ }
48+
49+ /**
50+ * Starts the mock server.
51+ * @returns {Promise<void>}
52+ */
53+ async start() {
54+ return new Promise((resolve, reject) => {
55+ this.server = http.createServer(async (req, res) => {
56+ try {
57+ const body = await readAllChunks(req);
58+ const jsonBody = tryParse(body.toString());
59+ if (req.method === 'POST' && req.url === '/v1/chat/completions') {
60+ const mockResponse = this.handleChatCompletions(jsonBody);
61+ res.writeHead(200, { 'Content-Type': 'application/json' });
62+ res.end(JSON.stringify(mockResponse));
63+ } else {
64+ res.writeHead(404);
65+ res.end();
66+ }
67+ } catch (error) {
68+ res.writeHead(500);
69+ res.end();
70+ }
71+ });
72+
73+ this.server.on('error', (err) => {
74+ reject(err);
75+ });
76+
77+ this.server.listen(this.port, this.host, () => {
78+ resolve();
79+ });
80+ });
81+ }
82+
83+ /**
84+ * Stops the mock server.
85+ * @returns {Promise<void>}
86+ */
87+ async stop() {
88+ return new Promise((resolve, reject) => {
89+ if (!this.server) {
90+ return reject(new Error('Server is not running.'));
91+ }
92+ this.server.closeAllConnections();
93+ this.server.close(( /** @type {NodeJS.ErrnoException|undefined} */ err) => {
94+ if (err && (err?.code !== 'ERR_SERVER_NOT_RUNNING')) {
95+ return reject(err);
96+ }
97+ resolve();
98+ });
99+ });
100+ }
101+}