mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-04 14:13:13 +00:00
196 lines
6.5 KiB
TypeScript
196 lines
6.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as http from 'node:http';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { getGlobalDispatcher } from 'undici';
|
|
import {
|
|
closeOpenAICompatProxyServer,
|
|
startOpenAICompatProxyServer,
|
|
} from '../../../src/proxy/server/proxy-server';
|
|
import type { OpenAICompatProfileConfig } from '../../../src/proxy/profile-router';
|
|
|
|
const PROXY_ENV_KEYS = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY'] as const;
|
|
|
|
function listen(server: http.Server): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
const address = server.address();
|
|
if (!address || typeof address === 'string') {
|
|
reject(new Error('Failed to resolve listening port'));
|
|
return;
|
|
}
|
|
resolve(address.port);
|
|
});
|
|
});
|
|
}
|
|
|
|
function requestMessages(port: number): Promise<{ status: number; body: string }> {
|
|
return new Promise((resolve, reject) => {
|
|
const req = http.request(
|
|
{
|
|
hostname: '127.0.0.1',
|
|
port,
|
|
path: '/v1/messages',
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'x-api-key': 'sentinel-local-token',
|
|
},
|
|
},
|
|
(res) => {
|
|
let body = '';
|
|
res.setEncoding('utf8');
|
|
res.on('data', (chunk) => (body += chunk));
|
|
res.on('end', () => resolve({ status: res.statusCode ?? 0, body }));
|
|
}
|
|
);
|
|
req.once('error', reject);
|
|
req.end(
|
|
JSON.stringify({
|
|
model: 'sentinel-model',
|
|
messages: [{ role: 'user', content: 'hi' }],
|
|
})
|
|
);
|
|
});
|
|
}
|
|
|
|
describe('runtime-aware upstream transport', () => {
|
|
const originalEnv = new Map<string, string | undefined>();
|
|
let ccsHome = '';
|
|
let corporateProxy: http.Server | undefined;
|
|
let corporateProxyHits = 0;
|
|
let sawProxyAuthorization = false;
|
|
let proxyServer: http.Server | undefined;
|
|
|
|
beforeEach(() => {
|
|
for (const key of PROXY_ENV_KEYS) {
|
|
originalEnv.set(key, process.env[key]);
|
|
delete process.env[key];
|
|
}
|
|
originalEnv.set('CCS_HOME', process.env.CCS_HOME);
|
|
ccsHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-runtime-transport-'));
|
|
process.env.CCS_HOME = ccsHome;
|
|
corporateProxy = undefined;
|
|
proxyServer = undefined;
|
|
corporateProxyHits = 0;
|
|
sawProxyAuthorization = false;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (proxyServer) {
|
|
await closeOpenAICompatProxyServer(proxyServer, 50);
|
|
}
|
|
if (corporateProxy) {
|
|
await new Promise<void>((resolve) => corporateProxy.close(() => resolve()));
|
|
}
|
|
for (const key of [...PROXY_ENV_KEYS, 'CCS_HOME'] as const) {
|
|
const value = originalEnv.get(key);
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
fs.rmSync(ccsHome, { recursive: true, force: true });
|
|
});
|
|
|
|
it('uses a credentialed proxy and honors NO_PROXY without global fetch mutation', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const upstreamPayload = JSON.stringify({
|
|
id: 'chatcmpl_proxy',
|
|
model: 'sentinel-model',
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message: { role: 'assistant', content: 'proxied-ok' },
|
|
finish_reason: 'stop',
|
|
},
|
|
],
|
|
usage: { prompt_tokens: 1, completion_tokens: 1 },
|
|
});
|
|
corporateProxy = http.createServer((req, res) => {
|
|
corporateProxyHits += 1;
|
|
sawProxyAuthorization = String(req.headers['proxy-authorization'] || '').startsWith('Basic ');
|
|
res.writeHead(200, {
|
|
'content-type': 'application/json',
|
|
'content-length': Buffer.byteLength(upstreamPayload),
|
|
});
|
|
res.end(upstreamPayload);
|
|
});
|
|
const corporateProxyPort = await listen(corporateProxy);
|
|
process.env.HTTP_PROXY = `http://sentinel-user:sentinel-password@127.0.0.1:${corporateProxyPort}`;
|
|
|
|
const profile: OpenAICompatProfileConfig = {
|
|
profileName: 'sentinel',
|
|
settingsPath: '/tmp/sentinel.settings.json',
|
|
baseUrl: 'http://upstream.invalid/v1',
|
|
apiKey: 'sentinel-api-key-not-real',
|
|
provider: 'generic-chat-completion-api',
|
|
model: 'sentinel-model',
|
|
};
|
|
proxyServer = startOpenAICompatProxyServer({
|
|
profile,
|
|
port: 0,
|
|
authToken: 'sentinel-local-token',
|
|
});
|
|
const proxyPort = await new Promise<number>((resolve, reject) => {
|
|
proxyServer.once('error', reject);
|
|
proxyServer.once('listening', () => {
|
|
const address = proxyServer.address();
|
|
if (!address || typeof address === 'string') reject(new Error('Missing proxy port'));
|
|
else resolve(address.port);
|
|
});
|
|
});
|
|
|
|
const proxied = await requestMessages(proxyPort);
|
|
expect(proxied.status).toBe(200);
|
|
expect(JSON.parse(proxied.body)).toMatchObject({
|
|
content: [{ type: 'text', text: 'proxied-ok' }],
|
|
});
|
|
expect(corporateProxyHits).toBe(1);
|
|
expect(sawProxyAuthorization).toBe(true);
|
|
|
|
process.env.NO_PROXY = 'upstream.invalid';
|
|
const hitsBeforeBypass = corporateProxyHits;
|
|
const bypassed = await requestMessages(proxyPort);
|
|
expect(bypassed.status).toBe(502);
|
|
expect(corporateProxyHits).toBe(hitsBeforeBypass);
|
|
expect(globalThis.fetch).toBe(originalFetch);
|
|
});
|
|
|
|
it('keeps global transport ownership unchanged across overlapping server lifecycles', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalDispatcher = getGlobalDispatcher();
|
|
const profile: OpenAICompatProfileConfig = {
|
|
profileName: 'sentinel',
|
|
settingsPath: '/tmp/sentinel.settings.json',
|
|
baseUrl: 'http://127.0.0.1:1/v1',
|
|
apiKey: 'sentinel-api-key-not-real',
|
|
provider: 'generic-chat-completion-api',
|
|
model: 'sentinel-model',
|
|
};
|
|
const serverA = startOpenAICompatProxyServer({
|
|
profile,
|
|
port: 0,
|
|
authToken: 'sentinel-local-token-a',
|
|
});
|
|
const serverB = startOpenAICompatProxyServer({
|
|
profile,
|
|
port: 0,
|
|
authToken: 'sentinel-local-token-b',
|
|
});
|
|
|
|
await Promise.all([
|
|
new Promise<void>((resolve) => serverA.once('listening', resolve)),
|
|
new Promise<void>((resolve) => serverB.once('listening', resolve)),
|
|
]);
|
|
await closeOpenAICompatProxyServer(serverA, 50);
|
|
expect(serverB.listening).toBe(true);
|
|
expect(globalThis.fetch).toBe(originalFetch);
|
|
expect(getGlobalDispatcher()).toBe(originalDispatcher);
|
|
|
|
await closeOpenAICompatProxyServer(serverB, 50);
|
|
expect(globalThis.fetch).toBe(originalFetch);
|
|
expect(getGlobalDispatcher()).toBe(originalDispatcher);
|
|
});
|
|
});
|