2025-12-25 10:28:44 -05:00
|
|
|
const { createMockInteraction } = require('../utils/testUtils');
|
2025-02-02 14:10:51 -05:00
|
|
|
|
|
|
|
|
// Mock axios
|
2025-12-25 10:28:44 -05:00
|
|
|
jest.mock('axios', () => ({
|
|
|
|
|
__esModule: true,
|
2025-12-24 11:21:30 -05:00
|
|
|
default: {
|
|
|
|
|
post: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
}));
|
2025-12-25 10:28:44 -05:00
|
|
|
const axios = require('axios').default;
|
2025-02-02 14:10:51 -05:00
|
|
|
|
2025-12-25 10:28:44 -05:00
|
|
|
// Mock discord.js
|
|
|
|
|
jest.mock('discord.js', () => ({
|
2025-02-02 14:10:51 -05:00
|
|
|
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 {
|
2025-12-24 11:21:30 -05:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
],
|
2025-02-02 14:10:51 -05:00
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
toJSON: jest.fn(),
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
2025-12-25 10:28:44 -05:00
|
|
|
const AskCommand = require('../../commands/ask').default;
|
2025-12-24 11:21:30 -05:00
|
|
|
const askCommand = new AskCommand();
|
2025-02-02 14:10:51 -05:00
|
|
|
|
|
|
|
|
describe('Ask Command', () => {
|
|
|
|
|
describe('Command Structure', () => {
|
|
|
|
|
it('should have correct name and description', () => {
|
2025-12-24 11:21:30 -05:00
|
|
|
const commandData = askCommand.defineCommand().toJSON();
|
2025-02-02 14:10:51 -05:00
|
|
|
expect(commandData.name).toBe('ask');
|
|
|
|
|
expect(commandData.description).toBe('Ask a question to the AI');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should have required command properties', () => {
|
2025-12-24 11:21:30 -05:00
|
|
|
expect(askCommand).toHaveProperty('defineCommand');
|
|
|
|
|
expect(askCommand).toHaveProperty('run');
|
|
|
|
|
expect(typeof askCommand.run).toBe('function');
|
2025-02-02 14:10:51 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should have correct option configuration', () => {
|
2025-12-24 11:21:30 -05:00
|
|
|
const commandData = askCommand.defineCommand().toJSON();
|
|
|
|
|
const [promptOption] = commandData.options;
|
2025-02-02 14:10:51 -05:00
|
|
|
|
|
|
|
|
expect(promptOption.name).toBe('prompt');
|
|
|
|
|
expect(promptOption.description).toBe('Your question or prompt');
|
|
|
|
|
expect(promptOption.required).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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(() => {
|
2025-12-24 11:21:30 -05:00
|
|
|
process.env.NANOGPT_API_KEY = 'test-api-key';
|
2025-02-02 14:10:51 -05:00
|
|
|
jest.clearAllMocks();
|
|
|
|
|
|
|
|
|
|
interaction = createMockInteraction({
|
|
|
|
|
commandName: 'ask',
|
|
|
|
|
stringOptions: {
|
|
|
|
|
prompt: mockPrompt,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
it('should handle successful API response', async () => {
|
2025-02-02 14:10:51 -05:00
|
|
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
await askCommand.run(interaction);
|
2025-02-02 14:10:51 -05:00
|
|
|
|
|
|
|
|
expect(axios.post).toHaveBeenCalledWith(
|
2025-12-24 11:21:30 -05:00
|
|
|
'https://nano-gpt.com/api/v1/chat/completions',
|
2025-02-02 14:10:51 -05:00
|
|
|
expect.objectContaining({
|
2025-12-24 11:21:30 -05:00
|
|
|
model: 'deepseek/deepseek-v3.2',
|
2025-02-02 14:10:51 -05:00
|
|
|
}),
|
2025-02-02 16:22:33 -05:00
|
|
|
expect.any(Object),
|
2025-02-02 14:10:51 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
|
|
|
|
content: expect.not.stringContaining('Web search enabled'),
|
|
|
|
|
split: false,
|
|
|
|
|
allowedMentions: { parse: [] },
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
it('should handle long responses with proper chunking', async () => {
|
2025-02-02 14:10:51 -05:00
|
|
|
const longResponse = 'A'.repeat(2500);
|
|
|
|
|
axios.post.mockResolvedValueOnce({
|
|
|
|
|
data: {
|
|
|
|
|
choices: [
|
|
|
|
|
{
|
|
|
|
|
message: {
|
|
|
|
|
content: longResponse,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
await askCommand.run(interaction);
|
2025-02-02 14:10:51 -05:00
|
|
|
|
|
|
|
|
expect(interaction.followUp).toHaveBeenCalledTimes(2);
|
2025-02-02 16:22:33 -05:00
|
|
|
expect(interaction.followUp.mock.calls[1][0].content).toContain('(continued)');
|
2025-02-02 14:10:51 -05:00
|
|
|
});
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
it('should handle code blocks in chunked responses', async () => {
|
2025-02-02 16:22:33 -05:00
|
|
|
const responseWithCodeBlock = "Here's a code example:\n```python\nprint('hello')\n```".repeat(
|
|
|
|
|
20,
|
|
|
|
|
);
|
2025-02-02 14:10:51 -05:00
|
|
|
axios.post.mockResolvedValueOnce({
|
|
|
|
|
data: {
|
|
|
|
|
choices: [
|
|
|
|
|
{
|
|
|
|
|
message: {
|
|
|
|
|
content: responseWithCodeBlock,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
await askCommand.run(interaction);
|
2025-02-02 14:10:51 -05:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
it('should handle API errors', async () => {
|
2025-02-02 14:10:51 -05:00
|
|
|
const error = new Error('API Error');
|
|
|
|
|
error.response = { data: 'API Error Details' };
|
|
|
|
|
axios.post.mockRejectedValueOnce(error);
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
await askCommand.run(interaction);
|
2025-02-02 14:10:51 -05:00
|
|
|
|
|
|
|
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
|
|
|
|
content: 'Sorry, there was an error processing your request.',
|
|
|
|
|
ephemeral: true,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-02-02 16:22:33 -05:00
|
|
|
it('should handle rate limit errors', async () => {
|
|
|
|
|
const error = new Error('Rate Limit Exceeded');
|
|
|
|
|
error.response = { status: 429, data: 'Too Many Requests' };
|
|
|
|
|
axios.post.mockRejectedValueOnce(error);
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
await askCommand.run(interaction);
|
2025-02-02 16:22:33 -05:00
|
|
|
|
|
|
|
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
|
|
|
|
content: 'The AI service is currently busy. Please try again in a few moments.',
|
|
|
|
|
ephemeral: true,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle timeout errors', async () => {
|
|
|
|
|
const error = new Error('Timeout');
|
|
|
|
|
error.code = 'ETIMEDOUT';
|
|
|
|
|
axios.post.mockRejectedValueOnce(error);
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
await askCommand.run(interaction);
|
2025-02-02 16:22:33 -05:00
|
|
|
|
|
|
|
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
|
|
|
|
content: 'The request timed out. Please try again.',
|
|
|
|
|
ephemeral: true,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle invalid request errors', async () => {
|
|
|
|
|
const error = new Error('Bad Request');
|
|
|
|
|
error.response = { status: 400, data: 'Invalid Request' };
|
|
|
|
|
axios.post.mockRejectedValueOnce(error);
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
await askCommand.run(interaction);
|
2025-02-02 16:22:33 -05:00
|
|
|
|
|
|
|
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
|
|
|
|
content: 'Invalid request. Please try rephrasing your question.',
|
|
|
|
|
ephemeral: true,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
it('should send API request with correct headers', async () => {
|
2025-02-02 14:10:51 -05:00
|
|
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
await askCommand.run(interaction);
|
2025-02-02 14:10:51 -05:00
|
|
|
|
2025-02-02 16:22:33 -05:00
|
|
|
expect(axios.post).toHaveBeenCalledWith(expect.any(String), expect.any(Object), {
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: 'Bearer test-api-key',
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-02-02 14:10:51 -05:00
|
|
|
});
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
it('should handle defer reply failure', async () => {
|
2025-02-02 14:10:51 -05:00
|
|
|
interaction = createMockInteraction({
|
|
|
|
|
commandName: 'ask',
|
|
|
|
|
stringOptions: {
|
|
|
|
|
prompt: mockPrompt,
|
|
|
|
|
},
|
|
|
|
|
deferFails: true,
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
await askCommand.run(interaction);
|
|
|
|
|
|
|
|
|
|
expect(interaction.followUp).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
content: 'Sorry, there was an error processing your request.',
|
|
|
|
|
ephemeral: true,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2025-02-02 14:10:51 -05:00
|
|
|
});
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
it('should handle follow up failure', async () => {
|
2025-02-02 14:10:51 -05:00
|
|
|
interaction = createMockInteraction({
|
|
|
|
|
commandName: 'ask',
|
|
|
|
|
stringOptions: {
|
|
|
|
|
prompt: mockPrompt,
|
|
|
|
|
},
|
|
|
|
|
followUpFails: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
|
|
|
|
|
2025-12-24 11:21:30 -05:00
|
|
|
await expect(askCommand.run(interaction)).rejects.toThrow('Failed to follow up');
|
2025-02-02 14:10:51 -05:00
|
|
|
});
|
|
|
|
|
});
|
2025-12-25 10:28:44 -05:00
|
|
|
});
|