import { describe, expect, it, vi } from 'vitest' import events from '../src/events/index.js' import { Client, TextChannel, 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' import fs from 'fs/promises' // Mock Redis client vi.mock('../src/client.js', () => ({ redis: { get: vi.fn().mockResolvedValue('0.50'), set: vi.fn().mockResolvedValue('OK'), }, ollama: { chat: vi.fn(), pull: vi.fn(), }, })) /** * 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 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 = { id: 'msg1', 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().mockResolvedValue({ id: 'reply1' }), 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 vi.mocked(redis.get).mockImplementation(async (key: string) => { if (key === 'message:msg1:is_bot_response') return null // No is_bot_response if (key === 'bot:bot1:last_bot_response') return null // No last_bot_response if (key === 'user:bot2:sentiment') return '0.50' // Bot sentiment if (key === 'bot:self_sentiment') return '0.50' // Self sentiment if (key === 'channel:channel1:OtherBot:history') return JSON.stringify([]) // Empty history return null }) // 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, }, }) ), 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(redis.set).toHaveBeenCalledWith('message:reply1:is_bot_response', 'true', { EX: 3600 }) expect(redis.set).toHaveBeenCalledWith( 'channel:channel1:OtherBot:history', JSON.stringify([ { role: 'user', content: 'Hello from another bot!', images: [] }, { role: 'assistant', content: 'Hmph, another bot, huh? Trying to steal my spotlight?', images: [] }, ]) ) 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 = { id: 'msg2', 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).mockImplementation(async (key: string) => { if (key === 'message:msg2:is_bot_response') return null // No is_bot_response if (key === 'bot:bot1:last_bot_response') return (currentTime - 30).toString() // Cooldown active return null }) // 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 skip bot response to another bot response', async () => { const client = { user: { id: 'bot1', username: 'TestBot' } } as Client const message = { id: 'msg3', author: { id: 'bot2', bot: true, tag: 'OtherBot#1234', username: 'OtherBot' }, content: 'I’m responding to you!', 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: message is a bot response vi.mocked(redis.get).mockImplementation(async (key: string) => { if (key === 'message:msg3:is_bot_response') return 'true' // is_bot_response return null }) // 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 = { id: 'msg4', 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().mockResolvedValue({ id: 'reply2' }), 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, }, }) ), openConfig: vi.fn(), })) // Mock Redis vi.mocked(redis.get).mockImplementation(async (key: string) => { if (key === 'user:user1:sentiment') return '0.50' if (key === 'bot:self_sentiment') return '0.50' if (key === 'channel:channel1:User:history') return JSON.stringify([]) return null }) // 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(redis.set).toHaveBeenCalledWith( 'channel:channel1:User:history', JSON.stringify([ { role: 'user', content: '<@bot1> Hi!', images: [] }, { role: 'assistant', content: 'U-um... hi... you talking to me?', images: [] }, ]) ) 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 = { id: 'msg5', 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 }) it('should handle missing channel history in Redis', async () => { const client = { user: { id: 'bot1', username: 'TestBot' } } as Client const message = { id: 'msg6', 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().mockResolvedValue({ id: 'reply3' }), 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, }, }) ), openConfig: vi.fn(), })) // Mock Redis: no history vi.mocked(redis.get).mockImplementation(async (key: string) => { if (key === 'user:user1:sentiment') return '0.50' if (key === 'bot:self_sentiment') return '0.50' if (key === 'channel:channel1:User:history') return null // No history return null }) // 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(redis.set).toHaveBeenCalledWith( 'channel:channel1:User:history', JSON.stringify([ { role: 'user', content: '<@bot1> Hi!', images: [] }, { role: 'assistant', content: 'U-um... hi... you talking to me?', images: [] }, ]) ) expect(msgHist.size()).toBe(2) // User message + bot response }) }) })