mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-28 06:08:50 +01: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
491 lines
17 KiB
TypeScript
491 lines
17 KiB
TypeScript
import { AuthType, Constants, EToolResources } from 'librechat-data-provider';
|
|
import type { TPlugin, FunctionTool } from 'librechat-data-provider';
|
|
import type { MCPManager } from '~/mcp/MCPManager';
|
|
import {
|
|
convertMCPToolsToPlugins,
|
|
filterUniquePlugins,
|
|
checkPluginAuth,
|
|
getToolkitKey,
|
|
} from './format';
|
|
|
|
describe('format.ts helper functions', () => {
|
|
describe('filterUniquePlugins', () => {
|
|
it('should return empty array when plugins is undefined', () => {
|
|
const result = filterUniquePlugins(undefined);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should return empty array when plugins is empty', () => {
|
|
const result = filterUniquePlugins([]);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should filter out duplicate plugins based on pluginKey', () => {
|
|
const plugins: TPlugin[] = [
|
|
{ name: 'Plugin1', pluginKey: 'key1', description: 'First plugin' },
|
|
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second plugin' },
|
|
{ name: 'Plugin1 Duplicate', pluginKey: 'key1', description: 'Duplicate of first' },
|
|
{ name: 'Plugin3', pluginKey: 'key3', description: 'Third plugin' },
|
|
];
|
|
|
|
const result = filterUniquePlugins(plugins);
|
|
expect(result).toHaveLength(3);
|
|
expect(result[0].pluginKey).toBe('key1');
|
|
expect(result[1].pluginKey).toBe('key2');
|
|
expect(result[2].pluginKey).toBe('key3');
|
|
// The first occurrence should be kept
|
|
expect(result[0].name).toBe('Plugin1');
|
|
});
|
|
|
|
it('should handle plugins with identical data', () => {
|
|
const plugin: TPlugin = { name: 'Plugin', pluginKey: 'key', description: 'Test' };
|
|
const plugins: TPlugin[] = [plugin, plugin, plugin];
|
|
|
|
const result = filterUniquePlugins(plugins);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toEqual(plugin);
|
|
});
|
|
});
|
|
|
|
describe('checkPluginAuth', () => {
|
|
const originalEnv = process.env;
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...originalEnv };
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
it('should return false when plugin is undefined', () => {
|
|
const result = checkPluginAuth(undefined);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when authConfig is undefined', () => {
|
|
const plugin: TPlugin = { name: 'Test', pluginKey: 'test', description: 'Test plugin' };
|
|
const result = checkPluginAuth(plugin);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when authConfig is empty array', () => {
|
|
const plugin: TPlugin = {
|
|
name: 'Test',
|
|
pluginKey: 'test',
|
|
description: 'Test plugin',
|
|
authConfig: [],
|
|
};
|
|
const result = checkPluginAuth(plugin);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true when all required auth fields have valid env values', () => {
|
|
process.env.API_KEY = 'valid-key';
|
|
process.env.SECRET_KEY = 'valid-secret';
|
|
|
|
const plugin: TPlugin = {
|
|
name: 'Test',
|
|
pluginKey: 'test',
|
|
description: 'Test plugin',
|
|
authConfig: [
|
|
{ authField: 'API_KEY', label: 'API Key', description: 'API Key' },
|
|
{ authField: 'SECRET_KEY', label: 'Secret Key', description: 'Secret Key' },
|
|
],
|
|
};
|
|
|
|
const result = checkPluginAuth(plugin);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when any required auth field is missing', () => {
|
|
process.env.API_KEY = 'valid-key';
|
|
// SECRET_KEY is not set
|
|
|
|
const plugin: TPlugin = {
|
|
name: 'Test',
|
|
pluginKey: 'test',
|
|
description: 'Test plugin',
|
|
authConfig: [
|
|
{ authField: 'API_KEY', label: 'API Key', description: 'API Key' },
|
|
{ authField: 'SECRET_KEY', label: 'Secret Key', description: 'Secret Key' },
|
|
],
|
|
};
|
|
|
|
const result = checkPluginAuth(plugin);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when auth field value is empty string', () => {
|
|
process.env.API_KEY = '';
|
|
|
|
const plugin: TPlugin = {
|
|
name: 'Test',
|
|
pluginKey: 'test',
|
|
description: 'Test plugin',
|
|
authConfig: [{ authField: 'API_KEY', label: 'API Key', description: 'API Key' }],
|
|
};
|
|
|
|
const result = checkPluginAuth(plugin);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when auth field value is whitespace only', () => {
|
|
process.env.API_KEY = ' ';
|
|
|
|
const plugin: TPlugin = {
|
|
name: 'Test',
|
|
pluginKey: 'test',
|
|
description: 'Test plugin',
|
|
authConfig: [{ authField: 'API_KEY', label: 'API Key', description: 'API Key' }],
|
|
};
|
|
|
|
const result = checkPluginAuth(plugin);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when auth field value is USER_PROVIDED', () => {
|
|
process.env.API_KEY = AuthType.USER_PROVIDED;
|
|
|
|
const plugin: TPlugin = {
|
|
name: 'Test',
|
|
pluginKey: 'test',
|
|
description: 'Test plugin',
|
|
authConfig: [{ authField: 'API_KEY', label: 'API Key', description: 'API Key' }],
|
|
};
|
|
|
|
const result = checkPluginAuth(plugin);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should handle alternate auth fields with || separator', () => {
|
|
process.env.ALTERNATE_KEY = 'valid-key';
|
|
|
|
const plugin: TPlugin = {
|
|
name: 'Test',
|
|
pluginKey: 'test',
|
|
description: 'Test plugin',
|
|
authConfig: [
|
|
{ authField: 'PRIMARY_KEY||ALTERNATE_KEY', label: 'API Key', description: 'API Key' },
|
|
],
|
|
};
|
|
|
|
const result = checkPluginAuth(plugin);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return true when at least one alternate auth field is valid', () => {
|
|
process.env.PRIMARY_KEY = '';
|
|
process.env.ALTERNATE_KEY = 'valid-key';
|
|
process.env.THIRD_KEY = AuthType.USER_PROVIDED;
|
|
|
|
const plugin: TPlugin = {
|
|
name: 'Test',
|
|
pluginKey: 'test',
|
|
description: 'Test plugin',
|
|
authConfig: [
|
|
{
|
|
authField: 'PRIMARY_KEY||ALTERNATE_KEY||THIRD_KEY',
|
|
label: 'API Key',
|
|
description: 'API Key',
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = checkPluginAuth(plugin);
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('convertMCPToolsToPlugins', () => {
|
|
it('should return undefined when functionTools is undefined', () => {
|
|
const result = convertMCPToolsToPlugins({ functionTools: undefined });
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should return undefined when functionTools is not an object', () => {
|
|
const result = convertMCPToolsToPlugins({
|
|
functionTools: 'not-an-object' as unknown as Record<string, FunctionTool>,
|
|
});
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should return empty array when functionTools is empty object', () => {
|
|
const result = convertMCPToolsToPlugins({ functionTools: {} });
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should skip entries without function property', () => {
|
|
const functionTools: Record<string, FunctionTool> = {
|
|
tool1: { type: 'function' } as FunctionTool,
|
|
tool2: { function: { name: 'tool2', description: 'Tool 2' } } as FunctionTool,
|
|
};
|
|
|
|
const result = convertMCPToolsToPlugins({ functionTools });
|
|
expect(result).toHaveLength(0); // tool2 doesn't have mcp_delimiter in key
|
|
});
|
|
|
|
it('should skip entries without mcp_delimiter in key', () => {
|
|
const functionTools: Record<string, FunctionTool> = {
|
|
'regular-tool': {
|
|
type: 'function',
|
|
function: { name: 'regular-tool', description: 'Regular tool' },
|
|
} as FunctionTool,
|
|
};
|
|
|
|
const result = convertMCPToolsToPlugins({ functionTools });
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
it('should convert MCP tools to plugins correctly', () => {
|
|
const functionTools: Record<string, FunctionTool> = {
|
|
[`tool1${Constants.mcp_delimiter}server1`]: {
|
|
type: 'function',
|
|
function: { name: 'tool1', description: 'Tool 1 description' },
|
|
} as FunctionTool,
|
|
};
|
|
|
|
const result = convertMCPToolsToPlugins({ functionTools });
|
|
expect(result).toHaveLength(1);
|
|
expect(result![0]).toEqual({
|
|
name: 'tool1',
|
|
pluginKey: `tool1${Constants.mcp_delimiter}server1`,
|
|
description: 'Tool 1 description',
|
|
authenticated: true,
|
|
icon: undefined,
|
|
authConfig: [],
|
|
});
|
|
});
|
|
|
|
it('should handle missing description', () => {
|
|
const functionTools: Record<string, FunctionTool> = {
|
|
[`tool1${Constants.mcp_delimiter}server1`]: {
|
|
type: 'function',
|
|
function: { name: 'tool1' },
|
|
} as FunctionTool,
|
|
};
|
|
|
|
const result = convertMCPToolsToPlugins({ functionTools });
|
|
expect(result).toHaveLength(1);
|
|
expect(result![0].description).toBe('');
|
|
});
|
|
|
|
it('should add icon from server config', () => {
|
|
const functionTools: Record<string, FunctionTool> = {
|
|
[`tool1${Constants.mcp_delimiter}server1`]: {
|
|
type: 'function',
|
|
function: { name: 'tool1', description: 'Tool 1' },
|
|
} as FunctionTool,
|
|
};
|
|
|
|
const mockMcpManager = {
|
|
getRawConfig: jest.fn().mockReturnValue({
|
|
command: 'test',
|
|
args: [],
|
|
iconPath: '/path/to/icon.png',
|
|
}),
|
|
} as unknown as MCPManager;
|
|
|
|
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
|
expect(result).toHaveLength(1);
|
|
expect(result![0].icon).toBe('/path/to/icon.png');
|
|
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
|
});
|
|
|
|
it('should handle customUserVars in server config', () => {
|
|
const functionTools: Record<string, FunctionTool> = {
|
|
[`tool1${Constants.mcp_delimiter}server1`]: {
|
|
type: 'function',
|
|
function: { name: 'tool1', description: 'Tool 1' },
|
|
} as FunctionTool,
|
|
};
|
|
|
|
const mockMcpManager = {
|
|
getRawConfig: jest.fn().mockReturnValue({
|
|
command: 'test',
|
|
args: [],
|
|
customUserVars: {
|
|
API_KEY: { title: 'API Key', description: 'Your API key' },
|
|
SECRET: { title: 'Secret', description: 'Your secret' },
|
|
},
|
|
}),
|
|
} as unknown as MCPManager;
|
|
|
|
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
|
expect(result).toHaveLength(1);
|
|
expect(result![0].authConfig).toHaveLength(2);
|
|
expect(result![0].authConfig).toEqual([
|
|
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
|
|
{ authField: 'SECRET', label: 'Secret', description: 'Your secret' },
|
|
]);
|
|
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
|
});
|
|
|
|
it('should use key as label when title is missing in customUserVars', () => {
|
|
const functionTools: Record<string, FunctionTool> = {
|
|
[`tool1${Constants.mcp_delimiter}server1`]: {
|
|
type: 'function',
|
|
function: { name: 'tool1', description: 'Tool 1' },
|
|
} as FunctionTool,
|
|
};
|
|
|
|
const mockMcpManager = {
|
|
getRawConfig: jest.fn().mockReturnValue({
|
|
command: 'test',
|
|
args: [],
|
|
customUserVars: {
|
|
API_KEY: { title: 'API Key', description: 'Your API key' },
|
|
},
|
|
}),
|
|
} as unknown as MCPManager;
|
|
|
|
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
|
expect(result).toHaveLength(1);
|
|
expect(result![0].authConfig).toEqual([
|
|
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
|
|
]);
|
|
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
|
});
|
|
|
|
it('should handle empty customUserVars', () => {
|
|
const functionTools: Record<string, FunctionTool> = {
|
|
[`tool1${Constants.mcp_delimiter}server1`]: {
|
|
type: 'function',
|
|
function: { name: 'tool1', description: 'Tool 1' },
|
|
} as FunctionTool,
|
|
};
|
|
|
|
const mockMcpManager = {
|
|
getRawConfig: jest.fn().mockReturnValue({
|
|
command: 'test',
|
|
args: [],
|
|
customUserVars: {},
|
|
}),
|
|
} as unknown as MCPManager;
|
|
|
|
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
|
expect(result).toHaveLength(1);
|
|
expect(result![0].authConfig).toEqual([]);
|
|
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
|
});
|
|
|
|
it('should handle missing mcpManager', () => {
|
|
const functionTools: Record<string, FunctionTool> = {
|
|
[`tool1${Constants.mcp_delimiter}server1`]: {
|
|
type: 'function',
|
|
function: { name: 'tool1', description: 'Tool 1' },
|
|
} as FunctionTool,
|
|
};
|
|
|
|
const result = convertMCPToolsToPlugins({ functionTools });
|
|
expect(result).toHaveLength(1);
|
|
expect(result![0].icon).toBeUndefined();
|
|
expect(result![0].authConfig).toEqual([]);
|
|
});
|
|
|
|
it('should handle when getRawConfig returns undefined', () => {
|
|
const functionTools: Record<string, FunctionTool> = {
|
|
[`tool1${Constants.mcp_delimiter}server1`]: {
|
|
type: 'function',
|
|
function: { name: 'tool1', description: 'Tool 1' },
|
|
} as FunctionTool,
|
|
};
|
|
|
|
const mockMcpManager = {
|
|
getRawConfig: jest.fn().mockReturnValue(undefined),
|
|
} as unknown as MCPManager;
|
|
|
|
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
|
expect(result).toHaveLength(1);
|
|
expect(result![0].icon).toBeUndefined();
|
|
expect(result![0].authConfig).toEqual([]);
|
|
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
|
});
|
|
});
|
|
|
|
describe('getToolkitKey', () => {
|
|
it('should return undefined when toolName is undefined', () => {
|
|
const toolkits: TPlugin[] = [
|
|
{ name: 'Toolkit1', pluginKey: 'toolkit1', description: 'Test toolkit' },
|
|
];
|
|
|
|
const result = getToolkitKey({ toolkits, toolName: undefined });
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should return undefined when toolName is empty string', () => {
|
|
const toolkits: TPlugin[] = [
|
|
{ name: 'Toolkit1', pluginKey: 'toolkit1', description: 'Test toolkit' },
|
|
];
|
|
|
|
const result = getToolkitKey({ toolkits, toolName: '' });
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should return undefined when no matching toolkit is found', () => {
|
|
const toolkits: TPlugin[] = [
|
|
{ name: 'Toolkit1', pluginKey: 'toolkit1', description: 'Test toolkit' },
|
|
{ name: 'Toolkit2', pluginKey: 'toolkit2', description: 'Test toolkit' },
|
|
];
|
|
|
|
const result = getToolkitKey({ toolkits, toolName: 'nonexistent_tool' });
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should match toolkit when toolName starts with pluginKey', () => {
|
|
const toolkits: TPlugin[] = [
|
|
{ name: 'Toolkit1', pluginKey: 'toolkit1', description: 'Test toolkit' },
|
|
{ name: 'Toolkit2', pluginKey: 'toolkit2', description: 'Test toolkit' },
|
|
];
|
|
|
|
const result = getToolkitKey({ toolkits, toolName: 'toolkit2_function' });
|
|
expect(result).toBe('toolkit2');
|
|
});
|
|
|
|
it('should handle image_edit tools with suffix matching', () => {
|
|
const toolkits: TPlugin[] = [
|
|
{ name: 'Image Editor', pluginKey: 'image_edit_v1', description: 'Image editing' },
|
|
{ name: 'Image Editor 2', pluginKey: 'image_edit_v2', description: 'Image editing v2' },
|
|
];
|
|
|
|
const result = getToolkitKey({
|
|
toolkits,
|
|
toolName: `${EToolResources.image_edit}_function_v2`,
|
|
});
|
|
expect(result).toBe('image_edit_v2');
|
|
});
|
|
|
|
it('should match the first toolkit when multiple matches are possible', () => {
|
|
const toolkits: TPlugin[] = [
|
|
{ name: 'Toolkit', pluginKey: 'toolkit', description: 'Base toolkit' },
|
|
{ name: 'Toolkit Extended', pluginKey: 'toolkit_extended', description: 'Extended' },
|
|
];
|
|
|
|
const result = getToolkitKey({ toolkits, toolName: 'toolkit_function' });
|
|
expect(result).toBe('toolkit');
|
|
});
|
|
|
|
it('should handle empty toolkits array', () => {
|
|
const toolkits: TPlugin[] = [];
|
|
|
|
const result = getToolkitKey({ toolkits, toolName: 'any_tool' });
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should handle complex plugin keys with underscores', () => {
|
|
const toolkits: TPlugin[] = [
|
|
{
|
|
name: 'Complex Toolkit',
|
|
pluginKey: 'complex_toolkit_with_underscores',
|
|
description: 'Complex',
|
|
},
|
|
];
|
|
|
|
const result = getToolkitKey({
|
|
toolkits,
|
|
toolName: 'complex_toolkit_with_underscores_function',
|
|
});
|
|
expect(result).toBe('complex_toolkit_with_underscores');
|
|
});
|
|
});
|
|
});
|