252 lines
8.5 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|