/** * Tests for helpers.ts functions */ jest.mock('../../logging', () => ({ logInfo: jest.fn(), logWarn: jest.fn(), logError: jest.fn(), })); jest.mock('../util', () => ({ REAL_NAMES: {}, LOSER_WHITELIST: [], })); jest.mock('node:path', () => ({ join: jest.fn(() => '/tmp/streaks.json'), })); jest.mock('node:fs', () => ({ existsSync: jest.fn(() => false), readFileSync: jest.fn(), writeFileSync: jest.fn(), })); // Mock Discord.js Collection class (mimics Map with filter method) class MockCollection { private map: Map; constructor(entries?: Array<[any, any]>) { this.map = new Map(entries || []); } get size() { return this.map.size; } filter(fn: (value: any, key: any) => boolean) { const result = new MockCollection(); for (const [key, value] of this.map.entries()) { if (fn(value, key)) { result.map.set(key, value); } } return result; } values() { return this.map.values(); } entries() { return this.map.entries(); } [Symbol.iterator]() { return this.map[Symbol.iterator](); } } const { dateToSnowflake, triggerThrowback, KAWAII_PHRASES, parseLoadingEmojis, getRandomLoadingEmoji, getRandomKawaiiPhrase, createStatusEmbed, createSimpleStatusEmbed, } = require('../commands/helpers'); describe('helpers.ts', () => { describe('dateToSnowflake', () => { it('should convert Discord epoch to snowflake 0', () => { const discordEpoch = new Date('2015-01-01T00:00:00.000Z'); const result = dateToSnowflake(discordEpoch); expect(result).toBe('0'); }); it('should convert a known date to snowflake', () => { const testDate = new Date('2024-01-01T00:00:00.000Z'); const result = dateToSnowflake(testDate); expect(result).toMatch(/^\d+$/); expect(result.length).toBeGreaterThan(10); }); it('should produce increasing snowflakes for increasing dates', () => { const date1 = new Date('2024-01-01T00:00:00.000Z'); const date2 = new Date('2024-01-02T00:00:00.000Z'); const snowflake1 = dateToSnowflake(date1); const snowflake2 = dateToSnowflake(date2); expect(BigInt(snowflake2)).toBeGreaterThan(BigInt(snowflake1)); }); }); describe('KAWAII_PHRASES', () => { it('should contain kawaii phrases', () => { expect(KAWAII_PHRASES.length).toBeGreaterThan(0); expect(KAWAII_PHRASES).toContain('Hmm... let me think~ ♪'); }); }); describe('parseLoadingEmojis', () => { it('should parse emojis from environment variable', () => { const original = process.env.LOADING_EMOJIS; process.env.LOADING_EMOJIS = '<:clueless:123>,,,'; const result = parseLoadingEmojis(); process.env.LOADING_EMOJIS = original; expect(result).toHaveLength(4); expect(result).toEqual([ '<:clueless:123>', '', '', '', ]); }); it('should return default emojis when LOADING_EMOJIS is empty', () => { const original = process.env.LOADING_EMOJIS; process.env.LOADING_EMOJIS = ''; const result = parseLoadingEmojis(); process.env.LOADING_EMOJIS = original; expect(result).toEqual(['🤔', '✨', '🎵']); }); it('should handle whitespace in emoji list', () => { const original = process.env.LOADING_EMOJIS; process.env.LOADING_EMOJIS = ' <:test:123> , '; const result = parseLoadingEmojis(); process.env.LOADING_EMOJIS = original; expect(result).toEqual(['<:test:123>', '']); }); }); describe('getRandomLoadingEmoji', () => { it('should return a valid emoji from the list', () => { const result = getRandomLoadingEmoji(); const validEmojis = parseLoadingEmojis(); expect(validEmojis).toContain(result); }); }); describe('getRandomKawaiiPhrase', () => { it('should return a valid kawaii phrase', () => { const result = getRandomKawaiiPhrase(); expect(KAWAII_PHRASES).toContain(result); }); }); describe('createStatusEmbed', () => { it('should create an embed with emoji, phrase, and status', () => { const embed = createStatusEmbed('🤔', 'Hmm... let me think~ ♪', 'Processing...'); expect(embed).toBeDefined(); expect(embed.data.author).toBeDefined(); expect(embed.data.author?.name).toBe('Hmm... let me think~ ♪'); }); }); describe('createSimpleStatusEmbed', () => { it('should create an embed with random emoji and phrase', () => { const embed = createSimpleStatusEmbed('Working...'); expect(embed).toBeDefined(); expect(embed.data.author).toBeDefined(); }); }); describe('triggerThrowback', () => { const mockClient = { guilds: { fetch: jest.fn(), }, }; const mockProvider = { requestLLMResponse: jest.fn(), }; const mockSysprompt = 'You are a helpful assistant.'; const mockLlmconf = { msg_context: 10, streaming: false, }; beforeEach(() => { jest.clearAllMocks(); }); it('should fetch messages from 1 year ago', async () => { const mockMessage = { id: '123456789', author: { username: 'testuser', bot: false }, cleanContent: 'Hello from a year ago!', type: 0, reply: jest.fn(), }; const mockChannel = { messages: { fetch: jest.fn().mockResolvedValue(new MockCollection([['123456789', mockMessage]])), }, }; mockProvider.requestLLMResponse.mockResolvedValue('Nice throwback!'); await triggerThrowback( mockClient as any, mockChannel as any, mockChannel as any, mockProvider, mockSysprompt, mockLlmconf ); // Verify messages.fetch was called with around date from 1 year ago const fetchCall = mockChannel.messages.fetch.mock.calls[0][0]; expect(fetchCall.around).toBeDefined(); expect(fetchCall.limit).toBe(50); }); it('should fetch message history for context before generating LLM response', async () => { const mockReply = jest.fn(); const mockMessage = { id: '123456789', author: { username: 'testuser', bot: false }, cleanContent: 'Hello from a year ago!', type: 0, reply: mockReply, }; const mockHistoryMessage = { id: '123456788', author: { username: 'testuser', bot: false }, cleanContent: 'Previous context', type: 0, }; const mockChannel = { messages: { fetch: jest .fn() .mockResolvedValueOnce( new MockCollection([ ['123456788', mockHistoryMessage], ['123456789', mockMessage], ]) ) .mockResolvedValueOnce(new MockCollection([['123456788', mockHistoryMessage]])), }, }; mockProvider.requestLLMResponse.mockResolvedValue('Nice throwback!'); await triggerThrowback( mockClient as any, mockChannel as any, mockChannel as any, mockProvider, mockSysprompt, mockLlmconf ); // Verify messages.fetch was called twice: once for throwback, once for history expect(mockChannel.messages.fetch).toHaveBeenCalledTimes(2); // Verify history fetch used msg_context from llmconf const historyFetchCall = mockChannel.messages.fetch.mock.calls[1][0]; expect(historyFetchCall.limit).toBe(mockLlmconf.msg_context - 1); expect(historyFetchCall.before).toBe(mockMessage.id); // Verify LLM was called with context (history + selected message) expect(mockProvider.requestLLMResponse).toHaveBeenCalledWith( expect.arrayContaining([expect.objectContaining({ id: '123456788' })]), mockSysprompt, mockLlmconf ); }); it('should reply to the original message', async () => { const mockReply = jest.fn(); const mockMessage = { id: '123456789', author: { username: 'testuser', bot: false }, cleanContent: 'Hello from a year ago!', type: 0, reply: mockReply, }; const mockChannel = { messages: { fetch: jest.fn().mockResolvedValue(new MockCollection([['123456789', mockMessage]])), }, }; mockProvider.requestLLMResponse.mockResolvedValue('Nice throwback!'); await triggerThrowback( mockClient as any, mockChannel as any, mockChannel as any, mockProvider, mockSysprompt, mockLlmconf ); // Verify reply was called on the original message, not send on channel expect(mockReply).toHaveBeenCalledWith('Nice throwback!'); }); it('should throw error when no messages found from 1 year ago', async () => { const mockChannel = { messages: { fetch: jest.fn().mockResolvedValue(new MockCollection()), }, }; await expect( triggerThrowback( mockClient as any, mockChannel as any, mockChannel as any, mockProvider, mockSysprompt, mockLlmconf ) ).rejects.toThrow('No messages found from 1 year ago.'); }); it('should filter out bot messages', async () => { const mockBotMessage = { id: '111', author: { username: 'bot', bot: true }, cleanContent: 'Bot message', type: 0, }; const mockUserMessage = { id: '222', author: { username: 'user', bot: false }, cleanContent: 'User message', type: 0, reply: jest.fn(), }; const mockChannel = { messages: { fetch: jest .fn() .mockResolvedValue(new MockCollection([['111', mockBotMessage], ['222', mockUserMessage]])), }, }; mockProvider.requestLLMResponse.mockResolvedValue('Reply!'); await triggerThrowback( mockClient as any, mockChannel as any, mockChannel as any, mockProvider, mockSysprompt, mockLlmconf ); // Verify only user message was considered (bot filtered out) expect(mockProvider.requestLLMResponse).toHaveBeenCalled(); }); it('should filter out messages without content', async () => { const mockEmptyMessage = { id: '111', author: { username: 'user1', bot: false }, cleanContent: '', type: 0, }; const mockValidMessage = { id: '222', author: { username: 'user2', bot: false }, cleanContent: 'Valid message', type: 0, reply: jest.fn(), }; const mockChannel = { messages: { fetch: jest .fn() .mockResolvedValue(new MockCollection([['111', mockEmptyMessage], ['222', mockValidMessage]])), }, }; mockProvider.requestLLMResponse.mockResolvedValue('Reply!'); await triggerThrowback( mockClient as any, mockChannel as any, mockChannel as any, mockProvider, mockSysprompt, mockLlmconf ); // Verify only valid message was considered expect(mockProvider.requestLLMResponse).toHaveBeenCalled(); }); it('should return throwback result with original message, author, and response', async () => { const mockMessage = { id: '123456789', author: { username: 'testuser', bot: false }, cleanContent: 'Hello from a year ago!', type: 0, reply: jest.fn(), }; const mockChannel = { messages: { fetch: jest.fn().mockResolvedValue(new MockCollection([['123456789', mockMessage]])), }, }; mockProvider.requestLLMResponse.mockResolvedValue('Nice throwback!'); const result = await triggerThrowback( mockClient as any, mockChannel as any, mockChannel as any, mockProvider, mockSysprompt, mockLlmconf ); expect(result).toEqual({ originalMessage: 'Hello from a year ago!', author: 'testuser', response: 'Nice throwback!', }); }); }); });