fix(llm-mock-server): harden fault boundaries

This commit is contained in:
Yichen Jiang
2026-07-25 22:45:58 +08:00
parent 5446714177
commit badf7d1c63
10 changed files with 109 additions and 31 deletions

View File

@@ -110,6 +110,9 @@ describe('mock LLM server CLI parser', () => {
[['--sequence', 'unknown'], /unknown behavior/],
[['--sequence', 'connection_refused,success', '--port', '0'], /nonzero/],
[['--sequence', 'success', '--listen-delay-ms', '5'], /requires connection_refused/],
[['--sequence', 'connection_refused,success', '--listen-delay-ms', '-1'], /integer between 0 and 2147483647/],
[['--sequence', 'connection_refused,success', '--listen-delay-ms', '1.5'], /integer between 0 and 2147483647/],
[['--sequence', 'connection_refused,success', '--listen-delay-ms', '2147483648'], /integer between 0 and 2147483647/],
[['--sequence', 'success', '--seed', '1'], /require random/],
[['--sequence', 'random', '--random-weights', 'success'], /expects behavior=weight/],
[['--sequence', 'random', '--random-weights', 'random=1'], /concrete behavior/],

View File

@@ -1,3 +1,4 @@
import { request } from 'node:http'
import { afterEach, describe, expect, it } from 'vitest'
import type { MockLlmBehavior, MockLlmServer, MockLlmServerEvent } from '../src/index.ts'
import { startMockLlmServer } from '../src/index.ts'
@@ -32,6 +33,22 @@ function chat(
})
}
function rawChat(server: MockLlmServer, chunks: readonly Buffer[]): Promise<void> {
return new Promise((resolve, reject) => {
const outgoing = request(`${server.baseURL}/v1/chat/completions`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
}, (response) => {
response.once('error', reject)
response.once('end', resolve)
response.resume()
})
outgoing.once('error', reject)
for (const chunk of chunks) outgoing.write(chunk)
outgoing.end()
})
}
describe('mock LLM server wire behaviors', () => {
it('streams a complete text response and captures the request', async () => {
const events: MockLlmServerEvent[] = []
@@ -151,10 +168,12 @@ describe('mock LLM server wire behaviors', () => {
['stream_disconnect', 100] as const,
['partial_disconnect', 100] as const,
])('records a client that closes during %s', async (behavior, delayMs) => {
const events: MockLlmServerEvent[] = []
const server = await start([behavior], {
chunkDelayMs: delayMs,
disconnectDelayMs: delayMs,
chunkSize: 1,
onEvent: (event) => { events.push(event) },
})
const controller = new AbortController()
const response = await chat(server, { signal: controller.signal })
@@ -163,6 +182,30 @@ describe('mock LLM server wire behaviors', () => {
await new Promise((resolve) => { setTimeout(resolve, 5) })
expect(server.requests[0]).toMatchObject({ behavior, outcome: 'client_closed' })
expect(events.filter(event => event.type === 'result')).toEqual([
expect.objectContaining({ behavior, outcome: 'client_closed' }),
])
})
it('preserves UTF-8 code points split across request chunks', async () => {
const server = await start(['success'])
const encoded = Buffer.from(JSON.stringify({ messages: [{ role: 'user', content: '你好' }] }))
const characterOffset = encoded.indexOf(Buffer.from('你'))
expect(characterOffset).toBeGreaterThanOrEqual(0)
await rawChat(server, [
encoded.subarray(0, characterOffset + 1),
encoded.subarray(characterOffset + 1),
])
expect(server.requests[0]?.body).toEqual({ messages: [{ role: 'user', content: '你好' }] })
})
it('formats an IPv6 listener as a valid base URL', async () => {
const server = await start(['success'], { host: '::1' })
expect(server.baseURL).toMatch(/^http:\/\/\[::1\]:\d+$/)
expect((await chat(server)).status).toBe(200)
})
it('emits reasoning, tool calls, max-token finishes, slow chunks, and a wrong content type', async () => {