mirror of
https://github.com/lobehub/lobe-chat.git
synced 2025-12-20 01:12:52 +08:00
✅ test: add test for v2 genai (#9123)
* add more tests * update tests * improve
This commit is contained in:
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "إضافة رسالة AI",
|
||||
"addUser": "إضافة رسالة مستخدم",
|
||||
"disclaimer": "قد يرتكب الذكاء الاصطناعي أخطاءً أيضًا، يرجى التحقق من المعلومات الهامة",
|
||||
"errorMsg": "فشل إرسال الرسالة، يرجى التحقق من الشبكة والمحاولة مرة أخرى: {{errorMsg}}",
|
||||
"more": "المزيد",
|
||||
"send": "إرسال",
|
||||
"sendWithCmdEnter": "اضغط <key/> للإرسال",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Добави AI съобщение",
|
||||
"addUser": "Добави потребителско съобщение",
|
||||
"disclaimer": "Изкуственият интелект също може да греши, моля проверете важната информация",
|
||||
"errorMsg": "Неуспешно изпращане на съобщението, моля, проверете мрежата и опитайте отново: {{errorMsg}}",
|
||||
"more": "още",
|
||||
"send": "Изпрати",
|
||||
"sendWithCmdEnter": "Натиснете <key/> за изпращане",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Fügen Sie eine AI-Nachricht hinzu",
|
||||
"addUser": "Fügen Sie eine Benutzer-Nachricht hinzu",
|
||||
"disclaimer": "KI kann auch Fehler machen, bitte überprüfen Sie wichtige Informationen",
|
||||
"errorMsg": "Nachricht konnte nicht gesendet werden, bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut: {{errorMsg}}",
|
||||
"more": "Mehr",
|
||||
"send": "Senden",
|
||||
"sendWithCmdEnter": "Drücken Sie <key/>, um zu senden",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Add an AI message",
|
||||
"addUser": "Add a user message",
|
||||
"disclaimer": "AI may also make mistakes, please verify important information",
|
||||
"errorMsg": "Message sending failed, please check your network and try again: {{errorMsg}}",
|
||||
"more": "more",
|
||||
"send": "Send",
|
||||
"sendWithCmdEnter": "Press <key/> to send",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Agregar un mensaje de IA",
|
||||
"addUser": "Agregar un mensaje de usuario",
|
||||
"disclaimer": "La IA también puede cometer errores, por favor verifique la información importante",
|
||||
"errorMsg": "Error al enviar el mensaje, por favor revise la conexión y vuelva a intentarlo: {{errorMsg}}",
|
||||
"more": "más",
|
||||
"send": "Enviar",
|
||||
"sendWithCmdEnter": "Presiona <key/> para enviar",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "افزودن یک پیام AI",
|
||||
"addUser": "افزودن یک پیام کاربر",
|
||||
"disclaimer": "هوش مصنوعی نیز ممکن است اشتباه کند، لطفاً اطلاعات مهم را بررسی کنید",
|
||||
"errorMsg": "ارسال پیام ناموفق بود، لطفاً پس از بررسی شبکه دوباره تلاش کنید: {{errorMsg}}",
|
||||
"more": "بیشتر",
|
||||
"send": "ارسال",
|
||||
"sendWithCmdEnter": "برای ارسال، کلید <key/> را فشار دهید",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Ajouter un message AI",
|
||||
"addUser": "Ajouter un message utilisateur",
|
||||
"disclaimer": "L'IA peut également faire des erreurs, veuillez vérifier les informations importantes",
|
||||
"errorMsg": "Échec de l'envoi du message, veuillez vérifier votre connexion réseau et réessayer : {{errorMsg}}",
|
||||
"more": "Plus",
|
||||
"send": "Envoyer",
|
||||
"sendWithCmdEnter": "Appuyez sur <key/> pour envoyer",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Aggiungi un messaggio AI",
|
||||
"addUser": "Aggiungi un messaggio utente",
|
||||
"disclaimer": "L'IA può anche commettere errori, si prega di verificare le informazioni importanti",
|
||||
"errorMsg": "Invio del messaggio fallito, controlla la rete e riprova: {{errorMsg}}",
|
||||
"more": "Ulteriori",
|
||||
"send": "Invia",
|
||||
"sendWithCmdEnter": "Premi <key/> per inviare",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "AIメッセージを追加",
|
||||
"addUser": "ユーザーメッセージを追加",
|
||||
"disclaimer": "AIも誤りを犯すことがありますので、重要な情報はご確認ください",
|
||||
"errorMsg": "メッセージの送信に失敗しました。ネットワークを確認してから再試行してください: {{errorMsg}}",
|
||||
"more": "もっと",
|
||||
"send": "送信",
|
||||
"sendWithCmdEnter": "<key/> キーを押して送信",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "AI 메시지 추가",
|
||||
"addUser": "사용자 메시지 추가",
|
||||
"disclaimer": "AI도 실수를 할 수 있으니 중요한 정보는 꼭 확인하세요",
|
||||
"errorMsg": "메시지 전송에 실패했습니다. 네트워크를 확인한 후 다시 시도해 주세요: {{errorMsg}}",
|
||||
"more": "더 많은",
|
||||
"send": "전송",
|
||||
"sendWithCmdEnter": "<key/> 키를 눌러 전송",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Voeg een AI-bericht toe",
|
||||
"addUser": "Voeg een gebruikersbericht toe",
|
||||
"disclaimer": "AI kan ook fouten maken, controleer belangrijke informatie alstublieft",
|
||||
"errorMsg": "Bericht verzenden mislukt, controleer uw netwerk en probeer het opnieuw: {{errorMsg}}",
|
||||
"more": "Meer",
|
||||
"send": "Verzenden",
|
||||
"sendWithCmdEnter": "Druk op <key/> om te verzenden",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Dodaj wiadomość AI",
|
||||
"addUser": "Dodaj wiadomość użytkownika",
|
||||
"disclaimer": "AI również może popełniać błędy, proszę sprawdzić ważne informacje",
|
||||
"errorMsg": "Wysyłanie wiadomości nie powiodło się, sprawdź połączenie sieciowe i spróbuj ponownie: {{errorMsg}}",
|
||||
"more": "więcej",
|
||||
"send": "Wyślij",
|
||||
"sendWithCmdEnter": "Naciśnij <key/>, aby wysłać",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Adicionar uma mensagem de IA",
|
||||
"addUser": "Adicionar uma mensagem de usuário",
|
||||
"disclaimer": "A IA também pode cometer erros, por favor verifique as informações importantes",
|
||||
"errorMsg": "Falha ao enviar a mensagem, verifique a rede e tente novamente: {{errorMsg}}",
|
||||
"more": "mais",
|
||||
"send": "Enviar",
|
||||
"sendWithCmdEnter": "Pressione <key/> para enviar",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Добавить сообщение AI",
|
||||
"addUser": "Добавить сообщение пользователя",
|
||||
"disclaimer": "ИИ также может ошибаться, пожалуйста, проверяйте важную информацию",
|
||||
"errorMsg": "Не удалось отправить сообщение, проверьте подключение к сети и попробуйте снова: {{errorMsg}}",
|
||||
"more": "больше",
|
||||
"send": "Отправить",
|
||||
"sendWithCmdEnter": "Нажмите <key/> для отправки",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Bir AI mesajı ekleyin",
|
||||
"addUser": "Bir kullanıcı mesajı ekleyin",
|
||||
"disclaimer": "Yapay zeka da hata yapabilir, lütfen önemli bilgileri kontrol edin",
|
||||
"errorMsg": "Mesaj gönderilemedi, lütfen ağı kontrol edip tekrar deneyin: {{errorMsg}}",
|
||||
"more": "Daha fazla",
|
||||
"send": "Gönder",
|
||||
"sendWithCmdEnter": "<key/> tuşuna basarak gönder",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "Thêm một tin nhắn AI",
|
||||
"addUser": "Thêm một tin nhắn người dùng",
|
||||
"disclaimer": "AI cũng có thể mắc lỗi, vui lòng kiểm tra kỹ thông tin quan trọng",
|
||||
"errorMsg": "Gửi tin nhắn thất bại, vui lòng kiểm tra mạng và thử lại: {{errorMsg}}",
|
||||
"more": "Thêm",
|
||||
"send": "Gửi",
|
||||
"sendWithCmdEnter": "Nhấn <key/> để gửi",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "添加一条 AI 消息",
|
||||
"addUser": "添加一条用户消息",
|
||||
"disclaimer": "AI 也可能会犯错,请检查重要信息",
|
||||
"errorMsg": "消息发送失败,请检查网络后重试: {{errorMsg}}",
|
||||
"more": "更多",
|
||||
"send": "发送",
|
||||
"sendWithCmdEnter": "按 <key/> 键发送",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"input": {
|
||||
"addAi": "新增一條 AI 訊息",
|
||||
"addUser": "新增一條使用者訊息",
|
||||
"disclaimer": "AI 也可能會犯錯,請檢查重要資訊",
|
||||
"errorMsg": "訊息發送失敗,請檢查網路後重試:{{errorMsg}}",
|
||||
"more": "更多",
|
||||
"send": "發送",
|
||||
"sendWithCmdEnter": "按 <key/> 鍵發送",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { ChatInput, ChatInputActionBar } from '@lobehub/editor/react';
|
||||
import { Text } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useChatInputStore } from '@/features/ChatInput/store';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
@@ -12,7 +14,6 @@ import { chatSelectors } from '@/store/chat/selectors';
|
||||
import ActionBar from '../ActionBar';
|
||||
import InputEditor from '../InputEditor';
|
||||
import SendArea from '../SendArea';
|
||||
import ShortcutHint from '../SendArea/ShortcutHint';
|
||||
import TypoBar from '../TypoBar';
|
||||
import FilePreview from './FilePreview';
|
||||
|
||||
@@ -28,6 +29,9 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
}
|
||||
}
|
||||
`,
|
||||
footnote: css`
|
||||
font-size: 10px;
|
||||
`,
|
||||
fullscreen: css`
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
@@ -42,6 +46,7 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
}));
|
||||
|
||||
const DesktopChatInput = memo<{ showFootnote?: boolean }>(({ showFootnote }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const [slashMenuRef, expand, showTypoBar, editor, leftActions] = useChatInputStore((s) => [
|
||||
s.slashMenuRef,
|
||||
s.expand,
|
||||
@@ -65,7 +70,8 @@ const DesktopChatInput = memo<{ showFootnote?: boolean }>(({ showFootnote }) =>
|
||||
{!expand && fileNode}
|
||||
<Flexbox
|
||||
className={cx(styles.container, expand && styles.fullscreen)}
|
||||
paddingBlock={showFootnote ? 0 : '0 12px'}
|
||||
gap={8}
|
||||
paddingBlock={showFootnote ? '0 8px' : '0 12px'}
|
||||
paddingInline={12}
|
||||
>
|
||||
<ChatInput
|
||||
@@ -85,7 +91,13 @@ const DesktopChatInput = memo<{ showFootnote?: boolean }>(({ showFootnote }) =>
|
||||
{expand && fileNode}
|
||||
<InputEditor />
|
||||
</ChatInput>
|
||||
{showFootnote && !expand && <ShortcutHint />}
|
||||
{showFootnote && !expand && (
|
||||
<Center style={{ pointerEvents: 'none', zIndex: 100 }}>
|
||||
<Text className={styles.footnote} type={'secondary'}>
|
||||
{t('input.disclaimer')}
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Flexbox>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -71,6 +71,7 @@ export default {
|
||||
input: {
|
||||
addAi: '添加一条 AI 消息',
|
||||
addUser: '添加一条用户消息',
|
||||
disclaimer: 'AI 也可能会犯错,请检查重要信息',
|
||||
errorMsg: '消息发送失败,请检查网络后重试: {{errorMsg}}',
|
||||
more: '更多',
|
||||
send: '发送',
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useChatStore } from '../../../../store';
|
||||
|
||||
describe('Cancel send message functionality tests', () => {
|
||||
describe('cancelSendMessageInServer', () => {
|
||||
it('should be able to call cancel method normally', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
// Initial state setup
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
activeId: 'session-1',
|
||||
activeTopicId: 'topic-1',
|
||||
mainSendMessageOperations: {},
|
||||
});
|
||||
});
|
||||
|
||||
// Test method exists
|
||||
expect(typeof result.current.cancelSendMessageInServer).toBe('function');
|
||||
|
||||
// Test method can be called safely
|
||||
expect(() => {
|
||||
act(() => {
|
||||
result.current.cancelSendMessageInServer();
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should be able to call with specified topic ID', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
activeId: 'session-1',
|
||||
mainSendMessageOperations: {},
|
||||
});
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
act(() => {
|
||||
result.current.cancelSendMessageInServer('topic-2');
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearSendMessageError', () => {
|
||||
it('should be able to call clear error method normally', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
activeId: 'session-1',
|
||||
activeTopicId: 'topic-1',
|
||||
mainSendMessageOperations: {},
|
||||
});
|
||||
});
|
||||
|
||||
expect(typeof result.current.clearSendMessageError).toBe('function');
|
||||
|
||||
expect(() => {
|
||||
act(() => {
|
||||
result.current.clearSendMessageError();
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Internal methods', () => {
|
||||
it('should have internal state management methods', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
expect(typeof result.current.internal_toggleSendMessageOperation).toBe('function');
|
||||
expect(typeof result.current.internal_updateSendMessageOperation).toBe('function');
|
||||
});
|
||||
|
||||
it('internal_toggleSendMessageOperation should work normally', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({ mainSendMessageOperations: {} });
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
act(() => {
|
||||
const abortController = result.current.internal_toggleSendMessageOperation(
|
||||
'test-key',
|
||||
true,
|
||||
);
|
||||
expect(abortController).toBeInstanceOf(AbortController);
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('State structure', () => {
|
||||
it('should have mainSendMessageOperations state', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
// Ensure state exists
|
||||
expect(result.current.mainSendMessageOperations).toBeDefined();
|
||||
expect(typeof result.current.mainSendMessageOperations).toBe('object');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { TRPCClientError } from '@trpc/client';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LOADING_FLAT } from '@/const/message';
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
} from '@/const/settings';
|
||||
import { aiChatService } from '@/services/aiChat';
|
||||
import { chatService } from '@/services/chat';
|
||||
//
|
||||
import { messageService } from '@/services/message';
|
||||
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
|
||||
import { sessionMetaSelectors } from '@/store/session/selectors';
|
||||
@@ -18,6 +18,8 @@ import { UploadFileItem } from '@/types/files/upload';
|
||||
import { ChatMessage } from '@/types/message';
|
||||
|
||||
import { useChatStore } from '../../../../store';
|
||||
import { messageMapKey } from '../../../../utils/messageMapKey';
|
||||
import { generateAIChatV2 } from '../generateAIChatV2';
|
||||
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
@@ -115,7 +117,10 @@ const mockState = {
|
||||
refreshTopic: vi.fn(),
|
||||
internal_execAgentRuntime: vi.fn(),
|
||||
saveToTopic: vi.fn(),
|
||||
};
|
||||
switchTopic: vi.fn(),
|
||||
internal_shouldUseRAG: () => false,
|
||||
internal_retrieveChunks: vi.fn(),
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -136,11 +141,11 @@ afterEach(() => {
|
||||
describe('generateAIChatV2 actions', () => {
|
||||
describe('sendMessageInServer', () => {
|
||||
it('should not send message if there is no active session', async () => {
|
||||
useChatStore.setState({ activeId: undefined });
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const message = 'Test message';
|
||||
|
||||
await act(async () => {
|
||||
useChatStore.setState({ activeId: undefined });
|
||||
await result.current.sendMessage({ message });
|
||||
});
|
||||
|
||||
@@ -252,7 +257,7 @@ describe('generateAIChatV2 actions', () => {
|
||||
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('当 isWelcomeQuestion 为 true 时,正确地传递给 internal_execAgentRuntime', async () => {
|
||||
it('should pass isWelcomeQuestion correctly to internal_execAgentRuntime when isWelcomeQuestion is true', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
await act(async () => {
|
||||
@@ -266,7 +271,7 @@ describe('generateAIChatV2 actions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('当只有文件而没有消息内容时,正确发送消息', async () => {
|
||||
it('should send message correctly when only files are provided without message content', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
await act(async () => {
|
||||
@@ -290,7 +295,7 @@ describe('generateAIChatV2 actions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('当同时有文件和消息内容时,正确发送消息并关联文件', async () => {
|
||||
it('should send message correctly when both files and message content are provided', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
await act(async () => {
|
||||
@@ -314,7 +319,7 @@ describe('generateAIChatV2 actions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('当 createMessage 抛出错误时,正确处理错误而不影响整个应用', async () => {
|
||||
it('should handle errors correctly when createMessage throws error without affecting the app', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
vi.spyOn(aiChatService, 'sendMessageInServer').mockRejectedValue(
|
||||
new Error('create message error'),
|
||||
@@ -443,4 +448,344 @@ describe('generateAIChatV2 actions', () => {
|
||||
expect(mockState.refreshMessages).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling tests', () => {
|
||||
it('should set error message when sendMessageInServer throws a regular error', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const errorMessage = 'Network error';
|
||||
const mockError = new TRPCClientError(errorMessage);
|
||||
(mockError as any).data = { code: 'BAD_REQUEST' };
|
||||
|
||||
vi.spyOn(aiChatService, 'sendMessageInServer').mockRejectedValue(mockError);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage({ message: 'test' });
|
||||
});
|
||||
|
||||
const operationKey = messageMapKey('session-id', 'topic-id');
|
||||
expect(result.current.mainSendMessageOperations[operationKey]?.inputSendErrorMsg).toBe(
|
||||
errorMessage,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not set error message when receiving a cancel signal', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const abortError = new Error('AbortError');
|
||||
abortError.name = 'AbortError';
|
||||
|
||||
vi.spyOn(aiChatService, 'sendMessageInServer').mockRejectedValue(abortError);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage({ message: 'test' });
|
||||
});
|
||||
|
||||
const operationKey = messageMapKey('session-id', 'topic-id');
|
||||
expect(
|
||||
result.current.mainSendMessageOperations[operationKey]?.inputSendErrorMsg,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Topic switching tests', () => {
|
||||
it('should automatically switch to newly created topic when no active topic exists', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const mockSwitchTopic = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
useChatStore.setState({
|
||||
...mockState,
|
||||
activeTopicId: undefined,
|
||||
switchTopic: mockSwitchTopic,
|
||||
});
|
||||
await result.current.sendMessage({ message: 'test' });
|
||||
});
|
||||
|
||||
expect(mockSwitchTopic).toHaveBeenCalledWith('topic-id', true);
|
||||
});
|
||||
|
||||
it('should not need to switch topic when active topic exists', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const mockSwitchTopic = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
useChatStore.setState({
|
||||
...mockState,
|
||||
switchTopic: mockSwitchTopic,
|
||||
});
|
||||
await result.current.sendMessage({ message: 'test' });
|
||||
});
|
||||
|
||||
expect(mockSwitchTopic).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cancel send message tests', () => {
|
||||
it('should correctly cancel the current active send operation', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const mockAbort = vi.fn();
|
||||
const mockSetJSONState = vi.fn();
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
activeId: 'session-1',
|
||||
activeTopicId: 'topic-1',
|
||||
mainSendMessageOperations: {
|
||||
[messageMapKey('session-1', 'topic-1')]: {
|
||||
isLoading: true,
|
||||
abortController: { abort: mockAbort, signal: {} as any },
|
||||
inputEditorTempState: { content: 'saved content' },
|
||||
},
|
||||
},
|
||||
mainInputEditor: { setJSONState: mockSetJSONState } as any,
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.cancelSendMessageInServer();
|
||||
});
|
||||
|
||||
expect(mockAbort).toHaveBeenCalledWith('User cancelled sendMessageInServer operation');
|
||||
expect(
|
||||
result.current.mainSendMessageOperations[messageMapKey('session-1', 'topic-1')]?.isLoading,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should cancel the operation for the corresponding topic when topic ID is specified', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const mockAbort = vi.fn();
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
activeId: 'session-1',
|
||||
mainSendMessageOperations: {
|
||||
[messageMapKey('session-1', 'topic-2')]: {
|
||||
isLoading: true,
|
||||
abortController: { abort: mockAbort, signal: {} as any },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.cancelSendMessageInServer('topic-2');
|
||||
});
|
||||
|
||||
expect(mockAbort).toHaveBeenCalledWith('User cancelled sendMessageInServer operation');
|
||||
});
|
||||
|
||||
it('should handle safely without throwing error when operation does not exist', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({ mainSendMessageOperations: {} });
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
act(() => {
|
||||
result.current.cancelSendMessageInServer('non-existing-topic');
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Clear send error tests', () => {
|
||||
it('should correctly clear error state for current topic', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
activeId: 'session-1',
|
||||
activeTopicId: 'topic-1',
|
||||
mainSendMessageOperations: {
|
||||
[messageMapKey('session-1', 'topic-1')]: {
|
||||
isLoading: false,
|
||||
inputSendErrorMsg: 'Some error',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.clearSendMessageError();
|
||||
});
|
||||
|
||||
expect(
|
||||
result.current.mainSendMessageOperations[messageMapKey('session-1', 'topic-1')],
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle safely when no error operation exists', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({ mainSendMessageOperations: {} });
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
act(() => {
|
||||
result.current.clearSendMessageError();
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Operation state management tests', () => {
|
||||
it('should correctly create new send operation', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
let abortController: AbortController | undefined;
|
||||
|
||||
act(() => {
|
||||
abortController = result.current.internal_toggleSendMessageOperation('test-key', true);
|
||||
});
|
||||
|
||||
expect(abortController!).toBeInstanceOf(AbortController);
|
||||
expect(result.current.mainSendMessageOperations['test-key']?.isLoading).toBe(true);
|
||||
expect(result.current.mainSendMessageOperations['test-key']?.abortController).toBe(
|
||||
abortController,
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly stop send operation', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const mockAbortController = { abort: vi.fn() } as any;
|
||||
|
||||
let abortController: AbortController | undefined;
|
||||
act(() => {
|
||||
result.current.internal_updateSendMessageOperation('test-key', {
|
||||
isLoading: true,
|
||||
abortController: mockAbortController,
|
||||
});
|
||||
|
||||
abortController = result.current.internal_toggleSendMessageOperation('test-key', false);
|
||||
});
|
||||
|
||||
expect(abortController).toBeUndefined();
|
||||
expect(result.current.mainSendMessageOperations['test-key']?.isLoading).toBe(false);
|
||||
expect(result.current.mainSendMessageOperations['test-key']?.abortController).toBeNull();
|
||||
});
|
||||
|
||||
it('should correctly handle cancel reason and call abort method', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const mockAbortController = { abort: vi.fn() } as any;
|
||||
|
||||
result.current.internal_updateSendMessageOperation('test-key', {
|
||||
isLoading: true,
|
||||
abortController: mockAbortController,
|
||||
});
|
||||
|
||||
result.current.internal_toggleSendMessageOperation('test-key', false, 'Test cancel reason');
|
||||
|
||||
expect(mockAbortController.abort).toHaveBeenCalledWith('Test cancel reason');
|
||||
});
|
||||
|
||||
it('should support multiple parallel operations', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
let abortController1, abortController2;
|
||||
act(() => {
|
||||
abortController1 = result.current.internal_toggleSendMessageOperation('pkey1', true);
|
||||
abortController2 = result.current.internal_toggleSendMessageOperation('pkey2', true);
|
||||
});
|
||||
|
||||
expect(result.current.mainSendMessageOperations['pkey1']?.isLoading).toBe(true);
|
||||
expect(result.current.mainSendMessageOperations['pkey2']?.isLoading).toBe(true);
|
||||
expect(abortController1).not.toBe(abortController2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Send operation state update tests', () => {
|
||||
it('should correctly update operation state', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const mockAbortController = new AbortController();
|
||||
|
||||
act(() => {
|
||||
result.current.internal_updateSendMessageOperation('abc', {
|
||||
isLoading: true,
|
||||
abortController: mockAbortController,
|
||||
inputSendErrorMsg: 'test error',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.mainSendMessageOperations['abc']).toEqual({
|
||||
isLoading: true,
|
||||
abortController: mockAbortController,
|
||||
inputSendErrorMsg: 'test error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support partial update of operation state', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const initialController = new AbortController();
|
||||
|
||||
act(() => {
|
||||
result.current.internal_updateSendMessageOperation('test-key', {
|
||||
isLoading: true,
|
||||
abortController: initialController,
|
||||
});
|
||||
|
||||
// Only update error message
|
||||
result.current.internal_updateSendMessageOperation('test-key', {
|
||||
inputSendErrorMsg: 'new error',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.mainSendMessageOperations['test-key']).toEqual({
|
||||
isLoading: true,
|
||||
abortController: initialController,
|
||||
inputSendErrorMsg: 'new error',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Editor state recovery tests', () => {
|
||||
it('should restore editor content when cancelling operation', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const mockSetJSONState = vi.fn();
|
||||
const mockAbort = vi.fn();
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
activeId: 'session-1',
|
||||
activeTopicId: 'topic-1',
|
||||
mainSendMessageOperations: {
|
||||
[messageMapKey('session-1', 'topic-1')]: {
|
||||
isLoading: true,
|
||||
abortController: { abort: mockAbort, signal: {} as any },
|
||||
inputEditorTempState: { content: 'saved content' },
|
||||
},
|
||||
},
|
||||
mainInputEditor: { setJSONState: mockSetJSONState } as any,
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.cancelSendMessageInServer();
|
||||
});
|
||||
|
||||
expect(mockSetJSONState).toHaveBeenCalledWith({ content: 'saved content' });
|
||||
});
|
||||
|
||||
it('should not restore when no saved editor state exists', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const mockSetJSONState = vi.fn();
|
||||
const mockAbort = vi.fn();
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
activeId: 'session-1',
|
||||
activeTopicId: 'topic-1',
|
||||
mainSendMessageOperations: {
|
||||
[messageMapKey('session-1', 'topic-1')]: {
|
||||
isLoading: true,
|
||||
abortController: { abort: mockAbort, signal: {} as any },
|
||||
},
|
||||
},
|
||||
mainInputEditor: { setJSONState: mockSetJSONState } as any,
|
||||
});
|
||||
result.current.cancelSendMessageInServer();
|
||||
});
|
||||
|
||||
expect(mockSetJSONState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -260,7 +260,8 @@ export const generateAIChatV2: StateCreator<
|
||||
// Only clear creating message state if it's the active session
|
||||
if (operationKey === messageMapKey(activeId, activeTopicId)) {
|
||||
const editorTempState = get().mainSendMessageOperations[operationKey]?.inputEditorTempState;
|
||||
get().mainInputEditor?.setJSONState(editorTempState);
|
||||
|
||||
if (editorTempState) get().mainInputEditor?.setJSONState(editorTempState);
|
||||
}
|
||||
},
|
||||
clearSendMessageError: () => {
|
||||
|
||||
Reference in New Issue
Block a user