refactor: update ask command and test utilities to use ES modules; improve error handling and response formatting
This commit is contained in:
+44
-177
@@ -1,11 +1,14 @@
|
|||||||
const axios = require('axios');
|
import { jest } from '@jest/globals';
|
||||||
const { createMockInteraction } = require('../utils/testUtils');
|
|
||||||
|
|
||||||
// Mock axios
|
// Mock axios
|
||||||
jest.mock('axios');
|
jest.unstable_mockModule('axios', () => ({
|
||||||
|
default: {
|
||||||
|
post: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
// Mock the discord.js module
|
// Mock the discord.js module
|
||||||
jest.mock('discord.js', () => ({
|
jest.unstable_mockModule('discord.js', () => ({
|
||||||
SlashCommandBuilder: jest.fn().mockReturnValue({
|
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||||
setName: jest.fn().mockReturnThis(),
|
setName: jest.fn().mockReturnThis(),
|
||||||
setDescription: jest.fn().mockReturnThis(),
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
@@ -16,14 +19,6 @@ jest.mock('discord.js', () => ({
|
|||||||
setRequired: jest.fn().mockReturnThis(),
|
setRequired: jest.fn().mockReturnThis(),
|
||||||
};
|
};
|
||||||
callback(option);
|
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 {
|
return {
|
||||||
toJSON: jest.fn().mockReturnValue({
|
toJSON: jest.fn().mockReturnValue({
|
||||||
name: 'ask',
|
name: 'ask',
|
||||||
@@ -35,49 +30,40 @@ jest.mock('discord.js', () => ({
|
|||||||
type: 3,
|
type: 3,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'websearch',
|
|
||||||
description: 'Enable web search for more up-to-date information',
|
|
||||||
type: 5,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
|
||||||
}),
|
|
||||||
toJSON: jest.fn(),
|
toJSON: jest.fn(),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const askCommand = require('../../commands/ask');
|
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('Ask Command', () => {
|
||||||
describe('Command Structure', () => {
|
describe('Command Structure', () => {
|
||||||
it('should have correct name and description', () => {
|
it('should have correct name and description', () => {
|
||||||
const commandData = askCommand.data.toJSON();
|
const commandData = askCommand.defineCommand().toJSON();
|
||||||
expect(commandData.name).toBe('ask');
|
expect(commandData.name).toBe('ask');
|
||||||
expect(commandData.description).toBe('Ask a question to the AI');
|
expect(commandData.description).toBe('Ask a question to the AI');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have required command properties', () => {
|
it('should have required command properties', () => {
|
||||||
expect(askCommand).toHaveProperty('data');
|
expect(askCommand).toHaveProperty('defineCommand');
|
||||||
expect(askCommand).toHaveProperty('execute');
|
expect(askCommand).toHaveProperty('run');
|
||||||
expect(typeof askCommand.execute).toBe('function');
|
expect(typeof askCommand.run).toBe('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct option configuration', () => {
|
it('should have correct option configuration', () => {
|
||||||
const commandData = askCommand.data.toJSON();
|
const commandData = askCommand.defineCommand().toJSON();
|
||||||
const [promptOption, websearchOption] = commandData.options;
|
const [promptOption] = commandData.options;
|
||||||
|
|
||||||
expect(promptOption.name).toBe('prompt');
|
expect(promptOption.name).toBe('prompt');
|
||||||
expect(promptOption.description).toBe('Your question or prompt');
|
expect(promptOption.description).toBe('Your question or prompt');
|
||||||
expect(promptOption.required).toBe(true);
|
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,8 +83,7 @@ describe('Ask Command', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env.OPENROUTER_API_KEY = 'test-api-key';
|
process.env.NANOGPT_API_KEY = 'test-api-key';
|
||||||
axios.post.mockReset();
|
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
||||||
interaction = createMockInteraction({
|
interaction = createMockInteraction({
|
||||||
@@ -106,22 +91,18 @@ describe('Ask Command', () => {
|
|||||||
stringOptions: {
|
stringOptions: {
|
||||||
prompt: mockPrompt,
|
prompt: mockPrompt,
|
||||||
},
|
},
|
||||||
booleanOptions: {
|
|
||||||
websearch: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle successful API response with websearch disabled', async () => {
|
it('should handle successful API response', async () => {
|
||||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(axios.post).toHaveBeenCalledWith(
|
expect(axios.post).toHaveBeenCalledWith(
|
||||||
'https://openrouter.ai/api/v1/chat/completions',
|
'https://nano-gpt.com/api/v1/chat/completions',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
model: 'google/gemini-2.0-flash-001',
|
model: 'deepseek/deepseek-v3.2',
|
||||||
plugins: undefined,
|
|
||||||
}),
|
}),
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
@@ -133,76 +114,7 @@ describe('Ask Command', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle successful API response with websearch enabled', async () => {
|
it('should handle long responses with proper chunking', 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-001: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-001',
|
|
||||||
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);
|
const longResponse = 'A'.repeat(2500);
|
||||||
axios.post.mockResolvedValueOnce({
|
axios.post.mockResolvedValueOnce({
|
||||||
data: {
|
data: {
|
||||||
@@ -216,24 +128,13 @@ describe('Ask Command', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(interaction.followUp).toHaveBeenCalledTimes(2);
|
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)');
|
expect(interaction.followUp.mock.calls[1][0].content).toContain('(continued)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle code blocks in chunked responses with websearch', async () => {
|
it('should handle code blocks in chunked responses', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: {
|
|
||||||
prompt: mockPrompt,
|
|
||||||
},
|
|
||||||
booleanOptions: {
|
|
||||||
websearch: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseWithCodeBlock = "Here's a code example:\n```python\nprint('hello')\n```".repeat(
|
const responseWithCodeBlock = "Here's a code example:\n```python\nprint('hello')\n```".repeat(
|
||||||
20,
|
20,
|
||||||
);
|
);
|
||||||
@@ -249,7 +150,7 @@ describe('Ask Command', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
// Verify that code blocks are not split in the middle
|
// Verify that code blocks are not split in the middle
|
||||||
interaction.followUp.mock.calls.forEach(call => {
|
interaction.followUp.mock.calls.forEach(call => {
|
||||||
@@ -257,23 +158,14 @@ describe('Ask Command', () => {
|
|||||||
const openBlocks = (content.match(/```/g) || []).length;
|
const openBlocks = (content.match(/```/g) || []).length;
|
||||||
expect(openBlocks % 2).toBe(0); // Should always be even
|
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 },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle API errors', async () => {
|
||||||
const error = new Error('API Error');
|
const error = new Error('API Error');
|
||||||
error.response = { data: 'API Error Details' };
|
error.response = { data: 'API Error Details' };
|
||||||
axios.post.mockRejectedValueOnce(error);
|
axios.post.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||||
content: 'Sorry, there was an error processing your request.',
|
content: 'Sorry, there was an error processing your request.',
|
||||||
@@ -282,16 +174,11 @@ describe('Ask Command', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle rate limit errors', async () => {
|
it('should handle rate limit errors', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: { prompt: mockPrompt },
|
|
||||||
});
|
|
||||||
|
|
||||||
const error = new Error('Rate Limit Exceeded');
|
const error = new Error('Rate Limit Exceeded');
|
||||||
error.response = { status: 429, data: 'Too Many Requests' };
|
error.response = { status: 429, data: 'Too Many Requests' };
|
||||||
axios.post.mockRejectedValueOnce(error);
|
axios.post.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||||
content: 'The AI service is currently busy. Please try again in a few moments.',
|
content: 'The AI service is currently busy. Please try again in a few moments.',
|
||||||
@@ -300,16 +187,11 @@ describe('Ask Command', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle timeout errors', async () => {
|
it('should handle timeout errors', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: { prompt: mockPrompt },
|
|
||||||
});
|
|
||||||
|
|
||||||
const error = new Error('Timeout');
|
const error = new Error('Timeout');
|
||||||
error.code = 'ETIMEDOUT';
|
error.code = 'ETIMEDOUT';
|
||||||
axios.post.mockRejectedValueOnce(error);
|
axios.post.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||||
content: 'The request timed out. Please try again.',
|
content: 'The request timed out. Please try again.',
|
||||||
@@ -318,16 +200,11 @@ describe('Ask Command', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle invalid request errors', async () => {
|
it('should handle invalid request errors', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: { prompt: mockPrompt },
|
|
||||||
});
|
|
||||||
|
|
||||||
const error = new Error('Bad Request');
|
const error = new Error('Bad Request');
|
||||||
error.response = { status: 400, data: 'Invalid Request' };
|
error.response = { status: 400, data: 'Invalid Request' };
|
||||||
axios.post.mockRejectedValueOnce(error);
|
axios.post.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||||
content: 'Invalid request. Please try rephrasing your question.',
|
content: 'Invalid request. Please try rephrasing your question.',
|
||||||
@@ -335,60 +212,50 @@ describe('Ask Command', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send API request with correct headers regardless of websearch', async () => {
|
it('should send API request with correct headers', async () => {
|
||||||
interaction = createMockInteraction({
|
|
||||||
commandName: 'ask',
|
|
||||||
stringOptions: {
|
|
||||||
prompt: mockPrompt,
|
|
||||||
},
|
|
||||||
booleanOptions: {
|
|
||||||
websearch: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||||
|
|
||||||
await askCommand.execute(interaction);
|
await askCommand.run(interaction);
|
||||||
|
|
||||||
expect(axios.post).toHaveBeenCalledWith(expect.any(String), expect.any(Object), {
|
expect(axios.post).toHaveBeenCalledWith(expect.any(String), expect.any(Object), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Bearer test-api-key',
|
Authorization: 'Bearer test-api-key',
|
||||||
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle defer reply failure with websearch', async () => {
|
it('should handle defer reply failure', async () => {
|
||||||
interaction = createMockInteraction({
|
interaction = createMockInteraction({
|
||||||
commandName: 'ask',
|
commandName: 'ask',
|
||||||
stringOptions: {
|
stringOptions: {
|
||||||
prompt: mockPrompt,
|
prompt: mockPrompt,
|
||||||
},
|
},
|
||||||
booleanOptions: {
|
|
||||||
websearch: true,
|
|
||||||
},
|
|
||||||
deferFails: true,
|
deferFails: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(askCommand.execute(interaction)).rejects.toThrow('Failed to defer reply');
|
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 with websearch', async () => {
|
it('should handle follow up failure', async () => {
|
||||||
interaction = createMockInteraction({
|
interaction = createMockInteraction({
|
||||||
commandName: 'ask',
|
commandName: 'ask',
|
||||||
stringOptions: {
|
stringOptions: {
|
||||||
prompt: mockPrompt,
|
prompt: mockPrompt,
|
||||||
},
|
},
|
||||||
booleanOptions: {
|
|
||||||
websearch: true,
|
|
||||||
},
|
|
||||||
followUpFails: true,
|
followUpFails: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||||
|
|
||||||
await expect(askCommand.execute(interaction)).rejects.toThrow('Failed to follow up');
|
await expect(askCommand.run(interaction)).rejects.toThrow('Failed to follow up');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// Test utilities for Discord.js bot testing
|
// Test utilities for Discord.js bot testing
|
||||||
|
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a mock interaction object with common properties and methods
|
* Creates a mock interaction object with common properties and methods
|
||||||
* @param {Object} options - Customization options for the mock interaction
|
* @param {Object} options - Customization options for the mock interaction
|
||||||
* @returns {Object} Mock interaction object
|
* @returns {Object} Mock interaction object
|
||||||
*/
|
*/
|
||||||
const createMockInteraction = (options = {}) => ({
|
export const createMockInteraction = (options = {}) => ({
|
||||||
commandName: options.commandName || 'test-command',
|
commandName: options.commandName || 'test-command',
|
||||||
user: {
|
user: {
|
||||||
id: options.userId || 'mock-user-id',
|
id: options.userId || 'mock-user-id',
|
||||||
@@ -74,7 +76,7 @@ const createMockInteraction = (options = {}) => ({
|
|||||||
* @param {Object} options - Customization options for the mock client
|
* @param {Object} options - Customization options for the mock client
|
||||||
* @returns {Object} Mock client object
|
* @returns {Object} Mock client object
|
||||||
*/
|
*/
|
||||||
const createMockClient = (options = {}) => ({
|
export const createMockClient = (options = {}) => ({
|
||||||
user: {
|
user: {
|
||||||
id: options.clientId || 'mock-client-id',
|
id: options.clientId || 'mock-client-id',
|
||||||
username: options.clientUsername || 'MockBot',
|
username: options.clientUsername || 'MockBot',
|
||||||
@@ -89,8 +91,3 @@ const createMockClient = (options = {}) => ({
|
|||||||
login: jest.fn().mockResolvedValue('token'),
|
login: jest.fn().mockResolvedValue('token'),
|
||||||
destroy: jest.fn().mockResolvedValue(),
|
destroy: jest.fn().mockResolvedValue(),
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createMockInteraction,
|
|
||||||
createMockClient,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
|
||||||
|
};
|
||||||
+11
-39
@@ -10,13 +10,6 @@ import { SlashCommandBuilder } from 'discord.js';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Command from '../utils/command.js';
|
import Command from '../utils/command.js';
|
||||||
|
|
||||||
const config = {
|
|
||||||
webSearch: {
|
|
||||||
enabled: false, // Default web search state
|
|
||||||
allowOverride: true, // Whether users can override the default state
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class AskCommand extends Command {
|
export default class AskCommand extends Command {
|
||||||
defineCommand() {
|
defineCommand() {
|
||||||
return new SlashCommandBuilder()
|
return new SlashCommandBuilder()
|
||||||
@@ -24,31 +17,19 @@ export default class AskCommand extends Command {
|
|||||||
.setDescription('Ask a question to the AI')
|
.setDescription('Ask a question to the AI')
|
||||||
.addStringOption(option =>
|
.addStringOption(option =>
|
||||||
option.setName('prompt').setDescription('Your question or prompt').setRequired(true),
|
option.setName('prompt').setDescription('Your question or prompt').setRequired(true),
|
||||||
)
|
|
||||||
.addBooleanOption(option =>
|
|
||||||
option
|
|
||||||
.setName('websearch')
|
|
||||||
.setDescription('Enable web search for more up-to-date information')
|
|
||||||
.setRequired(false),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(interaction) {
|
async run(interaction) {
|
||||||
|
try {
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
|
||||||
const prompt = interaction.options.getString('prompt');
|
const prompt = interaction.options.getString('prompt');
|
||||||
const userWebSearchOption = interaction.options.getBoolean('websearch');
|
|
||||||
|
|
||||||
// Determine if web search should be enabled based on config and user option
|
|
||||||
const webSearchEnabled =
|
|
||||||
config.webSearch.allowOverride && userWebSearchOption !== null
|
|
||||||
? userWebSearchOption // Use user's choice if override is allowed and option was provided
|
|
||||||
: config.webSearch.enabled; // Otherwise use default config
|
|
||||||
|
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
'https://openrouter.ai/api/v1/chat/completions',
|
'https://nano-gpt.com/api/v1/chat/completions',
|
||||||
{
|
{
|
||||||
model: webSearchEnabled ? 'xiaomi/mimo-v2-flash:free:online' : 'xiaomi/mimo-v2-flash:free',
|
model: 'deepseek/deepseek-v3.2',
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
@@ -57,32 +38,17 @@ export default class AskCommand extends Command {
|
|||||||
},
|
},
|
||||||
{ role: 'user', content: prompt },
|
{ role: 'user', content: prompt },
|
||||||
],
|
],
|
||||||
plugins: webSearchEnabled
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: 'web',
|
|
||||||
max_results: 3,
|
|
||||||
search_prompt:
|
|
||||||
`A web search was conducted on ${new Date().toISOString()}. ` +
|
|
||||||
'Incorporate the following web search results into your response. ' +
|
|
||||||
'IMPORTANT: Cite them using markdown links named using the domain of the source. ' +
|
|
||||||
'Example: [nytimes.com](https://nytimes.com/some-page).',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`,
|
||||||
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const aiResponse = response.data.choices[0].message.content;
|
const aiResponse = response.data.choices[0].message.content;
|
||||||
const webSearchStatus = webSearchEnabled ? '\n> *Web search enabled* 🔍\n' : '';
|
const formattedResponse = `> **Question:** ${prompt}\n${aiResponse}`;
|
||||||
const formattedResponse = `> **Question:** ${prompt}${webSearchStatus}\n${aiResponse}`;
|
|
||||||
|
|
||||||
if (formattedResponse.length <= 2000) {
|
if (formattedResponse.length <= 2000) {
|
||||||
// Send as a single message with proper formatting
|
// Send as a single message with proper formatting
|
||||||
@@ -135,6 +101,12 @@ export default class AskCommand extends Command {
|
|||||||
Promise.resolve(),
|
Promise.resolve(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await interaction.followUp({
|
||||||
|
content: this.getErrorMessage(error),
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getErrorMessage(error) {
|
getErrorMessage(error) {
|
||||||
|
|||||||
+8
-13
@@ -1,4 +1,4 @@
|
|||||||
module.exports = {
|
export default {
|
||||||
// Automatically clear mock calls and instances between every test
|
// Automatically clear mock calls and instances between every test
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
|
|
||||||
@@ -6,28 +6,23 @@ module.exports = {
|
|||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
|
|
||||||
// The directory where Jest should output its coverage files
|
// The directory where Jest should output its coverage files
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: 'coverage',
|
||||||
|
|
||||||
// The test environment that will be used for testing
|
// The test environment that will be used for testing
|
||||||
testEnvironment: "node",
|
testEnvironment: 'node',
|
||||||
|
|
||||||
// The glob patterns Jest uses to detect test files
|
// The glob patterns Jest uses to detect test files
|
||||||
testMatch: [
|
testMatch: [
|
||||||
"**/__tests__/**/*.test.[jt]s?(x)",
|
'**/__tests__/**/*.test.[jt]s?(x)',
|
||||||
"**/__tests__/**/*.spec.[jt]s?(x)",
|
'**/__tests__/**/*.spec.[jt]s?(x)',
|
||||||
"**/?(*.)+(spec|test).[jt]s?(x)"
|
'**/?(*.)+(spec|test).[jt]s?(x)',
|
||||||
],
|
],
|
||||||
|
|
||||||
// A map from regular expressions to paths to transformers
|
// A map from regular expressions to paths to transformers
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.jsx?$": "babel-jest"
|
'^.+\\.jsx?$': 'babel-jest',
|
||||||
},
|
},
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all test files
|
// An array of regexp pattern strings that are matched against all test files
|
||||||
testPathIgnorePatterns: [
|
testPathIgnorePatterns: ['/node_modules/'],
|
||||||
"/node_modules/"
|
|
||||||
],
|
|
||||||
|
|
||||||
// Setup files that will be run before each test
|
|
||||||
setupFiles: ["<rootDir>/jest.setup.js"]
|
|
||||||
};
|
};
|
||||||
+9
-7
@@ -1,3 +1,5 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
// Mock Discord.js Client and other commonly used classes
|
// Mock Discord.js Client and other commonly used classes
|
||||||
jest.mock('discord.js', () => ({
|
jest.mock('discord.js', () => ({
|
||||||
Client: jest.fn(() => ({
|
Client: jest.fn(() => ({
|
||||||
@@ -6,31 +8,31 @@ jest.mock('discord.js', () => ({
|
|||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
once: jest.fn(),
|
once: jest.fn(),
|
||||||
user: {
|
user: {
|
||||||
setActivity: jest.fn()
|
setActivity: jest.fn(),
|
||||||
}
|
},
|
||||||
})),
|
})),
|
||||||
Collection: jest.fn(() => ({
|
Collection: jest.fn(() => ({
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
set: jest.fn(),
|
set: jest.fn(),
|
||||||
has: jest.fn(),
|
has: jest.fn(),
|
||||||
delete: jest.fn()
|
delete: jest.fn(),
|
||||||
})),
|
})),
|
||||||
GatewayIntentBits: {
|
GatewayIntentBits: {
|
||||||
Guilds: 1,
|
Guilds: 1,
|
||||||
GuildMessages: 2,
|
GuildMessages: 2,
|
||||||
MessageContent: 3
|
MessageContent: 3,
|
||||||
},
|
},
|
||||||
Events: {
|
Events: {
|
||||||
ClientReady: 'ready',
|
ClientReady: 'ready',
|
||||||
InteractionCreate: 'interactionCreate'
|
InteractionCreate: 'interactionCreate',
|
||||||
},
|
},
|
||||||
SlashCommandBuilder: jest.fn().mockReturnValue({
|
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||||
setName: jest.fn().mockReturnThis(),
|
setName: jest.fn().mockReturnThis(),
|
||||||
setDescription: jest.fn().mockReturnThis(),
|
setDescription: jest.fn().mockReturnThis(),
|
||||||
addStringOption: jest.fn().mockReturnThis(),
|
addStringOption: jest.fn().mockReturnThis(),
|
||||||
addIntegerOption: jest.fn().mockReturnThis(),
|
addIntegerOption: jest.fn().mockReturnThis(),
|
||||||
addUserOption: jest.fn().mockReturnThis()
|
addUserOption: jest.fn().mockReturnThis(),
|
||||||
})
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock environment variables
|
// Mock environment variables
|
||||||
|
|||||||
Reference in New Issue
Block a user