订阅健康检查默认开启,编辑支持改名

内核没有默认的健康检查地址,url 为空时检查根本不会运行
(adapter/provider/healthcheck.go:196-199),所以默认值必须写全;
开关只翻 enable,手写的 url/interval 留着下次开启时还用。
改名和策略组 use: 的引用同步收在同一次变换里,链式副本由保存
链路的 syncGenerated 按新名字重建,缓存路径刻意不动——file 类型
的数据本体和副本认亲都靠它。
This commit is contained in:
2026-08-12 17:30:07 +08:00
parent b28fc572f7
commit d6942c5702
6 changed files with 237 additions and 49 deletions
+100 -3
View File
@@ -1,17 +1,27 @@
import { describe, expect, it } from 'bun:test'
import {
DEFAULT_HEALTH_CHECK,
DEFAULT_INTERVAL,
addFileSubscription,
addSubscription,
listSubscriptions,
providerNames,
removeSubscription,
renameSubscription,
setSubscriptionHealthCheck,
subscriptionPathConflict,
subscriptionReferences,
updateSubscription,
} from './providers.ts'
import { applySetup, chainProviderName, providerCachePath } from './inbound-groups.ts'
import {
applySetup,
chainProviderName,
providerCachePath,
pruneGenerated,
syncGenerated,
} from './inbound-groups.ts'
import type { OutboundSetup } from './inbound-groups.ts'
import { collectInbounds } from './inbounds.ts'
import type { ConfigObject } from '../vendor/config-editor/index.ts'
const URL = 'http://127.0.0.1:3001/token/download/V2House'
@@ -77,7 +87,7 @@ describe('读出订阅列表', () => {
})
describe('新增订阅', () => {
it('自动生成缓存路径和默认间隔', () => {
it('自动生成缓存路径、默认间隔和默认开启的健康检查', () => {
const next = addSubscription({}, 'subs2', URL)
expect(next['proxy-providers']).toEqual({
subs2: {
@@ -85,6 +95,7 @@ describe('新增订阅', () => {
url: URL,
path: './providers/subs2.yaml',
interval: DEFAULT_INTERVAL,
'health-check': DEFAULT_HEALTH_CHECK,
},
})
})
@@ -106,11 +117,97 @@ describe('新增订阅', () => {
it('从文件导入的订阅是 file 类型,没有地址和间隔', () => {
const next = addFileSubscription({}, 'subs2')
expect(next['proxy-providers']).toEqual({
subs2: { type: 'file', path: './providers/subs2.yaml' },
subs2: {
type: 'file',
path: './providers/subs2.yaml',
'health-check': DEFAULT_HEALTH_CHECK,
},
})
})
})
describe('健康检查开关', () => {
it('开启时缺的字段用默认值补齐', () => {
const data = base()
const subs = (data['proxy-providers'] as ConfigObject).subs as ConfigObject
delete subs['health-check']
const next = setSubscriptionHealthCheck(data, 'subs', true)
const entry = (next['proxy-providers'] as ConfigObject).subs as ConfigObject
expect(entry['health-check']).toEqual(DEFAULT_HEALTH_CHECK)
})
it('关闭只翻 enable,手写的 url 和 interval 留着', () => {
const data = base()
const subs = (data['proxy-providers'] as ConfigObject).subs as ConfigObject
subs['health-check'] = { enable: true, url: 'https://example.com/ping', interval: 60 }
const next = setSubscriptionHealthCheck(data, 'subs', false)
const entry = (next['proxy-providers'] as ConfigObject).subs as ConfigObject
expect(entry['health-check']).toEqual({
enable: false,
url: 'https://example.com/ping',
interval: 60,
})
// 再开启时它们原样回来
const back = setSubscriptionHealthCheck(next, 'subs', true)
expect(listSubscriptions(back)[0]?.healthCheck).toEqual({
url: 'https://example.com/ping',
interval: 60,
})
})
it('本来就没写又要关闭时不做改动', () => {
const data = base()
const subs = (data['proxy-providers'] as ConfigObject).subs as ConfigObject
delete subs['health-check']
expect(setSubscriptionHealthCheck(data, 'subs', false)).toBe(data)
})
it('订阅不存在时不做改动', () => {
const data = base()
expect(setSubscriptionHealthCheck(data, 'ghost', true)).toBe(data)
})
})
describe('订阅改名', () => {
it('改映射键并同步策略组的 use 引用,path 不动', () => {
const next = renameSubscription(base(), 'subs', 'subs-new')
const providers = next['proxy-providers'] as ConfigObject
expect(Object.keys(providers)).toEqual(['subs-new'])
expect((providers['subs-new'] as ConfigObject).path).toBe('./providers/subs.yaml')
const group = (next['proxy-groups'] as ConfigObject[])[0]!
expect(group.use).toEqual(['subs-new'])
})
it('保持条目在映射里的位置', () => {
const data = addSubscription(addSubscription(base(), 'a', URL), 'b', URL)
const next = renameSubscription(data, 'a', 'z')
expect(Object.keys(next['proxy-providers'] as ConfigObject)).toEqual(['subs', 'z', 'b'])
})
it('改名后保存链路按新名字重建链式副本', () => {
const data = applySetup(
{ ...base(), listeners: [{ name: 'test1', type: 'mixed', port: 7778 }] },
'test1',
chainedSetup,
)
const renamed = renameSubscription(data, 'subs', 'subs2')
// 页面 commit() 每次落盘前都跑这两步
const synced = pruneGenerated(syncGenerated(renamed, collectInbounds(renamed)))
expect(providerNames(synced)).toContain(chainProviderName('test1', 'subs2'))
expect(providerNames(synced)).not.toContain(chainProviderName('test1', 'subs'))
// 引用检查也要认得出新名字,删除保护不能因为改名失效
expect(subscriptionReferences(synced, 'subs2')).not.toEqual([])
})
it('原名不存在、新名已占用或没改时都不做改动', () => {
const data = base()
expect(renameSubscription(data, 'ghost', 'x')).toBe(data)
const withX = addSubscription(data, 'x', URL)
expect(renameSubscription(withX, 'subs', 'x')).toBe(withX)
expect(renameSubscription(data, 'subs', 'subs')).toBe(data)
})
})
describe('编辑订阅', () => {
it('只改地址和间隔,其余字段原样保留', () => {
const next = updateSubscription(base(), 'subs', 'http://example.com/new', 3600)