AGC 认证命令包装去掉运行时形状嗅探
- invokeClientAuth 不再检查 type 是否存在:认证命令的拒绝按 ts-rs 映射直接转成 ClientAuthFailure - requireInvoke 提前到 try 之外:认证桥未安装是我们自己的失败关闭错误,保持原样抛出 - Error.message 仍只取拒绝值里的可展示字符串,只做展示,不参与分流 - 测试:新增"桥未安装保持原样抛出"与"Tauri 意外形状也按映射包装"两条判据;authFailureReporting 的意外形状用例改为断言 source=auth + 命令名
This commit is contained in:
@@ -37,25 +37,26 @@ function requireInvoke() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证命令的统一入口:把 Rust 结构化拒绝转成 JS 侧的 `ClientAuthFailure`,
|
||||
* 非结构化拒绝(Tauri / JS 运行时自己的错误)原样抛出。
|
||||
* 认证命令的统一入口:把 Tauri 的拒绝按 ts-rs 映射转成 JS 侧的 `ClientAuthFailure`。
|
||||
*
|
||||
* 只判 `type` 是不是字符串(这是 ts-rs 判别联合的稳定键),**不校验字段名、不读文案判断、
|
||||
* 不兜底文案**。要不要上报、给不给用户提示,由调用方在 catch 里按 `payload.type` 决定。
|
||||
* **信任映射,不做运行时形状嗅探**:Rust 与 TS 同包发布,认证命令的拒绝就是
|
||||
* `ClientAuthError`;出现别的形状属于 Tauri / Rust 侧的缺陷,调用方 `switch` 的 `default`
|
||||
* 分支仍会把它抛出去上报。也不读文案判断、不兜底文案。
|
||||
*/
|
||||
async function invokeClientAuth<T>(
|
||||
command: string,
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<T> {
|
||||
// 认证桥未安装是我们自己的失败关闭错误,不是命令拒绝:放在 try 之外,原样抛出。
|
||||
const invoke = requireInvoke();
|
||||
try {
|
||||
const invoke = requireInvoke();
|
||||
// 不带参数时保持 `invoke(command)` 的单参调用形态,别给命令多塞一个 undefined。
|
||||
return args === undefined
|
||||
? await invoke<T>(command)
|
||||
: await invoke<T>(command, args);
|
||||
} catch (error) {
|
||||
const rejection = error as { type?: unknown; message?: unknown };
|
||||
if (typeof rejection?.type !== 'string') throw error;
|
||||
// `message` 只是给 Error 用的可展示字符串,不是分流判据;分流只按 `payload.type`。
|
||||
const rejection = error as { message?: unknown };
|
||||
throw new ClientAuthFailure(
|
||||
typeof rejection.message === 'string' ? rejection.message : '',
|
||||
error as ClientAuthError,
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('认证失败的上报判据', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('非结构化拒绝原样抛出,由 unhandledrejection 兜底上报', async () => {
|
||||
it('Tauri 的意外拒绝形状也被包装后原样抛出上报', async () => {
|
||||
const uninstall = installUnhandledRejectionBridge();
|
||||
try {
|
||||
await submitPasswordLogin(async (command) => {
|
||||
@@ -147,7 +147,8 @@ describe('认证失败的上报判据', () => {
|
||||
|
||||
await waitFor(() => expect(reportCalls()).toHaveLength(1));
|
||||
expect(reportCalls()[0]?.[1]).toMatchObject({
|
||||
source: 'unhandledrejection',
|
||||
source: 'auth',
|
||||
action: 'login_client_with_password',
|
||||
message: 'IPC 桥接不可用',
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -115,11 +115,8 @@ describe('ClientAuthFailure', () => {
|
||||
expect((failure as Error).message).toBe('请输入正确的手机号');
|
||||
});
|
||||
|
||||
it('非结构化拒绝原样抛出,不包装也不改写', async () => {
|
||||
const rejection = new Error('需要在 Tauri App 内登录');
|
||||
installInvoke(async () => {
|
||||
throw rejection;
|
||||
});
|
||||
it('认证桥未安装时保持原样抛出,不包装成命令失败', async () => {
|
||||
delete window.__TAURI__;
|
||||
|
||||
await expect(
|
||||
loginClientWithPassword(
|
||||
@@ -127,7 +124,28 @@ describe('ClientAuthFailure', () => {
|
||||
'secret',
|
||||
'https://dev.genarrative.world',
|
||||
),
|
||||
).rejects.toBe(rejection);
|
||||
).rejects.toThrow('需要在 Tauri App 内登录');
|
||||
});
|
||||
|
||||
it('Tauri 的意外形状也按映射包装:不做嗅探,但仍是可上报的 Error', async () => {
|
||||
const rejection = new Error('IPC 桥接异常');
|
||||
installInvoke(async () => {
|
||||
throw rejection;
|
||||
});
|
||||
|
||||
const failure = await loginClientWithPassword(
|
||||
'13800000000',
|
||||
'secret',
|
||||
'https://dev.genarrative.world',
|
||||
).catch((error: unknown) => error);
|
||||
|
||||
expect(failure).toBeInstanceOf(ClientAuthFailure);
|
||||
expect((failure as Error).message).toBe('IPC 桥接异常');
|
||||
expect((failure as ClientAuthFailure).payload).toBe(rejection);
|
||||
expect((failure as ClientAuthFailure).context).toEqual({
|
||||
source: 'auth',
|
||||
action: 'login_client_with_password',
|
||||
});
|
||||
});
|
||||
|
||||
it('结构化拒绝缺 message 时 Error.message 为空,不造兜底文案', async () => {
|
||||
|
||||
Reference in New Issue
Block a user