原来的规则会落在 AUTO 上,而 AUTO 是 url-test 类型——它按延迟自己挑,手动选它没有 意义,点完会被下一轮延迟测试算回去。内核里只有 Selector 是「由人来选」的 (constant/adapters.go:240-248 列了全部五种),URLTest 挑最快、Fallback 挑第一个可用、 LoadBalance 分摊,Relay 已被移除。 链条上现在每一跳都标出类型(PROXY Selector → AUTO URLTest → 线路 Vless),并明说 自动决定的那几层不接受手动指定——不说清楚的话,用户看到链条里有个组却点不了, 只会以为是坏了。
110 lines
4.5 KiB
TypeScript
110 lines
4.5 KiB
TypeScript
import { describe, expect, it } from 'bun:test'
|
|
import { acceptsManualChoice, autoDecidedHops, lastSwitchableGroup, resolveChain } from './outbound-chain.ts'
|
|
import type { RuntimeNode } from './outbound-chain.ts'
|
|
|
|
/**
|
|
* 仿照真实那台机器:PROXY 手动选,它选中了按延迟自动挑的 AUTO,AUTO 挑了一条订阅线路。
|
|
* PROXY 的成员是手写的 AUTO、DIRECT 加上订阅带进来的两条线路。
|
|
*/
|
|
const runtime: Record<string, RuntimeNode> = {
|
|
PROXY: { type: 'Selector', now: 'AUTO', all: ['AUTO', 'DIRECT', '[vmess]DMIT', '[vless]Gomami'] },
|
|
AUTO: { type: 'URLTest', now: '[vless]Gomami', all: ['[vmess]DMIT', '[vless]Gomami'] },
|
|
'[vless]Gomami': { type: 'Vless' },
|
|
'[vmess]DMIT': { type: 'Vmess' },
|
|
DIRECT: { type: 'Direct' },
|
|
}
|
|
|
|
describe('解析流量最终走到哪条线', () => {
|
|
it('一路跟着当前选中走到底', () => {
|
|
expect(resolveChain(runtime, 'PROXY')).toEqual(['PROXY', 'AUTO', '[vless]Gomami'])
|
|
})
|
|
|
|
it('起点就是线路时链条只有它自己', () => {
|
|
expect(resolveChain(runtime, '[vless]Gomami')).toEqual(['[vless]Gomami'])
|
|
})
|
|
|
|
it('没有落脚点时是空链条', () => {
|
|
expect(resolveChain(runtime, null)).toEqual([])
|
|
})
|
|
|
|
it('落脚在一个内核不认识的名字上时也只返回它自己', () => {
|
|
// 配置引用了一个不存在的出站时会走到这里,界面要能显示而不是崩掉
|
|
expect(resolveChain(runtime, '不存在的组')).toEqual(['不存在的组'])
|
|
})
|
|
|
|
it('策略组互相引用成环时停下来,不会转死', () => {
|
|
const looped: Record<string, RuntimeNode> = {
|
|
A: { type: 'Selector', now: 'B', all: ['B'] },
|
|
B: { type: 'Selector', now: 'A', all: ['A'] },
|
|
}
|
|
expect(resolveChain(looped, 'A')).toEqual(['A', 'B'])
|
|
})
|
|
})
|
|
|
|
describe('哪种策略组接受手动选择', () => {
|
|
it('只有 Selector', () => {
|
|
expect(acceptsManualChoice('Selector')).toBe(true)
|
|
})
|
|
|
|
it('自动决定的那几种都不接受', () => {
|
|
// 内核里 URLTest 按延迟挑、Fallback 挑第一个可用、LoadBalance 分摊,
|
|
// 它们的选中结果是算出来的,点了会被算回去
|
|
for (const type of ['URLTest', 'Fallback', 'LoadBalance', 'Relay']) {
|
|
expect(acceptsManualChoice(type)).toBe(false)
|
|
}
|
|
expect(acceptsManualChoice(undefined)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('找出用户能换选中的那一层', () => {
|
|
it('跳过自动决定的组,落在最后一个 Selector 上', () => {
|
|
// 链条是 PROXY(Selector) → AUTO(URLTest) → 线路。
|
|
// 能点的是 PROXY,不是 AUTO——让人去点 URLTest 的成员,点完会被延迟测试算回去。
|
|
const chain = resolveChain(runtime, 'PROXY')
|
|
expect(lastSwitchableGroup(runtime, chain)).toEqual({
|
|
group: 'PROXY',
|
|
members: ['AUTO', 'DIRECT', '[vmess]DMIT', '[vless]Gomami'],
|
|
now: 'AUTO',
|
|
})
|
|
})
|
|
|
|
it('有多个 Selector 时取最靠近线路的那个', () => {
|
|
// 越靠近线路越接近「换线路」这个意图;前面的层换的是「走哪套策略」
|
|
const nested: Record<string, RuntimeNode> = {
|
|
外层: { type: 'Selector', now: '内层', all: ['内层'] },
|
|
内层: { type: 'Selector', now: '线路A', all: ['线路A', '线路B'] },
|
|
线路A: { type: 'Vmess' },
|
|
}
|
|
expect(lastSwitchableGroup(nested, resolveChain(nested, '外层'))?.group).toBe('内层')
|
|
})
|
|
|
|
it('整条链都是自动决定时返回空', () => {
|
|
const auto: Record<string, RuntimeNode> = {
|
|
AUTO: { type: 'URLTest', now: '线路A', all: ['线路A'] },
|
|
线路A: { type: 'Vmess' },
|
|
}
|
|
expect(lastSwitchableGroup(auto, resolveChain(auto, 'AUTO'))).toBeNull()
|
|
})
|
|
|
|
it('链条上一个策略组都没有时返回空', () => {
|
|
expect(lastSwitchableGroup(runtime, ['[vless]Gomami'])).toBeNull()
|
|
expect(lastSwitchableGroup(runtime, [])).toBeNull()
|
|
})
|
|
|
|
it('Selector 存在但成员是空的,不算可切换', () => {
|
|
// 筛选条件没匹配到任何线路时会出现这种组,给出一个点不动的列表只会让人困惑
|
|
expect(lastSwitchableGroup({ 空组: { type: 'Selector', all: [], now: '' } }, ['空组'])).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('哪几层是自动决定的', () => {
|
|
it('列出来,界面用它解释为什么这一层点不了', () => {
|
|
const chain = resolveChain(runtime, 'PROXY')
|
|
expect(autoDecidedHops(runtime, chain)).toEqual([{ name: 'AUTO', type: 'URLTest' }])
|
|
})
|
|
|
|
it('线路本身不算——它不是组,本来就没得选', () => {
|
|
expect(autoDecidedHops(runtime, ['[vless]Gomami'])).toEqual([])
|
|
})
|
|
})
|