diff --git a/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx b/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx index c83a3a5fa..1b7c2bd4e 100644 --- a/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx +++ b/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx @@ -144,7 +144,10 @@ describe('认证失败的上报判据', () => { await waitFor(() => expect(reportCalls()).toHaveLength(1)); expect(reportCalls()[0]?.[1]).toMatchObject({ source: 'unhandledrejection', - message: '未知客户端错误', + message: JSON.stringify({ + type: 'authNetworkFailure', + reason: 'unreachable', + }), }); } finally { uninstall(); diff --git a/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts b/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts index ac048cd8f..c343247a6 100644 --- a/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts +++ b/apps/ai-game-creator-shell/tests/clientAuthErrorCarrier.test.ts @@ -3,8 +3,8 @@ * 认证命令失败的 JS 侧载体。 * * `invokeClientAuth` 是薄包装:Rust 的结构化拒绝原样装进 `ClientAuthErrorWrapper.error`,不读变体 - * 字段(`serverMessage` 之类不保证每个变体都有)、不注入上下文、载体自身 `message` 留空;分流只按 - * `error.error.type`。另外用编译期用例钉住变体全集:无字段变体直接取固定文案,带载荷变体先 + * 字段(`serverMessage` 之类不保证每个变体都有)、不注入上下文,载体构造时把整份载荷序列化进 + * `Error.message`;分流只按 `error.error.type`。另外用编译期用例钉住变体全集:无字段变体直接取固定文案,带载荷变体先 * `as` 取具名类型,可枚举的细分再按类型化 reason 分流;漏列任何变体或 reason, * `expectNever(...)` 都编译不过。 */ @@ -147,7 +147,7 @@ describe('认证命令失败的 JS 侧载体', () => { expect(failure).toBeInstanceOf(ClientAuthErrorWrapper); expect((failure as ClientAuthErrorWrapper).error).toBe(rejection); - expect((failure as Error).message).toBe(''); + expect((failure as Error).message).toBe(JSON.stringify(rejection)); }); it('认证桥未安装时保持原样抛出,不包装成命令失败', async () => { @@ -175,7 +175,8 @@ describe('认证命令失败的 JS 侧载体', () => { ).catch((error: unknown) => error); expect(failure).toBeInstanceOf(ClientAuthErrorWrapper); - expect((failure as Error).message).toBe(''); + // Error 没有可枚举属性,序列化就是空对象;上报链路对真 Error 优先用其自身 message/stack。 + expect((failure as Error).message).toBe('{}'); expect((failure as ClientAuthErrorWrapper).error).toBe(rejection); }); }); diff --git a/apps/ai-game-creator-shell/tests/errorReporting.test.ts b/apps/ai-game-creator-shell/tests/errorReporting.test.ts index 26f83c1bd..dc4e8e9c7 100644 --- a/apps/ai-game-creator-shell/tests/errorReporting.test.ts +++ b/apps/ai-game-creator-shell/tests/errorReporting.test.ts @@ -84,6 +84,7 @@ import { submitErrorReportBatch, subscribeClientErrorEvents, } from '../src/services/errorReporting'; +import type { ClientAuthError } from '../src/services/generated/ClientAuthError'; describe('客户端错误报告池', () => { afterEach(() => { @@ -209,7 +210,7 @@ describe('客户端错误报告池', () => { expect(await getPendingClientErrorEvents()).toEqual([event]); }); - it('解包 ClientAuthErrorWrapper 里的原始拒绝值,上下文只认显式入参', async () => { + it('解包 ClientAuthErrorWrapper,把载体序列化后的 message 原样上报,上下文只认显式入参', async () => { const original = { type: 'authNetworkFailure', reason: 'unreachable' }; await captureClientError(new ClientAuthErrorWrapper(original), { @@ -219,13 +220,31 @@ describe('客户端错误报告池', () => { expect(invoke).toHaveBeenCalledWith('report_client_error', { source: 'unhandledrejection', - message: '未知客户端错误', + message: JSON.stringify(original), stack: expect.any(String), action: 'login', page: undefined, }); }); + it('包装的真 Error 仍优先用它的 message/stack,而不是空对象序列化', async () => { + // Tauri 在映射外抛真 Error 属于它的缺陷:载体照装,但取文案时仍用这颗 Error 自己。 + const original = new Error('IPC 桥接异常'); + + await captureClientError( + new ClientAuthErrorWrapper(original as unknown as ClientAuthError), + { source: 'unhandledrejection' }, + ); + + expect(invoke).toHaveBeenCalledWith('report_client_error', { + source: 'unhandledrejection', + message: 'IPC 桥接异常', + stack: original.stack, + action: undefined, + page: undefined, + }); + }); + it('保留 API 路由但隐藏 URL origin 与查询参数', () => { const sanitized = normalizeDiagnosticText( '请求超时:https://dev.genarrative.world/api/llm/models?token=secret#fragment',