mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-04 06:47:19 +02:00
138 lines
4.7 KiB
JavaScript
138 lines
4.7 KiB
JavaScript
|
|
// ── Mocks ──────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
const mockConfigStoreDelete = jest.fn().mockResolvedValue(true);
|
||
|
|
const mockClearAppConfigCache = jest.fn().mockResolvedValue(undefined);
|
||
|
|
const mockClearOverrideCache = jest.fn().mockResolvedValue(undefined);
|
||
|
|
|
||
|
|
jest.mock('~/cache/getLogStores', () => {
|
||
|
|
return jest.fn(() => ({
|
||
|
|
delete: mockConfigStoreDelete,
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
jest.mock('~/server/services/start/tools', () => ({
|
||
|
|
loadAndFormatTools: jest.fn(() => ({})),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('../loadCustomConfig', () => jest.fn().mockResolvedValue({}));
|
||
|
|
|
||
|
|
jest.mock('@librechat/data-schemas', () => {
|
||
|
|
const actual = jest.requireActual('@librechat/data-schemas');
|
||
|
|
return { ...actual, AppService: jest.fn(() => ({ availableTools: {} })) };
|
||
|
|
});
|
||
|
|
|
||
|
|
jest.mock('~/models', () => ({
|
||
|
|
getApplicableConfigs: jest.fn().mockResolvedValue([]),
|
||
|
|
getUserPrincipals: jest.fn().mockResolvedValue([]),
|
||
|
|
}));
|
||
|
|
|
||
|
|
const mockInvalidateCachedTools = jest.fn().mockResolvedValue(undefined);
|
||
|
|
jest.mock('../getCachedTools', () => ({
|
||
|
|
setCachedTools: jest.fn().mockResolvedValue(undefined),
|
||
|
|
invalidateCachedTools: mockInvalidateCachedTools,
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('@librechat/api', () => ({
|
||
|
|
createAppConfigService: jest.fn(() => ({
|
||
|
|
getAppConfig: jest.fn().mockResolvedValue({ availableTools: {} }),
|
||
|
|
clearAppConfigCache: mockClearAppConfigCache,
|
||
|
|
clearOverrideCache: mockClearOverrideCache,
|
||
|
|
})),
|
||
|
|
}));
|
||
|
|
|
||
|
|
// ── Tests ──────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
const { CacheKeys } = require('librechat-data-provider');
|
||
|
|
const { invalidateConfigCaches } = require('../app');
|
||
|
|
|
||
|
|
describe('invalidateConfigCaches', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
jest.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('clears all four caches', async () => {
|
||
|
|
await invalidateConfigCaches();
|
||
|
|
|
||
|
|
expect(mockClearAppConfigCache).toHaveBeenCalledTimes(1);
|
||
|
|
expect(mockClearOverrideCache).toHaveBeenCalledTimes(1);
|
||
|
|
expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ invalidateGlobal: true });
|
||
|
|
expect(mockConfigStoreDelete).toHaveBeenCalledWith(CacheKeys.ENDPOINT_CONFIG);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('passes tenantId through to clearOverrideCache', async () => {
|
||
|
|
await invalidateConfigCaches('tenant-a');
|
||
|
|
|
||
|
|
expect(mockClearOverrideCache).toHaveBeenCalledWith('tenant-a');
|
||
|
|
expect(mockClearAppConfigCache).toHaveBeenCalledTimes(1);
|
||
|
|
expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ invalidateGlobal: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('does not throw when CONFIG_STORE.delete fails', async () => {
|
||
|
|
mockConfigStoreDelete.mockRejectedValueOnce(new Error('store not found'));
|
||
|
|
|
||
|
|
await expect(invalidateConfigCaches()).resolves.not.toThrow();
|
||
|
|
|
||
|
|
// Other caches should still have been invalidated
|
||
|
|
expect(mockClearAppConfigCache).toHaveBeenCalledTimes(1);
|
||
|
|
expect(mockClearOverrideCache).toHaveBeenCalledTimes(1);
|
||
|
|
expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ invalidateGlobal: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('all operations run in parallel (not sequentially)', async () => {
|
||
|
|
const order = [];
|
||
|
|
|
||
|
|
mockClearAppConfigCache.mockImplementation(
|
||
|
|
() =>
|
||
|
|
new Promise((r) =>
|
||
|
|
setTimeout(() => {
|
||
|
|
order.push('base');
|
||
|
|
r();
|
||
|
|
}, 10),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
mockClearOverrideCache.mockImplementation(
|
||
|
|
() =>
|
||
|
|
new Promise((r) =>
|
||
|
|
setTimeout(() => {
|
||
|
|
order.push('override');
|
||
|
|
r();
|
||
|
|
}, 10),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
mockInvalidateCachedTools.mockImplementation(
|
||
|
|
() =>
|
||
|
|
new Promise((r) =>
|
||
|
|
setTimeout(() => {
|
||
|
|
order.push('tools');
|
||
|
|
r();
|
||
|
|
}, 10),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
mockConfigStoreDelete.mockImplementation(
|
||
|
|
() =>
|
||
|
|
new Promise((r) =>
|
||
|
|
setTimeout(() => {
|
||
|
|
order.push('endpoint');
|
||
|
|
r();
|
||
|
|
}, 10),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
await invalidateConfigCaches();
|
||
|
|
|
||
|
|
// All four should have been called (parallel execution via Promise.allSettled)
|
||
|
|
expect(order).toHaveLength(4);
|
||
|
|
expect(new Set(order)).toEqual(new Set(['base', 'override', 'tools', 'endpoint']));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('resolves even when clearAppConfigCache throws (partial failure)', async () => {
|
||
|
|
mockClearAppConfigCache.mockRejectedValueOnce(new Error('cache connection lost'));
|
||
|
|
|
||
|
|
await expect(invalidateConfigCaches()).resolves.not.toThrow();
|
||
|
|
|
||
|
|
// Other caches should still have been invalidated despite the failure
|
||
|
|
expect(mockClearOverrideCache).toHaveBeenCalledTimes(1);
|
||
|
|
expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ invalidateGlobal: true });
|
||
|
|
});
|
||
|
|
});
|