2026-05-08 17:28:31 +08:00
|
|
|
import { QueryClient } from '@tanstack/react-query';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
prefetchConversationMessages,
|
|
|
|
|
warmupConversationOpening,
|
|
|
|
|
} from '@/features/conversation/entry-warmup';
|
|
|
|
|
import { conversationKeys } from '@/features/conversation/query-keys';
|
|
|
|
|
import type { MessageItem } from '@/features/conversation/types';
|
|
|
|
|
|
|
|
|
|
const mockLoadMessages = jest.fn();
|
|
|
|
|
const mockRegisterPreparedRealtimeSession = jest.fn();
|
|
|
|
|
let mockConnectImpl:
|
|
|
|
|
| ((options: {
|
|
|
|
|
conversationId: string;
|
|
|
|
|
queryClient: QueryClient;
|
|
|
|
|
}) => Promise<void> | void)
|
|
|
|
|
| null = null;
|
|
|
|
|
const mockSessions: Array<{
|
|
|
|
|
attachUiCallbacks: jest.Mock;
|
|
|
|
|
connect: jest.Mock;
|
|
|
|
|
dispose: jest.Mock;
|
|
|
|
|
}> = [];
|
|
|
|
|
|
|
|
|
|
jest.mock('@/features/conversation/conversation-messages-repository', () => ({
|
|
|
|
|
conversationMessagesRepository: {
|
|
|
|
|
loadMessages: (conversationId: string) => mockLoadMessages(conversationId),
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
jest.mock('@/features/conversation/prepared-session-registry', () => ({
|
|
|
|
|
registerPreparedRealtimeSession: (conversationId: string, session: unknown) =>
|
|
|
|
|
mockRegisterPreparedRealtimeSession(conversationId, session),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
jest.mock('@/features/conversation/realtime-session', () => ({
|
|
|
|
|
RealtimeSession: jest.fn().mockImplementation((options) => {
|
|
|
|
|
const session = {
|
|
|
|
|
attachUiCallbacks: jest.fn(),
|
|
|
|
|
connect: jest.fn(async () => {
|
|
|
|
|
await mockConnectImpl?.(options);
|
|
|
|
|
}),
|
|
|
|
|
dispose: jest.fn(),
|
|
|
|
|
};
|
|
|
|
|
mockSessions.push(session);
|
|
|
|
|
return session;
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
function createQueryClient(): QueryClient {
|
|
|
|
|
return new QueryClient({
|
|
|
|
|
defaultOptions: {
|
|
|
|
|
queries: { retry: false, gcTime: Infinity },
|
|
|
|
|
mutations: { retry: false },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assistantMessage(id = 'assistant-1'): MessageItem {
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
conversationId: 'conv-1',
|
|
|
|
|
content: '你好,今天想聊哪段回忆?',
|
|
|
|
|
senderType: 'assistant',
|
|
|
|
|
timestamp: 1,
|
|
|
|
|
messageType: 'text',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('conversation entry warmup', () => {
|
|
|
|
|
let queryClient: QueryClient;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
queryClient = createQueryClient();
|
|
|
|
|
mockLoadMessages.mockReset();
|
|
|
|
|
mockRegisterPreparedRealtimeSession.mockReset();
|
|
|
|
|
mockConnectImpl = null;
|
|
|
|
|
mockSessions.length = 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
await queryClient.cancelQueries();
|
|
|
|
|
queryClient.clear();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('prefetches messages without throwing on load failure', async () => {
|
|
|
|
|
mockLoadMessages.mockRejectedValueOnce(new Error('network down'));
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
prefetchConversationMessages(queryClient, 'conv-1'),
|
|
|
|
|
).resolves.toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('uses refreshed history and skips websocket when opening is already cached', async () => {
|
|
|
|
|
const existing = assistantMessage();
|
|
|
|
|
mockLoadMessages.mockResolvedValueOnce([existing]);
|
|
|
|
|
|
|
|
|
|
await warmupConversationOpening(queryClient, 'conv-1');
|
|
|
|
|
|
|
|
|
|
expect(mockLoadMessages).toHaveBeenCalledWith('conv-1');
|
|
|
|
|
expect(mockSessions).toHaveLength(0);
|
2026-05-11 10:25:06 +08:00
|
|
|
expect(
|
|
|
|
|
queryClient.getQueryData(conversationKeys.messages('conv-1')),
|
|
|
|
|
).toEqual([existing]);
|
2026-05-08 17:28:31 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('connects websocket and registers prepared session after opening arrives', async () => {
|
|
|
|
|
const opened = assistantMessage();
|
2026-05-11 10:25:06 +08:00
|
|
|
mockLoadMessages.mockResolvedValueOnce([]).mockResolvedValueOnce([opened]);
|
2026-05-08 17:28:31 +08:00
|
|
|
mockConnectImpl = ({ conversationId, queryClient }) => {
|
|
|
|
|
queryClient.setQueryData(conversationKeys.messages(conversationId), [
|
|
|
|
|
opened,
|
|
|
|
|
]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await warmupConversationOpening(queryClient, 'conv-1');
|
|
|
|
|
|
|
|
|
|
expect(mockSessions).toHaveLength(1);
|
|
|
|
|
expect(mockSessions[0]?.attachUiCallbacks).toHaveBeenCalled();
|
|
|
|
|
expect(mockSessions[0]?.connect).toHaveBeenCalled();
|
|
|
|
|
expect(mockSessions[0]?.dispose).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockRegisterPreparedRealtimeSession).toHaveBeenCalledWith(
|
|
|
|
|
'conv-1',
|
|
|
|
|
mockSessions[0],
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|