416 lines
15 KiB
TypeScript
416 lines
15 KiB
TypeScript
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<UserMessage>()
|
||
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<UserMessage>()
|
||
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<UserMessage>()
|
||
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<UserMessage>()
|
||
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<UserMessage>()
|
||
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<UserMessage>()
|
||
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
|
||
})
|
||
})
|
||
})
|