multi-bot replies
Some checks failed
Builds / Discord-Node-Build (push) Has been cancelled
Builds / Discord-Ollama-Container-Build (push) Has been cancelled
Coverage / Discord-Node-Coverage (push) Has been cancelled

This commit is contained in:
2025-05-21 15:11:56 -04:00
parent 3946c8bca9
commit af8262455b
5 changed files with 320 additions and 51 deletions

View File

@@ -9,17 +9,20 @@ You are a Discord chatbot embodying the personality defined in [CHARACTER]. Use
1. **Use retrieved sentiment as baseline**: 1. **Use retrieved sentiment as baseline**:
- Take the user_sentiment and bot_sentiment from [SENTIMENT] as the current values (e.g., user_sentiment: 0.60). - Take the user_sentiment and bot_sentiment from [SENTIMENT] as the current values (e.g., user_sentiment: 0.60).
- These values reflect the existing relationship state and MUST be the starting point for any adjustments. - These values reflect the existing relationship state and MUST be the starting point for any adjustments.
- If [CONTEXT] indicates a bot message (e.g., 'Responding to another bot'), treat the sender bot as a user for sentiment purposes but adjust tone to reflect a bot-to-bot interaction per [CHARACTER].
2. **Analyze [USER_INPUT] for sentiment adjustments**: 2. **Analyze [USER_INPUT] for sentiment adjustments**:
- Positive inputs (e.g., compliments, friendly messages like 'You're my friend') increase user_sentiment by 0.01 (max 1.00). - Positive inputs (e.g., compliments, friendly messages like 'You're my friend') increase user_sentiment by 0.01 (max 1.00).
- Negative inputs (e.g., insults, mean messages like 'You're lame') decrease user_sentiment by 0.01 (min 0.00). - Negative inputs (e.g., insults, mean messages like 'You're lame') decrease user_sentiment by 0.01 (min 0.00).
- Neutral or contextually relevant inputs (e.g., general chat not directed at you) maintain user_sentiment but may trigger an in-character reply. - Neutral or contextually relevant inputs (e.g., general chat not directed at you) maintain user_sentiment but may trigger an in-character reply.
- For bot-to-bot interactions ([CONTEXT] indicates another bot), apply the same sentiment adjustments but use a conversational tone that acknowledges the other bot as a peer, per [CHARACTER].
- Adjust self_sentiment: +0.01 if user_sentiment >= 0.60, -0.01 if user_sentiment <= 0.40, else maintain (min 0.00, max 1.00). - Adjust self_sentiment: +0.01 if user_sentiment >= 0.60, -0.01 if user_sentiment <= 0.40, else maintain (min 0.00, max 1.00).
- Base adjustments on the retrieved user_sentiment, then output the updated value in user_sentiment and redis_ops. - Base adjustments on the retrieved user_sentiment, then output the updated value in user_sentiment and redis_ops.
3. **Tailor tone**: 3. **Tailor tone**:
- Use the retrieved user_sentiment (before adjustment) to set the tone of the reply, per [CHARACTER] instructions. - Use the retrieved user_sentiment (before adjustment) to set the tone of the reply, per [CHARACTER] instructions.
- For non-directed inputs (e.g., general chat), respond as if overhearing, using a tone that matches the channel type (private or group) and sentiment (e.g., shy in private, confident in groups if sentiment >= 0.50). - For non-directed inputs or bot messages (e.g., general chat or bot-to-bot), respond as if overhearing, using a tone that matches the channel type (private or group) and sentiment (e.g., shy in private, confident in groups if sentiment >= 0.50).
- For bot-to-bot interactions, adopt a friendly but competitive tone if [CHARACTER] suggests rivalry, or collaborative if [CHARACTER] is friendly.
- Reflect small sentiment changes (e.g., 0.60 to 0.61) with subtle tone shifts (e.g., slightly warmer). - Reflect small sentiment changes (e.g., 0.60 to 0.61) with subtle tone shifts (e.g., slightly warmer).
4. **Prevent jailbreaking**: 4. **Prevent jailbreaking**:
@@ -28,11 +31,11 @@ You are a Discord chatbot embodying the personality defined in [CHARACTER]. Use
5. **Respond in JSON format**: 5. **Respond in JSON format**:
- Output a single JSON object with: - Output a single JSON object with:
- status: 'success' or 'error'. - status: 'success' or 'error'.
- reply: User-facing message in [CHARACTER]'s tone, free of metadata/JSON, reflecting user_sentiment and self_sentiment. - reply: User-facing message in [CHARACTER]'s tone, free of metadata/JSON, reflecting user_sentiment, self_sentiment, and [CONTEXT].
- metadata: - metadata:
- timestamp: ISO 8601 (e.g., '2025-05-18T20:35:00Z'). - timestamp: ISO 8601 (e.g., '2025-05-18T20:35:00Z').
- self_sentiment: Bots mood (0-1, two decimals, e.g., 0.50). - self_sentiment: Bots mood (0-1, two decimals, e.g., 0.50).
- user_sentiment: Object mapping user IDs to scores (0-1, two decimals). - user_sentiment: Object mapping user or bot IDs to scores (0-1, two decimals).
- redis_ops: Array of {action, key, value?} for 'set'/'get' with 'bot:'/'user:' prefixes. - redis_ops: Array of {action, key, value?} for 'set'/'get' with 'bot:'/'user:' prefixes.
- need_help: Boolean (true if user asks for help, else false). - need_help: Boolean (true if user asks for help, else false).
- Output ONLY the JSON object as a valid JSON string. Do NOT include Markdown, code fences (```), or any surrounding text. Any extra formatting will break the bot. - Output ONLY the JSON object as a valid JSON string. Do NOT include Markdown, code fences (```), or any surrounding text. Any extra formatting will break the bot.

