Files
cursor2api/test/unit-proxy-agent.mjs
小海 13098fb84d feat: 内网代理支持 (Issue #17)
- 新增 proxy-agent.ts: 基于 undici.ProxyAgent 让 Node.js fetch 走代理
- cursor-client.ts / vision.ts: 接入代理 dispatcher
- config.yaml: 补充代理配置说明 (含认证格式)
- 新增 16 个单元测试覆盖代理模块逻辑
- 版本升级至 v2.5.4
2026-03-11 14:42:17 +08:00

244 lines
9.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);