mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
✅ test: add unit tests for mcpStore selectors (#13240)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
495
src/store/tool/slices/mcpStore/selectors.test.ts
Normal file
495
src/store/tool/slices/mcpStore/selectors.test.ts
Normal file
@@ -0,0 +1,495 @@
|
||||
import { type PluginItem } from '@lobehub/market-sdk';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { MCPInstallStep } from '@/types/plugins';
|
||||
|
||||
import { initialState } from '../../initialState';
|
||||
import { type ToolStoreState } from '../../initialState';
|
||||
import { PluginStoreTabs } from '../oldStore/initialState';
|
||||
import { mcpStoreSelectors } from './selectors';
|
||||
|
||||
const createMockPluginItem = (id: string, overrides: Partial<PluginItem> = {}): PluginItem =>
|
||||
({
|
||||
author: `Author of ${id}`,
|
||||
capabilities: [],
|
||||
category: 'general',
|
||||
commentCount: 0,
|
||||
connectionType: 'http',
|
||||
createdAt: '2024-01-01',
|
||||
description: `Description of ${id}`,
|
||||
github: '',
|
||||
haveCloudEndpoint: false,
|
||||
homepage: `https://example.com/${id}`,
|
||||
icon: `https://example.com/${id}/icon.png`,
|
||||
identifier: id,
|
||||
installCount: 0,
|
||||
isClaimed: false,
|
||||
isFeatured: false,
|
||||
isOfficial: false,
|
||||
isValidated: false,
|
||||
manifestUrl: `https://example.com/${id}/manifest`,
|
||||
name: `Plugin ${id}`,
|
||||
promptsCount: 0,
|
||||
ratingAverage: 0,
|
||||
ratingCount: 0,
|
||||
resourcesCount: 0,
|
||||
tags: ['tag1'],
|
||||
toolsCount: 0,
|
||||
updatedAt: '2024-01-01',
|
||||
...overrides,
|
||||
}) as unknown as PluginItem;
|
||||
|
||||
const mockMcpPluginItems: PluginItem[] = [
|
||||
createMockPluginItem('plugin-a'),
|
||||
createMockPluginItem('plugin-b'),
|
||||
createMockPluginItem('plugin-c'),
|
||||
];
|
||||
|
||||
const baseState: ToolStoreState = {
|
||||
...initialState,
|
||||
mcpPluginItems: mockMcpPluginItems,
|
||||
installedPlugins: [
|
||||
{
|
||||
identifier: 'plugin-a',
|
||||
type: 'plugin',
|
||||
manifest: { identifier: 'plugin-a', api: [], type: 'default' } as any,
|
||||
settings: {},
|
||||
},
|
||||
],
|
||||
listType: PluginStoreTabs.MCP,
|
||||
};
|
||||
|
||||
describe('mcpStoreSelectors', () => {
|
||||
describe('mcpPluginList', () => {
|
||||
it('should return all mcp plugins when listType is MCP', () => {
|
||||
const state: ToolStoreState = { ...baseState, listType: PluginStoreTabs.MCP };
|
||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should return only installed plugins when listType is Installed', () => {
|
||||
const state: ToolStoreState = { ...baseState, listType: PluginStoreTabs.Installed };
|
||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].identifier).toBe('plugin-a');
|
||||
});
|
||||
|
||||
it('should map plugin items to InstallPluginMeta format', () => {
|
||||
const state: ToolStoreState = { ...baseState, listType: PluginStoreTabs.MCP };
|
||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||
const item = result[0];
|
||||
|
||||
expect(item).toMatchObject({
|
||||
author: 'Author of plugin-a',
|
||||
homepage: 'https://example.com/plugin-a',
|
||||
identifier: 'plugin-a',
|
||||
type: 'plugin',
|
||||
meta: {
|
||||
avatar: 'https://example.com/plugin-a/icon.png',
|
||||
description: 'Description of plugin-a',
|
||||
tags: ['tag1'],
|
||||
title: 'Plugin plugin-a',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array when no plugins are installed and listType is Installed', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
installedPlugins: [],
|
||||
listType: PluginStoreTabs.Installed,
|
||||
};
|
||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return empty array when mcpPluginItems is empty', () => {
|
||||
const state: ToolStoreState = { ...baseState, mcpPluginItems: [] };
|
||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should filter correctly when multiple plugins are installed', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
installedPlugins: [
|
||||
{
|
||||
identifier: 'plugin-a',
|
||||
type: 'plugin',
|
||||
manifest: { identifier: 'plugin-a', api: [], type: 'default' } as any,
|
||||
settings: {},
|
||||
},
|
||||
{
|
||||
identifier: 'plugin-c',
|
||||
type: 'plugin',
|
||||
manifest: { identifier: 'plugin-c', api: [], type: 'default' } as any,
|
||||
settings: {},
|
||||
},
|
||||
],
|
||||
listType: PluginStoreTabs.Installed,
|
||||
};
|
||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((p) => p.identifier)).toEqual(['plugin-a', 'plugin-c']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPluginInstallLoading', () => {
|
||||
it('should return true when plugin is loading', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
pluginInstallLoading: { 'plugin-a': true },
|
||||
};
|
||||
const result = mcpStoreSelectors.isPluginInstallLoading('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when plugin is not loading', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
pluginInstallLoading: { 'plugin-a': false },
|
||||
};
|
||||
const result = mcpStoreSelectors.isPluginInstallLoading('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return undefined when plugin id is not in loading map', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
pluginInstallLoading: {},
|
||||
};
|
||||
const result = mcpStoreSelectors.isPluginInstallLoading('nonexistent')(state);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMCPInstallProgress', () => {
|
||||
it('should return install progress for existing plugin', () => {
|
||||
const progress = { progress: 50, step: MCPInstallStep.INSTALLING_PLUGIN };
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: { 'plugin-a': progress },
|
||||
};
|
||||
const result = mcpStoreSelectors.getMCPInstallProgress('plugin-a')(state);
|
||||
|
||||
expect(result).toEqual(progress);
|
||||
});
|
||||
|
||||
it('should return undefined for plugin with no progress', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {},
|
||||
};
|
||||
const result = mcpStoreSelectors.getMCPInstallProgress('plugin-a')(state);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMCPInstalling', () => {
|
||||
it('should return true when install progress exists', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {
|
||||
'plugin-a': { progress: 30, step: MCPInstallStep.FETCHING_MANIFEST },
|
||||
},
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPInstalling('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when no install progress exists', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {},
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPInstalling('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when progress is undefined', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: { 'plugin-a': undefined },
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPInstalling('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPluginById', () => {
|
||||
it('should return plugin when found by id', () => {
|
||||
const result = mcpStoreSelectors.getPluginById('plugin-b')(baseState);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.identifier).toBe('plugin-b');
|
||||
});
|
||||
|
||||
it('should return undefined when plugin not found', () => {
|
||||
const result = mcpStoreSelectors.getPluginById('nonexistent')(baseState);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when mcpPluginItems is empty', () => {
|
||||
const state: ToolStoreState = { ...baseState, mcpPluginItems: [] };
|
||||
const result = mcpStoreSelectors.getPluginById('plugin-a')(state);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('activeMCPPluginIdentifier', () => {
|
||||
it('should return active MCP plugin identifier when set', () => {
|
||||
const state: ToolStoreState = { ...baseState, activeMCPIdentifier: 'plugin-a' };
|
||||
const result = mcpStoreSelectors.activeMCPPluginIdentifier(state);
|
||||
|
||||
expect(result).toBe('plugin-a');
|
||||
});
|
||||
|
||||
it('should return undefined when no active plugin', () => {
|
||||
const state: ToolStoreState = { ...baseState, activeMCPIdentifier: undefined };
|
||||
const result = mcpStoreSelectors.activeMCPPluginIdentifier(state);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMCPPluginRequiringConfig', () => {
|
||||
it('should return config schema when plugin requires config', () => {
|
||||
const configSchema = { type: 'object', properties: { apiKey: { type: 'string' } } };
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {
|
||||
'plugin-a': { progress: 100, step: MCPInstallStep.COMPLETED, configSchema },
|
||||
},
|
||||
};
|
||||
const result = mcpStoreSelectors.getMCPPluginRequiringConfig('plugin-a')(state);
|
||||
|
||||
expect(result).toEqual(configSchema);
|
||||
});
|
||||
|
||||
it('should return undefined when plugin has no config schema', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {
|
||||
'plugin-a': { progress: 100, step: MCPInstallStep.COMPLETED },
|
||||
},
|
||||
};
|
||||
const result = mcpStoreSelectors.getMCPPluginRequiringConfig('plugin-a')(state);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when plugin has no progress', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {},
|
||||
};
|
||||
const result = mcpStoreSelectors.getMCPPluginRequiringConfig('plugin-a')(state);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMCPPluginRequiringConfig', () => {
|
||||
it('should return true when plugin has config schema', () => {
|
||||
const configSchema = { type: 'object' };
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {
|
||||
'plugin-a': { progress: 100, step: MCPInstallStep.COMPLETED, configSchema },
|
||||
},
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPPluginRequiringConfig('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when plugin has no config schema', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {
|
||||
'plugin-a': { progress: 100, step: MCPInstallStep.COMPLETED },
|
||||
},
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPPluginRequiringConfig('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when plugin has no progress', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {},
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPPluginRequiringConfig('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMCPInstallInProgress', () => {
|
||||
it('should return true when plugin is installing (not error, no needsConfig)', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {
|
||||
'plugin-a': { progress: 50, step: MCPInstallStep.INSTALLING_PLUGIN },
|
||||
},
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPInstallInProgress('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when plugin step is Error', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {
|
||||
'plugin-a': { progress: 0, step: MCPInstallStep.ERROR },
|
||||
},
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPInstallInProgress('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when plugin needsConfig is true', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {
|
||||
'plugin-a': {
|
||||
progress: 100,
|
||||
step: MCPInstallStep.COMPLETED,
|
||||
needsConfig: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPInstallInProgress('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no progress exists', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {},
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPInstallInProgress('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when progress is FETCHING_MANIFEST (no error, no needsConfig)', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpInstallProgress: {
|
||||
'plugin-a': { progress: 20, step: MCPInstallStep.FETCHING_MANIFEST },
|
||||
},
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPInstallInProgress('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMCPConnectionTesting', () => {
|
||||
it('should return true when connection is being tested', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpTestLoading: { 'plugin-a': true },
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPConnectionTesting('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when connection is not being tested', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpTestLoading: { 'plugin-a': false },
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPConnectionTesting('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when plugin id is not in test loading map', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpTestLoading: {},
|
||||
};
|
||||
const result = mcpStoreSelectors.isMCPConnectionTesting('plugin-a')(state);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMCPConnectionTestError', () => {
|
||||
it('should return error message when test failed', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpTestErrors: { 'plugin-a': 'Connection refused' },
|
||||
};
|
||||
const result = mcpStoreSelectors.getMCPConnectionTestError('plugin-a')(state);
|
||||
|
||||
expect(result).toBe('Connection refused');
|
||||
});
|
||||
|
||||
it('should return undefined when no test error exists', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpTestErrors: {},
|
||||
};
|
||||
const result = mcpStoreSelectors.getMCPConnectionTestError('plugin-a')(state);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMCPConnectionTestState', () => {
|
||||
it('should return both error and loading state', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpTestErrors: { 'plugin-a': 'Timeout' },
|
||||
mcpTestLoading: { 'plugin-a': false },
|
||||
};
|
||||
const result = mcpStoreSelectors.getMCPConnectionTestState('plugin-a')(state);
|
||||
|
||||
expect(result).toEqual({ error: 'Timeout', loading: false });
|
||||
});
|
||||
|
||||
it('should return loading true when testing', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpTestErrors: {},
|
||||
mcpTestLoading: { 'plugin-a': true },
|
||||
};
|
||||
const result = mcpStoreSelectors.getMCPConnectionTestState('plugin-a')(state);
|
||||
|
||||
expect(result).toEqual({ error: undefined, loading: true });
|
||||
});
|
||||
|
||||
it('should return default values when plugin has no test state', () => {
|
||||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
mcpTestErrors: {},
|
||||
mcpTestLoading: {},
|
||||
};
|
||||
const result = mcpStoreSelectors.getMCPConnectionTestState('plugin-a')(state);
|
||||
|
||||
expect(result).toEqual({ error: undefined, loading: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user