refactor: update ask command and test utilities to use ES modules; improve error handling and response formatting
This commit is contained in:
+55
-188
@@ -1,11 +1,14 @@
|
||||
const axios = require('axios');
|
||||
const { createMockInteraction } = require('../utils/testUtils');
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock axios
|
||||
jest.mock('axios');
|
||||
jest.unstable_mockModule('axios', () => ({
|
||||
default: {
|
||||
post: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the discord.js module
|
||||
jest.mock('discord.js', () => ({
|
||||
jest.unstable_mockModule('discord.js', () => ({
|
||||
SlashCommandBuilder: jest.fn().mockReturnValue({
|
||||
setName: jest.fn().mockReturnThis(),
|
||||
setDescription: jest.fn().mockReturnThis(),
|
||||
@@ -17,33 +20,17 @@ jest.mock('discord.js', () => ({
|
||||
};
|
||||
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().mockReturnValue({
|
||||
name: 'ask',
|
||||
description: 'Ask a question to the AI',
|
||||
options: [
|
||||
{
|
||||
name: 'prompt',
|
||||
description: 'Your question or prompt',
|
||||
type: 3,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}),
|
||||
@@ -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('Command Structure', () => {
|
||||
it('should have correct name and description', () => {
|
||||
const commandData = askCommand.data.toJSON();
|
||||
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('data');
|
||||
expect(askCommand).toHaveProperty('execute');
|
||||
expect(typeof askCommand.execute).toBe('function');
|
||||
expect(askCommand).toHaveProperty('defineCommand');
|
||||
expect(askCommand).toHaveProperty('run');
|
||||
expect(typeof askCommand.run).toBe('function');
|
||||
});
|
||||
|
||||
it('should have correct option configuration', () => {
|
||||
const commandData = askCommand.data.toJSON();
|
||||
const [promptOption, websearchOption] = commandData.options;
|
||||
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);
|
||||
|
||||
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(() => {
|
||||
process.env.OPENROUTER_API_KEY = 'test-api-key';
|
||||
axios.post.mockReset();
|
||||
process.env.NANOGPT_API_KEY = 'test-api-key';
|
||||
jest.clearAllMocks();
|
||||
|
||||
interaction = createMockInteraction({
|
||||
@@ -106,22 +91,18 @@ describe('Ask Command', () => {
|
||||
stringOptions: {
|
||||
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);
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
await askCommand.run(interaction);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'https://openrouter.ai/api/v1/chat/completions',
|
||||
'https://nano-gpt.com/api/v1/chat/completions',
|
||||
expect.objectContaining({
|
||||
model: 'google/gemini-2.0-flash-001',
|
||||
plugins: undefined,
|
||||
model: 'deepseek/deepseek-v3.2',
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
@@ -133,76 +114,7 @@ describe('Ask Command', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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-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,
|
||||
},
|
||||
});
|
||||
|
||||
it('should handle long responses with proper chunking', async () => {
|
||||
const longResponse = 'A'.repeat(2500);
|
||||
axios.post.mockResolvedValueOnce({
|
||||
data: {
|
||||
@@ -216,24 +128,13 @@ describe('Ask Command', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
await askCommand.run(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,
|
||||
},
|
||||
});
|
||||
|
||||
it('should handle code blocks in chunked responses', async () => {
|
||||
const responseWithCodeBlock = "Here's a code example:\n```python\nprint('hello')\n```".repeat(
|
||||
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
|
||||
interaction.followUp.mock.calls.forEach(call => {
|
||||
@@ -257,23 +158,14 @@ describe('Ask Command', () => {
|
||||
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 },
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
const error = new Error('API Error');
|
||||
error.response = { data: 'API Error Details' };
|
||||
axios.post.mockRejectedValueOnce(error);
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
await askCommand.run(interaction);
|
||||
|
||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||
content: 'Sorry, there was an error processing your request.',
|
||||
@@ -282,16 +174,11 @@ describe('Ask Command', () => {
|
||||
});
|
||||
|
||||
it('should handle rate limit errors', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: { prompt: mockPrompt },
|
||||
});
|
||||
|
||||
const error = new Error('Rate Limit Exceeded');
|
||||
error.response = { status: 429, data: 'Too Many Requests' };
|
||||
axios.post.mockRejectedValueOnce(error);
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
await askCommand.run(interaction);
|
||||
|
||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||
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 () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: { prompt: mockPrompt },
|
||||
});
|
||||
|
||||
const error = new Error('Timeout');
|
||||
error.code = 'ETIMEDOUT';
|
||||
axios.post.mockRejectedValueOnce(error);
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
await askCommand.run(interaction);
|
||||
|
||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||
content: 'The request timed out. Please try again.',
|
||||
@@ -318,16 +200,11 @@ describe('Ask Command', () => {
|
||||
});
|
||||
|
||||
it('should handle invalid request errors', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: { prompt: mockPrompt },
|
||||
});
|
||||
|
||||
const error = new Error('Bad Request');
|
||||
error.response = { status: 400, data: 'Invalid Request' };
|
||||
axios.post.mockRejectedValueOnce(error);
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
await askCommand.run(interaction);
|
||||
|
||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||
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 () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: {
|
||||
prompt: mockPrompt,
|
||||
},
|
||||
booleanOptions: {
|
||||
websearch: true,
|
||||
},
|
||||
});
|
||||
|
||||
it('should send API request with correct headers', async () => {
|
||||
axios.post.mockResolvedValueOnce(mockApiResponse);
|
||||
|
||||
await askCommand.execute(interaction);
|
||||
await askCommand.run(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 () => {
|
||||
it('should handle defer reply failure', async () => {
|
||||
interaction = createMockInteraction({
|
||||
commandName: 'ask',
|
||||
stringOptions: {
|
||||
prompt: mockPrompt,
|
||||
},
|
||||
booleanOptions: {
|
||||
websearch: 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({
|
||||
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');
|
||||
await expect(askCommand.run(interaction)).rejects.toThrow('Failed to follow up');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// Test utilities for Discord.js bot testing
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
/**
|
||||
* Creates a mock interaction object with common properties and methods
|
||||
* @param {Object} options - Customization options for the mock interaction
|
||||
* @returns {Object} Mock interaction object
|
||||
*/
|
||||
const createMockInteraction = (options = {}) => ({
|
||||
export const createMockInteraction = (options = {}) => ({
|
||||
commandName: options.commandName || 'test-command',
|
||||
user: {
|
||||
id: options.userId || 'mock-user-id',
|
||||
@@ -74,7 +76,7 @@ const createMockInteraction = (options = {}) => ({
|
||||
* @param {Object} options - Customization options for the mock client
|
||||
* @returns {Object} Mock client object
|
||||
*/
|
||||
const createMockClient = (options = {}) => ({
|
||||
export const createMockClient = (options = {}) => ({
|
||||
user: {
|
||||
id: options.clientId || 'mock-client-id',
|
||||
username: options.clientUsername || 'MockBot',
|
||||
@@ -89,8 +91,3 @@ const createMockClient = (options = {}) => ({
|
||||
login: jest.fn().mockResolvedValue('token'),
|
||||
destroy: jest.fn().mockResolvedValue(),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createMockInteraction,
|
||||
createMockClient,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
|
||||
};
|
||||
+75
-103
@@ -10,13 +10,6 @@ import { SlashCommandBuilder } from 'discord.js';
|
||||
import axios from 'axios';
|
||||
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 {
|
||||
defineCommand() {
|
||||
return new SlashCommandBuilder()
|
||||
@@ -24,116 +17,95 @@ export default class AskCommand extends Command {
|
||||
.setDescription('Ask a question to the AI')
|
||||
.addStringOption(option =>
|
||||
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) {
|
||||
await interaction.deferReply();
|
||||
try {
|
||||
await interaction.deferReply();
|
||||
|
||||
const prompt = interaction.options.getString('prompt');
|
||||
const userWebSearchOption = interaction.options.getBoolean('websearch');
|
||||
const prompt = interaction.options.getString('prompt');
|
||||
|
||||
// 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(
|
||||
'https://openrouter.ai/api/v1/chat/completions',
|
||||
{
|
||||
model: webSearchEnabled ? 'xiaomi/mimo-v2-flash:free:online' : 'xiaomi/mimo-v2-flash:free',
|
||||
messages: [
|
||||
{
|
||||
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',
|
||||
const response = await axios.post(
|
||||
'https://nano-gpt.com/api/v1/chat/completions',
|
||||
{
|
||||
model: 'deepseek/deepseek-v3.2',
|
||||
messages: [
|
||||
{
|
||||
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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const aiResponse = response.data.choices[0].message.content;
|
||||
const webSearchStatus = webSearchEnabled ? '\n> *Web search enabled* 🔍\n' : '';
|
||||
const formattedResponse = `> **Question:** ${prompt}${webSearchStatus}\n${aiResponse}`;
|
||||
const aiResponse = response.data.choices[0].message.content;
|
||||
const formattedResponse = `> **Question:** ${prompt}\n${aiResponse}`;
|
||||
|
||||
if (formattedResponse.length <= 2000) {
|
||||
// Send as a single message with proper formatting
|
||||
await interaction.followUp({
|
||||
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,
|
||||
if (formattedResponse.length <= 2000) {
|
||||
// Send as a single message with proper formatting
|
||||
await interaction.followUp({
|
||||
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;
|
||||
|
||||
remainingText = remainingText.slice(chunk.length);
|
||||
isFirstChunk = false;
|
||||
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,
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
// Send chunks sequentially using reduce
|
||||
await chunks.reduce(
|
||||
(promise, chunk) =>
|
||||
promise.then(async () => {
|
||||
await interaction.followUp(chunk);
|
||||
}),
|
||||
Promise.resolve(),
|
||||
);
|
||||
} catch (error) {
|
||||
await interaction.followUp({
|
||||
content: this.getErrorMessage(error),
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-27
@@ -1,33 +1,28 @@
|
||||
module.exports = {
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
export default {
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
collectCoverage: true,
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
collectCoverage: true,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: "coverage",
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: 'node',
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: [
|
||||
"**/__tests__/**/*.test.[jt]s?(x)",
|
||||
"**/__tests__/**/*.spec.[jt]s?(x)",
|
||||
"**/?(*.)+(spec|test).[jt]s?(x)"
|
||||
],
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: [
|
||||
'**/__tests__/**/*.test.[jt]s?(x)',
|
||||
'**/__tests__/**/*.spec.[jt]s?(x)',
|
||||
'**/?(*.)+(spec|test).[jt]s?(x)',
|
||||
],
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
"^.+\\.jsx?$": "babel-jest"
|
||||
},
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test files
|
||||
testPathIgnorePatterns: [
|
||||
"/node_modules/"
|
||||
],
|
||||
|
||||
// Setup files that will be run before each test
|
||||
setupFiles: ["<rootDir>/jest.setup.js"]
|
||||
};
|
||||
// An array of regexp pattern strings that are matched against all test files
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
};
|
||||
|
||||
+33
-31
@@ -1,39 +1,41 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock Discord.js Client and other commonly used classes
|
||||
jest.mock('discord.js', () => ({
|
||||
Client: jest.fn(() => ({
|
||||
login: jest.fn().mockResolvedValue('token'),
|
||||
destroy: jest.fn().mockResolvedValue(),
|
||||
on: jest.fn(),
|
||||
once: jest.fn(),
|
||||
user: {
|
||||
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
|
||||
Client: jest.fn(() => ({
|
||||
login: jest.fn().mockResolvedValue('token'),
|
||||
destroy: jest.fn().mockResolvedValue(),
|
||||
on: jest.fn(),
|
||||
once: jest.fn(),
|
||||
user: {
|
||||
setActivity: jest.fn(),
|
||||
},
|
||||
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()
|
||||
})
|
||||
})),
|
||||
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',
|
||||
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
|
||||
process.env.DISCORD_TOKEN = 'mock-token';
|
||||
process.env.CLIENT_ID = 'mock-client-id';
|
||||
process.env.GUILD_ID = 'mock-guild-id';
|
||||
process.env.GUILD_ID = 'mock-guild-id';
|
||||
|
||||
Reference in New Issue
Block a user