import { jest } from '@jest/globals'; // Mock axios jest.unstable_mockModule('axios', () => ({ default: { post: jest.fn(), }, })); // Mock the discord.js module jest.unstable_mockModule('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 { 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, }, ], }), }; }), toJSON: jest.fn(), }), })); const axios = (await import('axios')).default; const { createMockInteraction } = await import('../utils/testUtils.js'); const AskCommand = (await import('../../commands/ask.js')).default; const askCommand = new AskCommand(); describe('Ask Command', () => { describe('Command Structure', () => { it('should have correct name and description', () => { const commandData = askCommand.defineCommand().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('defineCommand'); expect(askCommand).toHaveProperty('run'); expect(typeof askCommand.run).toBe('function'); }); it('should have correct option configuration', () => { const commandData = askCommand.defineCommand().toJSON(); const [promptOption] = commandData.options; 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(() => { process.env.NANOGPT_API_KEY = 'test-api-key'; jest.clearAllMocks(); interaction = createMockInteraction({ commandName: 'ask', stringOptions: { prompt: mockPrompt, }, }); }); it('should handle successful API response', async () => { axios.post.mockResolvedValueOnce(mockApiResponse); await askCommand.run(interaction); expect(axios.post).toHaveBeenCalledWith( 'https://nano-gpt.com/api/v1/chat/completions', expect.objectContaining({ model: 'deepseek/deepseek-v3.2', }), expect.any(Object), ); expect(interaction.followUp).toHaveBeenCalledWith({ content: expect.not.stringContaining('Web search enabled'), split: false, allowedMentions: { parse: [] }, }); }); it('should handle long responses with proper chunking', async () => { const longResponse = 'A'.repeat(2500); axios.post.mockResolvedValueOnce({ data: { choices: [ { message: { content: longResponse, }, }, ], }, }); await askCommand.run(interaction); expect(interaction.followUp).toHaveBeenCalledTimes(2); expect(interaction.followUp.mock.calls[1][0].content).toContain('(continued)'); }); it('should handle code blocks in chunked responses', async () => { const responseWithCodeBlock = "Here's a code example:\n```python\nprint('hello')\n```".repeat( 20, ); axios.post.mockResolvedValueOnce({ data: { choices: [ { message: { content: responseWithCodeBlock, }, }, ], }, }); await askCommand.run(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 }); }); it('should handle API errors', async () => { const error = new Error('API Error'); error.response = { data: 'API Error Details' }; axios.post.mockRejectedValueOnce(error); await askCommand.run(interaction); expect(interaction.followUp).toHaveBeenCalledWith({ content: 'Sorry, there was an error processing your request.', ephemeral: true, }); }); 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); await askCommand.run(interaction); 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); await askCommand.run(interaction); 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); await askCommand.run(interaction); expect(interaction.followUp).toHaveBeenCalledWith({ content: 'Invalid request. Please try rephrasing your question.', ephemeral: true, }); }); it('should send API request with correct headers', async () => { axios.post.mockResolvedValueOnce(mockApiResponse); await askCommand.run(interaction); expect(axios.post).toHaveBeenCalledWith(expect.any(String), expect.any(Object), { headers: { Authorization: 'Bearer test-api-key', 'Content-Type': 'application/json', }, }); }); it('should handle defer reply failure', async () => { interaction = createMockInteraction({ commandName: 'ask', stringOptions: { prompt: mockPrompt, }, deferFails: true, }); await askCommand.run(interaction); expect(interaction.followUp).toHaveBeenCalledWith( expect.objectContaining({ content: 'Sorry, there was an error processing your request.', ephemeral: true, }), ); }); it('should handle follow up failure', async () => { interaction = createMockInteraction({ commandName: 'ask', stringOptions: { prompt: mockPrompt, }, followUpFails: true, }); axios.post.mockResolvedValueOnce(mockApiResponse); await expect(askCommand.run(interaction)).rejects.toThrow('Failed to follow up'); }); }); });