361 lines
10 KiB
JavaScript
361 lines
10 KiB
JavaScript
const axios = require('axios');
|
|
const { createMockInteraction } = require('../utils/testUtils');
|
|
|
|
// Mock axios
|
|
jest.mock('axios');
|
|
|
|
// Mock the discord.js module
|
|
jest.mock('discord.js', () => ({
|
|
SlashCommandBuilder: jest.fn().mockReturnValue({
|
|
setName: jest.fn().mockReturnThis(),
|
|
setDescription: jest.fn().mockReturnThis(),
|
|
addStringOption: jest.fn().mockImplementation(callback => {
|
|
const option = {
|
|
setName: jest.fn().mockReturnThis(),
|
|
setDescription: jest.fn().mockReturnThis(),
|
|
setRequired: jest.fn().mockReturnThis(),
|
|
};
|
|
callback(option);
|
|
return {
|
|
addBooleanOption: jest.fn().mockImplementation(boolCallback => {
|
|
const boolOption = {
|
|
setName: jest.fn().mockReturnThis(),
|
|
setDescription: jest.fn().mockReturnThis(),
|
|
setRequired: jest.fn().mockReturnThis(),
|
|
};
|
|
boolCallback(boolOption);
|
|
return {
|
|
toJSON: jest.fn().mockReturnValue({
|
|
name: 'ask',
|
|
description: 'Ask a question to the AI',
|
|
options: [
|
|
{
|
|
name: 'prompt',
|
|
description: 'Your question or prompt',
|
|
type: 3,
|
|
required: true,
|
|
},
|
|
{
|
|
name: 'websearch',
|
|
description:
|
|
'Enable web search for more up-to-date information',
|
|
type: 5,
|
|
required: false,
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
}),
|
|
};
|
|
}),
|
|
toJSON: jest.fn(),
|
|
}),
|
|
}));
|
|
|
|
const askCommand = require('../../commands/ask');
|
|
|
|
describe('Ask Command', () => {
|
|
describe('Command Structure', () => {
|
|
it('should have correct name and description', () => {
|
|
const commandData = askCommand.data.toJSON();
|
|
expect(commandData.name).toBe('ask');
|
|
expect(commandData.description).toBe('Ask a question to the AI');
|
|
});
|
|
|
|
it('should have required command properties', () => {
|
|
expect(askCommand).toHaveProperty('data');
|
|
expect(askCommand).toHaveProperty('execute');
|
|
expect(typeof askCommand.execute).toBe('function');
|
|
});
|
|
|
|
it('should have correct option configuration', () => {
|
|
const commandData = askCommand.data.toJSON();
|
|
const [promptOption, websearchOption] = commandData.options;
|
|
|
|
expect(promptOption.name).toBe('prompt');
|
|
expect(promptOption.description).toBe('Your question or prompt');
|
|
expect(promptOption.required).toBe(true);
|
|
|
|
expect(websearchOption.name).toBe('websearch');
|
|
expect(websearchOption.description).toBe(
|
|
'Enable web search for more up-to-date information'
|
|
);
|
|
expect(websearchOption.required).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Command Execution', () => {
|
|
let interaction;
|
|
const mockPrompt = 'What is the meaning of life?';
|
|
const mockApiResponse = {
|
|
data: {
|
|
choices: [
|
|
{
|
|
message: {
|
|
content: 'The meaning of life is 42.',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
beforeEach(() => {
|
|
process.env.OPENROUTER_API_KEY = 'test-api-key';
|
|
axios.post.mockReset();
|
|
jest.clearAllMocks();
|
|
|
|
interaction = createMockInteraction({
|
|
commandName: 'ask',
|
|
stringOptions: {
|
|
prompt: mockPrompt,
|
|
},
|
|
booleanOptions: {
|
|
websearch: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should handle successful API response with websearch disabled', async () => {
|
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
|
|
|
await askCommand.execute(interaction);
|
|
|
|
expect(axios.post).toHaveBeenCalledWith(
|
|
'https://openrouter.ai/api/v1/chat/completions',
|
|
expect.objectContaining({
|
|
model: 'google/gemini-2.0-flash-exp:free',
|
|
plugins: undefined,
|
|
}),
|
|
expect.any(Object)
|
|
);
|
|
|
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
|
content: expect.not.stringContaining('Web search enabled'),
|
|
split: false,
|
|
allowedMentions: { parse: [] },
|
|
});
|
|
});
|
|
|
|
it('should handle successful API response with websearch enabled', async () => {
|
|
interaction = createMockInteraction({
|
|
commandName: 'ask',
|
|
stringOptions: {
|
|
prompt: mockPrompt,
|
|
},
|
|
booleanOptions: {
|
|
websearch: true,
|
|
},
|
|
});
|
|
|
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
|
|
|
await askCommand.execute(interaction);
|
|
|
|
expect(axios.post).toHaveBeenCalledWith(
|
|
'https://openrouter.ai/api/v1/chat/completions',
|
|
expect.objectContaining({
|
|
model: 'google/gemini-2.0-flash-exp:free:online',
|
|
plugins: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: 'web',
|
|
max_results: 3,
|
|
}),
|
|
]),
|
|
}),
|
|
expect.any(Object)
|
|
);
|
|
|
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
|
content: expect.stringContaining('Web search enabled'),
|
|
split: false,
|
|
allowedMentions: { parse: [] },
|
|
});
|
|
});
|
|
|
|
it('should use default websearch value when not provided', async () => {
|
|
interaction = createMockInteraction({
|
|
commandName: 'ask',
|
|
stringOptions: {
|
|
prompt: mockPrompt,
|
|
},
|
|
// Not providing websearch option
|
|
});
|
|
|
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
|
|
|
await askCommand.execute(interaction);
|
|
|
|
expect(axios.post).toHaveBeenCalledWith(
|
|
'https://openrouter.ai/api/v1/chat/completions',
|
|
expect.objectContaining({
|
|
model: 'google/gemini-2.0-flash-exp:free',
|
|
plugins: undefined,
|
|
}),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
|
|
it('should handle long responses with proper chunking and websearch enabled', async () => {
|
|
interaction = createMockInteraction({
|
|
commandName: 'ask',
|
|
stringOptions: {
|
|
prompt: mockPrompt,
|
|
},
|
|
booleanOptions: {
|
|
websearch: true,
|
|
},
|
|
});
|
|
|
|
const longResponse = 'A'.repeat(2500);
|
|
axios.post.mockResolvedValueOnce({
|
|
data: {
|
|
choices: [
|
|
{
|
|
message: {
|
|
content: longResponse,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
await askCommand.execute(interaction);
|
|
|
|
expect(interaction.followUp).toHaveBeenCalledTimes(2);
|
|
expect(interaction.followUp.mock.calls[0][0].content).toContain(
|
|
'Web search enabled'
|
|
);
|
|
expect(interaction.followUp.mock.calls[1][0].content).toContain(
|
|
'(continued)'
|
|
);
|
|
});
|
|
|
|
it('should handle code blocks in chunked responses with websearch', async () => {
|
|
interaction = createMockInteraction({
|
|
commandName: 'ask',
|
|
stringOptions: {
|
|
prompt: mockPrompt,
|
|
},
|
|
booleanOptions: {
|
|
websearch: true,
|
|
},
|
|
});
|
|
|
|
const responseWithCodeBlock =
|
|
"Here's a code example:\n```python\nprint('hello')\n```".repeat(20);
|
|
axios.post.mockResolvedValueOnce({
|
|
data: {
|
|
choices: [
|
|
{
|
|
message: {
|
|
content: responseWithCodeBlock,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
await askCommand.execute(interaction);
|
|
|
|
// Verify that code blocks are not split in the middle
|
|
interaction.followUp.mock.calls.forEach(call => {
|
|
const { content } = call[0];
|
|
const openBlocks = (content.match(/```/g) || []).length;
|
|
expect(openBlocks % 2).toBe(0); // Should always be even
|
|
});
|
|
|
|
// First chunk should contain websearch indicator
|
|
expect(interaction.followUp.mock.calls[0][0].content).toContain(
|
|
'Web search enabled'
|
|
);
|
|
});
|
|
|
|
it('should handle API errors with websearch enabled', async () => {
|
|
interaction = createMockInteraction({
|
|
commandName: 'ask',
|
|
stringOptions: {
|
|
prompt: mockPrompt,
|
|
},
|
|
booleanOptions: {
|
|
websearch: true,
|
|
},
|
|
});
|
|
|
|
const error = new Error('API Error');
|
|
error.response = { data: 'API Error Details' };
|
|
axios.post.mockRejectedValueOnce(error);
|
|
|
|
await askCommand.execute(interaction);
|
|
|
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
|
content: 'Sorry, there was an error processing your request.',
|
|
ephemeral: true,
|
|
});
|
|
});
|
|
|
|
it('should send API request with correct headers regardless of websearch', async () => {
|
|
interaction = createMockInteraction({
|
|
commandName: 'ask',
|
|
stringOptions: {
|
|
prompt: mockPrompt,
|
|
},
|
|
booleanOptions: {
|
|
websearch: true,
|
|
},
|
|
});
|
|
|
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
|
|
|
await askCommand.execute(interaction);
|
|
|
|
expect(axios.post).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.any(Object),
|
|
{
|
|
headers: {
|
|
Authorization: 'Bearer test-api-key',
|
|
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
);
|
|
});
|
|
|
|
it('should handle defer reply failure with websearch', async () => {
|
|
interaction = createMockInteraction({
|
|
commandName: 'ask',
|
|
stringOptions: {
|
|
prompt: mockPrompt,
|
|
},
|
|
booleanOptions: {
|
|
websearch: true,
|
|
},
|
|
deferFails: true,
|
|
});
|
|
|
|
await expect(askCommand.execute(interaction)).rejects.toThrow(
|
|
'Failed to defer reply'
|
|
);
|
|
});
|
|
|
|
it('should handle follow up failure with websearch', async () => {
|
|
interaction = createMockInteraction({
|
|
commandName: 'ask',
|
|
stringOptions: {
|
|
prompt: mockPrompt,
|
|
},
|
|
booleanOptions: {
|
|
websearch: true,
|
|
},
|
|
followUpFails: true,
|
|
});
|
|
|
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
|
|
|
await expect(askCommand.execute(interaction)).rejects.toThrow(
|
|
'Failed to follow up'
|
|
);
|
|
});
|
|
});
|
|
});
|