test(proxies): keep focused delay regression coverage

Reduce added delay coverage to profile-switch isolation and shared-key subscriptions. Append the changelog entry after existing general fixes.
This commit is contained in:
Tunglies
2026-11-10 05:58:17 +08:00
parent b02fb26c0c
commit 371ca6e907
2 changed files with 27 additions and 100 deletions
+1 -1
View File
@@ -24,12 +24,12 @@
<details>
<summary><strong> 🐞 修复问题 </strong></summary>
- 修复切换订阅后节点延迟显示旧订阅结果,进入代理页面时主动刷新延迟记录
- 修复检测更新后又发布新版时,应用内更新失败
- 修复订阅响应过大导致内存占用过高,限制单次下载为 4 MiB
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
- 修复订阅中未加引号的 REALITY `short-id`(如 `1234e123`)被误读为数字,导致订阅无法启用
- 修复订阅中唯一的代理组名为 `GLOBAL` 或没有代理组时,规则模式下代理页面显示「未获取到代理信息」
- 修复切换订阅后节点延迟显示旧订阅结果,进入代理页面时主动刷新延迟记录
**🍎 macOS**
+26 -99
View File
@@ -75,111 +75,33 @@ describe('group delay completion', () => {
})
describe('profile delay isolation', () => {
test('drops cached and queued results on switch, exposing current history', async () => {
const listener = vi.fn()
const stop = delayManager.addListener('a', 'g', listener)
test('notifies both shared-key occurrences and preserves the surviving subscription', async () => {
const first = vi.fn()
const second = vi.fn()
const stopFirst = delayManager.addListener('a', 'g', first)
const stopSecond = delayManager.addListener('a', 'g', second)
try {
delayManager.setDelay('a', 'g', 120)
const previousSnapshot = delayManager.groupDelays('g')
delayManager.setProfile('initial')
expect(delayManager.getDelay('a', 'g')).toBe(120)
delayManager.setProfile('next')
const current = node('a')
current.node.history = [{ time: new Date().toISOString(), delay: 42 }]
expect(delayManager.getDelayUpdate('a', 'g')).toBeUndefined()
expect(delayManager.getDelayFix(current, 'g')).toBe(42)
expect(delayManager.groupDelays('g')).not.toBe(previousSnapshot)
expect(listener).toHaveBeenCalledExactlyOnceWith({
delay: -1,
updatedAt: 0,
})
const checking = delayManager.checkDelay(node('a'), 'g', 5000)
await vi.runAllTimersAsync()
expect(listener).toHaveBeenCalledOnce()
expect(settles).toBe(1)
await checking
delayManager.setProfile('next')
stopFirst()
const nextCheck = delayManager.checkDelay(node('a'), 'g', 5000)
await vi.runAllTimersAsync()
await nextCheck
expect(first.mock.calls.map(([update]) => update.delay)).toEqual([
-2, 120, -1,
])
expect(second.mock.calls.map(([update]) => update.delay)).toEqual([
-2, 120, -1, -2, 120,
])
} finally {
stop()
stopFirst()
stopSecond()
}
})
test.each([0, 1])(
'keeps shared-key subscribers independent when occurrence %i unmounts',
async (removed) => {
const listeners = [vi.fn(), vi.fn()]
const stops = listeners.map((listener) =>
delayManager.addListener('a', 'g', listener),
)
try {
const checking = delayManager.checkDelay(node('a'), 'g', 5000)
await vi.advanceTimersByTimeAsync(1)
for (const listener of listeners) {
expect(listener).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ delay: -2 }),
)
}
await vi.runAllTimersAsync()
await checking
for (const listener of listeners) {
expect(listener.mock.calls.map(([update]) => update.delay)).toEqual([
-2, 120,
])
}
delayManager.setProfile('next')
for (const listener of listeners) {
expect(listener).toHaveBeenLastCalledWith({
delay: -1,
updatedAt: 0,
})
listener.mockClear()
}
const nextCheck = delayManager.checkDelay(node('a'), 'g', 5000)
stops[removed]()
await vi.runAllTimersAsync()
await nextCheck
expect(listeners[removed]).not.toHaveBeenCalled()
expect(
listeners[1 - removed].mock.calls.map(([update]) => update.delay),
).toEqual([-2, 120])
} finally {
for (const stop of stops) stop()
}
},
)
test.each(['success', 'failure'] as const)(
'ignores an old-profile single test completing with %s',
async (outcome) => {
let finish!: () => void
vi.mocked(delayProxyByName).mockImplementationOnce(
() =>
new Promise((resolve, reject) => {
finish = () =>
outcome === 'success'
? resolve({ delay: 900 })
: reject(new Error('old-profile request failed'))
}),
)
const oldCheck = delayManager.checkDelay(node('a'), 'g', 5000)
delayManager.setProfile('next')
const newCheck = delayManager.checkDelay(node('a'), 'g', 5000)
await vi.advanceTimersByTimeAsync(500)
await newCheck
await vi.advanceTimersByTimeAsync(1)
const settledBeforeOldReply = settles
finish()
await vi.runAllTimersAsync()
await oldCheck
expect(delayManager.getDelay('a', 'g')).toBe(120)
expect(settles).toBe(settledBeforeOldReply)
},
)
test('stops an old batch without settling a new batch for the same group', async () => {
test('clears cached delays and fences an old batch across a profile switch', async () => {
let finishOld!: (result: { delay: number }) => void
let finishNew!: (result: { delay: number }) => void
vi.mocked(delayProxyByName)
@@ -196,6 +118,7 @@ describe('profile delay isolation', () => {
}),
)
delayManager.setDelay('cached', 'g', 900)
const oldBatch = delayManager.checkListDelay(
[node('a'), node('not-started')],
'g',
@@ -203,6 +126,10 @@ describe('profile delay isolation', () => {
1,
)
delayManager.setProfile('next')
expect(delayManager.getDelayUpdate('cached', 'g')).toBeUndefined()
const current = node('a')
current.node.history = [{ time: new Date().toISOString(), delay: 42 }]
expect(delayManager.getDelayFix(current, 'g')).toBe(42)
const newBatch = delayManager.checkListDelay([node('a')], 'g', 5000)
await vi.advanceTimersByTimeAsync(1)
const settledAfterSwitch = settles