441 lines
14 KiB
TypeScript
441 lines
14 KiB
TypeScript
/**
|
|
* Tests for helpers.ts functions
|
|
*/
|
|
|
|
jest.mock('../../logging', () => ({
|
|
logInfo: jest.fn(),
|
|
logWarn: jest.fn(),
|
|
logError: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('../util', () => ({
|
|
REAL_NAMES: {},
|
|
LOSER_WHITELIST: [],
|
|
}));
|
|
|
|
jest.mock('node:path', () => ({
|
|
join: jest.fn(() => '/tmp/streaks.json'),
|
|
}));
|
|
|
|
jest.mock('node:fs', () => ({
|
|
existsSync: jest.fn(() => false),
|
|
readFileSync: jest.fn(),
|
|
writeFileSync: jest.fn(),
|
|
}));
|
|
|
|
// Mock Discord.js Collection class (mimics Map with filter method)
|
|
class MockCollection {
|
|
private map: Map<any, any>;
|
|
|
|
constructor(entries?: Array<[any, any]>) {
|
|
this.map = new Map(entries || []);
|
|
}
|
|
|
|
get size() {
|
|
return this.map.size;
|
|
}
|
|
|
|
filter(fn: (value: any, key: any) => boolean) {
|
|
const result = new MockCollection();
|
|
for (const [key, value] of this.map.entries()) {
|
|
if (fn(value, key)) {
|
|
result.map.set(key, value);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
values() {
|
|
return this.map.values();
|
|
}
|
|
|
|
entries() {
|
|
return this.map.entries();
|
|
}
|
|
|
|
[Symbol.iterator]() {
|
|
return this.map[Symbol.iterator]();
|
|
}
|
|
}
|
|
|
|
const {
|
|
dateToSnowflake,
|
|
triggerThrowback,
|
|
KAWAII_PHRASES,
|
|
parseLoadingEmojis,
|
|
getRandomLoadingEmoji,
|
|
getRandomKawaiiPhrase,
|
|
createStatusEmbed,
|
|
createSimpleStatusEmbed,
|
|
} = require('../commands/helpers');
|
|
|
|
describe('helpers.ts', () => {
|
|
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', () => {
|
|
const testDate = new Date('2024-01-01T00:00:00.000Z');
|
|
const result = dateToSnowflake(testDate);
|
|
expect(result).toMatch(/^\d+$/);
|
|
expect(result.length).toBeGreaterThan(10);
|
|
});
|
|
|
|
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('KAWAII_PHRASES', () => {
|
|
it('should contain kawaii phrases', () => {
|
|
expect(KAWAII_PHRASES.length).toBeGreaterThan(0);
|
|
expect(KAWAII_PHRASES).toContain('Hmm... let me think~ ♪');
|
|
});
|
|
});
|
|
|
|
describe('parseLoadingEmojis', () => {
|
|
it('should parse emojis from environment variable', () => {
|
|
const original = process.env.LOADING_EMOJIS;
|
|
process.env.LOADING_EMOJIS =
|
|
'<:clueless:123>,<a:hachune:456>,<a:chairspin:789>,<a:nekodance:012>';
|
|
const result = parseLoadingEmojis();
|
|
process.env.LOADING_EMOJIS = original;
|
|
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 = process.env.LOADING_EMOJIS;
|
|
process.env.LOADING_EMOJIS = '';
|
|
const result = parseLoadingEmojis();
|
|
process.env.LOADING_EMOJIS = original;
|
|
expect(result).toEqual(['🤔', '✨', '🎵']);
|
|
});
|
|
|
|
it('should handle whitespace in emoji list', () => {
|
|
const original = process.env.LOADING_EMOJIS;
|
|
process.env.LOADING_EMOJIS = ' <:test:123> , <a:spin:456> ';
|
|
const result = parseLoadingEmojis();
|
|
process.env.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('getRandomKawaiiPhrase', () => {
|
|
it('should return a valid kawaii phrase', () => {
|
|
const result = getRandomKawaiiPhrase();
|
|
expect(KAWAII_PHRASES).toContain(result);
|
|
});
|
|
});
|
|
|
|
describe('createStatusEmbed', () => {
|
|
it('should create an embed with emoji, phrase, and status', () => {
|
|
const embed = createStatusEmbed('🤔', 'Hmm... let me think~ ♪', 'Processing...');
|
|
expect(embed).toBeDefined();
|
|
expect(embed.data.author).toBeDefined();
|
|
expect(embed.data.author?.name).toBe('Hmm... let me think~ ♪');
|
|
});
|
|
});
|
|
|
|
describe('createSimpleStatusEmbed', () => {
|
|
it('should create an embed with random emoji and phrase', () => {
|
|
const embed = createSimpleStatusEmbed('Working...');
|
|
expect(embed).toBeDefined();
|
|
expect(embed.data.author).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('triggerThrowback', () => {
|
|
const mockClient = {
|
|
guilds: {
|
|
fetch: jest.fn(),
|
|
},
|
|
};
|
|
|
|
const mockProvider = {
|
|
requestLLMResponse: jest.fn(),
|
|
};
|
|
|
|
const mockSysprompt = 'You are a helpful assistant.';
|
|
const mockLlmconf = {
|
|
msg_context: 10,
|
|
streaming: false,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('should fetch messages from 1 year ago', async () => {
|
|
const mockMessage = {
|
|
id: '123456789',
|
|
author: { username: 'testuser', bot: false },
|
|
cleanContent: 'Hello from a year ago!',
|
|
type: 0,
|
|
reply: jest.fn(),
|
|
};
|
|
|
|
const mockChannel = {
|
|
messages: {
|
|
fetch: jest.fn().mockResolvedValue(new MockCollection([['123456789', mockMessage]])),
|
|
},
|
|
};
|
|
|
|
mockProvider.requestLLMResponse.mockResolvedValue('Nice throwback!');
|
|
|
|
await triggerThrowback(
|
|
mockClient as any,
|
|
mockChannel as any,
|
|
mockChannel as any,
|
|
mockProvider,
|
|
mockSysprompt,
|
|
mockLlmconf
|
|
);
|
|
|
|
// Verify messages.fetch was called with around date from 1 year ago
|
|
const fetchCall = mockChannel.messages.fetch.mock.calls[0][0];
|
|
expect(fetchCall.around).toBeDefined();
|
|
expect(fetchCall.limit).toBe(50);
|
|
});
|
|
|
|
it('should fetch message history for context before generating LLM response', async () => {
|
|
const mockReply = jest.fn();
|
|
const mockMessage = {
|
|
id: '123456789',
|
|
author: { username: 'testuser', bot: false },
|
|
cleanContent: 'Hello from a year ago!',
|
|
type: 0,
|
|
reply: mockReply,
|
|
};
|
|
|
|
const mockHistoryMessage = {
|
|
id: '123456788',
|
|
author: { username: 'testuser', bot: false },
|
|
cleanContent: 'Previous context',
|
|
type: 0,
|
|
};
|
|
|
|
const mockChannel = {
|
|
messages: {
|
|
fetch: jest
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
new MockCollection([
|
|
['123456788', mockHistoryMessage],
|
|
['123456789', mockMessage],
|
|
])
|
|
)
|
|
.mockResolvedValueOnce(new MockCollection([['123456788', mockHistoryMessage]])),
|
|
},
|
|
};
|
|
|
|
mockProvider.requestLLMResponse.mockResolvedValue('Nice throwback!');
|
|
|
|
await triggerThrowback(
|
|
mockClient as any,
|
|
mockChannel as any,
|
|
mockChannel as any,
|
|
mockProvider,
|
|
mockSysprompt,
|
|
mockLlmconf
|
|
);
|
|
|
|
// Verify messages.fetch was called twice: once for throwback, once for history
|
|
expect(mockChannel.messages.fetch).toHaveBeenCalledTimes(2);
|
|
|
|
// Verify history fetch used msg_context from llmconf
|
|
const historyFetchCall = mockChannel.messages.fetch.mock.calls[1][0];
|
|
expect(historyFetchCall.limit).toBe(mockLlmconf.msg_context - 1);
|
|
expect(historyFetchCall.before).toBe(mockMessage.id);
|
|
|
|
// Verify LLM was called with context (history + selected message)
|
|
expect(mockProvider.requestLLMResponse).toHaveBeenCalledWith(
|
|
expect.arrayContaining([expect.objectContaining({ id: '123456788' })]),
|
|
mockSysprompt,
|
|
mockLlmconf
|
|
);
|
|
});
|
|
|
|
it('should reply to the original message', async () => {
|
|
const mockReply = jest.fn();
|
|
const mockMessage = {
|
|
id: '123456789',
|
|
author: { username: 'testuser', bot: false },
|
|
cleanContent: 'Hello from a year ago!',
|
|
type: 0,
|
|
reply: mockReply,
|
|
};
|
|
|
|
const mockChannel = {
|
|
messages: {
|
|
fetch: jest.fn().mockResolvedValue(new MockCollection([['123456789', mockMessage]])),
|
|
},
|
|
};
|
|
|
|
mockProvider.requestLLMResponse.mockResolvedValue('Nice throwback!');
|
|
|
|
await triggerThrowback(
|
|
mockClient as any,
|
|
mockChannel as any,
|
|
mockChannel as any,
|
|
mockProvider,
|
|
mockSysprompt,
|
|
mockLlmconf
|
|
);
|
|
|
|
// Verify reply was called on the original message, not send on channel
|
|
expect(mockReply).toHaveBeenCalledWith('Nice throwback!');
|
|
});
|
|
|
|
it('should throw error when no messages found from 1 year ago', async () => {
|
|
const mockChannel = {
|
|
messages: {
|
|
fetch: jest.fn().mockResolvedValue(new MockCollection()),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
triggerThrowback(
|
|
mockClient as any,
|
|
mockChannel as any,
|
|
mockChannel as any,
|
|
mockProvider,
|
|
mockSysprompt,
|
|
mockLlmconf
|
|
)
|
|
).rejects.toThrow('No messages found from 1 year ago.');
|
|
});
|
|
|
|
it('should filter out bot messages', async () => {
|
|
const mockBotMessage = {
|
|
id: '111',
|
|
author: { username: 'bot', bot: true },
|
|
cleanContent: 'Bot message',
|
|
type: 0,
|
|
};
|
|
|
|
const mockUserMessage = {
|
|
id: '222',
|
|
author: { username: 'user', bot: false },
|
|
cleanContent: 'User message',
|
|
type: 0,
|
|
reply: jest.fn(),
|
|
};
|
|
|
|
const mockChannel = {
|
|
messages: {
|
|
fetch: jest
|
|
.fn()
|
|
.mockResolvedValue(new MockCollection([['111', mockBotMessage], ['222', mockUserMessage]])),
|
|
},
|
|
};
|
|
|
|
mockProvider.requestLLMResponse.mockResolvedValue('Reply!');
|
|
|
|
await triggerThrowback(
|
|
mockClient as any,
|
|
mockChannel as any,
|
|
mockChannel as any,
|
|
mockProvider,
|
|
mockSysprompt,
|
|
mockLlmconf
|
|
);
|
|
|
|
// Verify only user message was considered (bot filtered out)
|
|
expect(mockProvider.requestLLMResponse).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should filter out messages without content', async () => {
|
|
const mockEmptyMessage = {
|
|
id: '111',
|
|
author: { username: 'user1', bot: false },
|
|
cleanContent: '',
|
|
type: 0,
|
|
};
|
|
|
|
const mockValidMessage = {
|
|
id: '222',
|
|
author: { username: 'user2', bot: false },
|
|
cleanContent: 'Valid message',
|
|
type: 0,
|
|
reply: jest.fn(),
|
|
};
|
|
|
|
const mockChannel = {
|
|
messages: {
|
|
fetch: jest
|
|
.fn()
|
|
.mockResolvedValue(new MockCollection([['111', mockEmptyMessage], ['222', mockValidMessage]])),
|
|
},
|
|
};
|
|
|
|
mockProvider.requestLLMResponse.mockResolvedValue('Reply!');
|
|
|
|
await triggerThrowback(
|
|
mockClient as any,
|
|
mockChannel as any,
|
|
mockChannel as any,
|
|
mockProvider,
|
|
mockSysprompt,
|
|
mockLlmconf
|
|
);
|
|
|
|
// Verify only valid message was considered
|
|
expect(mockProvider.requestLLMResponse).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return throwback result with original message, author, and response', async () => {
|
|
const mockMessage = {
|
|
id: '123456789',
|
|
author: { username: 'testuser', bot: false },
|
|
cleanContent: 'Hello from a year ago!',
|
|
type: 0,
|
|
reply: jest.fn(),
|
|
};
|
|
|
|
const mockChannel = {
|
|
messages: {
|
|
fetch: jest.fn().mockResolvedValue(new MockCollection([['123456789', mockMessage]])),
|
|
},
|
|
};
|
|
|
|
mockProvider.requestLLMResponse.mockResolvedValue('Nice throwback!');
|
|
|
|
const result = await triggerThrowback(
|
|
mockClient as any,
|
|
mockChannel as any,
|
|
mockChannel as any,
|
|
mockProvider,
|
|
mockSysprompt,
|
|
mockLlmconf
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
originalMessage: 'Hello from a year ago!',
|
|
author: 'testuser',
|
|
response: 'Nice throwback!',
|
|
});
|
|
});
|
|
});
|
|
});
|