Files
mihomo-patchbay/shared/outbound-chain.test.ts
T
im0o e9e2eb555d 每路入站配一组专属策略组,换线不再改配置文件
原本换一条线要改 config.yaml、跑校验、等内核重载,几秒起步;而换线恰恰是这个工具
最高频的动作,把它放在最重的路径上方向就错了。现在每一路配一个 select 组加 AUTO、
FALLBACK 两个,入站的 proxy 永久指向 select,换线只是调一次内核接口。这样做的前提是
选中不会丢:内核把它写进 cache 文件,重启后由 patchSelectGroup 恢复,而
profile.store-selected 默认为真。

链式代理靠给来源订阅复制一份声明、挂上 override.dialer-proxy 实现,因为这个字段写在
策略组上内核只会打条日志然后忽略。副本是 type: file 读原订阅已经落盘的缓存,而不是
自己再拉一次:自己拉的话订阅地址一不可达副本就是空的,那一路入站会一条线都不剩,
表现是端口还在监听却没有任何出口。

顺带把 shared/outbound-chain.ts 里「只有 Selector 接受手动选择」那条断言删了,它是
错的(内核检查的是 SelectAble 接口,URLTest 和 Fallback 也实现了)。界面仍然只让
Selector 可点,但理由换成了另外两种的手动指定会被它们自己撤销。

完整取舍见工作区决策记录 0007。
2026-08-11 03:10:38 +08:00

86 lines
3.6 KiB
TypeScript
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.
import { describe, expect, it } from 'bun:test'
import { 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 上', () => {
// 链条是 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('整条链都不是 Selector 时返回空', () => {
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()
})
})