实现客户端登录七天滑动续期

需求:解决短效访问令牌到期后客户端一小时掉登录的问题。

实现:由 Electron Main 加密管理并轮换刷新凭据,按真实用户活动续期,七天闲置后清理会话,并补齐并发、迁移和终态回归测试。
This commit is contained in:
2026-08-07 16:11:10 +08:00
parent 7b23cce67a
commit 86ece3a430
20 changed files with 2941 additions and 326 deletions

View File

@@ -1175,7 +1175,7 @@ describe('Works Square AI design adapter', () => {
]);
});
it('keeps the persisted Session key when a close result is uncertain', async () => {
it('rotates the persisted Session key even when a close result is uncertain', async () => {
const rotate = vi.fn().mockResolvedValue('design-stream-next');
const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse({ session_id: 'session-one', status: 'active' }, 201))
@@ -1204,7 +1204,44 @@ describe('Works Square AI design adapter', () => {
await expect(adapter.closeEventSessions()).rejects.toThrow(
'Failed to close 1 AI design Agent Session(s)',
);
expect(rotate).not.toHaveBeenCalled();
expect(rotate).toHaveBeenCalledWith('workspace-one');
});
it('uses the captured terminal token and tolerates remote close failure after rotating', async () => {
const rotate = vi.fn().mockResolvedValue('design-stream-next');
const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse({ session_id: 'session-one', status: 'active' }, 201))
.mockResolvedValueOnce(jsonResponse({
stream_url: '/api/agents/sessions/session-one/ws?ticket=ticket-one',
}))
.mockResolvedValueOnce(jsonResponse({
detail: { code: 'token_expired', message: 'expired' },
}, 401));
const { webSocketFactory } = scriptedSockets([{ closeCode: 1000 }]);
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
webSocketFactory,
eventSessionClientIdStore: {
getOrCreate: vi.fn().mockResolvedValue('design-stream-current'),
rotate,
},
});
const stream = await adapter.openWorkspaceEvents({ workspaceId: 'workspace-one' });
for await (const _event of stream.events) {
// Empty stream.
}
await expect(adapter.closeEventSessions({
accessToken: 'captured-terminal-access-token',
tolerateRemoteFailure: true,
})).resolves.toBeUndefined();
const deleteCall = fetchMock.mock.calls.find(([, init]) => init?.method === 'DELETE');
expect(deleteCall?.[1]?.headers).toMatchObject({
Authorization: 'Bearer captured-terminal-access-token',
});
expect(rotate).toHaveBeenCalledWith('workspace-one');
});
it('reuses a stable Session idempotency key after an unclean application restart', async () => {