mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✅ test: add unit tests for gateway service (#12784)
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:
340
src/server/services/gateway/GatewayManager.test.ts
Normal file
340
src/server/services/gateway/GatewayManager.test.ts
Normal file
@@ -0,0 +1,340 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { getServerDB } from '@/database/core/db-adaptor';
|
||||
import { AgentBotProviderModel } from '@/database/models/agentBotProvider';
|
||||
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
||||
|
||||
import { createGatewayManager, GatewayManager, getGatewayManager } from './GatewayManager';
|
||||
|
||||
// Mock database and external dependencies
|
||||
const { mockFindEnabledByPlatform } = vi.hoisted(() => ({
|
||||
mockFindEnabledByPlatform: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
vi.mock('@/database/core/db-adaptor', () => ({
|
||||
getServerDB: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/database/models/agentBotProvider', () => ({
|
||||
AgentBotProviderModel: Object.assign(vi.fn(), {
|
||||
findEnabledByPlatform: mockFindEnabledByPlatform,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/server/modules/KeyVaultsEncrypt', () => ({
|
||||
KeyVaultsGateKeeper: {
|
||||
initWithEnvKey: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper: create a mock PlatformBot instance
|
||||
const createMockBot = () => ({
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
|
||||
// Helper: create a mock PlatformBot class (constructor)
|
||||
const createMockBotClass = (instance = createMockBot()) => {
|
||||
return vi.fn().mockImplementation(() => instance);
|
||||
};
|
||||
|
||||
describe('GatewayManager', () => {
|
||||
let mockDb: any;
|
||||
let mockGateKeeper: any;
|
||||
let mockAgentBotProviderModel: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDb = {};
|
||||
mockGateKeeper = {};
|
||||
mockAgentBotProviderModel = {
|
||||
findEnabledByApplicationId: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mocked(getServerDB).mockResolvedValue(mockDb as any);
|
||||
vi.mocked(KeyVaultsGateKeeper.initWithEnvKey).mockResolvedValue(mockGateKeeper as any);
|
||||
vi.mocked(AgentBotProviderModel).mockImplementation(() => mockAgentBotProviderModel);
|
||||
mockFindEnabledByPlatform.mockResolvedValue([]);
|
||||
|
||||
// Clean up global singleton between tests
|
||||
const globalForGateway = globalThis as any;
|
||||
delete globalForGateway.gatewayManager;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
const globalForGateway = globalThis as any;
|
||||
delete globalForGateway.gatewayManager;
|
||||
});
|
||||
|
||||
describe('constructor and isRunning', () => {
|
||||
it('should initialize with isRunning = false', () => {
|
||||
const manager = new GatewayManager({ registry: {} });
|
||||
expect(manager.isRunning).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept a registry configuration', () => {
|
||||
const BotClass = createMockBotClass();
|
||||
const manager = new GatewayManager({ registry: { slack: BotClass } });
|
||||
expect(manager.isRunning).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('should set isRunning to true after start', async () => {
|
||||
const manager = new GatewayManager({ registry: {} });
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(manager.isRunning).toBe(true);
|
||||
});
|
||||
|
||||
it('should not start again if already running', async () => {
|
||||
const manager = new GatewayManager({ registry: {} });
|
||||
|
||||
await manager.start();
|
||||
expect(manager.isRunning).toBe(true);
|
||||
|
||||
// Call start again — should be a no-op
|
||||
await manager.start();
|
||||
expect(manager.isRunning).toBe(true);
|
||||
|
||||
// getServerDB called once during initial sync
|
||||
// called again would indicate duplicate work
|
||||
const callCount = vi.mocked(getServerDB).mock.calls.length;
|
||||
// start was called twice but sync should only happen once
|
||||
await manager.start();
|
||||
expect(vi.mocked(getServerDB).mock.calls.length).toBe(callCount); // no additional sync
|
||||
});
|
||||
|
||||
it('should continue starting even if initial sync fails', async () => {
|
||||
vi.mocked(getServerDB).mockRejectedValueOnce(new Error('DB connection failed'));
|
||||
|
||||
const manager = new GatewayManager({ registry: {} });
|
||||
|
||||
// Should not throw
|
||||
await expect(manager.start()).resolves.toBeUndefined();
|
||||
expect(manager.isRunning).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
it('should set isRunning to false after stop', async () => {
|
||||
const manager = new GatewayManager({ registry: {} });
|
||||
await manager.start();
|
||||
expect(manager.isRunning).toBe(true);
|
||||
|
||||
await manager.stop();
|
||||
|
||||
expect(manager.isRunning).toBe(false);
|
||||
});
|
||||
|
||||
it('should do nothing if not running', async () => {
|
||||
const manager = new GatewayManager({ registry: {} });
|
||||
|
||||
// Should not throw
|
||||
await expect(manager.stop()).resolves.toBeUndefined();
|
||||
expect(manager.isRunning).toBe(false);
|
||||
});
|
||||
|
||||
it('should stop all running bots', async () => {
|
||||
const mockBot1 = createMockBot();
|
||||
const mockBot2 = createMockBot();
|
||||
const BotClass = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => mockBot1)
|
||||
.mockImplementationOnce(() => mockBot2);
|
||||
|
||||
// Pre-load two bots by calling startBot
|
||||
mockAgentBotProviderModel.findEnabledByApplicationId.mockResolvedValue({
|
||||
applicationId: 'app-1',
|
||||
credentials: { token: 'tok1' },
|
||||
});
|
||||
|
||||
const manager = new GatewayManager({ registry: { slack: BotClass } });
|
||||
await manager.start();
|
||||
|
||||
await manager.startBot('slack', 'app-1', 'user-1');
|
||||
await manager.startBot('slack', 'app-2', 'user-2');
|
||||
|
||||
await manager.stop();
|
||||
|
||||
expect(mockBot1.stop).toHaveBeenCalled();
|
||||
expect(mockBot2.stop).toHaveBeenCalled();
|
||||
expect(manager.isRunning).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startBot', () => {
|
||||
it('should do nothing when no provider is found in DB', async () => {
|
||||
mockAgentBotProviderModel.findEnabledByApplicationId.mockResolvedValue(null);
|
||||
const BotClass = createMockBotClass();
|
||||
|
||||
const manager = new GatewayManager({ registry: { slack: BotClass } });
|
||||
|
||||
await manager.startBot('slack', 'app-123', 'user-abc');
|
||||
|
||||
expect(BotClass).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing when the platform is not in registry', async () => {
|
||||
mockAgentBotProviderModel.findEnabledByApplicationId.mockResolvedValue({
|
||||
applicationId: 'app-123',
|
||||
credentials: { token: 'tok' },
|
||||
});
|
||||
|
||||
const manager = new GatewayManager({ registry: {} }); // empty registry
|
||||
|
||||
await manager.startBot('unsupported', 'app-123', 'user-abc');
|
||||
|
||||
// No bot should be created
|
||||
expect(vi.mocked(AgentBotProviderModel)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should start a bot and register it', async () => {
|
||||
const mockBot = createMockBot();
|
||||
const BotClass = createMockBotClass(mockBot);
|
||||
mockAgentBotProviderModel.findEnabledByApplicationId.mockResolvedValue({
|
||||
applicationId: 'app-123',
|
||||
credentials: { token: 'tok123' },
|
||||
});
|
||||
|
||||
const manager = new GatewayManager({ registry: { slack: BotClass } });
|
||||
|
||||
await manager.startBot('slack', 'app-123', 'user-abc');
|
||||
|
||||
expect(BotClass).toHaveBeenCalledWith({ token: 'tok123', applicationId: 'app-123' });
|
||||
expect(mockBot.start).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should stop existing bot before starting a new one for the same key', async () => {
|
||||
const mockBot1 = createMockBot();
|
||||
const mockBot2 = createMockBot();
|
||||
const BotClass = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => mockBot1)
|
||||
.mockImplementationOnce(() => mockBot2);
|
||||
|
||||
mockAgentBotProviderModel.findEnabledByApplicationId.mockResolvedValue({
|
||||
applicationId: 'app-123',
|
||||
credentials: { token: 'tok' },
|
||||
});
|
||||
|
||||
const manager = new GatewayManager({ registry: { slack: BotClass } });
|
||||
|
||||
// Start bot first time
|
||||
await manager.startBot('slack', 'app-123', 'user-abc');
|
||||
expect(mockBot1.start).toHaveBeenCalled();
|
||||
|
||||
// Start bot second time for same key — should stop first
|
||||
await manager.startBot('slack', 'app-123', 'user-abc');
|
||||
expect(mockBot1.stop).toHaveBeenCalled();
|
||||
expect(mockBot2.start).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass credentials merged with applicationId to the bot constructor', async () => {
|
||||
const mockBot = createMockBot();
|
||||
const BotClass = createMockBotClass(mockBot);
|
||||
mockAgentBotProviderModel.findEnabledByApplicationId.mockResolvedValue({
|
||||
applicationId: 'my-app',
|
||||
credentials: { apiKey: 'key-abc', secret: 'sec-xyz' },
|
||||
});
|
||||
|
||||
const manager = new GatewayManager({ registry: { discord: BotClass } });
|
||||
|
||||
await manager.startBot('discord', 'my-app', 'user-xyz');
|
||||
|
||||
expect(BotClass).toHaveBeenCalledWith({
|
||||
apiKey: 'key-abc',
|
||||
secret: 'sec-xyz',
|
||||
applicationId: 'my-app',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopBot', () => {
|
||||
it('should do nothing when bot is not found', async () => {
|
||||
const manager = new GatewayManager({ registry: {} });
|
||||
|
||||
// Should not throw
|
||||
await expect(manager.stopBot('slack', 'app-123')).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should stop and remove a running bot', async () => {
|
||||
const mockBot = createMockBot();
|
||||
const BotClass = createMockBotClass(mockBot);
|
||||
mockAgentBotProviderModel.findEnabledByApplicationId.mockResolvedValue({
|
||||
applicationId: 'app-123',
|
||||
credentials: { token: 'tok' },
|
||||
});
|
||||
|
||||
const manager = new GatewayManager({ registry: { slack: BotClass } });
|
||||
|
||||
// First start the bot
|
||||
await manager.startBot('slack', 'app-123', 'user-abc');
|
||||
expect(mockBot.start).toHaveBeenCalled();
|
||||
|
||||
// Then stop it
|
||||
await manager.stopBot('slack', 'app-123');
|
||||
expect(mockBot.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not affect other bots when stopping one', async () => {
|
||||
const mockBot1 = createMockBot();
|
||||
const mockBot2 = createMockBot();
|
||||
const BotClass = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => mockBot1)
|
||||
.mockImplementationOnce(() => mockBot2);
|
||||
|
||||
mockAgentBotProviderModel.findEnabledByApplicationId
|
||||
.mockResolvedValueOnce({ applicationId: 'app-1', credentials: {} })
|
||||
.mockResolvedValueOnce({ applicationId: 'app-2', credentials: {} });
|
||||
|
||||
const manager = new GatewayManager({ registry: { slack: BotClass } });
|
||||
|
||||
await manager.startBot('slack', 'app-1', 'user-1');
|
||||
await manager.startBot('slack', 'app-2', 'user-2');
|
||||
|
||||
await manager.stopBot('slack', 'app-1');
|
||||
|
||||
expect(mockBot1.stop).toHaveBeenCalled();
|
||||
expect(mockBot2.stop).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createGatewayManager / getGatewayManager', () => {
|
||||
beforeEach(() => {
|
||||
const globalForGateway = globalThis as any;
|
||||
delete globalForGateway.gatewayManager;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
const globalForGateway = globalThis as any;
|
||||
delete globalForGateway.gatewayManager;
|
||||
});
|
||||
|
||||
it('should return undefined when no manager has been created', () => {
|
||||
expect(getGatewayManager()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create and return a GatewayManager instance', () => {
|
||||
const manager = createGatewayManager({ registry: {} });
|
||||
expect(manager).toBeInstanceOf(GatewayManager);
|
||||
});
|
||||
|
||||
it('should return the same instance on subsequent calls (singleton)', () => {
|
||||
const manager1 = createGatewayManager({ registry: {} });
|
||||
const manager2 = createGatewayManager({ registry: { slack: vi.fn() as any } });
|
||||
|
||||
expect(manager1).toBe(manager2);
|
||||
});
|
||||
|
||||
it('should be accessible via getGatewayManager after creation', () => {
|
||||
const created = createGatewayManager({ registry: {} });
|
||||
const retrieved = getGatewayManager();
|
||||
|
||||
expect(retrieved).toBe(created);
|
||||
});
|
||||
});
|
||||
265
src/server/services/gateway/botConnectQueue.test.ts
Normal file
265
src/server/services/gateway/botConnectQueue.test.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { getAgentRuntimeRedisClient } from '@/server/modules/AgentRuntime/redis';
|
||||
|
||||
import { BotConnectQueue } from './botConnectQueue';
|
||||
|
||||
// Mock the redis client
|
||||
const mockRedis = {
|
||||
hset: vi.fn(),
|
||||
hgetall: vi.fn(),
|
||||
hdel: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('@/server/modules/AgentRuntime/redis', () => ({
|
||||
getAgentRuntimeRedisClient: vi.fn(() => mockRedis),
|
||||
}));
|
||||
|
||||
describe('BotConnectQueue', () => {
|
||||
let queue: BotConnectQueue;
|
||||
|
||||
beforeEach(() => {
|
||||
queue = new BotConnectQueue();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('push', () => {
|
||||
it('should enqueue a connect request into Redis', async () => {
|
||||
mockRedis.hset.mockResolvedValue(1);
|
||||
|
||||
await queue.push('slack', 'app-123', 'user-abc');
|
||||
|
||||
expect(mockRedis.hset).toHaveBeenCalledWith(
|
||||
'bot:gateway:connect_queue',
|
||||
'slack:app-123',
|
||||
expect.stringContaining('"userId":"user-abc"'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should include a timestamp in the stored value', async () => {
|
||||
mockRedis.hset.mockResolvedValue(1);
|
||||
const beforeTime = Date.now();
|
||||
|
||||
await queue.push('discord', 'app-456', 'user-xyz');
|
||||
|
||||
const afterTime = Date.now();
|
||||
const callArgs = mockRedis.hset.mock.calls[0];
|
||||
const storedValue = JSON.parse(callArgs[2]);
|
||||
|
||||
expect(storedValue.timestamp).toBeGreaterThanOrEqual(beforeTime);
|
||||
expect(storedValue.timestamp).toBeLessThanOrEqual(afterTime);
|
||||
expect(storedValue.userId).toBe('user-xyz');
|
||||
});
|
||||
|
||||
it('should throw an error when Redis is not available', async () => {
|
||||
vi.mocked(getAgentRuntimeRedisClient).mockReturnValueOnce(null);
|
||||
|
||||
await expect(queue.push('slack', 'app-123', 'user-abc')).rejects.toThrow(
|
||||
'Redis is not available, cannot enqueue bot connect request',
|
||||
);
|
||||
expect(mockRedis.hset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use platform:applicationId as the Redis hash field', async () => {
|
||||
mockRedis.hset.mockResolvedValue(1);
|
||||
|
||||
await queue.push('telegram', 'bot-789', 'user-001');
|
||||
|
||||
expect(mockRedis.hset).toHaveBeenCalledWith(
|
||||
'bot:gateway:connect_queue',
|
||||
'telegram:bot-789',
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('popAll', () => {
|
||||
it('should return empty array when Redis is not available', async () => {
|
||||
vi.mocked(getAgentRuntimeRedisClient).mockReturnValueOnce(null);
|
||||
|
||||
const result = await queue.popAll();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockRedis.hgetall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty array when hash is empty', async () => {
|
||||
mockRedis.hgetall.mockResolvedValue({});
|
||||
|
||||
const result = await queue.popAll();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when hgetall returns null', async () => {
|
||||
mockRedis.hgetall.mockResolvedValue(null);
|
||||
|
||||
const result = await queue.popAll();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse and return valid non-expired entries', async () => {
|
||||
const now = Date.now();
|
||||
mockRedis.hgetall.mockResolvedValue({
|
||||
'slack:app-123': JSON.stringify({ timestamp: now, userId: 'user-abc' }),
|
||||
'discord:app-456': JSON.stringify({ timestamp: now, userId: 'user-def' }),
|
||||
});
|
||||
mockRedis.hdel.mockResolvedValue(0);
|
||||
|
||||
const result = await queue.popAll();
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toContainEqual({
|
||||
platform: 'slack',
|
||||
applicationId: 'app-123',
|
||||
userId: 'user-abc',
|
||||
});
|
||||
expect(result).toContainEqual({
|
||||
platform: 'discord',
|
||||
applicationId: 'app-456',
|
||||
userId: 'user-def',
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter out expired entries and clean them from Redis', async () => {
|
||||
const expiredTimestamp = Date.now() - 11 * 60 * 1000; // 11 minutes ago (expired)
|
||||
const validTimestamp = Date.now(); // now (valid)
|
||||
|
||||
mockRedis.hgetall.mockResolvedValue({
|
||||
'slack:expired-app': JSON.stringify({ timestamp: expiredTimestamp, userId: 'user-old' }),
|
||||
'discord:valid-app': JSON.stringify({ timestamp: validTimestamp, userId: 'user-new' }),
|
||||
});
|
||||
mockRedis.hdel.mockResolvedValue(1);
|
||||
|
||||
const result = await queue.popAll();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
platform: 'discord',
|
||||
applicationId: 'valid-app',
|
||||
userId: 'user-new',
|
||||
});
|
||||
|
||||
// Expired entry should have been deleted
|
||||
expect(mockRedis.hdel).toHaveBeenCalledWith('bot:gateway:connect_queue', 'slack:expired-app');
|
||||
});
|
||||
|
||||
it('should handle malformed JSON entries by treating them as expired', async () => {
|
||||
const validTimestamp = Date.now();
|
||||
mockRedis.hgetall.mockResolvedValue({
|
||||
'slack:bad-app': 'not-valid-json{{{',
|
||||
'discord:good-app': JSON.stringify({ timestamp: validTimestamp, userId: 'user-ok' }),
|
||||
});
|
||||
mockRedis.hdel.mockResolvedValue(1);
|
||||
|
||||
const result = await queue.popAll();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].platform).toBe('discord');
|
||||
|
||||
// Malformed entry should have been deleted
|
||||
expect(mockRedis.hdel).toHaveBeenCalledWith('bot:gateway:connect_queue', 'slack:bad-app');
|
||||
});
|
||||
|
||||
it('should skip entries where field has no colon separator', async () => {
|
||||
const now = Date.now();
|
||||
mockRedis.hgetall.mockResolvedValue({
|
||||
'noseparator': JSON.stringify({ timestamp: now, userId: 'user-abc' }),
|
||||
'slack:valid': JSON.stringify({ timestamp: now, userId: 'user-valid' }),
|
||||
});
|
||||
mockRedis.hdel.mockResolvedValue(0);
|
||||
|
||||
const result = await queue.popAll();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
platform: 'slack',
|
||||
applicationId: 'valid',
|
||||
userId: 'user-valid',
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly parse applicationId that contains colons', async () => {
|
||||
const now = Date.now();
|
||||
// Application IDs with colons in them — platform is everything before first colon
|
||||
mockRedis.hgetall.mockResolvedValue({
|
||||
'slack:app:with:colons': JSON.stringify({ timestamp: now, userId: 'user-abc' }),
|
||||
});
|
||||
mockRedis.hdel.mockResolvedValue(0);
|
||||
|
||||
const result = await queue.popAll();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
platform: 'slack',
|
||||
applicationId: 'app:with:colons',
|
||||
userId: 'user-abc',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call hdel when there are no expired entries', async () => {
|
||||
const now = Date.now();
|
||||
mockRedis.hgetall.mockResolvedValue({
|
||||
'slack:app-123': JSON.stringify({ timestamp: now, userId: 'user-abc' }),
|
||||
});
|
||||
|
||||
const result = await queue.popAll();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(mockRedis.hdel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should batch delete all expired entries in a single hdel call', async () => {
|
||||
const expiredTs = Date.now() - 15 * 60 * 1000;
|
||||
mockRedis.hgetall.mockResolvedValue({
|
||||
'slack:expired-1': JSON.stringify({ timestamp: expiredTs, userId: 'u1' }),
|
||||
'discord:expired-2': JSON.stringify({ timestamp: expiredTs, userId: 'u2' }),
|
||||
'telegram:expired-3': JSON.stringify({ timestamp: expiredTs, userId: 'u3' }),
|
||||
});
|
||||
mockRedis.hdel.mockResolvedValue(3);
|
||||
|
||||
const result = await queue.popAll();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
expect(mockRedis.hdel).toHaveBeenCalledTimes(1);
|
||||
expect(mockRedis.hdel).toHaveBeenCalledWith(
|
||||
'bot:gateway:connect_queue',
|
||||
'slack:expired-1',
|
||||
'discord:expired-2',
|
||||
'telegram:expired-3',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove the entry from Redis hash', async () => {
|
||||
mockRedis.hdel.mockResolvedValue(1);
|
||||
|
||||
await queue.remove('slack', 'app-123');
|
||||
|
||||
expect(mockRedis.hdel).toHaveBeenCalledWith('bot:gateway:connect_queue', 'slack:app-123');
|
||||
});
|
||||
|
||||
it('should do nothing when Redis is not available', async () => {
|
||||
vi.mocked(getAgentRuntimeRedisClient).mockReturnValueOnce(null);
|
||||
|
||||
await queue.remove('slack', 'app-123');
|
||||
|
||||
expect(mockRedis.hdel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use platform:applicationId as the hash field', async () => {
|
||||
mockRedis.hdel.mockResolvedValue(1);
|
||||
|
||||
await queue.remove('telegram', 'bot-999');
|
||||
|
||||
expect(mockRedis.hdel).toHaveBeenCalledWith('bot:gateway:connect_queue', 'telegram:bot-999');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user