From 371ca6e907d279a0188219a0174a39581d5f126e Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 10 Nov 2026 05:58:17 +0800 Subject: [PATCH] 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. --- Changelog.md | 2 +- src/services/delay.test.ts | 125 ++++++++----------------------------- 2 files changed, 27 insertions(+), 100 deletions(-) diff --git a/Changelog.md b/Changelog.md index b39adaba5..4e4e5f17e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -24,12 +24,12 @@
🐞 修复问题 -- 修复切换订阅后节点延迟显示旧订阅结果,进入代理页面时主动刷新延迟记录 - 修复检测更新后又发布新版时,应用内更新失败 - 修复订阅响应过大导致内存占用过高,限制单次下载为 4 MiB - 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组 - 修复订阅中未加引号的 REALITY `short-id`(如 `1234e123`)被误读为数字,导致订阅无法启用 - 修复订阅中唯一的代理组名为 `GLOBAL` 或没有代理组时,规则模式下代理页面显示「未获取到代理信息」 +- 修复切换订阅后节点延迟显示旧订阅结果,进入代理页面时主动刷新延迟记录 **🍎 macOS** diff --git a/src/services/delay.test.ts b/src/services/delay.test.ts index d4a8d1e00..2a73d931f 100644 --- a/src/services/delay.test.ts +++ b/src/services/delay.test.ts @@ -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