Files
Genarrative/apps/ai-game-creator-shell/tests/clientAuthFailure.test.ts
T
k88936 ce54c729e8 AGC 认证命令失败改为 JS 侧载体并只按变体分流
- 新增 ClientAuthFailure:payload 为 ts-rs 判别联合,context 固定 source=auth + 命令名,cause 为原始拒绝值
- clientAuth 的六个命令统一走 invokeClientAuth:结构化拒绝转成 ClientAuthFailure,非结构化拒绝原样抛出,不兜底文案
- 删除 clientAuthError.ts 的形状读取与 getClientAuthErrorMessage 文案回落层
- AuthenticatedClient 的两个 catch 内联 19 个变体的 switch:业务 / 会话变体原样展示载荷 message,系统变体原样抛出经 unhandledrejection 入池,default: expectNever 保证漏接变体编译失败
- hydrateAuth 失败先离开 loading 态;系统变体不再伪装成 unavailable 投影
- ClientAuthState 收窄为 authenticated / unauthenticated,ClientAuthRefreshResult 收窄为 refreshed / unauthenticated / stale
- platformSession 续期只保留 refreshed / unauthenticated / stale 三条路径
- check-config 的 App 调用扫描登记 invokeClientAuth,继续静态证明命令名可达
- 测试:新增 clientAuthFailure 载体与编译期穷尽用例、jsdom unhandledrejection 桥;删除 clientAuthError.test.ts;clientAuthHost / authFailureReporting / authSurface 跟改新口径
- 共享记忆记录 check-config 扫描形态新增 invokeClientAuth
2026-10-01 20:49:48 +08:00

165 lines
6.7 KiB
TypeScript

