Files
clash-verge-rev/src/services/delay.test.ts
T
Tunglies 6e08a77a84 refactor(backend): specta ipc bindings (#8195)
* refactor(ipc): generate bindings with Specta

* fix(profile): project IPC inputs to consumed fields

* fix(events): remove unused refresh payloads

* fix(webdav): return only consumed backup metadata

* fix(network): trim unused interface metadata

* fix(sysinfo): return only displayed system fields

* chore(knip): check generated runtime exports

* fix(proxy): confirm runtime chain updates before UI changes

* fix(clipboard): propagate proxy environment copy failures

* refactor(dns): remove ignored runtime patch

* refactor(core): trim unused upgrade response fields

* refactor(ipc): avoid redundant response copies

* refactor(proxy): clarify automatic proxy query naming

* chore(tests): remove added IPC regression tests

* refactor(ipc): enforce typed command boundaries

* chore(contract): check generated bindings before push

* refactor(ipc): remove unused proxy liveness payloads
2026-10-12 06:16:05 +00:00

195 lines
6.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
vi.mock('tauri-plugin-mihomo-api', () => ({
delayProxyByName: vi.fn(async () => ({ delay: 120 })),
healthcheckNodeInProvider: vi.fn(async () => ({ delay: 120 })),
}))
import type { ResolvedProxyMember } from '@/types/proxy-view'
import delayManager from './delay'
const node = (name: string) =>
({
kind: 'node',
ref: { kind: 'node', name, recordId: `r:${name}` },
node: {
recordId: `r:${name}`,
name,
history: [],
source: { kind: 'core', proxyName: name },
},
}) as unknown as ResolvedProxyMember
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
let settles = 0
let unsubscribe: () => void
beforeEach(() => {
settles = 0
unsubscribe = delayManager.addGroupListener('g', () => {
settles += 1
})
})
afterEach(() => unsubscribe())
describe('group delay completion', () => {
test('notifies once after a batch settles', async () => {
const proxies = Array.from({ length: 6 }, (_, index) => node(`n${index}`))
await delayManager.checkListDelay(proxies as never, 'g', 5000, 2)
await flush()
expect(settles).toBe(1)
})
test('notifies only listeners for the completed group', async () => {
let other = 0
const stop = delayManager.addGroupListener('other', () => {
other += 1
})
await delayManager.checkDelay(node('a') as never, 'g', 5000)
await flush()
expect(settles).toBe(1)
expect(other).toBe(0)
stop()
})
})
test('uses newer core history after a tray test without hiding an active page test', () => {
const member = node('tray-refresh')
if (member.kind !== 'node') throw new Error('Expected node')
const cached = delayManager.setDelay(member, 'tray-group', 50)
member.node.extra = {
[delayManager.getUrl('tray-group')]: {
history: [
{ time: new Date(cached.updatedAt + 1000).toISOString(), delay: 300 },
],
},
}
expect(delayManager.getDelayFix(member, 'tray-group')).toBe(300)
expect(delayManager.getDelayUpdate(member, 'tray-group')?.delay).toBe(300)
delayManager.setDelay(member, 'tray-group', -2)
expect(delayManager.getDelayFix(member, 'tray-group')).toBe(-2)
})
test('latency URL validation rejects malformed hosts and ports and trims valid URLs', () => {
for (const url of [
'http://?',
'http://host:invalid',
'http://bad host/',
'ftp://localhost',
'http:localhost',
]) {
delayManager.setUrl('url-validation', url)
expect(delayManager.getUrl('url-validation')).toBe(
'http://cp.cloudflare.com/generate_204',
)
}
delayManager.setUrl('url-validation', ' HTTP://localhost:8080/ ')
expect(delayManager.getUrl('url-validation')).toBe('HTTP://localhost:8080/')
})
test('another URL cannot replace a group measurement, but a newer matching URL can', () => {
const member = node('url-isolation')
if (member.kind !== 'node') throw new Error('Expected node')
delayManager.setUrl('url-a', 'http://localhost/a')
const cached = delayManager.setDelay(member, 'url-a', 50)
const newer = new Date(cached.updatedAt + 1000).toISOString()
Object.assign(member.node, {
history: [{ time: newer, delay: 126 }],
extra: {
'http://localhost/a': {
history: [
{ time: new Date(cached.updatedAt - 1000).toISOString(), delay: 49 },
],
},
'http://localhost/b': {
history: [{ time: newer, delay: 126 }],
},
},
})
expect(delayManager.getDelayFix(member, 'url-a')).toBe(50)
Object.assign(member.node, {
extra: {
'http://localhost/a': {
history: [{ time: newer, delay: 75 }],
},
},
})
expect(delayManager.getDelayFix(member, 'url-a')).toBe(75)
})
test('same-named provider members keep separate measurements and listeners', async () => {
const first = node('same-provider-name')
const second = node('same-provider-name')
if (first.kind !== 'node' || second.kind !== 'node')
throw new Error('Expected nodes')
first.node.recordId = 'provider-a:N'
first.node.source = { kind: 'provider', providerName: 'a', proxyName: 'N' }
second.node.recordId = 'provider-b:N'
second.node.source = { kind: 'provider', providerName: 'b', proxyName: 'N' }
const { healthcheckNodeInProvider } = await import('tauri-plugin-mihomo-api')
vi.mocked(healthcheckNodeInProvider)
.mockResolvedValueOnce({ delay: 40 })
.mockResolvedValueOnce({ delay: 125 })
const firstListener = vi.fn()
const secondListener = vi.fn()
delayManager.setListener(first, 'provider-collision', firstListener)
delayManager.setListener(second, 'provider-collision', secondListener)
await delayManager.checkListDelay([first, second], 'provider-collision', 5000)
await flush()
expect(firstListener).toHaveBeenLastCalledWith(
expect.objectContaining({ delay: 40 }),
)
expect(secondListener).toHaveBeenLastCalledWith(
expect.objectContaining({ delay: 125 }),
)
delayManager.removeListener(first, 'provider-collision')
delayManager.removeListener(second, 'provider-collision')
expect(delayManager.getDelayFix(first, 'provider-collision')).toBe(40)
expect(delayManager.getDelayFix(second, 'provider-collision')).toBe(125)
})
test.each(['core', 'provider'] as const)(
'cached %s delays follow source identity after record IDs move',
(kind) => {
const original = node(`stable-${kind}`)
const replacement = node(`replacement-${kind}`)
if (original.kind !== 'node' || replacement.kind !== 'node')
throw new Error('Expected nodes')
const source = (proxyName: string) =>
kind === 'core'
? { kind, proxyName }
: { kind, providerName: 'subscription', proxyName }
original.node.source = source(original.ref.name)
replacement.node.source = source(replacement.ref.name)
original.node.recordId = 'position:0'
replacement.node.recordId = 'position:0'
delayManager.setDelay(original, `reorder-${kind}`, 40)
expect(delayManager.getDelayFix(replacement, `reorder-${kind}`)).toBe(-1)
original.node.recordId = 'position:1'
expect(delayManager.getDelayFix(original, `reorder-${kind}`)).toBe(40)
},
)
test('unmeasured selectors show automatic history without replacing a manual URL result', () => {
const member = node('selector-member')
if (member.kind !== 'node') throw new Error('Expected node')
member.node.history = [
{ time: new Date(Date.now() + 1000).toISOString(), delay: 126 },
]
member.node.extra = {
'http://localhost/automatic': { history: member.node.history },
}
expect(delayManager.getDelayFix(member, 'selector')).toBe(126)
delayManager.setDelay(member, 'selector', 50)
expect(delayManager.getDelayFix(member, 'selector')).toBe(50)
})