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 { return { host: 'example.com', destinationIP: '93.184.216.34', destinationPort: '443', inboundName: 'lxc-web', inboundPort: '17891', ...over, } } function conn(over: Partial = {}): 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('默认入站用内核给的内部名,附上端口', () => { 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') }) })