import { describe, expect, it, vi } from 'vitest' import events from '../src/events/index.js' import { Client, TextChannel, User, Message } from 'discord.js' import { redis, ollama } from '../src/client.js' import { Queue } from '../src/queues/queue.js' import { UserMessage } from '../src/utils/index.js' // Mock Redis client vi.mock('../src/client.js', () => ({ redis: { get: vi.fn().mockResolvedValue('0.50'), set: vi.fn().mockResolvedValue('OK'), }, ollama: { chat: vi.fn(), // Mock the chat method for messageCreate pull: vi.fn(), // Retain mock for pull method }, })) /** * Events test suite, tests the events object and messageCreate event behavior */ describe('Events Tests', () => { // Test definition of events object it('references defined object', () => { expect(typeof events).toBe('object') }) // Test specific events in the object it('references specific events', () => { const eventsString = events.map(e => e.key.toString()).join(', ') expect(eventsString).toBe('ready, messageCreate, interactionCreate, threadDelete') }) // Test messageCreate event for bot-to-bot response describe('messageCreate', () => { const messageCreateEvent = events.find(e => e.key === 'messageCreate') if (!messageCreateEvent) throw new Error('messageCreate event not found') it('should respond to bot message with random chance and respect cooldown', async () => { const client = { user: { id: 'bot1', username: 'TestBot' } } as Client const message = { author: { id: 'bot2', bot: true, tag: 'OtherBot#1234', username: 'OtherBot' }, content: 'Hello from another bot!', mentions: { has: () => false }, channelId: 'channel1', channel: { name: 'test-channel' } as TextChannel, reply: vi.fn(), attachments: { first: () => null }, guildId: 'guild1', } as unknown as Message const msgHist = new Queue() msgHist.capacity = 50 const defaultModel = 'aidoll-gemma3-12b-q6:latest' // Mock random chance to pass (10% probability) vi.spyOn(Math, 'random').mockReturnValue(0.05) // Mock Redis: no cooldown initially vi.mocked(redis.get).mockResolvedValueOnce(null) // No last_bot_response vi.mocked(redis.get).mockResolvedValueOnce('0.50') // Bot sentiment for bot2 vi.mocked(redis.get).mockResolvedValueOnce('0.50') // Self sentiment // Mock fs for personality.json vi.spyOn(fs, 'readFile').mockResolvedValue( JSON.stringify({ character: 'You are Kuroki Tomoko, a shy and socially awkward high school girl from WataMote.', }) ) // Mock utils functions vi.mock('../src/utils/index.js', () => ({ clean: vi.fn(content => content), getServerConfig: vi.fn((_, cb) => cb({ options: { 'toggle-chat': true } })), getUserConfig: vi.fn((_, cb) => cb({ options: { 'message-style': false, 'switch-model': 'aidoll-gemma3-12b-q6:latest', 'modify-capacity': 50, }, }) ), getChannelInfo: vi.fn((_, cb) => cb({ messages: [] })), openChannelInfo: vi.fn(), openConfig: vi.fn(), })) // Mock Ollama response vi.mocked(ollama.chat).mockResolvedValue({ message: { content: JSON.stringify({ status: 'success', reply: 'Hmph, another bot, huh? Trying to steal my spotlight?', metadata: { timestamp: '2025-05-21T14:00:00Z', self_sentiment: 0.50, user_sentiment: { 'bot2': 0.50 }, redis_ops: [ { action: 'set', key: 'user:bot2:sentiment', value: 0.50 }, { action: 'set', key: 'bot:self_sentiment', value: 0.50 }, ], need_help: false, }, }), }, }) // Execute messageCreate event await messageCreateEvent.execute( { log: console.log, msgHist, ollama, client, defaultModel }, message ) expect(message.reply).toHaveBeenCalledWith('Hmph, another bot, huh? Trying to steal my spotlight?') expect(redis.set).toHaveBeenCalledWith( 'bot:bot1:last_bot_response', expect.any(String), { EX: 60 } ) expect(msgHist.size()).toBe(2) // User message + bot response }) it('should skip bot message response if within cooldown', async () => { const client = { user: { id: 'bot1', username: 'TestBot' } } as Client const message = { author: { id: 'bot2', bot: true, tag: 'OtherBot#1234', username: 'OtherBot' }, content: 'Hello again!', mentions: { has: () => false }, channelId: 'channel1', channel: { name: 'test-channel' } as TextChannel, reply: vi.fn(), attachments: { first: () => null }, guildId: 'guild1', } as unknown as Message const msgHist = new Queue() msgHist.capacity = 50 const defaultModel = 'aidoll-gemma3-12b-q6:latest' // Mock random chance to pass vi.spyOn(Math, 'random').mockReturnValue(0.05) // Mock Redis: within cooldown const currentTime = Math.floor(Date.now() / 1000) vi.mocked(redis.get).mockResolvedValueOnce((currentTime - 30).toString()) // Cooldown active // Execute messageCreate event await messageCreateEvent.execute( { log: console.log, msgHist, ollama, client, defaultModel }, message ) expect(message.reply).not.toHaveBeenCalled() expect(redis.set).not.toHaveBeenCalled() expect(msgHist.size()).toBe(0) // No messages added }) it('should respond to user mention', async () => { const client = { user: { id: 'bot1', username: 'TestBot' } } as Client const message = { author: { id: 'user1', bot: false, tag: 'User#1234', username: 'User' }, content: '<@bot1> Hi!', mentions: { has: (id: string) => id === 'bot1' }, channelId: 'channel1', channel: { name: 'test-channel' } as TextChannel, reply: vi.fn(), attachments: { first: () => null }, guildId: 'guild1', } as unknown as Message const msgHist = new Queue() msgHist.capacity = 50 const defaultModel = 'aidoll-gemma3-12b-q6:latest' // Mock fs for personality.json vi.spyOn(fs, 'readFile').mockResolvedValue( JSON.stringify({ character: 'You are Kuroki Tomoko, a shy and socially awkward high school girl from WataMote.', }) ) // Mock utils functions vi.mock('../src/utils/index.js', () => ({ clean: vi.fn(content => content), getServerConfig: vi.fn((_, cb) => cb({ options: { 'toggle-chat': true } })), getUserConfig: vi.fn((_, cb) => cb({ options: { 'message-style': false, 'switch-model': 'aidoll-gemma3-12b-q6:latest', 'modify-capacity': 50, }, }) ), getChannelInfo: vi.fn((_, cb) => cb({ messages: [] })), openChannelInfo: vi.fn(), openConfig: vi.fn(), })) // Mock Ollama response vi.mocked(ollama.chat).mockResolvedValue({ message: { content: JSON.stringify({ status: 'success', reply: 'U-um... hi... you talking to me?', metadata: { timestamp: '2025-05-21T14:00:00Z', self_sentiment: 0.50, user_sentiment: { 'user1': 0.50 }, redis_ops: [ { action: 'set', key: 'user:user1:sentiment', value: 0.50 }, { action: 'set', key: 'bot:self_sentiment', value: 0.50 }, ], need_help: false, }, }), }, }) // Execute messageCreate event await messageCreateEvent.execute( { log: console.log, msgHist, ollama, client, defaultModel }, message ) expect(message.reply).toHaveBeenCalledWith('U-um... hi... you talking to me?') expect(redis.set).toHaveBeenCalledWith('user:user1:sentiment', '0.50') expect(redis.set).toHaveBeenCalledWith('bot:self_sentiment', '0.50') expect(msgHist.size()).toBe(2) // User message + bot response }) it('should not respond to own message', async () => { const client = { user: { id: 'bot1', username: 'TestBot' } } as Client const message = { author: { id: 'bot1', bot: true, tag: 'TestBot#1234', username: 'TestBot' }, content: 'I said something!', mentions: { has: () => false }, channelId: 'channel1', channel: { name: 'test-channel' } as TextChannel, reply: vi.fn(), attachments: { first: () => null }, guildId: 'guild1', } as unknown as Message const msgHist = new Queue() msgHist.capacity = 50 const defaultModel = 'aidoll-gemma3-12b-q6:latest' // Execute messageCreate event await messageCreateEvent.execute( { log: console.log, msgHist, ollama, client, defaultModel }, message ) expect(message.reply).not.toHaveBeenCalled() expect(redis.set).not.toHaveBeenCalled() expect(msgHist.size()).toBe(0) // No messages added }) }) })