release / release (push) Successful in 26s
顶层端口字段没有 proxy 可写(listener/inbound/base.go:97),界面上 切不了订阅。升级删掉顶层键、建同端口的 listener:listen 按 allow-lan 推导后写死,全局规则里 MATCH 之前的部分复制成附加规则,来源照抄 MATCH 目标组的 use:,让流量去向尽量不变。udp 不写,命名入站解析时 默认就是开的(listener/parse.go:52)。 「默认」tag 随术语表改成 legacy,旧叫法进 avoid 列表。
172 lines
5.0 KiB
TypeScript
172 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'bun:test'
|
||
import {
|
||
chainLabel,
|
||
filterConnections,
|
||
formatDuration,
|
||
groupByInbound,
|
||
hostLabel,
|
||
ruleLabel,
|
||
sortConnections,
|
||
} from './connections.ts'
|
||
import type { ConnectionItem, ConnectionMeta } from './connections.ts'
|
||
|
||
const NOW = Date.parse('2026-08-11T18:40:00+08:00')
|
||
|
||
function meta(over: Partial<ConnectionMeta> = {}): ConnectionMeta {
|
||
return {
|
||
host: 'example.com',
|
||
destinationIP: '93.184.216.34',
|
||
destinationPort: '443',
|
||
inboundName: 'lxc-web',
|
||
inboundPort: '17891',
|
||
...over,
|
||
}
|
||
}
|
||
|
||
function conn(over: Partial<ConnectionItem> = {}): ConnectionItem {
|
||
return {
|
||
id: 'a',
|
||
upload: 0,
|
||
download: 0,
|
||
start: '2026-08-11T18:39:00+08:00',
|
||
chains: ['[vless]某线路', 'AUTO', 'PROXY'],
|
||
rule: 'DomainSuffix',
|
||
rulePayload: 'example.com',
|
||
metadata: meta(),
|
||
...over,
|
||
}
|
||
}
|
||
|
||
describe('主机标签', () => {
|
||
it('有域名用域名,带端口', () => {
|
||
expect(hostLabel(meta())).toBe('example.com:443')
|
||
})
|
||
|
||
it('没域名退回 IP', () => {
|
||
expect(hostLabel(meta({ host: '' }))).toBe('93.184.216.34:443')
|
||
})
|
||
|
||
it('没端口就只给主机', () => {
|
||
expect(hostLabel(meta({ destinationPort: '' }))).toBe('example.com')
|
||
})
|
||
})
|
||
|
||
describe('出站链', () => {
|
||
it('倒过来显示:最先接触的在左,最终出口在右', () => {
|
||
expect(chainLabel(['[vless]某线路', 'AUTO', 'PROXY'])).toBe('PROXY → AUTO → [vless]某线路')
|
||
})
|
||
|
||
it('单跳就是直连', () => {
|
||
expect(chainLabel(['DIRECT'])).toBe('DIRECT')
|
||
})
|
||
})
|
||
|
||
describe('规则标签', () => {
|
||
it('类型和载荷用空格连起来', () => {
|
||
expect(ruleLabel('DomainSuffix', 'example.com')).toBe('DomainSuffix example.com')
|
||
})
|
||
|
||
it('兜底规则没有载荷', () => {
|
||
expect(ruleLabel('Match', '')).toBe('Match')
|
||
})
|
||
})
|
||
|
||
describe('持续时间', () => {
|
||
it('不满一分钟按秒', () => {
|
||
expect(formatDuration('2026-08-11T18:39:18+08:00', NOW)).toBe('42 秒')
|
||
})
|
||
|
||
it('满一分钟带分', () => {
|
||
expect(formatDuration('2026-08-11T18:36:36+08:00', NOW)).toBe('3 分 24 秒')
|
||
})
|
||
|
||
it('满一小时不再给秒', () => {
|
||
expect(formatDuration('2026-08-11T16:35:00+08:00', NOW)).toBe('2 小时 5 分')
|
||
})
|
||
|
||
it('start 带时区偏移,跨时区解析不能算错', () => {
|
||
// 同一时刻写成 UTC 和 +08:00,算出来的持续时间必须一样
|
||
const local = formatDuration('2026-08-11T18:39:00+08:00', NOW)
|
||
const utc = formatDuration('2026-08-11T10:39:00Z', NOW)
|
||
expect(local).toBe(utc)
|
||
})
|
||
|
||
it('坏数据返回空串,未来的时间按 0 算', () => {
|
||
expect(formatDuration('not-a-date', NOW)).toBe('')
|
||
expect(formatDuration('2026-08-11T19:00:00+08:00', NOW)).toBe('0 秒')
|
||
})
|
||
})
|
||
|
||
describe('排序', () => {
|
||
const list = [
|
||
conn({ id: 'small-new', download: 100, start: '2026-08-11T18:39:50+08:00' }),
|
||
conn({ id: 'big-new', download: 9000, start: '2026-08-11T18:39:40+08:00' }),
|
||
conn({ id: 'small-old', download: 50, start: '2026-08-11T18:00:00+08:00' }),
|
||
]
|
||
|
||
it('按下载量降序', () => {
|
||
expect(sortConnections(list, 'download', NOW).map((c) => c.id)).toEqual([
|
||
'big-new',
|
||
'small-new',
|
||
'small-old',
|
||
])
|
||
})
|
||
|
||
it('按持续时间降序:活得久的排前面', () => {
|
||
expect(sortConnections(list, 'duration', NOW).map((c) => c.id)).toEqual([
|
||
'small-old',
|
||
'big-new',
|
||
'small-new',
|
||
])
|
||
})
|
||
|
||
it('不动原数组', () => {
|
||
const before = list.map((c) => c.id)
|
||
sortConnections(list, 'download', NOW)
|
||
expect(list.map((c) => c.id)).toEqual(before)
|
||
})
|
||
})
|
||
|
||
describe('筛选', () => {
|
||
const list = [
|
||
conn({ id: 'a', metadata: meta({ host: 'speed.cloudflare.com' }) }),
|
||
conn({ id: 'b', metadata: meta({ host: '', destinationIP: '1.2.3.4' }) }),
|
||
]
|
||
|
||
it('匹配主机名,大小写不敏感', () => {
|
||
expect(filterConnections(list, 'CLOUDFLARE').map((c) => c.id)).toEqual(['a'])
|
||
})
|
||
|
||
it('没域名时也能按 IP 搜', () => {
|
||
expect(filterConnections(list, '1.2').map((c) => c.id)).toEqual(['b'])
|
||
})
|
||
|
||
it('空串不筛,空白也不算条件', () => {
|
||
expect(filterConnections(list, ' ')).toHaveLength(2)
|
||
})
|
||
})
|
||
|
||
describe('按入站聚合', () => {
|
||
it('同一路入站计数、流量都累加,连接多的排前面', () => {
|
||
const list = [
|
||
conn({ id: 'a', download: 10, upload: 1 }),
|
||
conn({ id: 'b', download: 20, upload: 2 }),
|
||
conn({ id: 'c', metadata: meta({ inboundName: 'lxc-db', inboundPort: '17892' }) }),
|
||
]
|
||
expect(groupByInbound(list)).toEqual([
|
||
{ label: 'lxc-web(17891)', count: 2, upload: 3, download: 30 },
|
||
{ label: 'lxc-db(17892)', count: 1, upload: 0, download: 0 },
|
||
])
|
||
})
|
||
|
||
it('legacy 入站用内核给的内部名,附上端口', () => {
|
||
const list = [conn({ metadata: meta({ inboundName: 'DEFAULT-MIXED', inboundPort: '17890' }) })]
|
||
expect(groupByInbound(list)[0]?.label).toBe('DEFAULT-MIXED(17890)')
|
||
})
|
||
|
||
it('入站名字缺失时用端口凑', () => {
|
||
const list = [conn({ metadata: meta({ inboundName: '' }) })]
|
||
expect(groupByInbound(list)[0]?.label).toBe('17891')
|
||
})
|
||
})
|