// @vitest-environment node /** * 平台会话续期重试的边界。 * * 凭据与续期 HTTP 都在 Rust;渲染层这一层只保证两件事:只有服务端明确否认身份时才会 * 续期一次并重放,以及身份代次围栏必须挡住换号/登出期间的重放——带着旧身份的请求重放 * 会把上一个账号的副作用打进新账号。 */ import { beforeEach, expect, test, vi } from 'vitest'; const refreshClientAuthSession = vi.hoisted(() => vi.fn()); vi.mock('../src/services/clientAuth', () => ({ refreshClientAuthSession, subscribeClientAuthState: () => () => {}, })); import { beginPlatformSessionTransition, resetPlatformSessionForTests, subscribePlatformSessionRefresh, } from '../src/services/platformSession'; import { isPlatformAuthenticationRequiredError, withPlatformSessionRefreshRetry, } from '../src/services/platformSessionRetry'; const testUser = { id: 'user-1', publicUserCode: 'tn-1', displayName: '测试用户', avatarUrl: null, phoneNumber: null, phoneNumberMasked: null, loginMethod: 'password', bindingStatus: 'active', wechatBound: false, wechatDisplayName: null, wechatAccount: null, }; beforeEach(() => { refreshClientAuthSession.mockReset(); resetPlatformSessionForTests(); }); test('只有服务端明确否认身份的错误才被识别为可续期', () => { expect( isPlatformAuthenticationRequiredError( new Error('authentication-required: 请重新登录'), ), ).toBe(true); expect( isPlatformAuthenticationRequiredError( new Error('codex-app-server-error:unauthorized'), ), ).toBe(true); expect( isPlatformAuthenticationRequiredError( new Error('provider failed kind=codex-app-server-unauthorized'), ), ).toBe(true); expect( isPlatformAuthenticationRequiredError(new Error('登录已失效,请重新登录')), ).toBe(true); // 网络抖动、超时、5xx 与业务拒绝都不能被当成身份否认。 expect( isPlatformAuthenticationRequiredError(new Error('请求超时,请稍后重试')), ).toBe(false); expect(isPlatformAuthenticationRequiredError(new Error('HTTP 503'))).toBe( false, ); expect(isPlatformAuthenticationRequiredError(new Error('泥点余额不足'))).toBe( false, ); expect( isPlatformAuthenticationRequiredError('unauthenticated-provider-response'), ).toBe(false); }); test('请求成功时不触碰续期', async () => { const operation = vi.fn().mockResolvedValue(42); await expect(withPlatformSessionRefreshRetry(operation)).resolves.toBe(42); expect(operation).toHaveBeenCalledTimes(1); expect(refreshClientAuthSession).not.toHaveBeenCalled(); }); test('业务失败原样抛出且不续期', async () => { const failure = new Error('该游戏名已被占用'); const operation = vi.fn().mockRejectedValue(failure); await expect(withPlatformSessionRefreshRetry(operation)).rejects.toBe( failure, ); expect(operation).toHaveBeenCalledTimes(1); expect(refreshClientAuthSession).not.toHaveBeenCalled(); }); test('身份被否认时续期一次并重放原请求', async () => { const operation = vi .fn() .mockRejectedValueOnce(new Error('authentication-required')) .mockResolvedValueOnce('done'); refreshClientAuthSession.mockResolvedValue({ status: 'refreshed', user: testUser, }); await expect(withPlatformSessionRefreshRetry(operation)).resolves.toBe( 'done', ); expect(operation).toHaveBeenCalledTimes(2); expect(refreshClientAuthSession).toHaveBeenCalledTimes(1); }); test('续期失败时抛回原始错误而不是续期错误', async () => { const failure = new Error('authentication-required'); const operation = vi.fn().mockRejectedValue(failure); refreshClientAuthSession.mockResolvedValue({ status: 'unauthenticated' }); await expect(withPlatformSessionRefreshRetry(operation)).rejects.toBe( failure, ); expect(operation).toHaveBeenCalledTimes(1); }); test('请求期间已经换号或登出时不续期也不重放', async () => { const failure = new Error('authentication-required'); const operation = vi.fn().mockImplementation(async () => { beginPlatformSessionTransition(); throw failure; }); await expect(withPlatformSessionRefreshRetry(operation)).rejects.toBe( failure, ); expect(refreshClientAuthSession).not.toHaveBeenCalled(); }); test('续期完成瞬间换号时拒绝用旧身份重放', async () => { const failure = new Error('authentication-required'); const operation = vi.fn().mockRejectedValueOnce(failure); refreshClientAuthSession.mockResolvedValue({ status: 'refreshed', user: testUser, }); subscribePlatformSessionRefresh(() => { beginPlatformSessionTransition(); }); await expect(withPlatformSessionRefreshRetry(operation)).rejects.toThrow( '登录账号已变化,原请求已停止', ); expect(operation).toHaveBeenCalledTimes(1); });