码桶
发现社区成员的开源项目
test-pool-events.js8.6 KB
'use strict';
// core/pool-events.js 测试。
// 跟test-starmap-data.js对pool.json的处理方式一致:测试前备份真实的
// data/pool-events.json(如果存在),测试结束后原样还原,不污染真实数据。
//
// 本轮修改(复查发现recordRound此前从没做过知识库匹配,跟前端public/pool-events.html
// 已经假设r.kb字段存在的实现对不上,已在core/pool-events.js里补上):recordRound()
// 现在是async函数(内部要await kb-manager.matchCode),这个测试文件相应地把所有
// recordRound调用都改成await,并把整个测试主体包进一个async函数里执行;
// 同时新增几条断言验证kb字段确实被正确写入/不写入。
const assert = require('assert');
const fs = require('fs');
const path = require('path');
let pass = 0, fail = 0;
function check(cond, msg) {
if (cond) { pass++; } else { fail++; console.error('FAIL:', msg); }
}
const DATA_DIR = path.join(__dirname, 'data');
const EVENTS_FILE = path.join(DATA_DIR, 'pool-events.json');
const backupPath = EVENTS_FILE + '.backup-before-pool-events-test';
let hadOriginal = false;
if (fs.existsSync(EVENTS_FILE)) {
hadOriginal = true;
fs.copyFileSync(EVENTS_FILE, backupPath);
}
function restore() {
if (hadOriginal) {
fs.copyFileSync(backupPath, EVENTS_FILE);
fs.unlinkSync(backupPath);
} else if (fs.existsSync(EVENTS_FILE)) {
fs.unlinkSync(EVENTS_FILE);
}
}
async function main() {
if (fs.existsSync(EVENTS_FILE)) fs.unlinkSync(EVENTS_FILE); // 每个测试文件独立从干净状态开始
delete require.cache[require.resolve('./core/pool-events')];
const poolEvents = require('./core/pool-events');
// 真实的vless/vmess示例链接,跟proxy-parse.js/pool-checker.js测试里用的风格一致。
const vlessLink = 'vless://[email protected]:47286?encryption=none&security=reality&flow=xtls-rprx-vision&type=tcp&sni=www.cloudflare.com&pbk=abc&fp=chrome#test-node-1';
const badLink = 'not-a-valid-link-at-all';
// 1. 空文件初始状态
check(poolEvents.getRecentRounds().length === 0, '初始状态应该没有任何轮次');
// 2. 记录一轮,candidates和checkResults一一对应
await poolEvents.recordRound('manual:test-source', [vlessLink, badLink], [
{ outcome: 'ok', code: 'POOL_AUTHENTIC_OK', detail: '不应该被存进去(ok的不存detail)' },
{ outcome: 'down', code: 'POOL_ALIVE_BAD_STATUS_CODE', detail: '连通性探测返回状态码 403' }
]);
let rounds = poolEvents.getRecentRounds();
check(rounds.length === 1, '记录一轮之后应该有1轮');
check(rounds[0].sourceId === 'manual:test-source', 'sourceId应该正确记录');
check(rounds[0].candidateCount === 2, 'candidateCount应该是2');
check(rounds[0].passedCount === 1, 'passedCount应该正确统计ok的数量(1个)');
check(rounds[0].results.length === 2, 'results数组长度应该跟candidates一致');
const okResult = rounds[0].results[0];
check(okResult.outcome === 'ok', '第一个结果应该是ok');
check(okResult.protocol === 'vless', '能从vless链接正确解析出协议类型');
check(okResult.host === '1.2.3.4', '能从链接正确解析出host');
check(okResult.port === 47286, '能从链接正确解析出port');
check(!('detail' in okResult), 'ok的结果不应该存detail字段(减少体积)');
check(!('kb' in okResult), 'ok的结果不应该做知识库匹配、也不应该存kb字段(避免点开显示无意义的"无匹配词条")');
check(!('link' in okResult) && !('key' in okResult), '不应该存原始链接本身(避免泄露UUID/密码等凭证)');
const downResult = rounds[0].results[1];
check(downResult.outcome === 'down', '第二个结果应该是down');
check(downResult.code === 'POOL_ALIVE_BAD_STATUS_CODE', 'down的结果应该带code');
check(downResult.detail === '连通性探测返回状态码 403', 'down的结果应该保留detail');
check(downResult.protocol === null && downResult.host === null, '解析失败的链接应该安全返回null字段,不抛异常');
check(Array.isArray(downResult.kb), 'down的结果应该带kb字段(数组,即使没匹配到也应该是空数组而不是undefined)');
check(downResult.kb.length > 0, 'POOL_ALIVE_BAD_STATUS_CODE在知识库里有对应词条,应该匹配到至少一条');
if (downResult.kb.length > 0) {
const hit = downResult.kb[0];
check(typeof hit.id === 'string', 'kb命中条目应该带id字段');
check(hit.explanation && typeof hit.explanation === 'object', 'kb命中条目应该带explanation字段(多语言对象)');
}
// 3. 空数组/参数异常不应该记录任何东西,也不应该抛异常
await poolEvents.recordRound('manual:empty', [], []);
check(poolEvents.getRecentRounds().length === 1, '空candidates数组不应该新增轮次');
await poolEvents.recordRound('manual:bad-args', null, undefined);
check(poolEvents.getRecentRounds().length === 1, '非法参数不应该新增轮次、也不应该抛异常');
// 4. 新轮次插入在最前面(最新的在前)
await poolEvents.recordRound('aggregator-default', [vlessLink], [{ outcome: 'ok', code: 'POOL_AUTHENTIC_OK' }]);
rounds = poolEvents.getRecentRounds();
check(rounds.length === 2, '再记一轮之后应该有2轮');
check(rounds[0].sourceId === 'aggregator-default', '最新的一轮应该排在最前面');
check(rounds[1].sourceId === 'manual:test-source', '旧的一轮应该往后排');
// 4b. 同一批候选里出现相同code的多条down结果,即使knowledge-base.matchCode
// 内部有60秒防刷冷却,靠"轮次时间+候选序号"拼出来的contextKey也应该保证
// 每一条都能各自独立匹配到,不会因为冷却互相挤掉。
await poolEvents.recordRound('manual:same-code-batch', [badLink, badLink, badLink], [
{ outcome: 'down', code: 'POOL_ALIVE_BAD_STATUS_CODE', detail: '第1条' },
{ outcome: 'down', code: 'POOL_ALIVE_BAD_STATUS_CODE', detail: '第2条' },
{ outcome: 'down', code: 'POOL_ALIVE_BAD_STATUS_CODE', detail: '第3条' }
]);
const sameCodeRound = poolEvents.getRecentRounds()[0];
check(sameCodeRound.sourceId === 'manual:same-code-batch', '刚记录的这一轮应该在最前面');
const allHaveKb = sameCodeRound.results.every((r) => Array.isArray(r.kb) && r.kb.length > 0);
check(allHaveKb, '同一轮里多条相同code的down结果应该各自都匹配到kb,不应该被冷却机制互相挤掉导致只有第一条有');
// 5. 超过MAX_ROUNDS上限时自动裁剪,保留最新的
for (let i = 0; i < poolEvents._internal.MAX_ROUNDS + 5; i++) {
await poolEvents.recordRound(`source-${i}`, [vlessLink], [{ outcome: 'ok', code: 'POOL_AUTHENTIC_OK' }]);
}
rounds = poolEvents.getRecentRounds();
check(rounds.length === poolEvents._internal.MAX_ROUNDS, `超过上限后应该裁剪到${poolEvents._internal.MAX_ROUNDS}轮,实际${rounds.length}轮`);
check(rounds[0].sourceId === `source-${poolEvents._internal.MAX_ROUNDS + 4}`, '裁剪后应该保留最新的那些轮次');
// 6. getRecentRounds支持limit参数
const limited = poolEvents.getRecentRounds(3);
check(limited.length === 3, 'limit参数应该正确生效');
// 7. describeLink对畸形/不支持的输入不抛异常
const desc = poolEvents._internal.describeLink('');
check(desc.protocol === null && desc.host === null && desc.port === null, '空字符串输入应该安全返回null字段');
// 8. 文件损坏时能自愈,不影响主流程
fs.writeFileSync(EVENTS_FILE, '{ this is not valid json');
const roundsAfterCorruption = poolEvents.getRecentRounds();
check(Array.isArray(roundsAfterCorruption) && roundsAfterCorruption.length === 0, '数据文件损坏时应该自愈重置为空数组,不抛异常');
await poolEvents.recordRound('manual:after-corruption', [vlessLink], [{ outcome: 'ok', code: 'POOL_AUTHENTIC_OK' }]);
check(poolEvents.getRecentRounds().length === 1, '自愈之后应该能正常继续记录');
// 9. 不存在的code不应该抛异常,应该安全返回空kb数组
await poolEvents.recordRound('manual:unknown-code', [badLink], [{ outcome: 'down', code: 'THIS_CODE_DOES_NOT_EXIST_IN_KB', detail: '测试用不存在的code' }]);
const unknownRound = poolEvents.getRecentRounds()[0];
check(Array.isArray(unknownRound.results[0].kb) && unknownRound.results[0].kb.length === 0, '知识库里没有对应词条的code应该安全返回空数组,不抛异常');
}
main()
.catch((err) => {
fail++;
console.error('测试过程中出现意外异常:', err);
})
.finally(() => {
restore();
console.log(`\n共 ${pass + fail} 项断言,通过 ${pass} 项,失败 ${fail} 项。`);
process.exit(fail > 0 ? 1 : 0);
});