diff --git a/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx b/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx new file mode 100644 index 000000000..5255f6c47 --- /dev/null +++ b/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx @@ -0,0 +1,128 @@ +/** @vitest-environment jsdom */ + +import { + cleanup, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { createElement } from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +/** + * 认证失败分流的上报判据(端到端,判据是"这条失败到底有没有进错误池")。 + * + * 认证命令与错误上报走的不是同一个 invoke 入口:认证命令来自 `window.__TAURI__.core.invoke` + * (本文件注入替身),错误上报来自 `@tauri-apps/api/core` 的模块级 `invoke`。所以只有 mock 掉 + * 模块级 invoke,才能观察到 `report_client_error` 是否真的被调用。 + */ +vi.mock('@tauri-apps/api/core', () => ({ + invoke: vi.fn(async (command: string, _args?: Record) => { + if (command === 'get_pending_error_reports') return []; + return undefined; + }), +})); + +import { invoke } from '@tauri-apps/api/core'; + +import { AuthenticatedClient } from '../src/app/AuthenticatedClient'; +import { resetPlatformSessionForTests } from '../src/services/platformSession'; + +type AuthInvoke = ( + command: string, + args?: Record, +) => Promise; + +function reportCalls() { + return vi + .mocked(invoke) + .mock.calls.filter(([command]) => command === 'report_client_error'); +} + +function renderLogin(invokeAuth: AuthInvoke) { + window.__TAURI__ = { core: { invoke: invokeAuth } } as never; + render( + createElement(AuthenticatedClient, null, () => + createElement('main', { 'aria-label': '已登录' }), + ), + ); +} + +async function submitPasswordLogin(invokeAuth: AuthInvoke) { + renderLogin(invokeAuth); + await screen.findByRole('main', { name: '登录' }); + fireEvent.click(screen.getByRole('button', { name: '密码登录' })); + fireEvent.change(screen.getByLabelText('手机号'), { + target: { value: '13800000000' }, + }); + fireEvent.change(screen.getByLabelText('密码'), { + target: { value: 'wrong-password' }, + }); + fireEvent.click(screen.getByRole('button', { name: '登录' })); +} + +describe('认证失败的上报判据', () => { + afterEach(() => { + cleanup(); + resetPlatformSessionForTests(); + delete window.__TAURI__; + vi.mocked(invoke).mockClear(); + }); + + it('业务拒绝(手机号或密码错误)只显示文案,不写错误池', async () => { + await submitPasswordLogin(async (command) => { + if (command === 'read_client_auth_state') { + return { status: 'unauthenticated' }; + } + if (command === 'login_client_with_password') { + throw { type: 'phoneOrPasswordMismatch', message: '手机号或密码错误' }; + } + return null; + }); + + expect(await screen.findByText('手机号或密码错误')).not.toBeNull(); + expect(reportCalls()).toHaveLength(0); + }); + + it('发码限流同样只显示文案,不写错误池', async () => { + renderLogin(async (command) => { + if (command === 'read_client_auth_state') { + return { status: 'unauthenticated' }; + } + if (command === 'send_client_phone_login_code') { + throw { type: 'smsCodeThrottled', message: '发送过于频繁,请稍后再试' }; + } + return null; + }); + await screen.findByRole('main', { name: '登录' }); + fireEvent.change(screen.getByLabelText('手机号'), { + target: { value: '13800000000' }, + }); + fireEvent.click(screen.getByRole('button', { name: '获取验证码' })); + + expect(await screen.findByText('发送过于频繁,请稍后再试')).not.toBeNull(); + expect(reportCalls()).toHaveLength(0); + }); + + it('系统变体(网络不可达)带 auth/login 上下文上报一次', async () => { + const message = '无法连接登录服务,请确认配套后端或 API 代理已启动后重试'; + await submitPasswordLogin(async (command) => { + if (command === 'read_client_auth_state') { + return { status: 'unauthenticated' }; + } + if (command === 'login_client_with_password') { + throw { type: 'authNetworkUnavailable', message }; + } + return null; + }); + + expect(await screen.findByText(message)).not.toBeNull(); + await waitFor(() => expect(reportCalls()).toHaveLength(1)); + expect(reportCalls()[0]?.[1]).toMatchObject({ + source: 'auth', + action: 'login', + message, + }); + }); +});