8701f43f75
- 删除渲染层传输层:services/clientApi.ts、clientHttp 的 fetchClientHttp 与超时封装整体退场,账户、认证、素材直传、错误上报与游戏分发发布改走 Rust typed command。 - 新增宿主封装:accountHost、editorAssetHost、platformSessionRetry、agentRuntimeErrorDetail 与 features/app-shell/workspacePreferences;渲染层不再读写 access token / refresh cookie,localStorage 只剩测试态旧键迁移 shim。 - 修复 DirectProject 失败文案 v2 映射与「查看详情」按需读取,画布标签面板高度夹紧、框选每帧重算、文档预览入口与场景标题栏 key、资源筛选面板样式。 - 补齐与调整回归:accountHost、agentRuntimeErrorDetail、clientAuthHost、clientHttp、platformSession、platformSessionRetry、resourceFilterPanelStyle、directThreadChatSubscription 等定向用例,并清理测试 harness 中无调用方的残留。
188 lines
5.5 KiB
TypeScript
188 lines
5.5 KiB
TypeScript
// @vitest-environment node
|
|
/**
|
|
* 平台会话代次的语义。
|
|
*
|
|
* 渲染层只持有这一个派生计数,用来识别换号/登出后到达的迟到结果。它必须在
|
|
* 「同一账号续期」和「同一身份重复采纳(StrictMode 双次 hydrate)」时保持不变——否则
|
|
* 在途生成会被自己的续期判成旧账号请求;而换账号、换 origin、登出又必须立刻推进。
|
|
*/
|
|
import { beforeEach, expect, test, vi } from 'vitest';
|
|
|
|
const refreshClientAuthSession = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock('../src/services/clientAuth', () => ({
|
|
refreshClientAuthSession,
|
|
subscribeClientAuthState: () => () => {},
|
|
}));
|
|
|
|
import {
|
|
beginPlatformSessionTransition,
|
|
commitAuthenticatedPlatformSession,
|
|
currentPlatformSessionGeneration,
|
|
requestPlatformSessionRefresh,
|
|
resetPlatformSessionForTests,
|
|
subscribePlatformSessionGeneration,
|
|
} from '../src/services/platformSession';
|
|
|
|
const userA = {
|
|
id: 'user-a',
|
|
publicUserCode: 'tn-a',
|
|
displayName: '账号 A',
|
|
avatarUrl: null,
|
|
phoneNumber: null,
|
|
phoneNumberMasked: null,
|
|
loginMethod: 'password',
|
|
bindingStatus: 'active',
|
|
wechatBound: false,
|
|
wechatDisplayName: null,
|
|
wechatAccount: null,
|
|
};
|
|
const userB = {
|
|
...userA,
|
|
id: 'user-b',
|
|
publicUserCode: 'tn-b',
|
|
displayName: '账号 B',
|
|
};
|
|
|
|
beforeEach(() => {
|
|
refreshClientAuthSession.mockReset();
|
|
resetPlatformSessionForTests();
|
|
});
|
|
|
|
test('同一身份重复采纳不推进代次,换 origin 或换账号才推进', async () => {
|
|
const observed: number[] = [];
|
|
const unsubscribe = subscribePlatformSessionGeneration((generation) => {
|
|
observed.push(generation);
|
|
});
|
|
|
|
const first = await commitAuthenticatedPlatformSession(
|
|
userA,
|
|
currentPlatformSessionGeneration(),
|
|
'https://a.example.com',
|
|
);
|
|
expect(first).toBe(1);
|
|
// StrictMode 双次 hydrate 或同一账号重新登录:代次必须原地不动。
|
|
const again = await commitAuthenticatedPlatformSession(
|
|
userA,
|
|
currentPlatformSessionGeneration(),
|
|
'https://a.example.com',
|
|
);
|
|
expect(again).toBe(first);
|
|
|
|
const otherOrigin = await commitAuthenticatedPlatformSession(
|
|
userA,
|
|
currentPlatformSessionGeneration(),
|
|
'https://b.example.com',
|
|
);
|
|
expect(otherOrigin).toBe(2);
|
|
const otherUser = await commitAuthenticatedPlatformSession(
|
|
userB,
|
|
currentPlatformSessionGeneration(),
|
|
'https://b.example.com',
|
|
);
|
|
expect(otherUser).toBe(3);
|
|
expect(observed).toEqual([1, 2, 3]);
|
|
unsubscribe();
|
|
});
|
|
|
|
test('期望代次已过期时不采纳身份', async () => {
|
|
await commitAuthenticatedPlatformSession(
|
|
userA,
|
|
currentPlatformSessionGeneration(),
|
|
'https://a.example.com',
|
|
);
|
|
|
|
await expect(
|
|
commitAuthenticatedPlatformSession(userB, 0, 'https://a.example.com'),
|
|
).resolves.toBeNull();
|
|
expect(currentPlatformSessionGeneration()).toBe(1);
|
|
});
|
|
|
|
test('登录切换与登出立刻推进代次', () => {
|
|
const before = currentPlatformSessionGeneration();
|
|
const next = beginPlatformSessionTransition();
|
|
|
|
expect(next).toBe(before + 1);
|
|
expect(currentPlatformSessionGeneration()).toBe(next);
|
|
});
|
|
|
|
test('同一账号续期不推进代次,并发续期只发一次请求', async () => {
|
|
await commitAuthenticatedPlatformSession(
|
|
userA,
|
|
currentPlatformSessionGeneration(),
|
|
'https://a.example.com',
|
|
);
|
|
const generationBefore = currentPlatformSessionGeneration();
|
|
refreshClientAuthSession.mockResolvedValue({
|
|
status: 'refreshed',
|
|
user: userA,
|
|
});
|
|
|
|
const [first, second] = await Promise.all([
|
|
requestPlatformSessionRefresh(),
|
|
requestPlatformSessionRefresh(),
|
|
]);
|
|
|
|
expect(refreshClientAuthSession).toHaveBeenCalledTimes(1);
|
|
expect(first).toEqual({
|
|
status: 'refreshed',
|
|
user: userA,
|
|
generation: generationBefore,
|
|
});
|
|
expect(second).toEqual(first);
|
|
expect(currentPlatformSessionGeneration()).toBe(generationBefore);
|
|
});
|
|
|
|
test('续期发现未登录时采纳空身份并给出权威失败', async () => {
|
|
await commitAuthenticatedPlatformSession(
|
|
userA,
|
|
currentPlatformSessionGeneration(),
|
|
'https://a.example.com',
|
|
);
|
|
const generationBefore = currentPlatformSessionGeneration();
|
|
refreshClientAuthSession.mockResolvedValue({ status: 'unauthenticated' });
|
|
|
|
const result = await requestPlatformSessionRefresh();
|
|
|
|
expect(result.status).toBe('failed');
|
|
if (result.status === 'failed') {
|
|
expect(result.authoritative).toBe(true);
|
|
expect(String(result.error)).toContain('登录已失效');
|
|
}
|
|
expect(currentPlatformSessionGeneration()).toBe(generationBefore + 1);
|
|
});
|
|
|
|
test('续期期间换号时返回 stale,不覆盖新身份', async () => {
|
|
await commitAuthenticatedPlatformSession(
|
|
userA,
|
|
currentPlatformSessionGeneration(),
|
|
'https://a.example.com',
|
|
);
|
|
refreshClientAuthSession.mockImplementation(async () => {
|
|
beginPlatformSessionTransition();
|
|
return { status: 'refreshed', user: userA };
|
|
});
|
|
|
|
await expect(requestPlatformSessionRefresh()).resolves.toEqual({
|
|
status: 'stale',
|
|
});
|
|
});
|
|
|
|
test('网络类续期失败保持会话且不标记为权威失败', async () => {
|
|
await commitAuthenticatedPlatformSession(
|
|
userA,
|
|
currentPlatformSessionGeneration(),
|
|
'https://a.example.com',
|
|
);
|
|
const generationBefore = currentPlatformSessionGeneration();
|
|
refreshClientAuthSession.mockRejectedValue(new Error('网络不可达'));
|
|
|
|
const result = await requestPlatformSessionRefresh();
|
|
|
|
expect(result.status).toBe('failed');
|
|
if (result.status === 'failed') {
|
|
expect(result.authoritative).toBe(false);
|
|
}
|
|
expect(currentPlatformSessionGeneration()).toBe(generationBefore);
|
|
});
|