refactor: update ask command and test utilities to use ES modules; improve error handling and response formatting

This commit is contained in:
2025-12-24 11:21:30 -05:00
parent 94494c1ca5
commit 2ac855b765
6 changed files with 192 additions and 356 deletions
+55 -188
View File
@@ -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(),
@@ -17,33 +20,17 @@ jest.mock('discord.js', () => ({
}; };
callback(option); callback(option);
return { return {
addBooleanOption: jest.fn().mockImplementation(boolCallback => { toJSON: jest.fn().mockReturnValue({
const boolOption = { name: 'ask',
setName: jest.fn().mockReturnThis(), description: 'Ask a question to the AI',
setDescription: jest.fn().mockReturnThis(), options: [
setRequired: jest.fn().mockReturnThis(), {
}; name: 'prompt',
boolCallback(boolOption); description: 'Your question or prompt',
return { type: 3,
toJSON: jest.fn().mockReturnValue({ required: true,
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,
},
],
}),
};
}), }),
}; };
}), }),
@@ -51,33 +38,32 @@ jest.mock('discord.js', () => ({
}), }),
})); }));
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 () => { it('should handle API errors', async () => {
interaction = createMockInteraction({
commandName: 'ask',
stringOptions: { prompt: mockPrompt },
booleanOptions: { websearch: true },
});
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');
}); });
}); });
}); });
+4 -7
View File
@@ -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,
};
+3
View File
@@ -0,0 +1,3 @@
export default {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
};
+75 -103
View File
@@ -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,116 +17,95 @@ 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) {
await interaction.deferReply(); try {
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 response = await axios.post(
const webSearchEnabled = 'https://nano-gpt.com/api/v1/chat/completions',
config.webSearch.allowOverride && userWebSearchOption !== null {
? userWebSearchOption // Use user's choice if override is allowed and option was provided model: 'deepseek/deepseek-v3.2',
: config.webSearch.enabled; // Otherwise use default config messages: [
{
const response = await axios.post( role: 'system',
'https://openrouter.ai/api/v1/chat/completions', content:
{ 'You are kekbot, a highly celebrated and knowledgeable computer scientist with decades of experience in various fields of computing. You are known for your ability to explain complex topics in a clear, concise, and insightful manner. Provide direct and to-the-point answers, avoiding unnecessary elaboration or repetition. Focus on delivering accurate and valuable information efficiently.',
model: webSearchEnabled ? 'xiaomi/mimo-v2-flash:free:online' : 'xiaomi/mimo-v2-flash:free', },
messages: [ { role: 'user', content: prompt },
{ ],
role: 'system',
content:
'You are kekbot, a highly celebrated and knowledgeable computer scientist with decades of experience in various fields of computing. You are known for your ability to explain complex topics in a clear, concise, and insightful manner. Provide direct and to-the-point answers, avoiding unnecessary elaboration or repetition. Focus on delivering accurate and valuable information efficiently.',
},
{ 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: {
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
'Content-Type': 'application/json',
}, },
}, {
); headers: {
Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`,
'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
await interaction.followUp({ await interaction.followUp({
content: formattedResponse, content: formattedResponse,
split: false,
allowedMentions: { parse: [] },
});
} else {
// For longer messages, split while preserving markdown
const maxLength = 2000;
const chunks = [];
let remainingText = formattedResponse;
let isFirstChunk = true;
while (remainingText.length > 0) {
let chunk = remainingText.slice(0, maxLength);
// If we're in the middle of a code block, find a safe split point
const lastCodeBlock = chunk.lastIndexOf('```');
if (lastCodeBlock !== -1 && !chunk.slice(lastCodeBlock).includes('\n```')) {
// Find the last newline before maxLength
const lastNewline = chunk.lastIndexOf('\n');
if (lastNewline !== -1) {
chunk = chunk.slice(0, lastNewline);
}
}
// For subsequent chunks, add continuation indicator
if (!isFirstChunk) {
chunk = `(continued)\n${chunk}`;
}
chunks.push({
content: chunk,
split: false, split: false,
allowedMentions: { parse: [] }, allowedMentions: { parse: [] },
}); });
} else {
// For longer messages, split while preserving markdown
const maxLength = 2000;
const chunks = [];
let remainingText = formattedResponse;
let isFirstChunk = true;
remainingText = remainingText.slice(chunk.length); while (remainingText.length > 0) {
isFirstChunk = false; let chunk = remainingText.slice(0, maxLength);
// If we're in the middle of a code block, find a safe split point
const lastCodeBlock = chunk.lastIndexOf('```');
if (lastCodeBlock !== -1 && !chunk.slice(lastCodeBlock).includes('\n```')) {
// Find the last newline before maxLength
const lastNewline = chunk.lastIndexOf('\n');
if (lastNewline !== -1) {
chunk = chunk.slice(0, lastNewline);
}
}
// For subsequent chunks, add continuation indicator
if (!isFirstChunk) {
chunk = `(continued)\n${chunk}`;
}
chunks.push({
content: chunk,
split: false,
allowedMentions: { parse: [] },
});
remainingText = remainingText.slice(chunk.length);
isFirstChunk = false;
}
// Send chunks sequentially using reduce
await chunks.reduce(
(promise, chunk) =>
promise.then(async () => {
await interaction.followUp(chunk);
}),
Promise.resolve(),
);
} }
} catch (error) {
// Send chunks sequentially using reduce await interaction.followUp({
await chunks.reduce( content: this.getErrorMessage(error),
(promise, chunk) => ephemeral: true,
promise.then(async () => { });
await interaction.followUp(chunk);
}),
Promise.resolve(),
);
} }
} }
+21 -26
View File
@@ -1,33 +1,28 @@
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,
// Indicates whether the coverage information should be collected while executing the test // Indicates whether the coverage information should be collected while executing the test
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"]
}; };
+32 -30
View File
@@ -1,36 +1,38 @@
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(() => ({
login: jest.fn().mockResolvedValue('token'), login: jest.fn().mockResolvedValue('token'),
destroy: jest.fn().mockResolvedValue(), destroy: jest.fn().mockResolvedValue(),
on: jest.fn(), on: jest.fn(),
once: jest.fn(), once: jest.fn(),
user: { user: {
setActivity: jest.fn() setActivity: jest.fn(),
}
})),
Collection: jest.fn(() => ({
get: jest.fn(),
set: jest.fn(),
has: jest.fn(),
delete: jest.fn()
})),
GatewayIntentBits: {
Guilds: 1,
GuildMessages: 2,
MessageContent: 3
}, },
Events: { })),
ClientReady: 'ready', Collection: jest.fn(() => ({
InteractionCreate: 'interactionCreate' get: jest.fn(),
}, set: jest.fn(),
SlashCommandBuilder: jest.fn().mockReturnValue({ has: jest.fn(),
setName: jest.fn().mockReturnThis(), delete: jest.fn(),
setDescription: jest.fn().mockReturnThis(), })),
addStringOption: jest.fn().mockReturnThis(), GatewayIntentBits: {
addIntegerOption: jest.fn().mockReturnThis(), Guilds: 1,
addUserOption: jest.fn().mockReturnThis() GuildMessages: 2,
}) MessageContent: 3,
},
Events: {
ClientReady: 'ready',
InteractionCreate: 'interactionCreate',
},
SlashCommandBuilder: jest.fn().mockReturnValue({
setName: jest.fn().mockReturnThis(),
setDescription: jest.fn().mockReturnThis(),
addStringOption: jest.fn().mockReturnThis(),
addIntegerOption: jest.fn().mockReturnThis(),
addUserOption: jest.fn().mockReturnThis(),
}),
})); }));
// Mock environment variables // Mock environment variables