mirror of
https://github.com/7836246/cursor2api.git
synced 2026-05-07 06:08:27 +08:00
- 新增 proxy-agent.ts: 基于 undici.ProxyAgent 让 Node.js fetch 走代理 - cursor-client.ts / vision.ts: 接入代理 dispatcher - config.yaml: 补充代理配置说明 (含认证格式) - 新增 16 个单元测试覆盖代理模块逻辑 - 版本升级至 v2.5.4
244 lines
9.6 KiB
JavaScript
244 lines
9.6 KiB
JavaScript
/**
|
||
* test/unit-proxy-agent.mjs
|
||
*
|
||
* 单元测试:proxy-agent 代理模块
|
||
* 运行方式:node test/unit-proxy-agent.mjs
|
||
*
|
||
* 测试逻辑均为纯内联实现,不依赖 dist 编译产物。
|
||
* 验证:
|
||
* 1. 无代理时 getProxyFetchOptions 返回空对象
|
||
* 2. 有代理时返回含 dispatcher 的对象
|
||
* 3. ProxyAgent 缓存(单例)
|
||
* 4. 各种代理 URL 格式支持
|
||
*/
|
||
|
||
// ─── 测试框架 ──────────────────────────────────────────────────────────
|
||
let passed = 0;
|
||
let failed = 0;
|
||
|
||
function test(name, fn) {
|
||
try {
|
||
fn();
|
||
console.log(` ✅ ${name}`);
|
||
passed++;
|
||
} catch (e) {
|
||
console.error(` ❌ ${name}`);
|
||
console.error(` ${e.message}`);
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
function assert(condition, msg) {
|
||
if (!condition) throw new Error(msg || 'Assertion failed');
|
||
}
|
||
|
||
function assertEqual(a, b, msg) {
|
||
const as = JSON.stringify(a), bs = JSON.stringify(b);
|
||
if (as !== bs) throw new Error(msg || `Expected ${bs}, got ${as}`);
|
||
}
|
||
|
||
// ─── 内联 mock 实现(模拟 proxy-agent.ts 核心逻辑,不依赖 dist)──────
|
||
|
||
// 模拟 config
|
||
let mockConfig = {};
|
||
|
||
function getConfig() {
|
||
return mockConfig;
|
||
}
|
||
|
||
// 模拟 ProxyAgent(轻量级)
|
||
class MockProxyAgent {
|
||
constructor(url) {
|
||
this.url = url;
|
||
this.type = 'ProxyAgent';
|
||
}
|
||
}
|
||
|
||
// 内联与 src/proxy-agent.ts 同逻辑的实现
|
||
let cachedAgent = undefined;
|
||
|
||
function resetCache() {
|
||
cachedAgent = undefined;
|
||
}
|
||
|
||
function getProxyDispatcher() {
|
||
const config = getConfig();
|
||
const proxyUrl = config.proxy;
|
||
|
||
if (!proxyUrl) return undefined;
|
||
|
||
if (!cachedAgent) {
|
||
cachedAgent = new MockProxyAgent(proxyUrl);
|
||
}
|
||
|
||
return cachedAgent;
|
||
}
|
||
|
||
function getProxyFetchOptions() {
|
||
const dispatcher = getProxyDispatcher();
|
||
return dispatcher ? { dispatcher } : {};
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// 1. 无代理配置 → 返回空对象
|
||
// ════════════════════════════════════════════════════════════════════
|
||
console.log('\n📦 [1] 无代理配置\n');
|
||
|
||
test('proxy 未设置时返回空对象', () => {
|
||
resetCache();
|
||
mockConfig = {};
|
||
const opts = getProxyFetchOptions();
|
||
assertEqual(Object.keys(opts).length, 0, '应返回空对象');
|
||
});
|
||
|
||
test('proxy 为 undefined 时返回空对象', () => {
|
||
resetCache();
|
||
mockConfig = { proxy: undefined };
|
||
const opts = getProxyFetchOptions();
|
||
assertEqual(Object.keys(opts).length, 0);
|
||
});
|
||
|
||
test('proxy 为空字符串时返回空对象', () => {
|
||
resetCache();
|
||
mockConfig = { proxy: '' };
|
||
const opts = getProxyFetchOptions();
|
||
assertEqual(Object.keys(opts).length, 0, '空字符串不应创建代理');
|
||
});
|
||
|
||
test('getProxyDispatcher 无代理时返回 undefined', () => {
|
||
resetCache();
|
||
mockConfig = {};
|
||
const d = getProxyDispatcher();
|
||
assertEqual(d, undefined);
|
||
});
|
||
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// 2. 有代理配置 → 返回含 dispatcher 的对象
|
||
// ════════════════════════════════════════════════════════════════════
|
||
console.log('\n📦 [2] 有代理配置\n');
|
||
|
||
test('设置 proxy 后返回含 dispatcher 的对象', () => {
|
||
resetCache();
|
||
mockConfig = { proxy: 'http://127.0.0.1:7890' };
|
||
const opts = getProxyFetchOptions();
|
||
assert(opts.dispatcher !== undefined, '应包含 dispatcher');
|
||
assert(opts.dispatcher instanceof MockProxyAgent, '应为 ProxyAgent 实例');
|
||
});
|
||
|
||
test('dispatcher 包含正确的代理 URL', () => {
|
||
resetCache();
|
||
mockConfig = { proxy: 'http://127.0.0.1:7890' };
|
||
const d = getProxyDispatcher();
|
||
assertEqual(d.url, 'http://127.0.0.1:7890');
|
||
});
|
||
|
||
test('带认证的代理 URL', () => {
|
||
resetCache();
|
||
mockConfig = { proxy: 'http://user:pass@proxy.corp.com:8080' };
|
||
const d = getProxyDispatcher();
|
||
assertEqual(d.url, 'http://user:pass@proxy.corp.com:8080');
|
||
});
|
||
|
||
test('HTTPS 代理 URL', () => {
|
||
resetCache();
|
||
mockConfig = { proxy: 'https://secure-proxy.corp.com:443' };
|
||
const d = getProxyDispatcher();
|
||
assertEqual(d.url, 'https://secure-proxy.corp.com:443');
|
||
});
|
||
|
||
test('带特殊字符密码的代理 URL', () => {
|
||
resetCache();
|
||
const url = 'http://admin:p%40ssw0rd@proxy:8080';
|
||
mockConfig = { proxy: url };
|
||
const d = getProxyDispatcher();
|
||
assertEqual(d.url, url, '应原样保留 URL 编码的特殊字符');
|
||
});
|
||
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// 3. 缓存(单例)行为
|
||
// ════════════════════════════════════════════════════════════════════
|
||
console.log('\n📦 [3] 缓存单例行为\n');
|
||
|
||
test('多次调用返回同一 ProxyAgent 实例', () => {
|
||
resetCache();
|
||
mockConfig = { proxy: 'http://127.0.0.1:7890' };
|
||
const d1 = getProxyDispatcher();
|
||
const d2 = getProxyDispatcher();
|
||
assert(d1 === d2, '应返回同一个缓存实例');
|
||
});
|
||
|
||
test('getProxyFetchOptions 多次调用复用同一 dispatcher', () => {
|
||
resetCache();
|
||
mockConfig = { proxy: 'http://127.0.0.1:7890' };
|
||
const opts1 = getProxyFetchOptions();
|
||
const opts2 = getProxyFetchOptions();
|
||
assert(opts1.dispatcher === opts2.dispatcher, 'dispatcher 应为同一实例');
|
||
});
|
||
|
||
test('重置缓存后创建新实例', () => {
|
||
resetCache();
|
||
mockConfig = { proxy: 'http://127.0.0.1:7890' };
|
||
const d1 = getProxyDispatcher();
|
||
resetCache();
|
||
mockConfig = { proxy: 'http://10.0.0.1:3128' };
|
||
const d2 = getProxyDispatcher();
|
||
assert(d1 !== d2, '重置后应创建新实例');
|
||
assertEqual(d2.url, 'http://10.0.0.1:3128');
|
||
});
|
||
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// 4. fetch options 展开语义验证
|
||
// ════════════════════════════════════════════════════════════════════
|
||
console.log('\n📦 [4] fetch options 展开语义\n');
|
||
|
||
test('无代理时展开不影响原始 options', () => {
|
||
resetCache();
|
||
mockConfig = {};
|
||
const original = { method: 'POST', headers: { 'Content-Type': 'application/json' } };
|
||
const merged = { ...original, ...getProxyFetchOptions() };
|
||
assertEqual(merged.method, 'POST');
|
||
assertEqual(merged.headers['Content-Type'], 'application/json');
|
||
assert(merged.dispatcher === undefined, '不应添加 dispatcher');
|
||
});
|
||
|
||
test('有代理时展开插入 dispatcher 且不覆盖其他字段', () => {
|
||
resetCache();
|
||
mockConfig = { proxy: 'http://127.0.0.1:7890' };
|
||
const original = { method: 'POST', body: '{}', signal: 'test-signal' };
|
||
const merged = { ...original, ...getProxyFetchOptions() };
|
||
assertEqual(merged.method, 'POST');
|
||
assertEqual(merged.body, '{}');
|
||
assertEqual(merged.signal, 'test-signal');
|
||
assert(merged.dispatcher instanceof MockProxyAgent, '应包含 dispatcher');
|
||
});
|
||
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// 5. config.ts 集成验证(环境变量优先级)
|
||
// ════════════════════════════════════════════════════════════════════
|
||
console.log('\n📦 [5] config 环境变量集成验证\n');
|
||
|
||
test('PROXY 环境变量应覆盖 config.yaml(逻辑验证)', () => {
|
||
// 模拟 config.ts 的覆盖逻辑:env > yaml
|
||
let config = { proxy: 'http://yaml-proxy:1234' };
|
||
const envProxy = 'http://env-proxy:5678';
|
||
// 模拟 config.ts 第 49 行逻辑
|
||
if (envProxy) config.proxy = envProxy;
|
||
assertEqual(config.proxy, 'http://env-proxy:5678', 'PROXY 环境变量应覆盖 yaml 配置');
|
||
});
|
||
|
||
test('PROXY 环境变量未设置时保持 yaml 值(逻辑验证)', () => {
|
||
let config = { proxy: 'http://yaml-proxy:1234' };
|
||
const envProxy = undefined;
|
||
if (envProxy) config.proxy = envProxy;
|
||
assertEqual(config.proxy, 'http://yaml-proxy:1234', '应保持 yaml 配置不变');
|
||
});
|
||
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// 汇总
|
||
// ════════════════════════════════════════════════════════════════════
|
||
console.log('\n' + '═'.repeat(55));
|
||
console.log(` 结果: ${passed} 通过 / ${failed} 失败 / ${passed + failed} 总计`);
|
||
console.log('═'.repeat(55) + '\n');
|
||
|
||
if (failed > 0) process.exit(1);
|