Files
FemScoreboard/discord/__tests__/bot.test.ts

252 lines
8.5 KiB
TypeScript

/**
* Tests for bot.ts helper functions
*/
// Mock dependencies before importing bot
jest.mock('../util', () => {
const actual = jest.requireActual('../util');
return {
...actual,
openDb: jest.fn(),
db: {
migrate: jest.fn(),
get: jest.fn(),
run: jest.fn(),
},
};
});
jest.mock('node-fetch', () => jest.fn());
jest.mock('tmp', () => ({
fileSync: jest.fn(() => ({ name: '/tmp/test' })),
setGracefulCleanup: jest.fn(),
}));
jest.mock('fs', () => ({
...jest.requireActual('fs'),
writeFileSync: jest.fn(),
readFileSync: jest.fn(),
existsSync: jest.fn(),
}));
// Mock environment variables
const mockEnv = {
LOADING_EMOJIS: '<:clueless:123>,<a:hachune:456>,<a:chairspin:789>,<a:nekodance:012>',
};
// Helper functions for testing
function parseLoadingEmojis(): string[] {
const emojiStr = mockEnv.LOADING_EMOJIS || '';
if (!emojiStr.trim()) {
return ['🤔', '✨', '🎵'];
}
return emojiStr
.split(',')
.map((e) => e.trim())
.filter((e) => e.length > 0);
}
function getRandomLoadingEmoji(): string {
const emojis = parseLoadingEmojis();
return emojis[Math.floor(Math.random() * emojis.length)];
}
function formatLoadingMessage(emoji: string, reasoning: string): string {
const kawaiiPhrases = [
'Hmm... let me think~ ♪',
'Processing nyaa~',
'Miku is thinking...',
'Calculating with magic ✨',
'Pondering desu~',
'Umm... one moment! ♪',
'Brain go brrr~',
'Assembling thoughts... ♪',
'Loading Miku-brain...',
'Thinking hard senpai~',
];
const phrase = kawaiiPhrases[Math.floor(Math.random() * kawaiiPhrases.length)];
let content = `${emoji}\n${phrase}`;
if (reasoning && reasoning.trim().length > 0) {
const displayReasoning =
reasoning.length > 500 ? reasoning.slice(0, 500) + '...' : reasoning;
content += `\n\n> ${displayReasoning}`;
}
return content;
}
describe('bot.ts helper functions', () => {
/**
* Convert a Date to a Discord snowflake ID (approximate)
* Discord epoch: 2015-01-01T00:00:00.000Z
*/
function dateToSnowflake(date: Date): string {
const DISCORD_EPOCH = 1420070400000n;
const timestamp = BigInt(date.getTime());
const snowflake = (timestamp - DISCORD_EPOCH) << 22n;
return snowflake.toString();
}
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', () => {
// Test with a known date
const testDate = new Date('2024-01-01T00:00:00.000Z');
const result = dateToSnowflake(testDate);
expect(result).toMatch(/^\d+$/); // Should be a numeric string
expect(result.length).toBeGreaterThan(10); // Snowflakes are large numbers
});
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('textOnlyMessages', () => {
function textOnlyMessages(message: { cleanContent: string; type: number }): boolean {
const { MessageType } = require('discord.js');
return (
message.cleanContent.length > 0 &&
(message.type === MessageType.Default || message.type === MessageType.Reply)
);
}
it('should return true for messages with content and default type', () => {
const mockMessage = {
cleanContent: 'Hello!',
type: 0, // Default
};
expect(textOnlyMessages(mockMessage)).toBe(true);
});
it('should return true for messages with content and reply type', () => {
const mockMessage = {
cleanContent: 'Reply!',
type: 19, // Reply
};
expect(textOnlyMessages(mockMessage)).toBe(true);
});
it('should return false for empty messages', () => {
const mockMessage = {
cleanContent: '',
type: 0,
};
expect(textOnlyMessages(mockMessage)).toBe(false);
});
it('should return false for system messages', () => {
const mockMessage = {
cleanContent: 'System message',
type: 1, // RecipientAdd
};
expect(textOnlyMessages(mockMessage)).toBe(false);
});
});
describe('isGoodResponse', () => {
const MAX_RESPONSE_LENGTH = 4000;
function isGoodResponse(response: string): boolean {
return response.length > 0 && response.length <= MAX_RESPONSE_LENGTH;
}
it('should return true for non-empty responses', () => {
expect(isGoodResponse('Hello!')).toBe(true);
expect(isGoodResponse('a')).toBe(true);
});
it('should return false for empty responses', () => {
expect(isGoodResponse('')).toBe(false);
});
it('should return true for responses at exactly 4000 characters', () => {
const response = 'a'.repeat(4000);
expect(isGoodResponse(response)).toBe(true);
});
it('should return false for responses exceeding 4000 characters', () => {
const response = 'a'.repeat(4001);
expect(isGoodResponse(response)).toBe(false);
});
it('should return false for responses significantly exceeding 4000 characters', () => {
const response = 'a'.repeat(5000);
expect(isGoodResponse(response)).toBe(false);
});
});
describe('parseLoadingEmojis', () => {
it('should parse emojis from environment variable', () => {
const result = parseLoadingEmojis();
expect(result).toHaveLength(4);
expect(result).toEqual([
'<:clueless:123>',
'<a:hachune:456>',
'<a:chairspin:789>',
'<a:nekodance:012>',
]);
});
it('should return default emojis when LOADING_EMOJIS is empty', () => {
const original = mockEnv.LOADING_EMOJIS;
mockEnv.LOADING_EMOJIS = '';
const result = parseLoadingEmojis();
mockEnv.LOADING_EMOJIS = original;
expect(result).toEqual(['🤔', '✨', '🎵']);
});
it('should handle whitespace in emoji list', () => {
const original = mockEnv.LOADING_EMOJIS;
mockEnv.LOADING_EMOJIS = ' <:test:123> , <a:spin:456> ';
const result = parseLoadingEmojis();
mockEnv.LOADING_EMOJIS = original;
expect(result).toEqual(['<:test:123>', '<a:spin:456>']);
});
});
describe('getRandomLoadingEmoji', () => {
it('should return a valid emoji from the list', () => {
const result = getRandomLoadingEmoji();
const validEmojis = parseLoadingEmojis();
expect(validEmojis).toContain(result);
});
});
describe('formatLoadingMessage', () => {
it('should format message with emoji and phrase only when no reasoning', () => {
const result = formatLoadingMessage('<:clueless:123>', '');
expect(result).toContain('<:clueless:123>');
// Check that there's no blockquote (newline followed by "> ")
expect(result).not.toMatch(/\n\n> /);
});
it('should include reasoning in blockquote when present', () => {
const reasoning = 'This is my thought process...';
const result = formatLoadingMessage('<a:hachune:456>', reasoning);
expect(result).toContain('<a:hachune:456>');
expect(result).toContain(`> ${reasoning}`);
});
it('should truncate long reasoning text', () => {
const longReasoning = 'a'.repeat(600);
const result = formatLoadingMessage('<:clueless:123>', longReasoning);
expect(result).toContain('...');
expect(result.length).toBeLessThan(longReasoning.length + 50);
});
});
});