mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00

* WIP: app.locals refactoring
WIP: appConfig
fix: update memory configuration retrieval to use getAppConfig based on user role
fix: update comment for AppConfig interface to clarify purpose
🏷️ refactor: Update tests to use getAppConfig for endpoint configurations
ci: Update AppService tests to initialize app config instead of app.locals
ci: Integrate getAppConfig into remaining tests
refactor: Update multer storage destination to use promise-based getAppConfig and improve error handling in tests
refactor: Rename initializeAppConfig to setAppConfig and update related tests
ci: Mock getAppConfig in various tests to provide default configurations
refactor: Update convertMCPToolsToPlugins to use mcpManager for server configuration and adjust related tests
chore: rename `Config/getAppConfig` -> `Config/app`
fix: streamline OpenAI image tools configuration by removing direct appConfig dependency and using function parameters
chore: correct parameter documentation for imageOutputType in ToolService.js
refactor: remove `getCustomConfig` dependency in config route
refactor: update domain validation to use appConfig for allowed domains
refactor: use appConfig registration property
chore: remove app parameter from AppService invocation
refactor: update AppConfig interface to correct registration and turnstile configurations
refactor: remove getCustomConfig dependency and use getAppConfig in PluginController, multer, and MCP services
refactor: replace getCustomConfig with getAppConfig in STTService, TTSService, and related files
refactor: replace getCustomConfig with getAppConfig in Conversation and Message models, update tempChatRetention functions to use AppConfig type
refactor: update getAppConfig calls in Conversation and Message models to include user role for temporary chat expiration
ci: update related tests
refactor: update getAppConfig call in getCustomConfigSpeech to include user role
fix: update appConfig usage to access allowedDomains from actions instead of registration
refactor: enhance AppConfig to include fileStrategies and update related file strategy logic
refactor: update imports to use normalizeEndpointName from @librechat/api and remove redundant definitions
chore: remove deprecated unused RunManager
refactor: get balance config primarily from appConfig
refactor: remove customConfig dependency for appConfig and streamline loadConfigModels logic
refactor: remove getCustomConfig usage and use app config in file citations
refactor: consolidate endpoint loading logic into loadEndpoints function
refactor: update appConfig access to use endpoints structure across various services
refactor: implement custom endpoints configuration and streamline endpoint loading logic
refactor: update getAppConfig call to include user role parameter
refactor: streamline endpoint configuration and enhance appConfig usage across services
refactor: replace getMCPAuthMap with getUserMCPAuthMap and remove unused getCustomConfig file
refactor: add type annotation for loadedEndpoints in loadEndpoints function
refactor: move /services/Files/images/parse to TS API
chore: add missing FILE_CITATIONS permission to IRole interface
refactor: restructure toolkits to TS API
refactor: separate manifest logic into its own module
refactor: consolidate tool loading logic into a new tools module for startup logic
refactor: move interface config logic to TS API
refactor: migrate checkEmailConfig to TypeScript and update imports
refactor: add FunctionTool interface and availableTools to AppConfig
refactor: decouple caching and DB operations from AppService, make part of consolidated `getAppConfig`
WIP: fix tests
* fix: rebase conflicts
* refactor: remove app.locals references
* refactor: replace getBalanceConfig with getAppConfig in various strategies and middleware
* refactor: replace appConfig?.balance with getBalanceConfig in various controllers and clients
* test: add balance configuration to titleConvo method in AgentClient tests
* chore: remove unused `openai-chat-tokens` package
* chore: remove unused imports in initializeMCPs.js
* refactor: update balance configuration to use getAppConfig instead of getBalanceConfig
* refactor: integrate configMiddleware for centralized configuration handling
* refactor: optimize email domain validation by removing unnecessary async calls
* refactor: simplify multer storage configuration by removing async calls
* refactor: reorder imports for better readability in user.js
* refactor: replace getAppConfig calls with req.config for improved performance
* chore: replace getAppConfig calls with req.config in tests for centralized configuration handling
* chore: remove unused override config
* refactor: add configMiddleware to endpoint route and replace getAppConfig with req.config
* chore: remove customConfig parameter from TTSService constructor
* refactor: pass appConfig from request to processFileCitations for improved configuration handling
* refactor: remove configMiddleware from endpoint route and retrieve appConfig directly in getEndpointsConfig if not in `req.config`
* test: add mockAppConfig to processFileCitations tests for improved configuration handling
* fix: pass req.config to hasCustomUserVars and call without await after synchronous refactor
* fix: type safety in useExportConversation
* refactor: retrieve appConfig using getAppConfig in PluginController and remove configMiddleware from plugins route, to avoid always retrieving when plugins are cached
* chore: change `MongoUser` typedef to `IUser`
* fix: Add `user` and `config` fields to ServerRequest and update JSDoc type annotations from Express.Request to ServerRequest
* fix: remove unused setAppConfig mock from Server configuration tests
668 lines
23 KiB
JavaScript
668 lines
23 KiB
JavaScript
const { Constants } = require('librechat-data-provider');
|
|
const { getCachedTools, getAppConfig } = require('~/server/services/Config');
|
|
const { getLogStores } = require('~/cache');
|
|
|
|
jest.mock('@librechat/data-schemas', () => ({
|
|
logger: {
|
|
debug: jest.fn(),
|
|
error: jest.fn(),
|
|
warn: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
jest.mock('~/server/services/Config', () => ({
|
|
getCachedTools: jest.fn(),
|
|
getAppConfig: jest.fn().mockResolvedValue({
|
|
filteredTools: [],
|
|
includedTools: [],
|
|
}),
|
|
setCachedTools: jest.fn(),
|
|
mergeUserTools: jest.fn(),
|
|
}));
|
|
|
|
// loadAndFormatTools mock removed - no longer used in PluginController
|
|
|
|
jest.mock('~/config', () => ({
|
|
getMCPManager: jest.fn(() => ({
|
|
getAllToolFunctions: jest.fn().mockResolvedValue({}),
|
|
getRawConfig: jest.fn().mockReturnValue({}),
|
|
})),
|
|
getFlowStateManager: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('~/app/clients/tools', () => ({
|
|
availableTools: [],
|
|
toolkits: [],
|
|
}));
|
|
|
|
jest.mock('~/cache', () => ({
|
|
getLogStores: jest.fn(),
|
|
}));
|
|
|
|
const { getAvailableTools, getAvailablePluginsController } = require('./PluginController');
|
|
|
|
describe('PluginController', () => {
|
|
let mockReq, mockRes, mockCache;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockReq = {
|
|
user: { id: 'test-user-id' },
|
|
config: {
|
|
filteredTools: [],
|
|
includedTools: [],
|
|
},
|
|
};
|
|
mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
|
mockCache = { get: jest.fn(), set: jest.fn() };
|
|
getLogStores.mockReturnValue(mockCache);
|
|
|
|
// Clear availableTools and toolkits arrays before each test
|
|
require('~/app/clients/tools').availableTools.length = 0;
|
|
require('~/app/clients/tools').toolkits.length = 0;
|
|
|
|
// Reset getCachedTools mock to ensure clean state
|
|
getCachedTools.mockReset();
|
|
|
|
// Reset getAppConfig mock to ensure clean state with default values
|
|
getAppConfig.mockReset();
|
|
getAppConfig.mockResolvedValue({
|
|
filteredTools: [],
|
|
includedTools: [],
|
|
});
|
|
});
|
|
|
|
describe('getAvailablePluginsController', () => {
|
|
it('should use filterUniquePlugins to remove duplicate plugins', async () => {
|
|
// Add plugins with duplicates to availableTools
|
|
const mockPlugins = [
|
|
{ name: 'Plugin1', pluginKey: 'key1', description: 'First' },
|
|
{ name: 'Plugin1', pluginKey: 'key1', description: 'First duplicate' },
|
|
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second' },
|
|
];
|
|
|
|
require('~/app/clients/tools').availableTools.push(...mockPlugins);
|
|
|
|
mockCache.get.mockResolvedValue(null);
|
|
|
|
// Configure getAppConfig to return the expected config
|
|
getAppConfig.mockResolvedValueOnce({
|
|
filteredTools: [],
|
|
includedTools: [],
|
|
});
|
|
|
|
await getAvailablePluginsController(mockReq, mockRes);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
const responseData = mockRes.json.mock.calls[0][0];
|
|
// The real filterUniquePlugins should have removed the duplicate
|
|
expect(responseData).toHaveLength(2);
|
|
expect(responseData[0].pluginKey).toBe('key1');
|
|
expect(responseData[1].pluginKey).toBe('key2');
|
|
});
|
|
|
|
it('should use checkPluginAuth to verify plugin authentication', async () => {
|
|
// checkPluginAuth returns false for plugins without authConfig
|
|
// so authenticated property won't be added
|
|
const mockPlugin = { name: 'Plugin1', pluginKey: 'key1', description: 'First' };
|
|
|
|
require('~/app/clients/tools').availableTools.push(mockPlugin);
|
|
mockCache.get.mockResolvedValue(null);
|
|
|
|
// Configure getAppConfig to return the expected config
|
|
getAppConfig.mockResolvedValueOnce({
|
|
filteredTools: [],
|
|
includedTools: [],
|
|
});
|
|
|
|
await getAvailablePluginsController(mockReq, mockRes);
|
|
|
|
const responseData = mockRes.json.mock.calls[0][0];
|
|
// The real checkPluginAuth returns false for plugins without authConfig, so authenticated property is not added
|
|
expect(responseData[0].authenticated).toBeUndefined();
|
|
});
|
|
|
|
it('should return cached plugins when available', async () => {
|
|
const cachedPlugins = [
|
|
{ name: 'CachedPlugin', pluginKey: 'cached', description: 'Cached plugin' },
|
|
];
|
|
|
|
mockCache.get.mockResolvedValue(cachedPlugins);
|
|
|
|
await getAvailablePluginsController(mockReq, mockRes);
|
|
|
|
// When cache is hit, we return immediately without processing
|
|
expect(mockRes.json).toHaveBeenCalledWith(cachedPlugins);
|
|
});
|
|
|
|
it('should filter plugins based on includedTools', async () => {
|
|
const mockPlugins = [
|
|
{ name: 'Plugin1', pluginKey: 'key1', description: 'First' },
|
|
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second' },
|
|
];
|
|
|
|
require('~/app/clients/tools').availableTools.push(...mockPlugins);
|
|
mockCache.get.mockResolvedValue(null);
|
|
|
|
// Configure getAppConfig to return config with includedTools
|
|
getAppConfig.mockResolvedValueOnce({
|
|
filteredTools: [],
|
|
includedTools: ['key1'],
|
|
});
|
|
|
|
await getAvailablePluginsController(mockReq, mockRes);
|
|
|
|
const responseData = mockRes.json.mock.calls[0][0];
|
|
expect(responseData).toHaveLength(1);
|
|
expect(responseData[0].pluginKey).toBe('key1');
|
|
});
|
|
});
|
|
|
|
describe('getAvailableTools', () => {
|
|
it('should use convertMCPToolsToPlugins for user-specific MCP tools', async () => {
|
|
const mockUserTools = {
|
|
[`tool1${Constants.mcp_delimiter}server1`]: {
|
|
type: 'function',
|
|
function: {
|
|
name: `tool1${Constants.mcp_delimiter}server1`,
|
|
description: 'Tool 1',
|
|
parameters: { type: 'object', properties: {} },
|
|
},
|
|
},
|
|
};
|
|
|
|
mockCache.get.mockResolvedValue(null);
|
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
|
mockReq.config = {
|
|
mcpConfig: null,
|
|
paths: { structuredTools: '/mock/path' },
|
|
};
|
|
|
|
// Mock second call to return tool definitions (includeGlobal: true)
|
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
const responseData = mockRes.json.mock.calls[0][0];
|
|
expect(responseData).toBeDefined();
|
|
expect(Array.isArray(responseData)).toBe(true);
|
|
expect(responseData.length).toBeGreaterThan(0);
|
|
const convertedTool = responseData.find(
|
|
(tool) => tool.pluginKey === `tool1${Constants.mcp_delimiter}server1`,
|
|
);
|
|
expect(convertedTool).toBeDefined();
|
|
// The real convertMCPToolsToPlugins extracts the name from the delimiter
|
|
expect(convertedTool.name).toBe('tool1');
|
|
});
|
|
|
|
it('should use filterUniquePlugins to deduplicate combined tools', async () => {
|
|
const mockUserTools = {
|
|
'user-tool': {
|
|
type: 'function',
|
|
function: {
|
|
name: 'user-tool',
|
|
description: 'User tool',
|
|
parameters: { type: 'object', properties: {} },
|
|
},
|
|
},
|
|
};
|
|
|
|
const mockCachedPlugins = [
|
|
{ name: 'user-tool', pluginKey: 'user-tool', description: 'Duplicate user tool' },
|
|
{ name: 'ManifestTool', pluginKey: 'manifest-tool', description: 'Manifest tool' },
|
|
];
|
|
|
|
mockCache.get.mockResolvedValue(mockCachedPlugins);
|
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
|
mockReq.config = {
|
|
mcpConfig: null,
|
|
paths: { structuredTools: '/mock/path' },
|
|
};
|
|
|
|
// Mock second call to return tool definitions
|
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
const responseData = mockRes.json.mock.calls[0][0];
|
|
expect(Array.isArray(responseData)).toBe(true);
|
|
// The real filterUniquePlugins should have deduplicated tools with same pluginKey
|
|
const userToolCount = responseData.filter((tool) => tool.pluginKey === 'user-tool').length;
|
|
expect(userToolCount).toBe(1);
|
|
});
|
|
|
|
it('should use checkPluginAuth to verify authentication status', async () => {
|
|
// Add a plugin to availableTools that will be checked
|
|
const mockPlugin = {
|
|
name: 'Tool1',
|
|
pluginKey: 'tool1',
|
|
description: 'Tool 1',
|
|
// No authConfig means checkPluginAuth returns false
|
|
};
|
|
|
|
require('~/app/clients/tools').availableTools.push(mockPlugin);
|
|
|
|
mockCache.get.mockResolvedValue(null);
|
|
// First call returns null for user tools
|
|
getCachedTools.mockResolvedValueOnce(null);
|
|
mockReq.config = {
|
|
mcpConfig: null,
|
|
paths: { structuredTools: '/mock/path' },
|
|
};
|
|
|
|
// Second call (with includeGlobal: true) returns the tool definitions
|
|
getCachedTools.mockResolvedValueOnce({
|
|
tool1: {
|
|
type: 'function',
|
|
function: {
|
|
name: 'tool1',
|
|
description: 'Tool 1',
|
|
parameters: {},
|
|
},
|
|
},
|
|
});
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
const responseData = mockRes.json.mock.calls[0][0];
|
|
expect(Array.isArray(responseData)).toBe(true);
|
|
const tool = responseData.find((t) => t.pluginKey === 'tool1');
|
|
expect(tool).toBeDefined();
|
|
// The real checkPluginAuth returns false for plugins without authConfig, so authenticated property is not added
|
|
expect(tool.authenticated).toBeUndefined();
|
|
});
|
|
|
|
it('should use getToolkitKey for toolkit validation', async () => {
|
|
const mockToolkit = {
|
|
name: 'Toolkit1',
|
|
pluginKey: 'toolkit1',
|
|
description: 'Toolkit 1',
|
|
toolkit: true,
|
|
};
|
|
|
|
require('~/app/clients/tools').availableTools.push(mockToolkit);
|
|
|
|
// Mock toolkits to have a mapping
|
|
require('~/app/clients/tools').toolkits.push({
|
|
name: 'Toolkit1',
|
|
pluginKey: 'toolkit1',
|
|
tools: ['toolkit1_function'],
|
|
});
|
|
|
|
mockCache.get.mockResolvedValue(null);
|
|
// First call returns null for user tools
|
|
getCachedTools.mockResolvedValueOnce(null);
|
|
mockReq.config = {
|
|
mcpConfig: null,
|
|
paths: { structuredTools: '/mock/path' },
|
|
};
|
|
|
|
// Second call (with includeGlobal: true) returns the tool definitions
|
|
getCachedTools.mockResolvedValueOnce({
|
|
toolkit1_function: {
|
|
type: 'function',
|
|
function: {
|
|
name: 'toolkit1_function',
|
|
description: 'Toolkit function',
|
|
parameters: {},
|
|
},
|
|
},
|
|
});
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
const responseData = mockRes.json.mock.calls[0][0];
|
|
expect(Array.isArray(responseData)).toBe(true);
|
|
const toolkit = responseData.find((t) => t.pluginKey === 'toolkit1');
|
|
expect(toolkit).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('plugin.icon behavior', () => {
|
|
const callGetAvailableToolsWithMCPServer = async (serverConfig) => {
|
|
mockCache.get.mockResolvedValue(null);
|
|
|
|
const functionTools = {
|
|
[`test-tool${Constants.mcp_delimiter}test-server`]: {
|
|
type: 'function',
|
|
function: {
|
|
name: `test-tool${Constants.mcp_delimiter}test-server`,
|
|
description: 'A test tool',
|
|
parameters: { type: 'object', properties: {} },
|
|
},
|
|
},
|
|
};
|
|
|
|
// Mock the MCP manager to return tools and server config
|
|
const mockMCPManager = {
|
|
getAllToolFunctions: jest.fn().mockResolvedValue(functionTools),
|
|
getRawConfig: jest.fn().mockReturnValue(serverConfig),
|
|
};
|
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
|
|
|
// First call returns empty user tools
|
|
getCachedTools.mockResolvedValueOnce({});
|
|
|
|
// Mock getAppConfig to return the mcpConfig
|
|
mockReq.config = {
|
|
mcpConfig: {
|
|
'test-server': serverConfig,
|
|
},
|
|
};
|
|
|
|
// Second call (with includeGlobal: true) returns the tool definitions
|
|
getCachedTools.mockResolvedValueOnce(functionTools);
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
const responseData = mockRes.json.mock.calls[0][0];
|
|
return responseData.find(
|
|
(tool) => tool.pluginKey === `test-tool${Constants.mcp_delimiter}test-server`,
|
|
);
|
|
};
|
|
|
|
it('should set plugin.icon when iconPath is defined', async () => {
|
|
const serverConfig = {
|
|
iconPath: '/path/to/icon.png',
|
|
};
|
|
const testTool = await callGetAvailableToolsWithMCPServer(serverConfig);
|
|
expect(testTool.icon).toBe('/path/to/icon.png');
|
|
});
|
|
|
|
it('should set plugin.icon to undefined when iconPath is not defined', async () => {
|
|
const serverConfig = {};
|
|
const testTool = await callGetAvailableToolsWithMCPServer(serverConfig);
|
|
expect(testTool.icon).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('helper function integration', () => {
|
|
it('should properly handle MCP tools with custom user variables', async () => {
|
|
const appConfig = {
|
|
mcpConfig: {
|
|
'test-server': {
|
|
customUserVars: {
|
|
API_KEY: { title: 'API Key', description: 'Your API key' },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
// Mock MCP tools returned by getAllToolFunctions
|
|
const mcpToolFunctions = {
|
|
[`tool1${Constants.mcp_delimiter}test-server`]: {
|
|
type: 'function',
|
|
function: {
|
|
name: `tool1${Constants.mcp_delimiter}test-server`,
|
|
description: 'Tool 1',
|
|
parameters: {},
|
|
},
|
|
},
|
|
};
|
|
|
|
// Mock the MCP manager to return tools
|
|
const mockMCPManager = {
|
|
getAllToolFunctions: jest.fn().mockResolvedValue(mcpToolFunctions),
|
|
getRawConfig: jest.fn().mockReturnValue({
|
|
customUserVars: {
|
|
API_KEY: { title: 'API Key', description: 'Your API key' },
|
|
},
|
|
}),
|
|
};
|
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
|
|
|
mockCache.get.mockResolvedValue(null);
|
|
mockReq.config = appConfig;
|
|
|
|
// First call returns user tools (empty in this case)
|
|
getCachedTools.mockResolvedValueOnce({});
|
|
|
|
// Second call (with includeGlobal: true) returns tool definitions including our MCP tool
|
|
getCachedTools.mockResolvedValueOnce(mcpToolFunctions);
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
const responseData = mockRes.json.mock.calls[0][0];
|
|
expect(Array.isArray(responseData)).toBe(true);
|
|
|
|
// Find the MCP tool in the response
|
|
const mcpTool = responseData.find(
|
|
(tool) => tool.pluginKey === `tool1${Constants.mcp_delimiter}test-server`,
|
|
);
|
|
|
|
// The actual implementation adds authConfig and sets authenticated to false when customUserVars exist
|
|
expect(mcpTool).toBeDefined();
|
|
expect(mcpTool.authConfig).toEqual([
|
|
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
|
|
]);
|
|
expect(mcpTool.authenticated).toBe(false);
|
|
});
|
|
|
|
it('should handle error cases gracefully', async () => {
|
|
mockCache.get.mockRejectedValue(new Error('Cache error'));
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Cache error' });
|
|
});
|
|
});
|
|
|
|
describe('edge cases with undefined/null values', () => {
|
|
it('should handle undefined cache gracefully', async () => {
|
|
getLogStores.mockReturnValue(undefined);
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
});
|
|
|
|
it('should handle null cachedTools and cachedUserTools', async () => {
|
|
mockCache.get.mockResolvedValue(null);
|
|
// First call returns null for user tools
|
|
getCachedTools.mockResolvedValueOnce(null);
|
|
mockReq.config = {
|
|
mcpConfig: null,
|
|
paths: { structuredTools: '/mock/path' },
|
|
};
|
|
|
|
// Mock MCP manager to return no tools
|
|
const mockMCPManager = {
|
|
getAllToolFunctions: jest.fn().mockResolvedValue({}),
|
|
getRawConfig: jest.fn().mockReturnValue({}),
|
|
};
|
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
|
|
|
// Second call (with includeGlobal: true) returns empty object instead of null
|
|
getCachedTools.mockResolvedValueOnce({});
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
// Should handle null values gracefully
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
expect(mockRes.json).toHaveBeenCalledWith([]);
|
|
});
|
|
|
|
it('should handle when getCachedTools returns undefined', async () => {
|
|
mockCache.get.mockResolvedValue(null);
|
|
mockReq.config = {
|
|
mcpConfig: null,
|
|
paths: { structuredTools: '/mock/path' },
|
|
};
|
|
|
|
// Mock getCachedTools to return undefined for both calls
|
|
getCachedTools.mockReset();
|
|
getCachedTools.mockResolvedValueOnce(undefined).mockResolvedValueOnce(undefined);
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
// Should handle undefined values gracefully
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
expect(mockRes.json).toHaveBeenCalledWith([]);
|
|
});
|
|
|
|
it('should handle cachedToolsArray and userPlugins both being defined', async () => {
|
|
const cachedTools = [{ name: 'CachedTool', pluginKey: 'cached-tool', description: 'Cached' }];
|
|
// Use MCP delimiter for the user tool so convertMCPToolsToPlugins works
|
|
const userTools = {
|
|
[`user-tool${Constants.mcp_delimiter}server1`]: {
|
|
type: 'function',
|
|
function: {
|
|
name: `user-tool${Constants.mcp_delimiter}server1`,
|
|
description: 'User tool',
|
|
parameters: {},
|
|
},
|
|
},
|
|
};
|
|
|
|
mockCache.get.mockResolvedValue(cachedTools);
|
|
getCachedTools.mockResolvedValueOnce(userTools);
|
|
mockReq.config = {
|
|
mcpConfig: null,
|
|
paths: { structuredTools: '/mock/path' },
|
|
};
|
|
|
|
// The controller expects a second call to getCachedTools
|
|
getCachedTools.mockResolvedValueOnce({
|
|
'cached-tool': { type: 'function', function: { name: 'cached-tool' } },
|
|
[`user-tool${Constants.mcp_delimiter}server1`]:
|
|
userTools[`user-tool${Constants.mcp_delimiter}server1`],
|
|
});
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
const responseData = mockRes.json.mock.calls[0][0];
|
|
// Should have both cached and user tools
|
|
expect(responseData.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it('should handle empty toolDefinitions object', async () => {
|
|
mockCache.get.mockResolvedValue(null);
|
|
// Reset getCachedTools to ensure clean state
|
|
getCachedTools.mockReset();
|
|
getCachedTools.mockResolvedValue({});
|
|
mockReq.config = {}; // No mcpConfig at all
|
|
|
|
// Ensure no plugins are available
|
|
require('~/app/clients/tools').availableTools.length = 0;
|
|
|
|
// Reset MCP manager to default state
|
|
const mockMCPManager = {
|
|
getAllToolFunctions: jest.fn().mockResolvedValue({}),
|
|
getRawConfig: jest.fn().mockReturnValue({}),
|
|
};
|
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
// With empty tool definitions, no tools should be in the final output
|
|
expect(mockRes.json).toHaveBeenCalledWith([]);
|
|
});
|
|
|
|
it('should handle MCP tools without customUserVars', async () => {
|
|
const appConfig = {
|
|
mcpConfig: {
|
|
'test-server': {
|
|
// No customUserVars defined
|
|
},
|
|
},
|
|
};
|
|
|
|
const mockUserTools = {
|
|
[`tool1${Constants.mcp_delimiter}test-server`]: {
|
|
type: 'function',
|
|
function: {
|
|
name: `tool1${Constants.mcp_delimiter}test-server`,
|
|
description: 'Tool 1',
|
|
parameters: { type: 'object', properties: {} },
|
|
},
|
|
},
|
|
};
|
|
|
|
// Mock the MCP manager to return the tools
|
|
const mockMCPManager = {
|
|
getAllToolFunctions: jest.fn().mockResolvedValue(mockUserTools),
|
|
getRawConfig: jest.fn().mockReturnValue({
|
|
// No customUserVars defined
|
|
}),
|
|
};
|
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
|
|
|
mockCache.get.mockResolvedValue(null);
|
|
mockReq.config = appConfig;
|
|
// First call returns empty user tools
|
|
getCachedTools.mockResolvedValueOnce({});
|
|
|
|
// Second call (with includeGlobal: true) returns the tool definitions
|
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
|
|
|
// Ensure no plugins in availableTools for clean test
|
|
require('~/app/clients/tools').availableTools.length = 0;
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
const responseData = mockRes.json.mock.calls[0][0];
|
|
expect(Array.isArray(responseData)).toBe(true);
|
|
expect(responseData.length).toBeGreaterThan(0);
|
|
|
|
const mcpTool = responseData.find(
|
|
(tool) => tool.pluginKey === `tool1${Constants.mcp_delimiter}test-server`,
|
|
);
|
|
|
|
expect(mcpTool).toBeDefined();
|
|
expect(mcpTool.authenticated).toBe(true);
|
|
// The actual implementation sets authConfig to empty array when no customUserVars
|
|
expect(mcpTool.authConfig).toEqual([]);
|
|
});
|
|
|
|
it('should handle undefined filteredTools and includedTools', async () => {
|
|
mockReq.config = {};
|
|
mockCache.get.mockResolvedValue(null);
|
|
|
|
// Configure getAppConfig to return config with undefined properties
|
|
// The controller will use default values [] for filteredTools and includedTools
|
|
getAppConfig.mockResolvedValueOnce({});
|
|
|
|
await getAvailablePluginsController(mockReq, mockRes);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
expect(mockRes.json).toHaveBeenCalledWith([]);
|
|
});
|
|
|
|
it('should handle toolkit with undefined toolDefinitions keys', async () => {
|
|
const mockToolkit = {
|
|
name: 'Toolkit1',
|
|
pluginKey: 'toolkit1',
|
|
description: 'Toolkit 1',
|
|
toolkit: true,
|
|
};
|
|
|
|
// No need to mock app.locals anymore as it's not used
|
|
|
|
// Add the toolkit to availableTools
|
|
require('~/app/clients/tools').availableTools.push(mockToolkit);
|
|
|
|
mockCache.get.mockResolvedValue(null);
|
|
// First call returns empty object
|
|
getCachedTools.mockResolvedValueOnce({});
|
|
mockReq.config = {
|
|
mcpConfig: null,
|
|
paths: { structuredTools: '/mock/path' },
|
|
};
|
|
|
|
// Second call (with includeGlobal: true) returns empty object to avoid null reference error
|
|
getCachedTools.mockResolvedValueOnce({});
|
|
|
|
await getAvailableTools(mockReq, mockRes);
|
|
|
|
// Should handle null toolDefinitions gracefully
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
});
|
|
});
|
|
});
|