diff --git a/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts index 3f7c5912b..375b53b2a 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/auth.suite.ts @@ -95,6 +95,26 @@ async function loginWithCode() { fireEvent.click(screen.getByRole('button', { name: '登录' })); } +/** + * 捕获 window 侧 `unhandledrejection`:返回已收到的 `reason` 列表与释放函数, + * 释放时同时摘掉监听和 Node 侧桥接。 + */ +function captureWindowRejections() { + const uninstallBridge = installUnhandledRejectionBridge(); + const reasons: unknown[] = []; + const onRejection = (event: Event) => { + reasons.push((event as Event & { reason?: unknown }).reason); + }; + window.addEventListener('unhandledrejection', onRejection); + return { + reasons, + uninstall() { + window.removeEventListener('unhandledrejection', onRejection); + uninstallBridge(); + }, + }; +} + export function registerAuthTests() { afterEach(() => { resetPlatformSessionForTests(); @@ -102,12 +122,7 @@ export function registerAuthTests() { }); it('throws a startup timeout through to unhandledrejection and leaves the loading state', async () => { - const uninstall = installUnhandledRejectionBridge(); - const reasons: unknown[] = []; - const onRejection = (event: Event) => { - reasons.push((event as Event & { reason?: unknown }).reason); - }; - window.addEventListener('unhandledrejection', onRejection); + const rejections = captureWindowRejections(); try { renderAuthSurface(async (command: string) => { if (command === 'read_client_auth_state') { @@ -119,7 +134,7 @@ export function registerAuthTests() { await waitFor( () => expect( - reasons.some( + rejections.reasons.some( (reason) => reason instanceof Error && reason.message.includes('检查登录状态超时'), @@ -138,8 +153,7 @@ export function registerAuthTests() { ).toHaveProperty('disabled', false); await screen.findByRole('main', { name: '登录' }); } finally { - window.removeEventListener('unhandledrejection', onRejection); - uninstall(); + rejections.uninstall(); } }, 30_000); @@ -282,12 +296,7 @@ export function registerAuthTests() { }); it('shows the system variant copy and still throws it through', async () => { - const uninstall = installUnhandledRejectionBridge(); - const reasons: unknown[] = []; - const onRejection = (event: Event) => { - reasons.push((event as Event & { reason?: unknown }).reason); - }; - window.addEventListener('unhandledrejection', onRejection); + const rejections = captureWindowRejections(); try { const invoke = vi.fn(async (command: string) => { if (command === 'read_client_auth_state') { @@ -302,10 +311,12 @@ export function registerAuthTests() { await loginWithCode(); // 先等系统变体真的经 unhandledrejection 抛出。 - await waitFor(() => expect(reasons).toHaveLength(1)); - // 载体自身 message 留空;诊断信息在 error 字段里,由上报链路取。 - expect(reasons[0]).toBeInstanceOf(ClientAuthErrorWrapper); - expect((reasons[0] as ClientAuthErrorWrapper).error).toMatchObject({ + await waitFor(() => expect(rejections.reasons).toHaveLength(1)); + // 载体 message 是载荷序列化(上报链路直接取用);结构化诊断在 error 字段里。 + expect(rejections.reasons[0]).toBeInstanceOf(ClientAuthErrorWrapper); + expect( + (rejections.reasons[0] as ClientAuthErrorWrapper).error, + ).toMatchObject({ type: 'authNetworkFailure', reason: 'unreachable', }); @@ -318,8 +329,7 @@ export function registerAuthTests() { expect(document.body.textContent).not.toContain('ECONNREFUSED'); await screen.findByRole('main', { name: '登录' }); } finally { - window.removeEventListener('unhandledrejection', onRejection); - uninstall(); + rejections.uninstall(); } });