mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-25 05:02:07 +08:00
* 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
195 lines
6.6 KiB
TypeScript
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)
|
|
})
|