mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
fix(test): stabilize proxy integration server ports
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import getPort from 'get-port';
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as os from 'os';
|
||||
@@ -16,6 +15,29 @@ let tempDir: string;
|
||||
let originalTimeoutEnv: string | undefined;
|
||||
let originalCcsHome: string | undefined;
|
||||
|
||||
function resolveListeningPort(server: http.Server): number {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
throw new Error('Failed to resolve server port');
|
||||
}
|
||||
return address.port;
|
||||
}
|
||||
|
||||
async function waitForServerListening(server: http.Server): Promise<number> {
|
||||
if (server.listening) {
|
||||
return resolveListeningPort(server);
|
||||
}
|
||||
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
const onError = (error: Error) => reject(error);
|
||||
server.once('error', onError);
|
||||
server.once('listening', () => {
|
||||
server.off('error', onError);
|
||||
resolve(resolveListeningPort(server));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function startUpstream(
|
||||
handler: (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void> | void
|
||||
): Promise<void> {
|
||||
@@ -28,9 +50,8 @@ async function startUpstream(
|
||||
upstreamSockets.delete(socket);
|
||||
});
|
||||
});
|
||||
await new Promise<void>((resolve) =>
|
||||
upstreamServer.listen(upstreamPort, '127.0.0.1', () => resolve())
|
||||
);
|
||||
upstreamServer.listen(0, '127.0.0.1');
|
||||
upstreamPort = await waitForServerListening(upstreamServer);
|
||||
}
|
||||
|
||||
async function requestProxy(payload: unknown, signal?: AbortSignal): Promise<Response> {
|
||||
@@ -46,8 +67,6 @@ async function requestProxy(payload: unknown, signal?: AbortSignal): Promise<Res
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
upstreamPort = await getPort();
|
||||
proxyPort = await getPort();
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-proxy-edge-'));
|
||||
originalTimeoutEnv = process.env.CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS;
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
@@ -94,9 +113,10 @@ describe('openai proxy message edge cases', () => {
|
||||
};
|
||||
proxyServer = startOpenAICompatProxyServer({
|
||||
profile,
|
||||
port: proxyPort,
|
||||
port: 0,
|
||||
authToken: 'test-proxy-token',
|
||||
});
|
||||
proxyPort = await waitForServerListening(proxyServer);
|
||||
}
|
||||
|
||||
it('preserves rate-limit errors from the upstream provider', async () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import getPort from 'get-port';
|
||||
import * as http from 'http';
|
||||
import { startOpenAICompatProxyServer } from '../../../src/proxy/server/proxy-server';
|
||||
import type { OpenAICompatProfileConfig } from '../../../src/proxy/profile-router';
|
||||
@@ -10,52 +9,64 @@ let upstreamBody: unknown;
|
||||
let upstreamPort: number;
|
||||
let proxyPort: number;
|
||||
|
||||
function startMockUpstream(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
upstreamServer = http.createServer(async (req, res) => {
|
||||
if (req.method !== 'POST' || req.url !== '/v1/chat/completions') {
|
||||
res.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
function resolveListeningPort(server: http.Server): number {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
throw new Error('Failed to resolve server port');
|
||||
}
|
||||
return address.port;
|
||||
}
|
||||
|
||||
let body = '';
|
||||
for await (const chunk of req) {
|
||||
body += chunk.toString();
|
||||
}
|
||||
upstreamBody = JSON.parse(body);
|
||||
const parsed = upstreamBody as {
|
||||
stream?: boolean;
|
||||
messages?: Array<{ role?: string; content?: string | Array<{ type?: string; text?: string }> }>;
|
||||
};
|
||||
async function waitForServerListening(server: http.Server): Promise<number> {
|
||||
if (server.listening) {
|
||||
return resolveListeningPort(server);
|
||||
}
|
||||
|
||||
if (parsed.stream) {
|
||||
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
const onError = (error: Error) => reject(error);
|
||||
server.once('error', onError);
|
||||
server.once('listening', () => {
|
||||
server.off('error', onError);
|
||||
resolve(resolveListeningPort(server));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (parsed.messages?.[0]?.content === 'interleaved tool fragments') {
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"search"}}]}}]}\n\n'
|
||||
);
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_2","type":"function","function":{"name":"open"}}]}}]}\n\n'
|
||||
);
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"path\\":\\"a.ts\\"}"}}]}}]}\n\n'
|
||||
);
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\\"path\\":\\"b.ts\\"}"}}]}}]}\n\n'
|
||||
);
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":9,"completion_tokens":4}}\n\n'
|
||||
);
|
||||
res.end('data: [DONE]\n\n');
|
||||
return;
|
||||
}
|
||||
async function startMockUpstream(): Promise<void> {
|
||||
upstreamServer = http.createServer(async (req, res) => {
|
||||
if (req.method !== 'POST' || req.url !== '/v1/chat/completions') {
|
||||
res.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
|
||||
let body = '';
|
||||
for await (const chunk of req) {
|
||||
body += chunk.toString();
|
||||
}
|
||||
upstreamBody = JSON.parse(body);
|
||||
const parsed = upstreamBody as {
|
||||
stream?: boolean;
|
||||
messages?: Array<{
|
||||
role?: string;
|
||||
content?: string | Array<{ type?: string; text?: string }>;
|
||||
}>;
|
||||
};
|
||||
|
||||
if (parsed.stream) {
|
||||
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
||||
|
||||
if (parsed.messages?.[0]?.content === 'interleaved tool fragments') {
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"}}]}\n\n'
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"search"}}]}}]}\n\n'
|
||||
);
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"search","arguments":"{\\"q\\":\\"docs\\"}"}}]}}]}\n\n'
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_2","type":"function","function":{"name":"open"}}]}}]}\n\n'
|
||||
);
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"path\\":\\"a.ts\\"}"}}]}}]}\n\n'
|
||||
);
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\\"path\\":\\"b.ts\\"}"}}]}}]}\n\n'
|
||||
);
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":9,"completion_tokens":4}}\n\n'
|
||||
@@ -64,25 +75,38 @@ function startMockUpstream(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
id: 'chatcmpl_1',
|
||||
model: 'hf-model',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: 'assistant', content: 'Plain answer' },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 2, completion_tokens: 3 },
|
||||
})
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"}}]}\n\n'
|
||||
);
|
||||
});
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"search","arguments":"{\\"q\\":\\"docs\\"}"}}]}}]}\n\n'
|
||||
);
|
||||
res.write(
|
||||
'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":9,"completion_tokens":4}}\n\n'
|
||||
);
|
||||
res.end('data: [DONE]\n\n');
|
||||
return;
|
||||
}
|
||||
|
||||
upstreamServer.listen(upstreamPort, '127.0.0.1', () => resolve());
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
id: 'chatcmpl_1',
|
||||
model: 'hf-model',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: 'assistant', content: 'Plain answer' },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 2, completion_tokens: 3 },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
upstreamServer.listen(0, '127.0.0.1');
|
||||
upstreamPort = await waitForServerListening(upstreamServer);
|
||||
}
|
||||
|
||||
async function requestProxy(payload: unknown): Promise<Response> {
|
||||
@@ -98,8 +122,6 @@ async function requestProxy(payload: unknown): Promise<Response> {
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
upstreamPort = await getPort();
|
||||
proxyPort = await getPort();
|
||||
upstreamBody = undefined;
|
||||
await startMockUpstream();
|
||||
const profile: OpenAICompatProfileConfig = {
|
||||
@@ -112,9 +134,10 @@ beforeEach(async () => {
|
||||
};
|
||||
proxyServer = startOpenAICompatProxyServer({
|
||||
profile,
|
||||
port: proxyPort,
|
||||
port: 0,
|
||||
authToken: 'test-proxy-token',
|
||||
});
|
||||
proxyPort = await waitForServerListening(proxyServer);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import getPort from 'get-port';
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as os from 'os';
|
||||
@@ -13,40 +12,61 @@ let proxyServer: http.Server;
|
||||
let upstreamServers: http.Server[] = [];
|
||||
let proxyPort: number;
|
||||
|
||||
function startMockUpstream(
|
||||
port: number,
|
||||
function resolveListeningPort(server: http.Server): number {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
throw new Error('Failed to resolve server port');
|
||||
}
|
||||
return address.port;
|
||||
}
|
||||
|
||||
async function waitForServerListening(server: http.Server): Promise<number> {
|
||||
if (server.listening) {
|
||||
return resolveListeningPort(server);
|
||||
}
|
||||
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
const onError = (error: Error) => reject(error);
|
||||
server.once('error', onError);
|
||||
server.once('listening', () => {
|
||||
server.off('error', onError);
|
||||
resolve(resolveListeningPort(server));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function startMockUpstream(
|
||||
hitLabel: string,
|
||||
hits: string[],
|
||||
bodies: Array<{ label: string; body: unknown }>
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
let body = '';
|
||||
for await (const chunk of req) {
|
||||
body += chunk.toString();
|
||||
}
|
||||
hits.push(hitLabel);
|
||||
bodies.push({ label: hitLabel, body: JSON.parse(body) });
|
||||
): Promise<number> {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
let body = '';
|
||||
for await (const chunk of req) {
|
||||
body += chunk.toString();
|
||||
}
|
||||
hits.push(hitLabel);
|
||||
bodies.push({ label: hitLabel, body: JSON.parse(body) });
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
id: `chatcmpl_${hitLabel}`,
|
||||
model: hitLabel,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: 'assistant', content: `Reply from ${hitLabel}` },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 2, completion_tokens: 3 },
|
||||
})
|
||||
);
|
||||
});
|
||||
upstreamServers.push(server);
|
||||
server.listen(port, '127.0.0.1', () => resolve());
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
id: `chatcmpl_${hitLabel}`,
|
||||
model: hitLabel,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: 'assistant', content: `Reply from ${hitLabel}` },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 2, completion_tokens: 3 },
|
||||
})
|
||||
);
|
||||
});
|
||||
upstreamServers.push(server);
|
||||
server.listen(0, '127.0.0.1');
|
||||
return waitForServerListening(server);
|
||||
}
|
||||
|
||||
function writeSettings(profileName: string, env: Record<string, string>): string {
|
||||
@@ -71,7 +91,7 @@ beforeEach(async () => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-proxy-routing-'));
|
||||
fs.mkdirSync(path.join(tempDir, '.ccs'), { recursive: true });
|
||||
process.env.CCS_HOME = tempDir;
|
||||
proxyPort = await getPort();
|
||||
proxyPort = 0;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -97,12 +117,10 @@ afterEach(async () => {
|
||||
|
||||
describe('openai proxy request routing', () => {
|
||||
it('routes explicit profile:model selectors to the matching upstream profile', async () => {
|
||||
const primaryPort = await getPort();
|
||||
const secondaryPort = await getPort();
|
||||
const hits: string[] = [];
|
||||
const bodies: Array<{ label: string; body: unknown }> = [];
|
||||
await startMockUpstream(primaryPort, 'primary', hits, bodies);
|
||||
await startMockUpstream(secondaryPort, 'secondary', hits, bodies);
|
||||
const primaryPort = await startMockUpstream('primary', hits, bodies);
|
||||
const secondaryPort = await startMockUpstream('secondary', hits, bodies);
|
||||
|
||||
const primarySettings = writeSettings('hf', {
|
||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${primaryPort}`,
|
||||
@@ -133,9 +151,10 @@ describe('openai proxy request routing', () => {
|
||||
};
|
||||
proxyServer = startOpenAICompatProxyServer({
|
||||
profile,
|
||||
port: proxyPort,
|
||||
port: 0,
|
||||
authToken: 'test-proxy-token',
|
||||
});
|
||||
proxyPort = await waitForServerListening(proxyServer);
|
||||
|
||||
const response = await requestProxy({
|
||||
model: 'deepseek:deepseek-reasoner',
|
||||
@@ -150,12 +169,10 @@ describe('openai proxy request routing', () => {
|
||||
});
|
||||
|
||||
it('routes thinking requests through the configured think scenario', async () => {
|
||||
const primaryPort = await getPort();
|
||||
const thinkPort = await getPort();
|
||||
const hits: string[] = [];
|
||||
const bodies: Array<{ label: string; body: unknown }> = [];
|
||||
await startMockUpstream(primaryPort, 'primary', hits, bodies);
|
||||
await startMockUpstream(thinkPort, 'thinker', hits, bodies);
|
||||
const primaryPort = await startMockUpstream('primary', hits, bodies);
|
||||
const thinkPort = await startMockUpstream('thinker', hits, bodies);
|
||||
|
||||
const primarySettings = writeSettings('hf', {
|
||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${primaryPort}`,
|
||||
@@ -197,9 +214,10 @@ describe('openai proxy request routing', () => {
|
||||
};
|
||||
proxyServer = startOpenAICompatProxyServer({
|
||||
profile,
|
||||
port: proxyPort,
|
||||
port: 0,
|
||||
authToken: 'test-proxy-token',
|
||||
});
|
||||
proxyPort = await waitForServerListening(proxyServer);
|
||||
|
||||
const response = await requestProxy({
|
||||
model: 'hf-default',
|
||||
@@ -216,12 +234,10 @@ describe('openai proxy request routing', () => {
|
||||
});
|
||||
|
||||
it('routes adaptive thinking requests through the configured think scenario', async () => {
|
||||
const primaryPort = await getPort();
|
||||
const thinkPort = await getPort();
|
||||
const hits: string[] = [];
|
||||
const bodies: Array<{ label: string; body: unknown }> = [];
|
||||
await startMockUpstream(primaryPort, 'primary', hits, bodies);
|
||||
await startMockUpstream(thinkPort, 'thinker', hits, bodies);
|
||||
const primaryPort = await startMockUpstream('primary', hits, bodies);
|
||||
const thinkPort = await startMockUpstream('thinker', hits, bodies);
|
||||
|
||||
const primarySettings = writeSettings('hf', {
|
||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${primaryPort}`,
|
||||
@@ -263,9 +279,10 @@ describe('openai proxy request routing', () => {
|
||||
};
|
||||
proxyServer = startOpenAICompatProxyServer({
|
||||
profile,
|
||||
port: proxyPort,
|
||||
port: 0,
|
||||
authToken: 'test-proxy-token',
|
||||
});
|
||||
proxyPort = await waitForServerListening(proxyServer);
|
||||
|
||||
const response = await requestProxy({
|
||||
model: 'hf-default',
|
||||
|
||||
Reference in New Issue
Block a user