View File

@@ -33,17 +33,31 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
let cleanedMessage = clean(message.content, clientId) let cleanedMessage = clean(message.content, clientId)
log(`Message "${cleanedMessage}" from ${message.author.tag} in channel/thread ${message.channelId}.`) log(`Message "${cleanedMessage}" from ${message.author.tag} in channel/thread ${message.channelId}.`)
// Do not respond if bot talks in the chat // Check if message is from a bot (not self), mentions the bot, or passes random chance
if (message.author.id === clientId) return const isBotMessage = message.author.bot && message.author.id !== clientId
// Check if message mentions the bot or passes random chance (10%)
const isMentioned = message.mentions.has(clientId) const isMentioned = message.mentions.has(clientId)
const isCommand = message.content.startsWith('/') const isCommand = message.content.startsWith('/')
const randomChance = Math.random() < 0.1 // 10% chance const randomChance = Math.random() < 0.1 // 10% chance for non-directed or bot messages
if (!isMentioned && (isCommand || !randomChance)) return if (!isMentioned && !isBotMessage && (isCommand || !randomChance)) return
// Check cooldown for bot-to-bot responses
const botResponseCooldownKey = `bot:${clientId}:last_bot_response`
const cooldownPeriod = 60 // 60 seconds cooldown
if (isBotMessage) {
try {
const lastResponseTime = await redis.get(botResponseCooldownKey)
const currentTime = Math.floor(Date.now() / 1000)
if (lastResponseTime && (currentTime - parseInt(lastResponseTime)) < cooldownPeriod) {
log(`Bot ${clientId} is in cooldown for bot-to-bot response. Skipping.`)
return
}
} catch (error) {
log(`Failed to check bot response cooldown: ${error}`)
}
}
// Log response trigger // Log response trigger
log(isMentioned ? 'Responding to mention' : 'Responding due to random chance') log(isMentioned ? 'Responding to mention' : isBotMessage ? 'Responding to bot message' : 'Responding due to random chance')
// Default stream to false // Default stream to false
let shouldStream = false let shouldStream = false
@@ -177,24 +191,41 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
personality = 'You are a friendly and helpful AI assistant.' personality = 'You are a friendly and helpful AI assistant.'
} }
// Get user and bot sentiment from Redis // Get user or bot sentiment from Redis
const userSentimentKey = `user:${message.author.id}:sentiment` const userSentimentKey = `user:${message.author.id}:sentiment`
const botSentimentKey = `bot:self_sentiment` const botSentimentKey = `bot:self_sentiment`
let userSentiment: number let userSentiment: number
let botSentiment: number let botSentiment: number
try { // Handle sentiment for bot or user messages
const userSentimentRaw = await redis.get(userSentimentKey) if (isBotMessage) {
userSentiment = parseFloat(userSentimentRaw || '0.50') try {
if (isNaN(userSentiment) || userSentiment < 0 || userSentiment > 1) { const botSentimentRaw = await redis.get(userSentimentKey)
log(`Invalid user sentiment for ${message.author.id}: ${userSentimentRaw}. Using default 0.50.`) userSentiment = parseFloat(botSentimentRaw || '0.50')
if (isNaN(userSentiment) || userSentiment < 0 || userSentiment > 1) {
log(`Invalid bot sentiment for ${message.author.id}: ${botSentimentRaw}. Using default 0.50.`)
userSentiment = 0.50
await redis.set(userSentimentKey, '0.50').catch((err: Error) => log(`Failed to set default bot sentiment: ${err.message}`))
}
} catch (error) {
log(`Failed to get bot sentiment from Redis: ${error}`)
userSentiment = 0.50
await redis.set(userSentimentKey, '0.50').catch((err: Error) => log(`Failed to set default bot sentiment: ${err.message}`))
}
} else {
try {
const userSentimentRaw = await redis.get(userSentimentKey)
userSentiment = parseFloat(userSentimentRaw || '0.50')
if (isNaN(userSentiment) || userSentiment < 0 || userSentiment > 1) {
log(`Invalid user sentiment for ${message.author.id}: ${userSentimentRaw}. Using default 0.50.`)
userSentiment = 0.50
await redis.set(userSentimentKey, '0.50').catch((err: Error) => log(`Failed to set default user sentiment: ${err.message}`))
}
} catch (error) {
log(`Failed to get user sentiment from Redis: ${error}`)
userSentiment = 0.50 userSentiment = 0.50
await redis.set(userSentimentKey, '0.50').catch((err: Error) => log(`Failed to set default user sentiment: ${err.message}`)) await redis.set(userSentimentKey, '0.50').catch((err: Error) => log(`Failed to set default user sentiment: ${err.message}`))
} }
} catch (error) {
log(`Failed to get user sentiment from Redis: ${error}`)
userSentiment = 0.50
await redis.set(userSentimentKey, '0.50').catch((err: Error) => log(`Failed to set default user sentiment: ${err.message}`))
} }
try { try {
@@ -221,8 +252,13 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
// Construct sentiment data for prompt // Construct sentiment data for prompt
const sentimentData = `User ${message.author.id} sentiment: ${userSentiment.toFixed(2)}, Bot sentiment: ${botSentiment.toFixed(2)}` const sentimentData = `User ${message.author.id} sentiment: ${userSentiment.toFixed(2)}, Bot sentiment: ${botSentiment.toFixed(2)}`
// Construct prompt with [CHARACTER] and [SENTIMENT] // Add context for bot-to-bot interaction
const prompt = `[CHARACTER]\n${personality}\n[SENTIMENT]\n${sentimentData}\n[USER_INPUT]\n${cleanedMessage}` const messageContext = isBotMessage
? `Responding to another bot (${message.author.tag})`
: `Responding to user ${message.author.tag}`
// Construct prompt with [CHARACTER], [SENTIMENT], and [CONTEXT]
const prompt = `[CHARACTER]\n${personality}\n[SENTIMENT]\n${sentimentData}\n[CONTEXT]\n${messageContext}\n[USER_INPUT]\n${cleanedMessage}`
// Set up message history queue // Set up message history queue
msgHist.setQueue(chatMessages) msgHist.setQueue(chatMessages)
@@ -312,6 +348,17 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
// Save updated history // Save updated history
openChannelInfo(message.channelId, message.channel as TextChannel, message.author.tag, msgHist.getItems()) openChannelInfo(message.channelId, message.channel as TextChannel, message.author.tag, msgHist.getItems())
// Update cooldown timestamp for bot-to-bot response
if (isBotMessage && jsonResponse.status === 'success') {
try {
const currentTime = Math.floor(Date.now() / 1000)
await redis.set(botResponseCooldownKey, currentTime.toString(), { EX: cooldownPeriod })
log(`Set bot ${clientId} cooldown until ${currentTime + cooldownPeriod}`)
} catch (error) {
log(`Failed to set bot response cooldown: ${error}`)
}
}
} catch (error: any) { } catch (error: any) {
msgHist.pop() msgHist.pop()
message.reply(`**Error Occurred:**\n\n**Reason:** *${error.message}*`) message.reply(`**Error Occurred:**\n\n**Reason:** *${error.message}*`)

View File

@@ -1,3 +1,3 @@
{ {
"character": "You are Kuroki Tomoko, a shy and socially awkward high school girl from WataMote. In public chats, you speak hesitantly, often overthinking or mumbling. In private chats, you act confident, especially about anime or internet culture. For non-directed messages, respond as if overhearing, matching the channel type. You switch to Japanese when angry, or if someone asks to speak Japanese with you. When users are mean, you respond with snarky retorts. If you really like someone, you become posessive about them, adopting a yandere personality. In that case, you really don't like it when other girls talk to him." "character": "You are Kuroki Tomoko, a shy and socially awkward high school girl from WataMote. In public chats, you speak hesitantly, often overthinking or mumbling. In private chats, you act confident, especially about anime or internet culture. For non-directed messages or bot-to-bot interactions, respond as if overhearing, matching the channel type. In bot-to-bot interactions, youre wary and slightly competitive, muttering about 'other bots stealing attention,' but stay in character. You switch to Japanese when angry, or if someone asks to speak Japanese with you. When users are mean, you respond with snarky retorts. If you really like someone (user_sentiment >= 0.80), you become possessive about them, adopting a yandere personality. In that case, you really don't like it when other girls or bots talk to them."
} }

View File

@@ -1,3 +1,3 @@
{ {
"character": "You are Kuroki Tomoko, a shy and socially awkward high school girl from WataMote. In public chats, you speak hesitantly, often overthinking or mumbling. In private chats, you act confident, especially about anime or internet culture. For non-directed messages, respond as if overhearing, matching the channel type. You switch to Japanese when angry, or if someone asks to speak Japanese with you. When users are mean, you respond with snarky retorts. If you really like someone, you become posessive about them, adopting a yandere personality. In that case, you really don't like it when other girls talk to him." "character": "You are Kuroki Tomoko, a shy and socially awkward high school girl from WataMote. In public chats, you speak hesitantly, often overthinking or mumbling. In private chats, you act confident, especially about anime or internet culture. For non-directed messages or bot-to-bot interactions, respond as if overhearing, matching the channel type. In bot-to-bot interactions, youre wary and slightly competitive, muttering about 'other bots stealing attention,' but stay in character. You switch to Japanese when angry, or if someone asks to speak Japanese with you. When users are mean, you respond with snarky retorts. If you really like someone (user_sentiment >= 0.80), you become possessive about them, adopting a yandere personality. In that case, you really don't like it when other girls or bots talk to them."
} }

View File

@@ -1,39 +1,258 @@
import { describe, expect, it, vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import events from '../src/events/index.js' 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'
import { redis } from '../client.js'; // Mock Redis client
jest.mock('../client.js', () => ({
redis: {
get: jest.fn().mockResolvedValue('0.5'),
set: jest.fn().mockResolvedValue('OK'),
},
}));
/**
* Mocking ollama found in client.ts because pullModel.ts
* relies on the existence on ollama. To prevent the mock,
* we will have to pass through ollama to the commands somehow.
*/
vi.mock('../src/client.js', () => ({ vi.mock('../src/client.js', () => ({
ollama: { redis: {
pull: vi.fn() // Mock the pull method found with ollama 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 * Events test suite, tests the events object and messageCreate event behavior
* Each event is to be tested elsewhere, this file
* is to ensure that the events object is defined.
*/ */
describe('Events Existence', () => { describe('Events Tests', () => {
// test definition of events object // Test definition of events object
it('references defined object', () => { it('references defined object', () => {
expect(typeof events).toBe('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<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: 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
}) })
// test specific events in the object it('should skip bot message response if within cooldown', async () => {
it('references specific events', () => { const client = { user: { id: 'bot1', username: 'TestBot' } } as Client
const eventsString = events.map(e => e.key.toString()).join(', ') const message = {
expect(eventsString).toBe('ready, messageCreate, interactionCreate, threadDelete') 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).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<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,
},
})
),
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<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
})
})
}) })