diff --git a/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js b/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js index 768d81e888..4481a7d70f 100644 --- a/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js +++ b/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js @@ -1,43 +1,9 @@ const DALLE3 = require('../DALLE3'); const { ProxyAgent } = require('undici'); +jest.mock('tiktoken'); const processFileURL = jest.fn(); -jest.mock('~/server/services/Files/images', () => ({ - getImageBasename: jest.fn().mockImplementation((url) => { - const parts = url.split('/'); - const lastPart = parts.pop(); - const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i; - if (imageExtensionRegex.test(lastPart)) { - return lastPart; - } - return ''; - }), -})); - -jest.mock('fs', () => { - return { - existsSync: jest.fn(), - mkdirSync: jest.fn(), - promises: { - writeFile: jest.fn(), - readFile: jest.fn(), - unlink: jest.fn(), - }, - }; -}); - -jest.mock('path', () => { - return { - resolve: jest.fn(), - join: jest.fn(), - relative: jest.fn(), - extname: jest.fn().mockImplementation((filename) => { - return filename.slice(filename.lastIndexOf('.')); - }), - }; -}); - describe('DALLE3 Proxy Configuration', () => { let originalEnv; diff --git a/api/app/clients/tools/structured/specs/DALLE3.spec.js b/api/app/clients/tools/structured/specs/DALLE3.spec.js index 2def575fb3..d2040989f9 100644 --- a/api/app/clients/tools/structured/specs/DALLE3.spec.js +++ b/api/app/clients/tools/structured/specs/DALLE3.spec.js @@ -1,9 +1,8 @@ const OpenAI = require('openai'); +const { logger } = require('@librechat/data-schemas'); const DALLE3 = require('../DALLE3'); -const logger = require('~/config/winston'); jest.mock('openai'); - jest.mock('@librechat/data-schemas', () => { return { logger: { @@ -26,25 +25,6 @@ jest.mock('tiktoken', () => { const processFileURL = jest.fn(); -jest.mock('~/server/services/Files/images', () => ({ - getImageBasename: jest.fn().mockImplementation((url) => { - // Split the URL by '/' - const parts = url.split('/'); - - // Get the last part of the URL - const lastPart = parts.pop(); - - // Check if the last part of the URL matches the image extension regex - const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i; - if (imageExtensionRegex.test(lastPart)) { - return lastPart; - } - - // If the regex test fails, return an empty string - return ''; - }), -})); - const generate = jest.fn(); OpenAI.mockImplementation(() => ({ images: { diff --git a/api/server/services/AppService.interface.spec.js b/api/server/services/AppService.interface.spec.js index 32a65393a2..f4b1c67c38 100644 --- a/api/server/services/AppService.interface.spec.js +++ b/api/server/services/AppService.interface.spec.js @@ -1,15 +1,4 @@ -jest.mock('~/models', () => ({ - initializeRoles: jest.fn(), - seedDefaultRoles: jest.fn(), - ensureDefaultCategories: jest.fn(), -})); -jest.mock('~/models/Role', () => ({ - updateAccessPermissions: jest.fn(), - getRoleByName: jest.fn().mockResolvedValue(null), - updateRoleByName: jest.fn(), -})); - -jest.mock('~/config', () => ({ +jest.mock('@librechat/data-schemas', () => ({ logger: { info: jest.fn(), warn: jest.fn(), @@ -17,10 +6,11 @@ jest.mock('~/config', () => ({ }, })); -jest.mock('./start/interface', () => ({ +jest.mock('@librechat/api', () => ({ + ...jest.requireActual('@librechat/api'), loadDefaultInterface: jest.fn(), })); -jest.mock('./ToolService', () => ({ +jest.mock('./start/tools', () => ({ loadAndFormatTools: jest.fn().mockReturnValue({}), })); jest.mock('./start/checks', () => ({ @@ -31,33 +21,27 @@ jest.mock('./start/checks', () => ({ checkWebSearchConfig: jest.fn(), })); -jest.mock('./Config', () => ({ - setAppConfig: jest.fn(), - getAppConfig: jest.fn(), - setCachedTools: jest.fn(), - loadCustomConfig: jest.fn(), -})); +jest.mock('./Config/loadCustomConfig', () => jest.fn()); const AppService = require('./AppService'); -const { loadDefaultInterface } = require('./start/interface'); +const { loadDefaultInterface } = require('@librechat/api'); describe('AppService interface configuration', () => { let mockLoadCustomConfig; - const { setAppConfig } = require('./Config'); beforeEach(() => { jest.resetModules(); jest.clearAllMocks(); - ({ loadCustomConfig: mockLoadCustomConfig } = require('./Config')); + mockLoadCustomConfig = require('./Config/loadCustomConfig'); }); it('should set prompts and bookmarks to true when loadDefaultInterface returns true for both', async () => { mockLoadCustomConfig.mockResolvedValue({}); loadDefaultInterface.mockResolvedValue({ prompts: true, bookmarks: true }); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ interfaceConfig: expect.objectContaining({ prompts: true, @@ -72,9 +56,9 @@ describe('AppService interface configuration', () => { mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: false, bookmarks: false } }); loadDefaultInterface.mockResolvedValue({ prompts: false, bookmarks: false }); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ interfaceConfig: expect.objectContaining({ prompts: false, @@ -89,18 +73,17 @@ describe('AppService interface configuration', () => { mockLoadCustomConfig.mockResolvedValue({}); loadDefaultInterface.mockResolvedValue({}); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ interfaceConfig: expect.anything(), }), ); // Verify that prompts and bookmarks are undefined when not provided - const initCall = setAppConfig.mock.calls[0][0]; - expect(initCall.interfaceConfig.prompts).toBeUndefined(); - expect(initCall.interfaceConfig.bookmarks).toBeUndefined(); + expect(result.interfaceConfig.prompts).toBeUndefined(); + expect(result.interfaceConfig.bookmarks).toBeUndefined(); expect(loadDefaultInterface).toHaveBeenCalled(); }); @@ -108,9 +91,9 @@ describe('AppService interface configuration', () => { mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: true, bookmarks: false } }); loadDefaultInterface.mockResolvedValue({ prompts: true, bookmarks: false }); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ interfaceConfig: expect.objectContaining({ prompts: true, @@ -139,9 +122,9 @@ describe('AppService interface configuration', () => { }, }); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ interfaceConfig: expect.objectContaining({ peoplePicker: expect.objectContaining({ @@ -173,9 +156,9 @@ describe('AppService interface configuration', () => { }, }); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ interfaceConfig: expect.objectContaining({ peoplePicker: expect.objectContaining({ @@ -198,9 +181,9 @@ describe('AppService interface configuration', () => { }, }); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ interfaceConfig: expect.objectContaining({ peoplePicker: expect.objectContaining({ diff --git a/api/server/services/AppService.spec.js b/api/server/services/AppService.spec.js index 87efd09b0c..f7f25d49e3 100644 --- a/api/server/services/AppService.spec.js +++ b/api/server/services/AppService.spec.js @@ -10,25 +10,24 @@ const { conflictingAzureVariables, } = require('librechat-data-provider'); +jest.mock('@librechat/data-schemas', () => ({ + ...jest.requireActual('@librechat/data-schemas'), + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, +})); + const AppService = require('./AppService'); jest.mock('./Files/Firebase/initialize', () => ({ initializeFirebase: jest.fn(), })); -jest.mock('~/models', () => ({ - initializeRoles: jest.fn(), - seedDefaultRoles: jest.fn(), - ensureDefaultCategories: jest.fn(), -})); -jest.mock('~/models/Role', () => ({ - updateAccessPermissions: jest.fn(), - getRoleByName: jest.fn().mockResolvedValue(null), -})); -jest.mock('./Config', () => ({ - setAppConfig: jest.fn(), - getAppConfig: jest.fn(), - setCachedTools: jest.fn(), - loadCustomConfig: jest.fn(() => + +jest.mock('./Config/loadCustomConfig', () => + jest.fn(() => Promise.resolve({ registration: { socialLogins: ['testLogin'] }, fileStrategy: 'testStrategy', @@ -37,25 +36,9 @@ jest.mock('./Config', () => ({ }, }), ), - getCachedTools: jest.fn().mockResolvedValue({ - ExampleTool: { - type: 'function', - function: { - description: 'Example tool function', - name: 'exampleFunction', - parameters: { - type: 'object', - properties: { - param1: { type: 'string', description: 'An example parameter' }, - }, - required: ['param1'], - }, - }, - }, - }), -})); +); -jest.mock('./ToolService', () => ({ +jest.mock('./start/tools', () => ({ loadAndFormatTools: jest.fn().mockReturnValue({ ExampleTool: { type: 'function', @@ -117,12 +100,17 @@ const azureGroups = [ }, ]; +jest.mock('./start/checks', () => ({ + ...jest.requireActual('./start/checks'), + checkHealth: jest.fn(), +})); + describe('AppService', () => { const mockedTurnstileConfig = { siteKey: 'default-site-key', options: {}, }; - const { setAppConfig, loadCustomConfig } = require('./Config'); + const loadCustomConfig = require('./Config/loadCustomConfig'); beforeEach(() => { process.env.CDN_PROVIDER = undefined; @@ -130,11 +118,11 @@ describe('AppService', () => { }); it('should correctly assign process.env and initialize app config based on custom config', async () => { - await AppService(); + const result = await AppService(); expect(process.env.CDN_PROVIDER).toEqual('testStrategy'); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ config: expect.objectContaining({ fileStrategy: 'testStrategy', @@ -196,7 +184,7 @@ describe('AppService', () => { await AppService(); - const { logger } = require('~/config'); + const { logger } = require('@librechat/data-schemas'); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Outdated Config version')); }); @@ -208,8 +196,8 @@ describe('AppService', () => { }), ); - await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + const result = await AppService(); + expect(result).toEqual( expect.objectContaining({ imageOutputType: EImageOutputType.WEBP, }), @@ -223,8 +211,8 @@ describe('AppService', () => { }), ); - await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + const result = await AppService(); + expect(result).toEqual( expect.objectContaining({ imageOutputType: EImageOutputType.PNG, }), @@ -234,8 +222,8 @@ describe('AppService', () => { it('should default to `PNG` `imageOutputType` with no provided config', async () => { loadCustomConfig.mockImplementationOnce(() => Promise.resolve(undefined)); - await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + const result = await AppService(); + expect(result).toEqual( expect.objectContaining({ imageOutputType: EImageOutputType.PNG, }), @@ -259,9 +247,8 @@ describe('AppService', () => { it('should load and format tools accurately with defined structure', async () => { const { loadAndFormatTools } = require('./ToolService'); - const { setCachedTools, getCachedTools } = require('./Config'); - await AppService(); + const result = await AppService(); expect(loadAndFormatTools).toHaveBeenCalledWith({ adminFilter: undefined, @@ -271,31 +258,9 @@ describe('AppService', () => { fileStrategy: expect.any(String), }); - // Verify setCachedTools was called with the tools - expect(setCachedTools).toHaveBeenCalledWith( - { - ExampleTool: { - type: 'function', - function: { - description: 'Example tool function', - name: 'exampleFunction', - parameters: { - type: 'object', - properties: { - param1: { type: 'string', description: 'An example parameter' }, - }, - required: ['param1'], - }, - }, - }, - }, - { isGlobal: true }, - ); - - // Verify we can retrieve the tools from cache - const cachedTools = await getCachedTools({ includeGlobal: true }); - expect(cachedTools.ExampleTool).toBeDefined(); - expect(cachedTools.ExampleTool).toEqual({ + // Verify tools are included in the returned config + expect(result.availableTools).toBeDefined(); + expect(result.availableTools.ExampleTool).toEqual({ type: 'function', function: { description: 'Example tool function', @@ -326,9 +291,9 @@ describe('AppService', () => { }), ); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ endpoints: expect.objectContaining({ [EModelEndpoint.assistants]: expect.objectContaining({ @@ -358,9 +323,9 @@ describe('AppService', () => { }), ); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ endpoints: expect.objectContaining({ [EModelEndpoint.agents]: expect.objectContaining({ @@ -381,9 +346,9 @@ describe('AppService', () => { it('should configure Agents endpoint with defaults when no config is provided', async () => { loadCustomConfig.mockImplementationOnce(() => Promise.resolve({})); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ endpoints: expect.objectContaining({ [EModelEndpoint.agents]: expect.objectContaining({ @@ -406,9 +371,9 @@ describe('AppService', () => { }), ); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ endpoints: expect.objectContaining({ [EModelEndpoint.agents]: expect.objectContaining({ @@ -439,8 +404,8 @@ describe('AppService', () => { process.env.WESTUS_API_KEY = 'westus-key'; process.env.EASTUS_API_KEY = 'eastus-key'; - await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + const result = await AppService(); + expect(result).toEqual( expect.objectContaining({ endpoints: expect.objectContaining({ [EModelEndpoint.azureAssistants]: expect.objectContaining({ @@ -469,10 +434,10 @@ describe('AppService', () => { process.env.WESTUS_API_KEY = 'westus-key'; process.env.EASTUS_API_KEY = 'eastus-key'; - await AppService(); + const result = await AppService(); const { modelNames, modelGroupMap, groupMap } = validateAzureGroups(azureGroups); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ endpoints: expect.objectContaining({ [EModelEndpoint.azureOpenAI]: expect.objectContaining({ @@ -629,9 +594,9 @@ describe('AppService', () => { }), ); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ endpoints: expect.objectContaining({ // Check OpenAI endpoint configuration @@ -679,9 +644,9 @@ describe('AppService', () => { }), ); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ endpoints: expect.objectContaining({ [EModelEndpoint.agents]: expect.objectContaining({ @@ -714,9 +679,9 @@ describe('AppService', () => { }), ); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ endpoints: expect.objectContaining({ [EModelEndpoint.openAI]: expect.objectContaining({ @@ -727,10 +692,9 @@ describe('AppService', () => { ); // Verify that optional fields are not set when not provided - const initCall = setAppConfig.mock.calls[0][0]; - expect(initCall.endpoints[EModelEndpoint.openAI].titlePrompt).toBeUndefined(); - expect(initCall.endpoints[EModelEndpoint.openAI].titlePromptTemplate).toBeUndefined(); - expect(initCall.endpoints[EModelEndpoint.openAI].titleMethod).toBeUndefined(); + expect(result.endpoints[EModelEndpoint.openAI].titlePrompt).toBeUndefined(); + expect(result.endpoints[EModelEndpoint.openAI].titlePromptTemplate).toBeUndefined(); + expect(result.endpoints[EModelEndpoint.openAI].titleMethod).toBeUndefined(); }); it('should correctly configure titleEndpoint when specified', async () => { @@ -751,9 +715,9 @@ describe('AppService', () => { }), ); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ endpoints: expect.objectContaining({ // Check OpenAI endpoint has titleEndpoint @@ -794,9 +758,9 @@ describe('AppService', () => { }), ); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ // Check that 'all' endpoint config is loaded endpoints: expect.objectContaining({ @@ -822,7 +786,7 @@ describe('AppService', () => { describe('AppService updating app config and issuing warnings', () => { let initialEnv; - const { setAppConfig, loadCustomConfig } = require('./Config'); + const loadCustomConfig = require('./Config/loadCustomConfig'); beforeEach(() => { // Store initial environment variables to restore them after each test @@ -841,9 +805,9 @@ describe('AppService updating app config and issuing warnings', () => { // Mock loadCustomConfig to return undefined loadCustomConfig.mockImplementationOnce(() => Promise.resolve(undefined)); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ paths: expect.anything(), config: {}, @@ -875,9 +839,9 @@ describe('AppService updating app config and issuing warnings', () => { }; loadCustomConfig.mockImplementationOnce(() => Promise.resolve(customConfig)); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ paths: expect.anything(), config: customConfig, @@ -903,9 +867,9 @@ describe('AppService updating app config and issuing warnings', () => { }; loadCustomConfig.mockImplementationOnce(() => Promise.resolve(mockConfig)); - await AppService(); + const result = await AppService(); - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ endpoints: expect.objectContaining({ assistants: expect.objectContaining({ @@ -919,8 +883,7 @@ describe('AppService updating app config and issuing warnings', () => { ); // Verify excludedIds is undefined when not provided - const initCall = setAppConfig.mock.calls[0][0]; - expect(initCall.endpoints.assistants.excludedIds).toBeUndefined(); + expect(result.endpoints.assistants.excludedIds).toBeUndefined(); }); it('should log a warning when both supportedIds and excludedIds are provided', async () => { @@ -939,7 +902,7 @@ describe('AppService updating app config and issuing warnings', () => { await AppService(); - const { logger } = require('~/config'); + const { logger } = require('@librechat/data-schemas'); expect(logger.warn).toHaveBeenCalledWith( expect.stringContaining( "The 'assistants' endpoint has both 'supportedIds' and 'excludedIds' defined.", @@ -960,7 +923,7 @@ describe('AppService updating app config and issuing warnings', () => { await AppService(); - const { logger } = require('~/config'); + const { logger } = require('@librechat/data-schemas'); expect(logger.warn).toHaveBeenCalledWith( expect.stringContaining( "The 'assistants' endpoint has both 'privateAssistants' and 'supportedIds' or 'excludedIds' defined.", @@ -985,7 +948,7 @@ describe('AppService updating app config and issuing warnings', () => { await AppService(); - const { logger } = require('~/config'); + const { logger } = require('@librechat/data-schemas'); deprecatedAzureVariables.forEach(({ key, description }) => { expect(logger.warn).toHaveBeenCalledWith( `The \`${key}\` environment variable (related to ${description}) should not be used in combination with the \`azureOpenAI\` endpoint configuration, as you will experience conflicts and errors.`, @@ -1010,7 +973,7 @@ describe('AppService updating app config and issuing warnings', () => { await AppService(); - const { logger } = require('~/config'); + const { logger } = require('@librechat/data-schemas'); conflictingAzureVariables.forEach(({ key }) => { expect(logger.warn).toHaveBeenCalledWith( `The \`${key}\` environment variable should not be used in combination with the \`azureOpenAI\` endpoint configuration, as you may experience with the defined placeholders for mapping to the current model grouping using the same name.`, @@ -1035,10 +998,10 @@ describe('AppService updating app config and issuing warnings', () => { process.env.OCR_API_KEY_CUSTOM_VAR_NAME = 'actual-api-key'; process.env.OCR_BASEURL_CUSTOM_VAR_NAME = 'https://actual-ocr-url.com'; - await AppService(); + const result = await AppService(); // Verify that the raw string references were preserved and not interpolated - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ ocr: expect.objectContaining({ apiKey: '${OCR_API_KEY_CUSTOM_VAR_NAME}', @@ -1063,10 +1026,10 @@ describe('AppService updating app config and issuing warnings', () => { loadCustomConfig.mockImplementationOnce(() => Promise.resolve(mockConfig)); - await AppService(); + const result = await AppService(); // Check that interface config includes the permissions - expect(setAppConfig).toHaveBeenCalledWith( + expect(result).toEqual( expect.objectContaining({ interfaceConfig: expect.objectContaining({ peoplePicker: expect.objectContaining({ diff --git a/api/server/services/start/checks.spec.js b/api/server/services/start/checks.spec.js index d6b95006d7..1281331266 100644 --- a/api/server/services/start/checks.spec.js +++ b/api/server/services/start/checks.spec.js @@ -1,11 +1,10 @@ -// Mock librechat-data-provider jest.mock('librechat-data-provider', () => ({ ...jest.requireActual('librechat-data-provider'), extractVariableName: jest.fn(), })); -// Mock the config logger -jest.mock('~/config', () => ({ +jest.mock('@librechat/data-schemas', () => ({ + ...jest.requireActual('@librechat/data-schemas'), logger: { debug: jest.fn(), warn: jest.fn(), @@ -13,7 +12,7 @@ jest.mock('~/config', () => ({ })); const { checkWebSearchConfig } = require('./checks'); -const { logger } = require('~/config'); +const { logger } = require('@librechat/data-schemas'); const { extractVariableName } = require('librechat-data-provider'); describe('checkWebSearchConfig', () => {