// @vitest-environment jsdom
/**
* 认证命令失败的 JS 侧载体。
*
* Rust 结构化拒绝(普通对象)必须被 `invokeClientAuth` 转成 `ClientAuthFailure`,非结构化拒绝
* 原样抛出;分流只按 `payload.type`,不读文案。另外用编译期用例钉住"每个变体一个具名载荷
* 类型":漏列任何变体,`expectNever(error)` 都编译不过。
*/
import { describe, expect, it, vi } from 'vitest';
import { ClientActionError } from '../src/services/clientActionError';
import { loginClientWithPassword } from '../src/services/clientAuth';
import { ClientAuthFailure } from '../src/services/clientAuthFailure';
import type { AuthClientInitFailed } from '../src/services/generated/AuthClientInitFailed';
import type { AuthNetworkUnavailable } from '../src/services/generated/AuthNetworkUnavailable';
import type { AuthResponseMalformed } from '../src/services/generated/AuthResponseMalformed';
import type { AuthServiceUnavailable } from '../src/services/generated/AuthServiceUnavailable';
import type { ClientAuthError } from '../src/services/generated/ClientAuthError';
import type { ClientSessionPersistFailed } from '../src/services/generated/ClientSessionPersistFailed';
import type { LoginCodeMissing } from '../src/services/generated/LoginCodeMissing';
import type { PasswordEntryInputRejected } from '../src/services/generated/PasswordEntryInputRejected';
import type { PasswordMissing } from '../src/services/generated/PasswordMissing';
import type { PermissionDenied } from '../src/services/generated/PermissionDenied';
import type { PhoneLoginInputRejected } from '../src/services/generated/PhoneLoginInputRejected';
import type { PhoneNumberInvalid } from '../src/services/generated/PhoneNumberInvalid';
import type { PhoneOrPasswordMismatch } from '../src/services/generated/PhoneOrPasswordMismatch';
import type { RuntimeSessionInstallFailed } from '../src/services/generated/RuntimeSessionInstallFailed';
import type { SendCodeInputRejected } from '../src/services/generated/SendCodeInputRejected';
import type { ServerAddressRejected } from '../src/services/generated/ServerAddressRejected';
import type { SessionAuthorityRejected } from '../src/services/generated/SessionAuthorityRejected';
import type { SmsCodeInvalidOrExpired } from '../src/services/generated/SmsCodeInvalidOrExpired';
import type { SmsCodeThrottled } from '../src/services/generated/SmsCodeThrottled';
import type { UnexpectedRejection } from '../src/services/generated/UnexpectedRejection';
function expectNever(value: never): void {
void value;
}
/** 编译期用例:19 个变体逐个列全,每个分支用 `as` 取具名载荷类型。 */
function variantMessage(error: ClientAuthError): string {
switch (error.type) {
case 'serverAddressRejected':
return (error as ServerAddressRejected).message;
case 'phoneNumberInvalid':
return (error as PhoneNumberInvalid).message;
case 'passwordMissing':
return (error as PasswordMissing).message;
case 'loginCodeMissing':
return (error as LoginCodeMissing).message;
case 'passwordEntryInputRejected':
return (error as PasswordEntryInputRejected).message;
case 'phoneOrPasswordMismatch':
return (error as PhoneOrPasswordMismatch).message;
case 'sendCodeInputRejected':
return (error as SendCodeInputRejected).message;
case 'smsCodeThrottled':
return (error as SmsCodeThrottled).message;
case 'phoneLoginInputRejected':
return (error as PhoneLoginInputRejected).message;
case 'smsCodeInvalidOrExpired':
return (error as SmsCodeInvalidOrExpired).message;
case 'sessionAuthorityRejected':
return (error as SessionAuthorityRejected).message;
case 'permissionDenied':
return (error as PermissionDenied).message;
case 'authNetworkUnavailable':
return (error as AuthNetworkUnavailable).message;
case 'authServiceUnavailable':
return (error as AuthServiceUnavailable).message;
case 'unexpectedRejection':
return (error as UnexpectedRejection).message;
case 'authResponseMalformed':
return (error as AuthResponseMalformed).message;
case 'clientSessionPersistFailed':
return (error as ClientSessionPersistFailed).message;
case 'runtimeSessionInstallFailed':
return (error as RuntimeSessionInstallFailed).message;
case 'authClientInitFailed':
return (error as AuthClientInitFailed).message;
default: {
expectNever(error);
return '';
}
}
}
function installInvoke(handler: () => Promise<unknown>) {
window.__TAURI__ = { core: { invoke: vi.fn(handler) } } as never;
}
describe('ClientAuthFailure', () => {
it('结构化拒绝转成带 payload / context / cause 的 JS 错误', async () => {
const rejection = {
type: 'phoneNumberInvalid',
message: '请输入正确的手机号',
};
installInvoke(async () => {
throw rejection;
});
const failure = await loginClientWithPassword(
'13800000000',
'secret',
'https://dev.genarrative.world',
).catch((error: unknown) => error);
expect(failure).toBeInstanceOf(ClientAuthFailure);
expect(failure).toBeInstanceOf(ClientActionError);
expect((failure as ClientAuthFailure).payload).toEqual(rejection);
expect((failure as ClientAuthFailure).context).toEqual({
source: 'auth',
action: 'login_client_with_password',
});
expect((failure as ClientAuthFailure).cause).toBe(rejection);
expect((failure as Error).message).toBe('请输入正确的手机号');
});
it('非结构化拒绝原样抛出,不包装也不改写', async () => {
const rejection = new Error('需要在 Tauri App 内登录');
installInvoke(async () => {
throw rejection;
});
await expect(
loginClientWithPassword(
'13800000000',
'secret',
'https://dev.genarrative.world',
),
).rejects.toBe(rejection);
});
it('结构化拒绝缺 message 时 Error.message 为空,不造兜底文案', async () => {
installInvoke(async () => {
throw { type: 'unexpectedRejection', status: 409 };
});
const failure = await loginClientWithPassword(
'13800000000',
'secret',
'https://dev.genarrative.world',
).catch((error: unknown) => error);
expect((failure as Error).message).toBe('');
expect((failure as ClientAuthFailure).payload.type).toBe(
'unexpectedRejection',
);
});
});
describe('变体分流', () => {
it('判别后按具名载荷类型取值', () => {
expect(
variantMessage({ type: 'phoneNumberInvalid', message: '格式不对' }),
).toBe('格式不对');
expect(
variantMessage({
type: 'authServiceUnavailable',
status: 503,
message: '服务暂不可用',
}),
).toBe('服务暂不可用');
});
});