mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
* test(e2e): make fake OpenAI reachable from docker sandbox * test(e2e): harden fake OpenAI server routing
250 lines
6.6 KiB
TypeScript
250 lines
6.6 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
import {
|
|
fakeToolCall,
|
|
startFakeOpenAIServer,
|
|
type FakeOpenAIServer,
|
|
} from './fake-openai-server.js';
|
|
|
|
type StreamToolCallDelta = {
|
|
index: number;
|
|
id?: string;
|
|
type?: string;
|
|
function?: {
|
|
name?: string;
|
|
arguments?: string;
|
|
};
|
|
};
|
|
|
|
type StreamChunk = {
|
|
choices: Array<{
|
|
delta: {
|
|
tool_calls?: StreamToolCallDelta[];
|
|
};
|
|
}>;
|
|
};
|
|
|
|
let server: FakeOpenAIServer | undefined;
|
|
|
|
afterEach(async () => {
|
|
await server?.close();
|
|
server = undefined;
|
|
});
|
|
|
|
describe('fake OpenAI server', () => {
|
|
it('serves non-streaming and streaming chat completions', async () => {
|
|
server = await startFakeOpenAIServer(({ requestIndex }) =>
|
|
requestIndex === 0
|
|
? { content: 'hello from fake model' }
|
|
: {
|
|
toolCalls: [
|
|
fakeToolCall('write_file', {
|
|
file_path: '/tmp/fake.txt',
|
|
content: 'fake',
|
|
}),
|
|
],
|
|
},
|
|
);
|
|
|
|
const nonStreaming = await fetch(`${server.baseUrl}/chat/completions`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: 'fake-model',
|
|
messages: [{ role: 'user', content: 'hi' }],
|
|
}),
|
|
});
|
|
expect(nonStreaming.status).toBe(200);
|
|
await expect(nonStreaming.json()).resolves.toMatchObject({
|
|
choices: [
|
|
{
|
|
message: {
|
|
role: 'assistant',
|
|
content: 'hello from fake model',
|
|
},
|
|
finish_reason: 'stop',
|
|
},
|
|
],
|
|
});
|
|
|
|
const streaming = await fetch(`${server.baseUrl}/chat/completions`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: 'fake-model',
|
|
stream: true,
|
|
messages: [{ role: 'user', content: 'write' }],
|
|
}),
|
|
});
|
|
expect(streaming.status).toBe(200);
|
|
const streamText = await streaming.text();
|
|
expect(streamText).toContain('"tool_calls"');
|
|
expect(streamText).toContain('"write_file"');
|
|
expect(streamText).toContain('data: [DONE]');
|
|
expect(server.requests).toHaveLength(2);
|
|
});
|
|
|
|
it('serves non-streaming tool calls with null content', async () => {
|
|
server = await startFakeOpenAIServer(() => ({
|
|
toolCalls: [
|
|
fakeToolCall('write_file', {
|
|
file_path: '/tmp/fake.txt',
|
|
content: 'fake',
|
|
}),
|
|
],
|
|
}));
|
|
|
|
const response = await fetch(`${server.baseUrl}/chat/completions`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: 'fake-model',
|
|
messages: [{ role: 'user', content: 'use a tool' }],
|
|
}),
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
await expect(response.json()).resolves.toMatchObject({
|
|
choices: [
|
|
{
|
|
message: {
|
|
content: null,
|
|
tool_calls: [{ function: { name: 'write_file' } }],
|
|
},
|
|
finish_reason: 'tool_calls',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('streams tool call arguments as deltas', async () => {
|
|
server = await startFakeOpenAIServer(() => ({
|
|
toolCalls: [
|
|
fakeToolCall(
|
|
'write_file',
|
|
{
|
|
file_path: '/tmp/fake.txt',
|
|
content: 'fake',
|
|
},
|
|
'call_fixed',
|
|
),
|
|
],
|
|
}));
|
|
|
|
const response = await fetch(`${server.baseUrl}/chat/completions`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: 'fake-model',
|
|
stream: true,
|
|
messages: [{ role: 'user', content: 'write' }],
|
|
}),
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
const toolCallDeltas = (await response.text())
|
|
.split('\n\n')
|
|
.filter((line) => line.startsWith('data: ') && line !== 'data: [DONE]')
|
|
.map((line) => JSON.parse(line.slice('data: '.length)) as StreamChunk)
|
|
.flatMap((chunk) => chunk.choices[0]?.delta.tool_calls ?? []);
|
|
expect(toolCallDeltas).toEqual([
|
|
{
|
|
index: 0,
|
|
id: 'call_fixed',
|
|
type: 'function',
|
|
function: { name: 'write_file', arguments: '' },
|
|
},
|
|
{
|
|
index: 0,
|
|
function: {
|
|
arguments: '{"file_path":"/tmp/fake.txt","content":"fake"}',
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('returns 404 for wrong methods or paths', async () => {
|
|
server = await startFakeOpenAIServer(() => ({ content: 'unused' }));
|
|
|
|
const response = await fetch(`${server.baseUrl}/chat/completions`, {
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('returns 400 for malformed JSON bodies', async () => {
|
|
server = await startFakeOpenAIServer(() => ({ content: 'unused' }));
|
|
|
|
const response = await fetch(`${server.baseUrl}/chat/completions`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: 'not json',
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
});
|
|
|
|
it('rejects oversized request bodies', async () => {
|
|
let handled = false;
|
|
server = await startFakeOpenAIServer(() => {
|
|
handled = true;
|
|
return { content: 'unused' };
|
|
});
|
|
|
|
const response = await fetch(`${server.baseUrl}/chat/completions`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: 'x'.repeat(10 * 1024 * 1024 + 1),
|
|
});
|
|
|
|
expect(response.status).toBe(413);
|
|
expect(handled).toBe(false);
|
|
});
|
|
|
|
it('returns 500 without exposing handler error details', async () => {
|
|
server = await startFakeOpenAIServer(() => {
|
|
throw new Error('secret stack detail');
|
|
});
|
|
|
|
const response = await fetch(`${server.baseUrl}/chat/completions`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: 'fake-model',
|
|
messages: [{ role: 'user', content: 'hi' }],
|
|
}),
|
|
});
|
|
|
|
expect(response.status).toBe(500);
|
|
await expect(response.json()).resolves.toMatchObject({
|
|
error: {
|
|
message: 'fake OpenAI server handler failed',
|
|
type: 'server_error',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('closes the response when streaming fails after headers are sent', async () => {
|
|
server = await startFakeOpenAIServer(() => ({
|
|
content: 1n as unknown as string,
|
|
}));
|
|
|
|
await expect(
|
|
fetch(`${server.baseUrl}/chat/completions`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: 'fake-model',
|
|
stream: true,
|
|
messages: [{ role: 'user', content: 'hi' }],
|
|
}),
|
|
}),
|
|
).rejects.toThrow();
|
|
});
|
|
});
|