import { createServer } from 'node:http' import type { IncomingMessage, Server, ServerResponse } from 'node:http' /** One scripted behavior for the next request the mock server receives. */ export type Behavior = | { kind: 'sse'; events: string[]; delayMs?: number } | { kind: 'http-error'; status: number; body: string; contentType?: string; headers?: Record } | { kind: 'close-early'; events: string[] } export interface MockServer { url: string /** Bodies of received requests, in order. */ requests: unknown[] /** Header bags of received requests, in order (parallel to `requests`). */ headers: IncomingMessage['headers'][] script: Behavior[] close(): Promise } const servers: Server[] = [] /** Close every server opened since the last call; run from each spec's afterEach. */ export async function closeMockServers(): Promise { await Promise.all(servers.splice(0).map(server => new Promise(resolve => server.close(resolve)))) } /** A minimal complete text generation, reused by request-shape assertions. */ export const textEvents = [ '{"choices":[{"delta":{"role":"assistant","content":null,"reasoning_content":""}}]}', '{"choices":[{"delta":{"content":"hello"}}]}', '{"choices":[{"delta":{"content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}', '[DONE]', ] /** Local chat-completions stand-in: replays scripted behaviors per request. */ export async function mockServer(script: Behavior[]): Promise { const requests: unknown[] = [] const headers: IncomingMessage['headers'][] = [] const server = createServer((request: IncomingMessage, response: ServerResponse) => { let body = '' request.on('data', (chunk: Buffer) => { body += chunk.toString('utf8') }) request.on('end', () => { requests.push(JSON.parse(body)) headers.push(request.headers) const behavior = script.shift() if (!behavior) { response.writeHead(500).end('mock script exhausted') return } if (behavior.kind === 'http-error') { response.writeHead(behavior.status, { 'content-type': behavior.contentType ?? 'application/json', ...behavior.headers, }) response.end(behavior.body) return } response.writeHead(200, { 'content-type': 'text/event-stream' }) const write = (index: number): void => { if (index >= behavior.events.length) { if (behavior.kind === 'sse') response.end() else response.destroy() // close-early: drop the socket mid-stream return } response.write(`data: ${behavior.events[index]}\n\n`) setTimeout(() => { write(index + 1) }, behavior.kind === 'sse' ? behavior.delayMs ?? 0 : 5) } write(0) }) }) servers.push(server) await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) const address = server.address() if (address === null || typeof address === 'string') throw new Error('no port') return { url: `http://127.0.0.1:${address.port}`, requests, headers, script, close: () => new Promise(resolve => server.close(() => { resolve() })), } }