码桶

发现社区成员的开源项目

main
NodeNanny/test-classify-probe-error.js
test-classify-probe-error.js5.6 KB
'use strict';

// 2026-07-14新增:验证 pool-checker.js 的 classifyProbeError 阶段感知分类逻辑。
// 背景:真机诊断发现 SOCKS5连接超时 卡在 connect 阶段(本机代理正常,远程节点没接通)
// 之前被错误归类成 check_error(本机问题),应该判 down(节点问题)。
// 这个函数不依赖真实 sing-box,直接单测,不需要真机环境。

const assert = require('assert');
const {
  _internal: { classifyProbeError, pickRandomTargets, KNOWN_AUTHENTIC_TARGETS, DEFAULT_AUTHENTIC_SAMPLE_SIZE }
} = require('./core/pool-checker');

let passed = 0;
function test(name, fn) {
  try {
    fn();
    passed++;
    console.log(`  ok - ${name}`);
  } catch (err) {
    console.error(`  FAIL - ${name}`);
    console.error(err);
    process.exitCode = 1;
  }
}

console.log('classifyProbeError 阶段感知分类:');

test('connect阶段超时 -> down(节点连不上,不是本机问题)', () => {
  const err = new Error('SOCKS5连接超时[阶段:connect]');
  assert.strictEqual(classifyProbeError(err), 'down');
});

test('connecting阶段超时 -> check_error(本机连自己sing-box端口都没连上)', () => {
  const err = new Error('SOCKS5连接超时[阶段:connecting]');
  assert.strictEqual(classifyProbeError(err), 'check_error');
});

test('greeting阶段超时 -> check_error(本机sing-box没回应握手)', () => {
  const err = new Error('SOCKS5连接超时[阶段:greeting]');
  assert.strictEqual(classifyProbeError(err), 'check_error');
});

test('没有阶段标记的旧格式消息 -> 兜底按check_error处理', () => {
  const err = new Error('SOCKS5连接超时');
  assert.strictEqual(classifyProbeError(err), 'check_error');
});

test('SOCKS5握手失败(非超时类) -> check_error不变', () => {
  const err = new Error('SOCKS5握手失败(本机代理客户端没起来或配置有误)');
  assert.strictEqual(classifyProbeError(err), 'check_error');
});

test('SOCKS5CONNECT失败 -> down不变(这条链路真的走不通)', () => {
  const err = new Error('SOCKS5CONNECT失败(节点这条链路实际走不通)');
  assert.strictEqual(classifyProbeError(err), 'down');
});

test('HTTP请求超时 -> down不变', () => {
  const err = new Error('HTTP请求超时');
  assert.strictEqual(classifyProbeError(err), 'down');
});

test('连接被对端关闭 -> down不变', () => {
  const err = new Error('连接被对端关闭,且没有收到任何HTTP响应');
  assert.strictEqual(classifyProbeError(err), 'down');
});

test('TLS握手失败 -> down不变', () => {
  const err = new Error('TLS握手失败:unable to verify the first certificate');
  assert.strictEqual(classifyProbeError(err), 'down');
});

test('TLS握手超时 -> down(自查发现,此前会被误判check_error)', () => {
  // 背景:socks-fetch.js里tls.connect()握手迟迟没响应、被本地setTimeout掐断时抛的
  // 是"TLS握手超时",跟tls.connect()真正报错时的"TLS握手失败:xxx"是两条不同的文案,
  // 此前只覆盖了"失败"没覆盖"超时",导致这类节点被错误归类成check_error而不是down,
  // 跟本文件测的SOCKS5连接超时[阶段:connect]是完全相同性质的遗漏。
  const err = new Error('TLS握手超时');
  assert.strictEqual(classifyProbeError(err), 'down');
});

test('目标返回非预期格式响应 -> down不变', () => {
  const err = new Error('目标返回了非预期格式的响应:garbage');
  assert.strictEqual(classifyProbeError(err), 'down');
});

test('完全未知的错误 -> 兜底check_error(宁可错杀检测本身也不错杀节点)', () => {
  const err = new Error('some totally unexpected node internal error');
  assert.strictEqual(classifyProbeError(err), 'check_error');
});

console.log('\n真实性验证目标池 + 随机抽样(2026-07-15新增,交接文档45.9节"检测标准可能被应试"修复):');

test('目标池覆盖了用户常用网站,不再只有三家CDN大厂', () => {
  const urls = Object.keys(KNOWN_AUTHENTIC_TARGETS);
  assert.ok(urls.length >= 7, '目标池应该扩充到至少7个');
  assert.ok(urls.some((u) => u.includes('youtube.com')), '应该包含YouTube');
  assert.ok(urls.some((u) => u.includes('github.com')), '应该包含GitHub');
  assert.ok(urls.some((u) => u.includes('telegram.org')), '应该包含Telegram');
  assert.ok(urls.some((u) => u.includes('claude.ai')), '应该包含Claude');
});

test('pickRandomTargets: 抽样数量正确,且不重复', () => {
  const pool = Object.keys(KNOWN_AUTHENTIC_TARGETS);
  const picked = pickRandomTargets(pool, 3);
  assert.strictEqual(picked.length, 3);
  assert.strictEqual(new Set(picked).size, 3, '抽出来的应该是3个不同的URL,不能重复');
  picked.forEach((u) => assert.ok(pool.includes(u), '抽出来的必须是池子里已有的URL'));
});

test('pickRandomTargets: 请求数量超过池子大小时,不报错、只返回池子全部', () => {
  const pool = ['a', 'b'];
  const picked = pickRandomTargets(pool, 10);
  assert.strictEqual(picked.length, 2);
});

test('pickRandomTargets: 默认抽样数量是3', () => {
  assert.strictEqual(DEFAULT_AUTHENTIC_SAMPLE_SIZE, 3);
});

test('pickRandomTargets: 多次调用结果会变化(证明是真随机,不是每次固定同一批)', () => {
  const pool = Object.keys(KNOWN_AUTHENTIC_TARGETS);
  const samples = new Set();
  for (let i = 0; i < 30; i += 1) {
    samples.add(pickRandomTargets(pool, 3).slice().sort().join(','));
  }
  // 池子有7个,一次抽3个,组合数远大于1,30次里几乎不可能全部抽到同一种组合
  assert.ok(samples.size > 1, '多次抽样应该出现不同的组合,不能每次都一样');
});

console.log(`\n${passed} 项通过`);
if (process.exitCode) {
  console.error('存在失败项');
  process.exit(1);
}