UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

392 lines (344 loc) 11.3 kB
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; import { act, renderHook } from '@testing-library/react'; import useSWR from 'swr'; import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { notification } from '@/components/AntdStaticMethods'; import { pluginService } from '@/services/plugin'; import { toolService } from '@/services/tool'; import { DiscoverPluginItem } from '@/types/discover'; import { useToolStore } from '../../store'; // Mock necessary modules and functions vi.mock('@/components/AntdStaticMethods', () => ({ notification: { error: vi.fn(), }, })); // Mock the pluginService.getToolList method vi.mock('@/services/plugin', () => ({ pluginService: { uninstallPlugin: vi.fn(), installPlugin: vi.fn(), }, })); vi.mock('@/services/tool', () => ({ toolService: { getToolManifest: vi.fn(), getToolList: vi.fn(), getOldPluginList: vi.fn(), }, })); // Mock i18next vi.mock('i18next', () => ({ t: vi.fn((key) => key), })); const pluginManifestMock = { $schema: '../node_modules/@lobehub/chat-plugin-sdk/schema.json', api: [ { url: 'https://realtime-weather.chat-plugin.lobehub.com/api/v1', name: 'fetchCurrentWeather', description: '获取当前天气情况', parameters: { properties: { city: { description: '城市名称', type: 'string', }, }, required: ['city'], type: 'object', }, }, ], author: 'LobeHub', createAt: '2023-08-12', homepage: 'https://github.com/lobehub/chat-plugin-realtime-weather', identifier: 'realtime-weather', meta: { avatar: '🌈', tags: ['weather', 'realtime'], title: 'Realtime Weather', description: 'Get realtime weather information', }, ui: { url: 'https://realtime-weather.chat-plugin.lobehub.com/iframe', height: 310, }, version: '1', }; // Mock useSWR vi.mock('swr', async () => { const actual = await vi.importActual('swr'); return { ...(actual as any), default: vi.fn(), }; }); const logError = console.error; beforeEach(() => { vi.restoreAllMocks(); useToolStore.setState({ oldPluginItems: [ { identifier: 'plugin1', title: 'plugin1', avatar: '🍏', manifest: 'https://abc.com/manifest.json', } as DiscoverPluginItem, ], }); console.error = () => {}; }); afterEach(() => { console.error = logError; }); describe('useToolStore:pluginStore', () => { describe('loadPluginStore', () => { it('should load plugin list and update state', async () => { // Given const pluginListMock = [{ identifier: 'plugin1' }, { identifier: 'plugin2' }]; (toolService.getOldPluginList as Mock).mockResolvedValue({ items: pluginListMock }); // When let pluginList; await act(async () => { pluginList = await useToolStore.getState().loadPluginStore(); }); // Then expect(toolService.getOldPluginList).toHaveBeenCalled(); expect(pluginList).toEqual(pluginListMock); expect(useToolStore.getState().oldPluginItems).toEqual(pluginListMock); }); it('should handle errors when loading plugin list', async () => { // Given const error = new Error('Failed to load plugin list'); (toolService.getOldPluginList as Mock).mockRejectedValue(error); // When let pluginList; let errorOccurred = false; try { await act(async () => { pluginList = await useToolStore.getState().loadPluginStore(); }); } catch (e) { errorOccurred = true; } // Then expect(toolService.getOldPluginList).toHaveBeenCalled(); expect(errorOccurred).toBe(true); expect(pluginList).toBeUndefined(); // Ensure the state is not updated with an undefined value expect(useToolStore.getState().oldPluginItems).not.toBeUndefined(); }); }); describe('useFetchPluginStore', () => { it('should use SWR to fetch plugin store', async () => { // Given const pluginListMock = [{ identifier: 'plugin1' }, { identifier: 'plugin2' }]; (useSWR as Mock).mockReturnValue({ data: pluginListMock, error: null, isValidating: false, }); // When const { result } = renderHook(() => useToolStore.getState().useFetchPluginStore()); // Then expect(useSWR).toHaveBeenCalledWith('loadPluginStore', expect.any(Function), { fallbackData: [], revalidateOnFocus: false, suspense: true, }); expect(result.current.data).toEqual(pluginListMock); expect(result.current.error).toBeNull(); expect(result.current.isValidating).toBe(false); }); it('should handle errors when fetching plugin store with SWR', async () => { // Given const error = new Error('Failed to fetch plugin store'); (useSWR as Mock).mockReturnValue({ data: null, error: error, isValidating: false, }); // When const { result } = renderHook(() => useToolStore.getState().useFetchPluginStore()); // Then expect(useSWR).toHaveBeenCalledWith('loadPluginStore', expect.any(Function), { fallbackData: [], revalidateOnFocus: false, suspense: true, }); expect(result.current.data).toBeNull(); expect(result.current.error).toEqual(error); expect(result.current.isValidating).toBe(false); }); }); describe('installPlugin', () => { it('should install a plugin with valid manifest', async () => { const pluginIdentifier = 'plugin1'; const originalUpdateInstallLoadingState = useToolStore.getState().updateInstallLoadingState; const updateInstallLoadingStateMock = vi.fn(); act(() => { useToolStore.setState({ updateInstallLoadingState: updateInstallLoadingStateMock, }); }); const pluginManifestMock = { $schema: '../node_modules/@lobehub/chat-plugin-sdk/schema.json', api: [ { url: 'https://realtime-weather.chat-plugin.lobehub.com/api/v1', name: 'fetchCurrentWeather', description: '获取当前天气情况', parameters: { properties: { city: { description: '城市名称', type: 'string', }, }, required: ['city'], type: 'object', }, }, ], author: 'LobeHub', createAt: '2023-08-12', homepage: 'https://github.com/lobehub/chat-plugin-realtime-weather', identifier: 'realtime-weather', meta: { avatar: '🌈', tags: ['weather', 'realtime'], title: 'Realtime Weather', description: 'Get realtime weather information', }, ui: { url: 'https://realtime-weather.chat-plugin.lobehub.com/iframe', height: 310, }, version: '1', }; (toolService.getToolManifest as Mock).mockResolvedValue(pluginManifestMock); await act(async () => { await useToolStore.getState().installPlugin(pluginIdentifier); }); // Then expect(toolService.getToolManifest).toHaveBeenCalled(); expect(notification.error).not.toHaveBeenCalled(); expect(updateInstallLoadingStateMock).toHaveBeenCalledTimes(2); expect(pluginService.installPlugin).toHaveBeenCalledWith({ identifier: 'plugin1', type: 'plugin', manifest: pluginManifestMock, }); act(() => { useToolStore.setState({ updateInstallLoadingState: originalUpdateInstallLoadingState, }); }); }); it('should throw error with no error', async () => { // Given const error = new TypeError('noManifest'); // Mock necessary modules and functions (toolService.getToolManifest as Mock).mockRejectedValue(error); useToolStore.setState({ oldPluginItems: [ { identifier: 'plugin1', title: 'plugin1', avatar: '🍏', } as DiscoverPluginItem, ], }); await act(async () => { await useToolStore.getState().installPlugin('plugin1'); }); expect(notification.error).toHaveBeenCalledWith({ description: 'error.noManifest', message: 'error.installError', }); }); }); describe('installPlugins', () => { it('should install multiple plugins', async () => { // Given act(() => { useToolStore.setState({ oldPluginItems: [ { identifier: 'plugin1', title: 'plugin1', avatar: '🍏', manifest: 'https://abc.com/manifest.json', } as DiscoverPluginItem, { identifier: 'plugin2', title: 'plugin2', avatar: '🍏', manifest: 'https://abc.com/manifest.json', } as DiscoverPluginItem, ], }); }); const plugins = ['plugin1', 'plugin2']; (toolService.getToolManifest as Mock).mockResolvedValue(pluginManifestMock); // When await act(async () => { await useToolStore.getState().installPlugins(plugins); }); expect(pluginService.installPlugin).toHaveBeenCalledTimes(2); }); }); describe('unInstallPlugin', () => { it('should uninstall a plugin and remove its manifest', async () => { // Given const pluginIdentifier = 'plugin1'; act(() => { useToolStore.setState({ installedPlugins: [ { identifier: pluginIdentifier, type: 'plugin', manifest: { identifier: pluginIdentifier, meta: {}, } as LobeChatPluginManifest, }, ], }); }); // When act(() => { useToolStore.getState().uninstallPlugin(pluginIdentifier); }); // Then expect(pluginService.uninstallPlugin).toBeCalledWith(pluginIdentifier); }); }); describe('updateInstallLoadingState', () => { it('should update the loading state for a plugin', () => { const pluginIdentifier = 'abc'; const loadingState = true; const { result } = renderHook(() => useToolStore()); act(() => { result.current.updateInstallLoadingState(pluginIdentifier, loadingState); }); expect(result.current.pluginInstallLoading[pluginIdentifier]).toBe(loadingState); }); it('should clear the loading state for a plugin', () => { // Given const pluginIdentifier = 'dddd'; const loadingState = undefined; act(() => { useToolStore.setState({ pluginInstallLoading: { [pluginIdentifier]: true } }); }); const { result } = renderHook(() => useToolStore()); // When act(() => { result.current.updateInstallLoadingState(pluginIdentifier, loadingState); }); // Then expect(result.current.pluginInstallLoading[pluginIdentifier]).toBe(loadingState); }); }); });